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