1 package org.apache.maven.plugins.surefire.report;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 import org.apache.maven.doxia.markup.HtmlMarkup;
23 import org.apache.maven.doxia.sink.Sink;
24 import org.apache.maven.doxia.sink.SinkEventAttributeSet;
25 import org.apache.maven.doxia.sink.SinkEventAttributes;
26 import org.apache.maven.reporting.MavenReportException;
27
28 import java.text.NumberFormat;
29 import java.util.Iterator;
30 import java.util.List;
31 import java.util.ListIterator;
32 import java.util.Locale;
33 import java.util.Map;
34 import java.util.ResourceBundle;
35 import java.util.StringTokenizer;
36
37
38
39
40 public class SurefireReportGenerator
41 {
42 private final SurefireReportParser report;
43
44 private List testSuites;
45
46 private final boolean showSuccess;
47
48 private final String xrefLocation;
49
50 public SurefireReportGenerator( List reportsDirectories, Locale locale, boolean showSuccess, String xrefLocation )
51 {
52 report = new SurefireReportParser( reportsDirectories, locale );
53
54 this.xrefLocation = xrefLocation;
55
56 this.showSuccess = showSuccess;
57 }
58
59 public void doGenerateReport( ResourceBundle bundle, Sink sink )
60 throws MavenReportException
61 {
62 testSuites = report.parseXMLReportFiles();
63
64 sink.head();
65
66 sink.title();
67 sink.text( bundle.getString( "report.surefire.header" ) );
68 sink.title_();
69
70 sink.head_();
71
72 sink.body();
73
74 SinkEventAttributeSet atts = new SinkEventAttributeSet();
75 atts.addAttribute( SinkEventAttributes.TYPE, "text/javascript" );
76 sink.unknown( "script", new Object[]{new Integer( HtmlMarkup.TAG_TYPE_START )}, atts );
77 sink.unknown( "cdata", new Object[]{new Integer( HtmlMarkup.CDATA_TYPE ), javascriptToggleDisplayCode() }, null );
78 sink.unknown( "script", new Object[]{new Integer( HtmlMarkup.TAG_TYPE_END )}, null );
79
80 sink.section1();
81 sink.sectionTitle1();
82 sink.text( bundle.getString( "report.surefire.header" ) );
83 sink.sectionTitle1_();
84 sink.section1_();
85
86 constructSummarySection( bundle, sink );
87
88 Map suitePackages = report.getSuitesGroupByPackage( testSuites );
89 if ( !suitePackages.isEmpty() )
90 {
91 constructPackagesSection( bundle, sink, suitePackages );
92 }
93
94 if ( !testSuites.isEmpty() )
95 {
96 constructTestCasesSection( bundle, sink );
97 }
98
99 List failureList = report.getFailureDetails( testSuites );
100 if ( !failureList.isEmpty() )
101 {
102 constructFailureDetails( sink, bundle, failureList );
103 }
104
105 sink.body_();
106
107 sink.flush();
108
109 sink.close();
110 }
111
112 private void constructSummarySection( ResourceBundle bundle, Sink sink )
113 {
114 Map summary = report.getSummary( testSuites );
115
116 sink.section1();
117 sink.sectionTitle1();
118 sink.text( bundle.getString( "report.surefire.label.summary" ) );
119 sink.sectionTitle1_();
120
121 sinkAnchor( sink, "Summary" );
122
123 constructHotLinks( sink, bundle );
124
125 sinkLineBreak( sink );
126
127 sink.table();
128
129 sink.tableRow();
130
131 sinkHeader( sink, bundle.getString( "report.surefire.label.tests" ) );
132
133 sinkHeader( sink, bundle.getString( "report.surefire.label.errors" ) );
134
135 sinkHeader( sink, bundle.getString( "report.surefire.label.failures" ) );
136
137 sinkHeader( sink, bundle.getString( "report.surefire.label.skipped" ) );
138
139 sinkHeader( sink, bundle.getString( "report.surefire.label.successrate" ) );
140
141 sinkHeader( sink, bundle.getString( "report.surefire.label.time" ) );
142
143 sink.tableRow_();
144
145 sink.tableRow();
146
147 sinkCell( sink, (String) summary.get( "totalTests" ) );
148
149 sinkCell( sink, (String) summary.get( "totalErrors" ) );
150
151 sinkCell( sink, (String) summary.get( "totalFailures" ) );
152
153 sinkCell( sink, (String) summary.get( "totalSkipped" ) );
154
155 sinkCell( sink, summary.get( "totalPercentage" ) + "%" );
156
157 sinkCell( sink, (String) summary.get( "totalElapsedTime" ) );
158
159 sink.tableRow_();
160
161 sink.table_();
162
163 sink.lineBreak();
164
165 sink.paragraph();
166 sink.text( bundle.getString( "report.surefire.text.note1" ) );
167 sink.paragraph_();
168
169 sinkLineBreak( sink );
170
171 sink.section1_();
172 }
173
174 private void constructPackagesSection( ResourceBundle bundle, Sink sink, Map suitePackages )
175 {
176 NumberFormat numberFormat = report.getNumberFormat();
177
178 sink.section1();
179 sink.sectionTitle1();
180 sink.text( bundle.getString( "report.surefire.label.packagelist" ) );
181 sink.sectionTitle1_();
182
183 sinkAnchor( sink, "Package_List" );
184
185 constructHotLinks( sink, bundle );
186
187 sinkLineBreak( sink );
188
189 sink.table();
190
191 sink.tableRow();
192
193 sinkHeader( sink, bundle.getString( "report.surefire.label.package" ) );
194
195 sinkHeader( sink, bundle.getString( "report.surefire.label.tests" ) );
196
197 sinkHeader( sink, bundle.getString( "report.surefire.label.errors" ) );
198
199 sinkHeader( sink, bundle.getString( "report.surefire.label.failures" ) );
200
201 sinkHeader( sink, bundle.getString( "report.surefire.label.skipped" ) );
202
203 sinkHeader( sink, bundle.getString( "report.surefire.label.successrate" ) );
204
205 sinkHeader( sink, bundle.getString( "report.surefire.label.time" ) );
206
207 sink.tableRow_();
208
209 Iterator packIter = suitePackages.keySet().iterator();
210
211 while ( packIter.hasNext() )
212 {
213 sink.tableRow();
214
215 String packageName = (String) packIter.next();
216
217 List testSuiteList = (List) suitePackages.get( packageName );
218
219 Map packageSummary = report.getSummary( testSuiteList );
220
221 sinkCellLink( sink, packageName, "#" + packageName );
222
223 sinkCell( sink, (String) packageSummary.get( "totalTests" ) );
224
225 sinkCell( sink, (String) packageSummary.get( "totalErrors" ) );
226
227 sinkCell( sink, (String) packageSummary.get( "totalFailures" ) );
228
229 sinkCell( sink, (String) packageSummary.get( "totalSkipped" ) );
230
231 sinkCell( sink, packageSummary.get( "totalPercentage" ) + "%" );
232
233 sinkCell( sink, (String) packageSummary.get( "totalElapsedTime" ) );
234
235 sink.tableRow_();
236 }
237
238 sink.table_();
239
240 sink.lineBreak();
241
242 sink.paragraph();
243 sink.text( bundle.getString( "report.surefire.text.note2" ) );
244 sink.paragraph_();
245
246 packIter = suitePackages.keySet().iterator();
247
248 while ( packIter.hasNext() )
249 {
250 String packageName = (String) packIter.next();
251
252 List testSuiteList = (List) suitePackages.get( packageName );
253
254 Iterator suiteIterator = testSuiteList.iterator();
255
256 sink.section2();
257 sink.sectionTitle2();
258 sink.text( packageName );
259 sink.sectionTitle2_();
260
261 sinkAnchor( sink, packageName );
262
263 sink.table();
264
265 sink.tableRow();
266
267 sinkHeader( sink, "" );
268
269 sinkHeader( sink, bundle.getString( "report.surefire.label.class" ) );
270
271 sinkHeader( sink, bundle.getString( "report.surefire.label.tests" ) );
272
273 sinkHeader( sink, bundle.getString( "report.surefire.label.errors" ) );
274
275 sinkHeader( sink, bundle.getString( "report.surefire.label.failures" ) );
276
277 sinkHeader( sink, bundle.getString( "report.surefire.label.skipped" ) );
278
279 sinkHeader( sink, bundle.getString( "report.surefire.label.successrate" ) );
280
281 sinkHeader( sink, bundle.getString( "report.surefire.label.time" ) );
282
283 sink.tableRow_();
284
285 while ( suiteIterator.hasNext() )
286 {
287 ReportTestSuite suite = (ReportTestSuite) suiteIterator.next();
288
289 if ( showSuccess || suite.getNumberOfErrors() != 0 || suite.getNumberOfFailures() != 0 )
290 {
291
292 sink.tableRow();
293
294 sink.tableCell();
295
296 sink.link( "#" + suite.getPackageName() + suite.getName() );
297
298 if ( suite.getNumberOfErrors() > 0 )
299 {
300 sinkIcon( "error", sink );
301 }
302 else if ( suite.getNumberOfFailures() > 0 )
303 {
304 sinkIcon( "junit.framework", sink );
305 }
306 else if ( suite.getNumberOfSkipped() > 0 )
307 {
308 sinkIcon( "skipped", sink );
309 }
310 else
311 {
312 sinkIcon( "success", sink );
313 }
314
315 sink.link_();
316
317 sink.tableCell_();
318
319 sinkCellLink( sink, suite.getName(), "#" + suite.getPackageName() + suite.getName() );
320
321 sinkCell( sink, Integer.toString( suite.getNumberOfTests() ) );
322
323 sinkCell( sink, Integer.toString( suite.getNumberOfErrors() ) );
324
325 sinkCell( sink, Integer.toString( suite.getNumberOfFailures() ) );
326
327 sinkCell( sink, Integer.toString( suite.getNumberOfSkipped() ) );
328
329 String percentage = report.computePercentage( suite.getNumberOfTests(), suite.getNumberOfErrors(),
330 suite.getNumberOfFailures(), suite
331 .getNumberOfSkipped() );
332 sinkCell( sink, percentage + "%" );
333
334 sinkCell( sink, numberFormat.format( suite.getTimeElapsed() ) );
335
336 sink.tableRow_();
337 }
338 }
339
340 sink.table_();
341
342 sink.section2_();
343 }
344
345 sinkLineBreak( sink );
346
347 sink.section1_();
348 }
349
350 private void constructTestCasesSection( ResourceBundle bundle, Sink sink )
351 {
352 NumberFormat numberFormat = report.getNumberFormat();
353
354 sink.section1();
355 sink.sectionTitle1();
356 sink.text( bundle.getString( "report.surefire.label.testcases" ) );
357 sink.sectionTitle1_();
358
359 sinkAnchor( sink, "Test_Cases" );
360
361 constructHotLinks( sink, bundle );
362
363 ListIterator suiteIterator = testSuites.listIterator();
364
365 while ( suiteIterator.hasNext() )
366 {
367 ReportTestSuite suite = (ReportTestSuite) suiteIterator.next();
368
369 List testCases = suite.getTestCases();
370
371 if ( testCases != null && !testCases.isEmpty() )
372 {
373 ListIterator caseIterator = testCases.listIterator();
374
375 sink.section2();
376 sink.sectionTitle2();
377 sink.text( suite.getName() );
378 sink.sectionTitle2_();
379
380 sinkAnchor( sink, suite.getPackageName() + suite.getName() );
381
382 sink.table();
383
384 while ( caseIterator.hasNext() )
385 {
386 ReportTestCase testCase = (ReportTestCase) caseIterator.next();
387
388 if ( testCase.getFailure() != null || showSuccess )
389 {
390 sink.tableRow();
391
392 sink.tableCell();
393
394 Map failure = testCase.getFailure();
395
396 if ( failure != null )
397 {
398 sink.link( "#" + testCase.getFullName() );
399
400 sinkIcon( (String) failure.get( "type" ), sink );
401
402 sink.link_();
403 }
404 else
405 {
406 sinkIcon( "success", sink );
407 }
408
409 sink.tableCell_();
410
411 if ( failure != null )
412 {
413 sink.tableCell();
414
415 sinkLink( sink, testCase.getName(), "#" + testCase.getFullName() );
416
417 SinkEventAttributeSet atts = new SinkEventAttributeSet();
418 atts.addAttribute( SinkEventAttributes.CLASS, "detailToggle" );
419 atts.addAttribute( SinkEventAttributes.STYLE, "display:inline" );
420 sink.unknown( "div", new Object[]{new Integer( HtmlMarkup.TAG_TYPE_START )}, atts );
421
422 sink.link( "javascript:toggleDisplay('" + toHtmlId( testCase.getFullName() ) + "');" );
423
424 atts = new SinkEventAttributeSet();
425 atts.addAttribute( SinkEventAttributes.STYLE, "display:inline;" );
426 atts.addAttribute( SinkEventAttributes.ID, toHtmlId( testCase.getFullName() ) + "off" );
427 sink.unknown( "span", new Object[]{new Integer( HtmlMarkup.TAG_TYPE_START )}, atts );
428 sink.text( " + " );
429 sink.unknown( "span", new Object[]{new Integer( HtmlMarkup.TAG_TYPE_END )}, null );
430
431 atts = new SinkEventAttributeSet();
432 atts.addAttribute( SinkEventAttributes.STYLE, "display:none;" );
433 atts.addAttribute( SinkEventAttributes.ID, toHtmlId( testCase.getFullName() ) + "on" );
434 sink.unknown( "span", new Object[]{new Integer( HtmlMarkup.TAG_TYPE_START )}, atts );
435 sink.text( " - " );
436 sink.unknown( "span", new Object[]{new Integer( HtmlMarkup.TAG_TYPE_END )}, null );
437
438 sink.text( "[ Detail ]" );
439 sink.link_();
440
441 sink.unknown( "div", new Object[]{new Integer( HtmlMarkup.TAG_TYPE_END )}, null );
442
443 sink.tableCell_();
444 }
445 else
446 {
447 sinkCell( sink, testCase.getName() );
448 }
449
450 sinkCell( sink, numberFormat.format( testCase.getTime() ) );
451
452 sink.tableRow_();
453
454 if ( failure != null )
455 {
456 sink.tableRow();
457
458 sinkCell( sink, "" );
459 sinkCell( sink, (String) failure.get( "message" ) );
460 sinkCell( sink, "" );
461 sink.tableRow_();
462
463 List detail = (List) failure.get( "detail" );
464 if ( detail != null )
465 {
466
467 sink.tableRow();
468 sinkCell( sink, "" );
469
470 sink.tableCell();
471 SinkEventAttributeSet atts = new SinkEventAttributeSet();
472 atts.addAttribute( SinkEventAttributes.ID, toHtmlId( testCase.getFullName() ) + "error" );
473 atts.addAttribute( SinkEventAttributes.STYLE, "display:none;" );
474 sink.unknown( "div", new Object[]{new Integer( HtmlMarkup.TAG_TYPE_START )}, atts );
475
476 Iterator it = detail.iterator();
477
478 sink.verbatim( null );
479 while ( it.hasNext() )
480 {
481 sink.text( it.next().toString() );
482 sink.lineBreak();
483 }
484 sink.verbatim_();
485
486 sink.unknown( "div", new Object[]{new Integer( HtmlMarkup.TAG_TYPE_END )}, null );
487 sink.tableCell_();
488
489 sinkCell( sink, "" );
490
491 sink.tableRow_();
492 }
493 }
494 }
495 }
496
497 sink.table_();
498
499 sink.section2_();
500 }
501 }
502
503 sinkLineBreak( sink );
504
505 sink.section1_();
506 }
507
508
509 private String toHtmlId(String id){
510 return id.replace(".", "_");
511 }
512 private void constructFailureDetails( Sink sink, ResourceBundle bundle, List failureList )
513 {
514 Iterator failIter = failureList.iterator();
515
516 if ( failIter != null )
517 {
518 sink.section1();
519 sink.sectionTitle1();
520 sink.text( bundle.getString( "report.surefire.label.failuredetails" ) );
521 sink.sectionTitle1_();
522
523 sinkAnchor( sink, "Failure_Details" );
524
525 constructHotLinks( sink, bundle );
526
527 sinkLineBreak( sink );
528
529 sink.table();
530
531 while ( failIter.hasNext() )
532 {
533 ReportTestCase tCase = (ReportTestCase) failIter.next();
534
535 Map failure = tCase.getFailure();
536
537 sink.tableRow();
538
539 sink.tableCell();
540
541 String type = (String) failure.get( "type" );
542 sinkIcon( type, sink );
543
544 sink.tableCell_();
545
546 sinkCellAnchor( sink, tCase.getName(), tCase.getFullName() );
547
548 sink.tableRow_();
549
550 String message = (String) failure.get( "message" );
551
552 sink.tableRow();
553
554 sinkCell( sink, "" );
555
556 StringBuffer sb = new StringBuffer();
557 sb.append( type );
558
559 if ( message != null )
560 {
561 sb.append( ": " );
562 sb.append( message );
563 }
564
565 sinkCell( sink, sb.toString() );
566
567 sink.tableRow_();
568
569 List detail = (List) failure.get( "detail" );
570 if ( detail != null )
571 {
572 Iterator it = detail.iterator();
573
574 boolean firstLine = true;
575
576 String techMessage = "";
577 while ( it.hasNext() )
578 {
579 techMessage = it.next().toString();
580 if ( firstLine )
581 {
582 firstLine = false;
583 }
584 else
585 {
586 sink.text( " " );
587 }
588 }
589
590 sink.tableRow();
591
592 sinkCell( sink, "" );
593
594 sink.tableCell();
595 SinkEventAttributeSet atts = new SinkEventAttributeSet();
596 atts.addAttribute( SinkEventAttributes.ID, tCase.getName() + "error" );
597 sink.unknown( "div", new Object[]{new Integer( HtmlMarkup.TAG_TYPE_START )}, atts );
598
599 if ( xrefLocation != null )
600 {
601 String path = tCase.getFullClassName().replace( '.', '/' );
602
603 sink.link( xrefLocation + "/" + path + ".html#" +
604 getErrorLineNumber( tCase.getFullName(), techMessage ) );
605 }
606 sink.text(
607 tCase.getFullClassName() + ":" + getErrorLineNumber( tCase.getFullName(), techMessage ) );
608
609 if ( xrefLocation != null )
610 {
611 sink.link_();
612 }
613 sink.unknown( "div", new Object[]{new Integer( HtmlMarkup.TAG_TYPE_END )}, null );
614
615 sink.tableCell_();
616
617 sink.tableRow_();
618 }
619 }
620
621 sink.table_();
622 }
623
624 sinkLineBreak( sink );
625
626 sink.section1_();
627 }
628
629 private String getErrorLineNumber( String className, String source )
630 {
631 StringTokenizer tokenizer = new StringTokenizer( source );
632
633 String lineNo = "";
634
635 while ( tokenizer.hasMoreTokens() )
636 {
637 String token = tokenizer.nextToken();
638 if ( token.startsWith( className ) )
639 {
640 int idx = token.indexOf( ":" );
641 lineNo = token.substring( idx + 1, token.indexOf( ")" ) );
642 break;
643 }
644 }
645 return lineNo;
646 }
647
648 private void constructHotLinks( Sink sink, ResourceBundle bundle )
649 {
650 if ( !testSuites.isEmpty() )
651 {
652 sink.paragraph();
653
654 sink.text( "[" );
655 sinkLink( sink, bundle.getString( "report.surefire.label.summary" ), "#Summary" );
656 sink.text( "]" );
657
658 sink.text( " [" );
659 sinkLink( sink, bundle.getString( "report.surefire.label.packagelist" ), "#Package_List" );
660 sink.text( "]" );
661
662 sink.text( " [" );
663 sinkLink( sink, bundle.getString( "report.surefire.label.testcases" ), "#Test_Cases" );
664 sink.text( "]" );
665
666 sink.paragraph_();
667 }
668 }
669
670 private void sinkLineBreak( Sink sink )
671 {
672 sink.lineBreak();
673 }
674
675 private void sinkIcon( String type, Sink sink )
676 {
677 sink.figure();
678
679 if ( type.startsWith( "junit.framework" ) || "skipped".equals( type ) )
680 {
681 sink.figureGraphics( "images/icon_warning_sml.gif" );
682 }
683 else if ( type.startsWith( "success" ) )
684 {
685 sink.figureGraphics( "images/icon_success_sml.gif" );
686 }
687 else
688 {
689 sink.figureGraphics( "images/icon_error_sml.gif" );
690 }
691
692 sink.figure_();
693 }
694
695 private void sinkHeader( Sink sink, String header )
696 {
697 sink.tableHeaderCell();
698 sink.text( header );
699 sink.tableHeaderCell_();
700 }
701
702 private void sinkCell( Sink sink, String text )
703 {
704 sink.tableCell();
705 sink.text( text );
706 sink.tableCell_();
707 }
708
709 private void sinkLink( Sink sink, String text, String link )
710 {
711 sink.link( link );
712 sink.text( text );
713 sink.link_();
714 }
715
716 private void sinkCellLink( Sink sink, String text, String link )
717 {
718 sink.tableCell();
719 sinkLink( sink, text, link );
720 sink.tableCell_();
721 }
722
723 private void sinkCellAnchor( Sink sink, String text, String anchor )
724 {
725 sink.tableCell();
726 sinkAnchor( sink, anchor );
727 sink.text( text );
728 sink.tableCell_();
729 }
730
731 private void sinkAnchor( Sink sink, String anchor )
732 {
733 sink.anchor( anchor );
734 sink.anchor_();
735 }
736
737 private static String javascriptToggleDisplayCode()
738 {
739 final StringBuffer str = new StringBuffer( 64 );
740
741
742
743 str.append( "\n" );
744 str.append( "function toggleDisplay(elementId) {\n" );
745 str.append( " var elm = document.getElementById(elementId + 'error');\n" );
746 str.append( " if (elm && typeof elm.style != \"undefined\") {\n" );
747 str.append( " if (elm.style.display == \"none\") {\n" );
748 str.append( " elm.style.display = \"\";\n" );
749 str.append( " document.getElementById(elementId + 'off').style.display = \"none\";\n" );
750 str.append( " document.getElementById(elementId + 'on').style.display = \"inline\";\n" );
751 str.append( " }" );
752 str.append( " else if (elm.style.display == \"\") {" );
753 str.append( " elm.style.display = \"none\";\n" );
754 str.append( " document.getElementById(elementId + 'off').style.display = \"inline\";\n" );
755 str.append( " document.getElementById(elementId + 'on').style.display = \"none\";\n" );
756 str.append( " } \n" );
757 str.append( " } \n" );
758 str.append( " }\n" );
759 str.append( "//" );
760
761 return str.toString();
762 }
763 }