View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.plugins.surefire.report;
20  
21  import java.io.File;
22  import java.text.NumberFormat;
23  import java.util.List;
24  import java.util.Locale;
25  import java.util.Map;
26  
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   * This generator creates HTML Report from Surefire and Failsafe XML Report.
44   */
45  public final class SurefireReportGenerator {
46      private static final int LEFT = JUSTIFY_LEFT;
47      private static final Object[] TAG_TYPE_START = {HtmlMarkup.TAG_TYPE_START};
48      private static final Object[] TAG_TYPE_END = {HtmlMarkup.TAG_TYPE_END};
49  
50      private final SurefireReportParser report;
51      private final boolean showSuccess;
52      private final String xrefLocation;
53      private List<ReportTestSuite> testSuites;
54  
55      public SurefireReportGenerator(
56              List<File> reportsDirectories,
57              Locale locale,
58              boolean showSuccess,
59              String xrefLocation,
60              ConsoleLogger consoleLogger) {
61          report = new SurefireReportParser(reportsDirectories, locale, consoleLogger);
62          this.showSuccess = showSuccess;
63          this.xrefLocation = xrefLocation;
64      }
65  
66      public void doGenerateReport(LocalizedProperties bundle, Sink sink) throws MavenReportException {
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              constructPackagesSection(bundle, sink, suitePackages);
96          }
97  
98          if (!testSuites.isEmpty()) {
99              constructTestCasesSection(bundle, sink);
100         }
101 
102         List<ReportTestCase> failureList = report.getFailureDetails(testSuites);
103         if (!failureList.isEmpty()) {
104             constructFailureDetails(sink, bundle, failureList);
105         }
106 
107         sink.body_();
108 
109         sink.flush();
110 
111         sink.close();
112     }
113 
114     private void constructSummarySection(LocalizedProperties bundle, Sink sink) {
115         Map<String, String> summary = report.getSummary(testSuites);
116 
117         sink.section1();
118         sinkAnchor(sink, "Summary");
119         sink.sectionTitle1();
120         sink.text(bundle.getReportLabelSummary());
121         sink.sectionTitle1_();
122 
123         constructHotLinks(sink, bundle);
124 
125         sinkLineBreak(sink);
126 
127         sink.table();
128 
129         sink.tableRows(new int[] {LEFT, LEFT, LEFT, LEFT, LEFT, LEFT}, false);
130 
131         sink.tableRow();
132 
133         sinkHeader(sink, bundle.getReportLabelTests());
134 
135         sinkHeader(sink, bundle.getReportLabelErrors());
136 
137         sinkHeader(sink, bundle.getReportLabelFailures());
138 
139         sinkHeader(sink, bundle.getReportLabelSkipped());
140 
141         sinkHeader(sink, bundle.getReportLabelSuccessRate());
142 
143         sinkHeader(sink, bundle.getReportLabelTime());
144 
145         sink.tableRow_();
146 
147         sink.tableRow();
148 
149         sinkCell(sink, summary.get("totalTests"));
150 
151         sinkCell(sink, summary.get("totalErrors"));
152 
153         sinkCell(sink, summary.get("totalFailures"));
154 
155         sinkCell(sink, summary.get("totalSkipped"));
156 
157         sinkCell(sink, summary.get("totalPercentage") + "%");
158 
159         sinkCell(sink, summary.get("totalElapsedTime"));
160 
161         sink.tableRow_();
162 
163         sink.tableRows_();
164 
165         sink.table_();
166 
167         sink.lineBreak();
168 
169         sink.paragraph();
170         sink.text(bundle.getReportTextNode1());
171         sink.paragraph_();
172 
173         sinkLineBreak(sink);
174 
175         sink.section1_();
176     }
177 
178     private void constructPackagesSection(
179             LocalizedProperties bundle, Sink sink, Map<String, List<ReportTestSuite>> suitePackages) {
180         NumberFormat numberFormat = report.getNumberFormat();
181 
182         sink.section1();
183         sinkAnchor(sink, "Package_List");
184         sink.sectionTitle1();
185         sink.text(bundle.getReportLabelPackageList());
186         sink.sectionTitle1_();
187 
188         constructHotLinks(sink, bundle);
189 
190         sinkLineBreak(sink);
191 
192         sink.table();
193 
194         sink.tableRows(new int[] {LEFT, LEFT, LEFT, LEFT, LEFT, LEFT, LEFT}, false);
195 
196         sink.tableRow();
197 
198         sinkHeader(sink, bundle.getReportLabelPackage());
199 
200         sinkHeader(sink, bundle.getReportLabelTests());
201 
202         sinkHeader(sink, bundle.getReportLabelErrors());
203 
204         sinkHeader(sink, bundle.getReportLabelFailures());
205 
206         sinkHeader(sink, bundle.getReportLabelSkipped());
207 
208         sinkHeader(sink, bundle.getReportLabelSuccessRate());
209 
210         sinkHeader(sink, bundle.getReportLabelTime());
211 
212         sink.tableRow_();
213 
214         for (Map.Entry<String, List<ReportTestSuite>> entry : suitePackages.entrySet()) {
215             sink.tableRow();
216 
217             String packageName = entry.getKey();
218 
219             List<ReportTestSuite> testSuiteList = entry.getValue();
220 
221             Map<String, String> packageSummary = report.getSummary(testSuiteList);
222 
223             sinkCellLink(sink, packageName, "#" + packageName);
224 
225             sinkCell(sink, packageSummary.get("totalTests"));
226 
227             sinkCell(sink, packageSummary.get("totalErrors"));
228 
229             sinkCell(sink, packageSummary.get("totalFailures"));
230 
231             sinkCell(sink, packageSummary.get("totalSkipped"));
232 
233             sinkCell(sink, packageSummary.get("totalPercentage") + "%");
234 
235             sinkCell(sink, packageSummary.get("totalElapsedTime"));
236 
237             sink.tableRow_();
238         }
239 
240         sink.tableRows_();
241 
242         sink.table_();
243 
244         sink.lineBreak();
245 
246         sink.paragraph();
247         sink.text(bundle.getReportTextNode2());
248         sink.paragraph_();
249 
250         for (Map.Entry<String, List<ReportTestSuite>> entry : suitePackages.entrySet()) {
251             String packageName = entry.getKey();
252 
253             List<ReportTestSuite> testSuiteList = entry.getValue();
254 
255             sink.section2();
256             sinkAnchor(sink, packageName);
257             sink.sectionTitle2();
258             sink.text(packageName);
259             sink.sectionTitle2_();
260 
261             boolean showTable = false;
262 
263             for (ReportTestSuite suite : testSuiteList) {
264                 if (showSuccess || suite.getNumberOfErrors() != 0 || suite.getNumberOfFailures() != 0) {
265                     showTable = true;
266 
267                     break;
268                 }
269             }
270 
271             if (showTable) {
272                 sink.table();
273 
274                 sink.tableRows(new int[] {LEFT, LEFT, LEFT, LEFT, LEFT, LEFT, LEFT, LEFT}, false);
275 
276                 sink.tableRow();
277 
278                 sinkHeader(sink, "");
279 
280                 sinkHeader(sink, bundle.getReportLabelClass());
281 
282                 sinkHeader(sink, bundle.getReportLabelTests());
283 
284                 sinkHeader(sink, bundle.getReportLabelErrors());
285 
286                 sinkHeader(sink, bundle.getReportLabelFailures());
287 
288                 sinkHeader(sink, bundle.getReportLabelSkipped());
289 
290                 sinkHeader(sink, bundle.getReportLabelSuccessRate());
291 
292                 sinkHeader(sink, bundle.getReportLabelTime());
293 
294                 sink.tableRow_();
295 
296                 for (ReportTestSuite suite : testSuiteList) {
297                     if (showSuccess || suite.getNumberOfErrors() != 0 || suite.getNumberOfFailures() != 0) {
298                         constructTestSuiteSection(sink, numberFormat, suite);
299                     }
300                 }
301 
302                 sink.tableRows_();
303 
304                 sink.table_();
305             }
306 
307             sink.section2_();
308         }
309 
310         sinkLineBreak(sink);
311 
312         sink.section1_();
313     }
314 
315     private void constructTestSuiteSection(Sink sink, NumberFormat numberFormat, ReportTestSuite suite) {
316         sink.tableRow();
317 
318         sink.tableCell();
319 
320         sink.link("#" + suite.getPackageName() + '.' + suite.getName());
321 
322         if (suite.getNumberOfErrors() > 0) {
323             sinkIcon("error", sink);
324         } else if (suite.getNumberOfFailures() > 0) {
325             sinkIcon("junit.framework", sink);
326         } else if (suite.getNumberOfSkipped() > 0) {
327             sinkIcon("skipped", sink);
328         } else {
329             sinkIcon("success", sink);
330         }
331 
332         sink.link_();
333 
334         sink.tableCell_();
335 
336         sinkCellLink(sink, suite.getName(), "#" + suite.getPackageName() + '.' + suite.getName());
337 
338         sinkCell(sink, Integer.toString(suite.getNumberOfTests()));
339 
340         sinkCell(sink, Integer.toString(suite.getNumberOfErrors()));
341 
342         sinkCell(sink, Integer.toString(suite.getNumberOfFailures()));
343 
344         sinkCell(sink, Integer.toString(suite.getNumberOfSkipped()));
345 
346         String percentage = report.computePercentage(
347                 suite.getNumberOfTests(), suite.getNumberOfErrors(),
348                 suite.getNumberOfFailures(), suite.getNumberOfSkipped());
349         sinkCell(sink, percentage + "%");
350 
351         sinkCell(sink, numberFormat.format(suite.getTimeElapsed()));
352 
353         sink.tableRow_();
354     }
355 
356     private void constructTestCasesSection(LocalizedProperties bundle, Sink sink) {
357         NumberFormat numberFormat = report.getNumberFormat();
358 
359         sink.section1();
360         sinkAnchor(sink, "Test_Cases");
361         sink.sectionTitle1();
362         sink.text(bundle.getReportLabelTestCases());
363         sink.sectionTitle1_();
364 
365         constructHotLinks(sink, bundle);
366 
367         for (ReportTestSuite suite : testSuites) {
368             List<ReportTestCase> testCases = suite.getTestCases();
369 
370             if (!testCases.isEmpty()) {
371                 sink.section2();
372                 sinkAnchor(sink, suite.getPackageName() + '.' + suite.getName());
373                 sink.sectionTitle2();
374                 sink.text(suite.getName());
375                 sink.sectionTitle2_();
376 
377                 boolean showTable = false;
378 
379                 for (ReportTestCase testCase : testCases) {
380                     if (!testCase.isSuccessful() || showSuccess) {
381                         showTable = true;
382 
383                         break;
384                     }
385                 }
386 
387                 if (showTable) {
388                     sink.table();
389 
390                     sink.tableRows(new int[] {LEFT, LEFT, LEFT}, false);
391 
392                     for (ReportTestCase testCase : testCases) {
393                         if (!testCase.isSuccessful() || showSuccess) {
394                             constructTestCaseSection(sink, numberFormat, testCase);
395                         }
396                     }
397 
398                     sink.tableRows_();
399 
400                     sink.table_();
401                 }
402 
403                 sink.section2_();
404             }
405         }
406 
407         sinkLineBreak(sink);
408 
409         sink.section1_();
410     }
411 
412     private static void constructTestCaseSection(Sink sink, NumberFormat numberFormat, ReportTestCase testCase) {
413         sink.tableRow();
414 
415         sink.tableCell();
416 
417         if (testCase.getFailureType() != null) {
418             sink.link("#" + toHtmlId(testCase.getFullName()));
419 
420             sinkIcon(testCase.getFailureType(), sink);
421 
422             sink.link_();
423         } else {
424             sinkIcon("success", sink);
425         }
426 
427         sink.tableCell_();
428 
429         if (!testCase.isSuccessful()) {
430             sink.tableCell();
431             sinkAnchor(sink, "TC_" + toHtmlId(testCase.getFullName()));
432 
433             sinkLink(sink, testCase.getName(), "#" + toHtmlId(testCase.getFullName()));
434 
435             SinkEventAttributeSet atts = new SinkEventAttributeSet();
436             atts.addAttribute(CLASS, "detailToggle");
437             atts.addAttribute(STYLE, "display:inline");
438             sink.unknown("div", TAG_TYPE_START, atts);
439 
440             sinkLink(sink, "javascript:toggleDisplay('" + toHtmlId(testCase.getFullName()) + "');");
441 
442             atts = new SinkEventAttributeSet();
443             atts.addAttribute(STYLE, "display:inline;");
444             atts.addAttribute(ID, toHtmlId(testCase.getFullName()) + "-off");
445             sink.unknown("span", TAG_TYPE_START, atts);
446             sink.text(" + ");
447             sink.unknown("span", TAG_TYPE_END, null);
448 
449             atts = new SinkEventAttributeSet();
450             atts.addAttribute(STYLE, "display:none;");
451             atts.addAttribute(ID, toHtmlId(testCase.getFullName()) + "-on");
452             sink.unknown("span", TAG_TYPE_START, atts);
453             sink.text(" - ");
454             sink.unknown("span", TAG_TYPE_END, null);
455 
456             sink.text("[ Detail ]");
457             sinkLink_(sink);
458 
459             sink.unknown("div", TAG_TYPE_END, null);
460 
461             sink.tableCell_();
462         } else {
463             sinkCellAnchor(sink, testCase.getName(), "TC_" + toHtmlId(testCase.getFullName()));
464         }
465 
466         sinkCell(sink, numberFormat.format(testCase.getTime()));
467 
468         sink.tableRow_();
469 
470         if (!testCase.isSuccessful()) {
471             sink.tableRow();
472 
473             sinkCell(sink, "");
474             sinkCell(sink, testCase.getFailureMessage());
475             sinkCell(sink, "");
476             sink.tableRow_();
477 
478             String detail = testCase.getFailureDetail();
479             if (detail != null) {
480                 sink.tableRow();
481                 sinkCell(sink, "");
482 
483                 sink.tableCell();
484                 SinkEventAttributeSet atts = new SinkEventAttributeSet();
485                 atts.addAttribute(ID, toHtmlId(testCase.getFullName()) + toHtmlIdFailure(testCase));
486                 atts.addAttribute(STYLE, "display:none;");
487                 sink.unknown("div", TAG_TYPE_START, atts);
488 
489                 sink.verbatim(null);
490                 sink.text(detail);
491                 sink.verbatim_();
492 
493                 sink.unknown("div", TAG_TYPE_END, null);
494                 sink.tableCell_();
495 
496                 sinkCell(sink, "");
497 
498                 sink.tableRow_();
499             }
500         }
501     }
502 
503     private static String toHtmlId(String id) {
504         return DoxiaUtils.isValidId(id) ? id : DoxiaUtils.encodeId(id, true);
505     }
506 
507     private void constructFailureDetails(Sink sink, LocalizedProperties bundle, List<ReportTestCase> failures) {
508         sink.section1();
509         sinkAnchor(sink, "Failure_Details");
510         sink.sectionTitle1();
511         sink.text(bundle.getReportLabelFailureDetails());
512         sink.sectionTitle1_();
513 
514         constructHotLinks(sink, bundle);
515 
516         sinkLineBreak(sink);
517 
518         sink.table();
519 
520         sink.tableRows(new int[] {LEFT, LEFT}, false);
521 
522         for (ReportTestCase tCase : failures) {
523             sink.tableRow();
524 
525             sink.tableCell();
526 
527             String type = tCase.getFailureType();
528 
529             sinkIcon(type, sink);
530 
531             sink.tableCell_();
532 
533             sinkCellAnchor(sink, tCase.getName(), toHtmlId(tCase.getFullName()));
534 
535             sink.tableRow_();
536 
537             String message = tCase.getFailureMessage();
538 
539             sink.tableRow();
540 
541             sinkCell(sink, "");
542 
543             sinkCell(sink, message == null ? type : type + ": " + message);
544 
545             sink.tableRow_();
546 
547             String detail = tCase.getFailureDetail();
548             if (detail != null) {
549                 sink.tableRow();
550 
551                 sinkCell(sink, "");
552 
553                 sink.tableCell();
554                 SinkEventAttributeSet atts = new SinkEventAttributeSet();
555                 atts.addAttribute(ID, tCase.getName() + toHtmlIdFailure(tCase));
556                 sink.unknown("div", TAG_TYPE_START, atts);
557 
558                 String fullClassName = tCase.getFullClassName();
559                 String errorLineNumber = tCase.getFailureErrorLine();
560                 if (xrefLocation != null) {
561                     String path = fullClassName.replace('.', '/');
562                     sink.link(xrefLocation + "/" + path + ".html#L" + errorLineNumber);
563                 }
564                 sink.text(fullClassName + ":" + errorLineNumber);
565 
566                 if (xrefLocation != null) {
567                     sink.link_();
568                 }
569                 sink.unknown("div", TAG_TYPE_END, null);
570 
571                 sink.tableCell_();
572 
573                 sink.tableRow_();
574             }
575         }
576 
577         sink.tableRows_();
578 
579         sink.table_();
580 
581         sinkLineBreak(sink);
582 
583         sink.section1_();
584     }
585 
586     private void constructHotLinks(Sink sink, LocalizedProperties bundle) {
587         if (!testSuites.isEmpty()) {
588             sink.paragraph();
589 
590             sink.text("[");
591             sinkLink(sink, bundle.getReportLabelSummary(), "#Summary");
592             sink.text("]");
593 
594             sink.text(" [");
595             sinkLink(sink, bundle.getReportLabelPackageList(), "#Package_List");
596             sink.text("]");
597 
598             sink.text(" [");
599             sinkLink(sink, bundle.getReportLabelTestCases(), "#Test_Cases");
600             sink.text("]");
601 
602             sink.paragraph_();
603         }
604     }
605 
606     private static String toHtmlIdFailure(ReportTestCase tCase) {
607         return tCase.hasError() ? "-error" : "-failure";
608     }
609 
610     private static void sinkLineBreak(Sink sink) {
611         sink.lineBreak();
612     }
613 
614     private static void sinkIcon(String type, Sink sink) {
615         if (type.startsWith("junit.framework") || "skipped".equals(type)) {
616             sink.figureGraphics("images/icon_warning_sml.gif");
617         } else if (type.startsWith("success")) {
618             sink.figureGraphics("images/icon_success_sml.gif");
619         } else {
620             sink.figureGraphics("images/icon_error_sml.gif");
621         }
622     }
623 
624     private static void sinkHeader(Sink sink, String header) {
625         sink.tableHeaderCell();
626         sink.text(header);
627         sink.tableHeaderCell_();
628     }
629 
630     private static void sinkCell(Sink sink, String text) {
631         sink.tableCell();
632         sink.text(text);
633         sink.tableCell_();
634     }
635 
636     private static void sinkLink(Sink sink, String text, String link) {
637         sink.link(link);
638         sink.text(text);
639         sink.link_();
640     }
641 
642     private static void sinkCellLink(Sink sink, String text, String link) {
643         sink.tableCell();
644         sinkLink(sink, text, link);
645         sink.tableCell_();
646     }
647 
648     private static void sinkCellAnchor(Sink sink, String text, String anchor) {
649         sink.tableCell();
650         sinkAnchor(sink, anchor);
651         sink.text(text);
652         sink.tableCell_();
653     }
654 
655     private static void sinkAnchor(Sink sink, String anchor) {
656         // Dollar '$' for nested classes is not valid character in sink.anchor() and therefore it is ignored
657         // https://issues.apache.org/jira/browse/SUREFIRE-1443
658         sink.unknown(A.toString(), TAG_TYPE_START, new SinkEventAttributeSet(ID, anchor));
659         sink.unknown(A.toString(), TAG_TYPE_END, null);
660     }
661 
662     private static void sinkLink(Sink sink, String href) {
663         // The "'" argument in this JavaScript function would be escaped to "&apos;"
664         // sink.link( "javascript:toggleDisplay('" + toHtmlId( testCase.getFullName() ) + "');" );
665         sink.unknown(A.toString(), TAG_TYPE_START, new SinkEventAttributeSet(HREF, href));
666     }
667 
668     @SuppressWarnings("checkstyle:methodname")
669     private static void sinkLink_(Sink sink) {
670         sink.unknown(A.toString(), TAG_TYPE_END, null);
671     }
672 
673     private static String javascriptToggleDisplayCode() {
674 
675         // the javascript code is emitted within a commented CDATA section
676         // so we have to start with a newline and comment the CDATA closing in the end
677 
678         return "\n" + "function toggleDisplay(elementId) {\n"
679                 + " var elm = document.getElementById(elementId + '-error');\n"
680                 + " if (elm == null) {\n"
681                 + "  elm = document.getElementById(elementId + '-failure');\n"
682                 + " }\n"
683                 + " if (elm && typeof elm.style != \"undefined\") {\n"
684                 + "  if (elm.style.display == \"none\") {\n"
685                 + "   elm.style.display = \"\";\n"
686                 + "   document.getElementById(elementId + '-off').style.display = \"none\";\n"
687                 + "   document.getElementById(elementId + '-on').style.display = \"inline\";\n"
688                 + "  } else if (elm.style.display == \"\") {"
689                 + "   elm.style.display = \"none\";\n"
690                 + "   document.getElementById(elementId + '-off').style.display = \"inline\";\n"
691                 + "   document.getElementById(elementId + '-on').style.display = \"none\";\n"
692                 + "  } \n"
693                 + " } \n"
694                 + " }\n"
695                 + "//";
696     }
697 }