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