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