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