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.plugin.surefire.log.api.ConsoleLogger;
34  import org.apache.maven.reporting.MavenReportException;
35  
36  import static org.apache.maven.doxia.sink.Sink.JUSTIFY_LEFT;
37  
38  /**
39   *
40   */
41  public final class SurefireReportGenerator
42  {
43      private static final int LEFT = JUSTIFY_LEFT;
44  
45      private static final Object[] TAG_TYPE_START = { HtmlMarkup.TAG_TYPE_START };
46  
47      private static final Object[] TAG_TYPE_END = { HtmlMarkup.TAG_TYPE_END };
48  
49      private final SurefireReportParser report;
50  
51      private final boolean showSuccess;
52  
53      private final String xrefLocation;
54  
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( ResourceBundle bundle, Sink sink )
66          throws MavenReportException
67      {
68          testSuites = report.parseXMLReportFiles();
69  
70          sink.head();
71  
72          sink.title();
73          sink.text( bundle.getString( "report.surefire.header" ) );
74          sink.title_();
75  
76          sink.head_();
77  
78          sink.body();
79  
80          SinkEventAttributeSet atts = new SinkEventAttributeSet();
81          atts.addAttribute( SinkEventAttributes.TYPE, "text/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.getString( "report.surefire.header" ) );
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( ResourceBundle bundle, Sink sink )
119     {
120         Map<String, String> summary = report.getSummary( testSuites );
121 
122         sink.section1();
123         sink.sectionTitle1();
124         sink.text( bundle.getString( "report.surefire.label.summary" ) );
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.getString( "report.surefire.label.tests" ) );
140 
141         sinkHeader( sink, bundle.getString( "report.surefire.label.errors" ) );
142 
143         sinkHeader( sink, bundle.getString( "report.surefire.label.failures" ) );
144 
145         sinkHeader( sink, bundle.getString( "report.surefire.label.skipped" ) );
146 
147         sinkHeader( sink, bundle.getString( "report.surefire.label.successrate" ) );
148 
149         sinkHeader( sink, bundle.getString( "report.surefire.label.time" ) );
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.getString( "report.surefire.text.note1" ) );
177         sink.paragraph_();
178 
179         sinkLineBreak( sink );
180 
181         sink.section1_();
182     }
183 
184     private void constructPackagesSection( ResourceBundle 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.getString( "report.surefire.label.packagelist" ) );
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.getString( "report.surefire.label.package" ) );
207 
208         sinkHeader( sink, bundle.getString( "report.surefire.label.tests" ) );
209 
210         sinkHeader( sink, bundle.getString( "report.surefire.label.errors" ) );
211 
212         sinkHeader( sink, bundle.getString( "report.surefire.label.failures" ) );
213 
214         sinkHeader( sink, bundle.getString( "report.surefire.label.skipped" ) );
215 
216         sinkHeader( sink, bundle.getString( "report.surefire.label.successrate" ) );
217 
218         sinkHeader( sink, bundle.getString( "report.surefire.label.time" ) );
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.getString( "report.surefire.text.note2" ) );
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.getString( "report.surefire.label.class" ) );
295 
296                 sinkHeader( sink, bundle.getString( "report.surefire.label.tests" ) );
297 
298                 sinkHeader( sink, bundle.getString( "report.surefire.label.errors" ) );
299 
300                 sinkHeader( sink, bundle.getString( "report.surefire.label.failures" ) );
301 
302                 sinkHeader( sink, bundle.getString( "report.surefire.label.skipped" ) );
303 
304                 sinkHeader( sink, bundle.getString( "report.surefire.label.successrate" ) );
305 
306                 sinkHeader( sink, bundle.getString( "report.surefire.label.time" ) );
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( ResourceBundle bundle, Sink sink )
380     {
381         NumberFormat numberFormat = report.getNumberFormat();
382 
383         sink.section1();
384         sink.sectionTitle1();
385         sink.text( bundle.getString( "report.surefire.label.testcases" ) );
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 != null && !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.hasFailure() || 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.hasFailure() || 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 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.hasFailure() )
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( SinkEventAttributes.CLASS, "detailToggle" );
476             atts.addAttribute( SinkEventAttributes.STYLE, "display:inline" );
477             sink.unknown( "div", TAG_TYPE_START, atts );
478 
479             sink.link( "javascript:toggleDisplay('" + toHtmlId( testCase.getFullName() ) + "');" );
480 
481             atts = new SinkEventAttributeSet();
482             atts.addAttribute( SinkEventAttributes.STYLE, "display:inline;" );
483             atts.addAttribute( SinkEventAttributes.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( SinkEventAttributes.STYLE, "display:none;" );
490             atts.addAttribute( SinkEventAttributes.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             sink.link_();
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.hasFailure() )
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( SinkEventAttributes.ID, toHtmlId( testCase.getFullName() ) + "error" );
529                 atts.addAttribute( SinkEventAttributes.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 String toHtmlId( String id )
547     {
548         return DoxiaUtils.isValidId( id ) ? id : DoxiaUtils.encodeId( id, true );
549     }
550 
551     private void constructFailureDetails( Sink sink, ResourceBundle bundle, List<ReportTestCase> failures )
552     {
553         sink.section1();
554         sink.sectionTitle1();
555         sink.text( bundle.getString( "report.surefire.label.failuredetails" ) );
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( SinkEventAttributes.ID, tCase.getName() + "error" );
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, ResourceBundle bundle )
637     {
638         if ( !testSuites.isEmpty() )
639         {
640             sink.paragraph();
641 
642             sink.text( "[" );
643             sinkLink( sink, bundle.getString( "report.surefire.label.summary" ), "#Summary" );
644             sink.text( "]" );
645 
646             sink.text( " [" );
647             sinkLink( sink, bundle.getString( "report.surefire.label.packagelist" ), "#Package_List" );
648             sink.text( "]" );
649 
650             sink.text( " [" );
651             sinkLink( sink, bundle.getString( "report.surefire.label.testcases" ), "#Test_Cases" );
652             sink.text( "]" );
653 
654             sink.paragraph_();
655         }
656     }
657 
658     private void sinkLineBreak( Sink sink )
659     {
660         sink.lineBreak();
661     }
662 
663     private void sinkIcon( String type, Sink sink )
664     {
665         sink.figure();
666 
667         if ( type.startsWith( "junit.framework" ) || "skipped".equals( type ) )
668         {
669             sink.figureGraphics( "images/icon_warning_sml.gif" );
670         }
671         else if ( type.startsWith( "success" ) )
672         {
673             sink.figureGraphics( "images/icon_success_sml.gif" );
674         }
675         else
676         {
677             sink.figureGraphics( "images/icon_error_sml.gif" );
678         }
679 
680         sink.figure_();
681     }
682 
683     private void sinkHeader( Sink sink, String header )
684     {
685         sink.tableHeaderCell();
686         sink.text( header );
687         sink.tableHeaderCell_();
688     }
689 
690     private void sinkCell( Sink sink, String text )
691     {
692         sink.tableCell();
693         sink.text( text );
694         sink.tableCell_();
695     }
696 
697     private void sinkLink( Sink sink, String text, String link )
698     {
699         sink.link( link );
700         sink.text( text );
701         sink.link_();
702     }
703 
704     private void sinkCellLink( Sink sink, String text, String link )
705     {
706         sink.tableCell();
707         sinkLink( sink, text, link );
708         sink.tableCell_();
709     }
710 
711     private void sinkCellAnchor( Sink sink, String text, String anchor )
712     {
713         sink.tableCell();
714         sinkAnchor( sink, anchor );
715         sink.text( text );
716         sink.tableCell_();
717     }
718 
719     private void sinkAnchor( Sink sink, String anchor )
720     {
721         sink.anchor( anchor );
722         sink.anchor_();
723     }
724 
725     private static String javascriptToggleDisplayCode()
726     {
727 
728         // the javascript code is emitted within a commented CDATA section
729         // so we have to start with a newline and comment the CDATA closing in the end
730 
731         return "\n" + "function toggleDisplay(elementId) {\n"
732                 + " var elm = document.getElementById(elementId + 'error');\n"
733                 + " if (elm && typeof elm.style != \"undefined\") {\n"
734                 + " if (elm.style.display == \"none\") {\n"
735                 + " elm.style.display = \"\";\n"
736                 + " document.getElementById(elementId + 'off').style.display = \"none\";\n"
737                 + " document.getElementById(elementId + 'on').style.display = \"inline\";\n"
738                 + " }" + " else if (elm.style.display == \"\") {"
739                 + " elm.style.display = \"none\";\n"
740                 + " document.getElementById(elementId + 'off').style.display = \"inline\";\n"
741                 + " document.getElementById(elementId + 'on').style.display = \"none\";\n"
742                 + " } \n"
743                 + " } \n"
744                 + " }\n"
745                 + "//";
746     }
747 }