View Javadoc
1   package org.apache.maven.plugins.surefire.report;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *     http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
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         // the javascript code is emitted within a commented CDATA section
728         // so we have to start with a newline and comment the CDATA closing in the end
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 }