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