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