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.util.List;
23  import java.util.Locale;
24  import java.util.Map;
25  
26  import org.apache.maven.doxia.markup.HtmlMarkup;
27  import org.apache.maven.doxia.markup.Markup;
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.AbstractMavenReportRenderer;
33  import org.codehaus.plexus.i18n.I18N;
34  
35  import static org.apache.maven.doxia.markup.HtmlMarkup.A;
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  
41  /**
42   * This generator creates HTML Report from Surefire and Failsafe XML Report.
43   */
44  public class SurefireReportRenderer extends AbstractMavenReportRenderer {
45      private static final Object[] TAG_TYPE_START = {HtmlMarkup.TAG_TYPE_START};
46      private static final Object[] TAG_TYPE_END = {HtmlMarkup.TAG_TYPE_END};
47  
48      private final I18N i18n;
49      private final String i18nSection;
50      private final Locale locale;
51  
52      private final SurefireReportParser parser;
53      private final boolean showSuccess;
54      private final String xrefLocation;
55      private final List<ReportTestSuite> testSuites;
56  
57      public SurefireReportRenderer(
58              Sink sink,
59              I18N i18n,
60              String i18nSection,
61              Locale locale,
62              ConsoleLogger consoleLogger,
63              boolean showSuccess,
64              List<File> reportsDirectories,
65              String xrefLocation) {
66          super(sink);
67          this.i18n = i18n;
68          this.i18nSection = i18nSection;
69          this.locale = locale;
70          parser = new SurefireReportParser(reportsDirectories, consoleLogger);
71          testSuites = parser.parseXMLReportFiles();
72          this.showSuccess = showSuccess;
73          this.xrefLocation = xrefLocation;
74      }
75  
76      @Override
77      public String getTitle() {
78          return getI18nString("title");
79      }
80  
81      /**
82       * @param key The key.
83       * @return The translated string.
84       */
85      private String getI18nString(String key) {
86          return getI18nString(getI18nSection(), key);
87      }
88  
89      private String getI18nSection() {
90          return i18nSection;
91      }
92  
93      /**
94       * @param section The section.
95       * @param key The key to translate.
96       * @return the translated key.
97       */
98      private String getI18nString(String section, String key) {
99          return i18n.getString("surefire-report", locale, "report." + section + '.' + key);
100     }
101 
102     /**
103      * @param section The section.
104      * @param key The key to translate.
105      * @param args The args to pass to translated string.
106      * @return the translated key.
107      */
108     private String formatI18nString(String section, String key, Object... args) {
109         return i18n.format("surefire-report", locale, "report." + section + '.' + key, args);
110     }
111 
112     public void renderBody() {
113         javaScript(javascriptToggleDisplayCode());
114 
115         sink.section1();
116         sink.sectionTitle1();
117         sink.text(getTitle());
118         sink.sectionTitle1_();
119         sink.section1_();
120 
121         renderSectionSummary();
122 
123         renderSectionPackages();
124 
125         renderSectionTestCases();
126 
127         renderSectionFailureDetails();
128     }
129 
130     private void renderSectionSummary() {
131         Map<String, Object> summary = parser.getSummary(testSuites);
132 
133         sink.section1();
134         sinkAnchor("Summary");
135         sink.sectionTitle1();
136         sink.text(getI18nString("surefire", "label.summary"));
137         sink.sectionTitle1_();
138 
139         constructHotLinks();
140 
141         sink.lineBreak();
142 
143         startTable();
144 
145         tableHeader(new String[] {
146             getI18nString("surefire", "label.tests"),
147             getI18nString("surefire", "label.errors"),
148             getI18nString("surefire", "label.failures"),
149             getI18nString("surefire", "label.skipped"),
150             getI18nString("surefire", "label.successrate"),
151             getI18nString("surefire", "label.time")
152         });
153 
154         tableRow(new String[] {
155             String.valueOf(summary.get("totalTests")),
156             String.valueOf(summary.get("totalErrors")),
157             String.valueOf(summary.get("totalFailures")),
158             String.valueOf(summary.get("totalSkipped")),
159             formatI18nString("surefire", "value.successrate", summary.get("totalPercentage")),
160             formatI18nString("surefire", "value.time", summary.get("totalElapsedTime"))
161         });
162 
163         endTable();
164 
165         sink.lineBreak();
166 
167         paragraph(getI18nString("surefire", "text.note1"));
168 
169         sink.lineBreak();
170 
171         sink.section1_();
172     }
173 
174     private void renderSectionPackages() {
175         Map<String, List<ReportTestSuite>> suitePackages = parser.getSuitesGroupByPackage(testSuites);
176         if (suitePackages.isEmpty()) {
177             return;
178         }
179 
180         sink.section1();
181         sinkAnchor("Package_List");
182         sink.sectionTitle1();
183         sink.text(getI18nString("surefire", "label.packagelist"));
184         sink.sectionTitle1_();
185 
186         constructHotLinks();
187 
188         sink.lineBreak();
189 
190         startTable();
191 
192         tableHeader(new String[] {
193             getI18nString("surefire", "label.package"),
194             getI18nString("surefire", "label.tests"),
195             getI18nString("surefire", "label.errors"),
196             getI18nString("surefire", "label.failures"),
197             getI18nString("surefire", "label.skipped"),
198             getI18nString("surefire", "label.successrate"),
199             getI18nString("surefire", "label.time")
200         });
201 
202         for (Map.Entry<String, List<ReportTestSuite>> entry : suitePackages.entrySet()) {
203             String packageName = entry.getKey();
204 
205             List<ReportTestSuite> testSuiteList = entry.getValue();
206 
207             Map<String, Object> packageSummary = parser.getSummary(testSuiteList);
208 
209             tableRow(new String[] {
210                 createLinkPatternedText(packageName, '#' + packageName),
211                 String.valueOf(packageSummary.get("totalTests")),
212                 String.valueOf(packageSummary.get("totalErrors")),
213                 String.valueOf(packageSummary.get("totalFailures")),
214                 String.valueOf(packageSummary.get("totalSkipped")),
215                 formatI18nString("surefire", "value.successrate", packageSummary.get("totalPercentage")),
216                 formatI18nString("surefire", "value.time", packageSummary.get("totalElapsedTime"))
217             });
218         }
219 
220         endTable();
221         sink.lineBreak();
222 
223         paragraph(getI18nString("surefire", "text.note2"));
224 
225         for (Map.Entry<String, List<ReportTestSuite>> entry : suitePackages.entrySet()) {
226             String packageName = entry.getKey();
227 
228             List<ReportTestSuite> testSuiteList = entry.getValue();
229 
230             sink.section2();
231             sinkAnchor(packageName);
232             sink.sectionTitle2();
233             sink.text(packageName);
234             sink.sectionTitle2_();
235 
236             boolean showTable = false;
237 
238             for (ReportTestSuite suite : testSuiteList) {
239                 if (showSuccess || suite.getNumberOfErrors() != 0 || suite.getNumberOfFailures() != 0) {
240                     showTable = true;
241 
242                     break;
243                 }
244             }
245 
246             if (showTable) {
247                 startTable();
248 
249                 tableHeader(new String[] {
250                     "",
251                     getI18nString("surefire", "label.class"),
252                     getI18nString("surefire", "label.tests"),
253                     getI18nString("surefire", "label.errors"),
254                     getI18nString("surefire", "label.failures"),
255                     getI18nString("surefire", "label.skipped"),
256                     getI18nString("surefire", "label.successrate"),
257                     getI18nString("surefire", "label.time")
258                 });
259 
260                 for (ReportTestSuite suite : testSuiteList) {
261                     if (showSuccess || suite.getNumberOfErrors() != 0 || suite.getNumberOfFailures() != 0) {
262                         renderSectionTestSuite(suite);
263                     }
264                 }
265 
266                 endTable();
267             }
268 
269             sink.section2_();
270         }
271 
272         sink.lineBreak();
273 
274         sink.section1_();
275     }
276 
277     private void renderSectionTestSuite(ReportTestSuite suite) {
278         sink.tableRow();
279 
280         sink.tableCell();
281 
282         sink.link("#" + suite.getPackageName() + '.' + suite.getName());
283 
284         if (suite.getNumberOfErrors() > 0) {
285             sinkIcon("error");
286         } else if (suite.getNumberOfFailures() > 0) {
287             sinkIcon("junit.framework");
288         } else if (suite.getNumberOfSkipped() > 0) {
289             sinkIcon("skipped");
290         } else {
291             sinkIcon("success");
292         }
293 
294         sink.link_();
295 
296         sink.tableCell_();
297 
298         tableCell(createLinkPatternedText(suite.getName(), '#' + suite.getPackageName() + '.' + suite.getName()));
299 
300         tableCell(Integer.toString(suite.getNumberOfTests()));
301 
302         tableCell(Integer.toString(suite.getNumberOfErrors()));
303 
304         tableCell(Integer.toString(suite.getNumberOfFailures()));
305 
306         tableCell(Integer.toString(suite.getNumberOfSkipped()));
307 
308         float percentage = parser.computePercentage(
309                 suite.getNumberOfTests(), suite.getNumberOfErrors(),
310                 suite.getNumberOfFailures(), suite.getNumberOfSkipped());
311         tableCell(formatI18nString("surefire", "value.successrate", percentage));
312 
313         tableCell(formatI18nString("surefire", "value.time", suite.getTimeElapsed()));
314 
315         sink.tableRow_();
316     }
317 
318     private void renderSectionTestCases() {
319         if (testSuites.isEmpty()) {
320             return;
321         }
322 
323         sink.section1();
324         sinkAnchor("Test_Cases");
325         sink.sectionTitle1();
326         sink.text(getI18nString("surefire", "label.testcases"));
327         sink.sectionTitle1_();
328 
329         constructHotLinks();
330 
331         for (ReportTestSuite suite : testSuites) {
332             List<ReportTestCase> testCases = suite.getTestCases();
333 
334             if (!testCases.isEmpty()) {
335                 sink.section2();
336                 sinkAnchor(suite.getPackageName() + '.' + suite.getName());
337                 sink.sectionTitle2();
338                 sink.text(suite.getName());
339                 sink.sectionTitle2_();
340 
341                 boolean showTable = false;
342 
343                 for (ReportTestCase testCase : testCases) {
344                     if (!testCase.isSuccessful() || showSuccess) {
345                         showTable = true;
346 
347                         break;
348                     }
349                 }
350 
351                 if (showTable) {
352                     startTable();
353 
354                     for (ReportTestCase testCase : testCases) {
355                         if (!testCase.isSuccessful() || showSuccess) {
356                             constructTestCaseSection(testCase);
357                         }
358                     }
359 
360                     endTable();
361                 }
362 
363                 sink.section2_();
364             }
365         }
366 
367         sink.lineBreak();
368 
369         sink.section1_();
370     }
371 
372     private void constructTestCaseSection(ReportTestCase testCase) {
373         sink.tableRow();
374 
375         sink.tableCell();
376 
377         if (testCase.getFailureType() != null) {
378             sink.link("#" + toHtmlId(testCase.getFullName()));
379 
380             sinkIcon(testCase.getFailureType());
381 
382             sink.link_();
383         } else {
384             sinkIcon("success");
385         }
386 
387         sink.tableCell_();
388 
389         if (!testCase.isSuccessful()) {
390             sink.tableCell();
391             sinkAnchor("TC_" + toHtmlId(testCase.getFullName()));
392 
393             link("#" + toHtmlId(testCase.getFullName()), testCase.getName());
394 
395             SinkEventAttributeSet atts = new SinkEventAttributeSet();
396             atts.addAttribute(CLASS, "detailToggle");
397             atts.addAttribute(STYLE, "display:inline");
398             sink.unknown("div", TAG_TYPE_START, atts);
399 
400             sinkLink("javascript:toggleDisplay('" + toHtmlId(testCase.getFullName()) + "');");
401 
402             atts = new SinkEventAttributeSet();
403             atts.addAttribute(STYLE, "display:inline;");
404             atts.addAttribute(ID, toHtmlId(testCase.getFullName()) + "-off");
405             sink.unknown("span", TAG_TYPE_START, atts);
406             sink.text(" + ");
407             sink.unknown("span", TAG_TYPE_END, null);
408 
409             atts = new SinkEventAttributeSet();
410             atts.addAttribute(STYLE, "display:none;");
411             atts.addAttribute(ID, toHtmlId(testCase.getFullName()) + "-on");
412             sink.unknown("span", TAG_TYPE_START, atts);
413             sink.text(" - ");
414             sink.unknown("span", TAG_TYPE_END, null);
415 
416             sink.text("[ Detail ]");
417             sinkLink_();
418 
419             sink.unknown("div", TAG_TYPE_END, null);
420 
421             sink.tableCell_();
422         } else {
423             sinkCellAnchor(testCase.getName(), "TC_" + toHtmlId(testCase.getFullName()));
424         }
425 
426         tableCell(formatI18nString("surefire", "value.time", testCase.getTime()));
427 
428         sink.tableRow_();
429 
430         if (!testCase.isSuccessful()) {
431             String message = testCase.getFailureMessage();
432             if (message != null) {
433                 sink.tableRow();
434 
435                 tableCell("");
436 
437                 sink.tableCell();
438 
439                 // This shall not be subject to #linkPatternedText()
440                 text(message);
441 
442                 sink.tableCell_();
443 
444                 tableCell("");
445 
446                 sink.tableRow_();
447             }
448 
449             String detail = testCase.getFailureDetail();
450             if (detail != null) {
451                 SinkEventAttributeSet atts = new SinkEventAttributeSet();
452                 atts.addAttribute(ID, toHtmlId(testCase.getFullName()) + toHtmlIdFailure(testCase));
453                 atts.addAttribute(STYLE, "display:none;");
454                 sink.tableRow(atts);
455 
456                 tableCell("");
457 
458                 sink.tableCell();
459 
460                 verbatimText(detail);
461 
462                 sink.tableCell_();
463 
464                 tableCell("");
465 
466                 sink.tableRow_();
467             }
468         }
469     }
470 
471     private String toHtmlId(String id) {
472         return DoxiaUtils.isValidId(id) ? id : DoxiaUtils.encodeId(id, true);
473     }
474 
475     private void renderSectionFailureDetails() {
476         List<ReportTestCase> failures = parser.getFailureDetails(testSuites);
477         if (failures.isEmpty()) {
478             return;
479         }
480 
481         sink.section1();
482         sinkAnchor("Failure_Details");
483         sink.sectionTitle1();
484         sink.text(getI18nString("surefire", "label.failuredetails"));
485         sink.sectionTitle1_();
486 
487         constructHotLinks();
488 
489         sink.lineBreak();
490 
491         startTable();
492 
493         for (ReportTestCase testCase : failures) {
494             sink.tableRow();
495 
496             sink.tableCell();
497 
498             String type = testCase.getFailureType();
499 
500             sinkIcon(type);
501 
502             sink.tableCell_();
503 
504             sinkCellAnchor(testCase.getName(), toHtmlId(testCase.getFullName()));
505 
506             sink.tableRow_();
507 
508             String message = testCase.getFailureMessage();
509 
510             sink.tableRow();
511 
512             tableCell("");
513 
514             sink.tableCell();
515 
516             // This shall not be subject to #linkPatternedText()
517             text(message == null ? type : type + ": " + message);
518 
519             sink.tableCell_();
520 
521             sink.tableRow_();
522 
523             String detail = testCase.getFailureDetail();
524             if (detail != null) {
525                 sink.tableRow();
526 
527                 tableCell("");
528 
529                 sink.tableCell();
530                 SinkEventAttributeSet atts = new SinkEventAttributeSet();
531                 atts.addAttribute(ID, testCase.getName() + toHtmlIdFailure(testCase));
532                 sink.unknown("div", TAG_TYPE_START, atts);
533 
534                 String fullClassName = testCase.getFullClassName();
535                 String errorLineNumber = testCase.getFailureErrorLine();
536                 if (xrefLocation != null) {
537                     String path = fullClassName.replace('.', '/');
538                     sink.link(xrefLocation + "/" + path + ".html#L" + errorLineNumber);
539                 }
540                 sink.text(fullClassName + ":" + errorLineNumber);
541 
542                 if (xrefLocation != null) {
543                     sink.link_();
544                 }
545                 sink.unknown("div", TAG_TYPE_END, null);
546 
547                 sink.tableCell_();
548 
549                 sink.tableRow_();
550             }
551         }
552 
553         endTable();
554 
555         sink.lineBreak();
556 
557         sink.section1_();
558     }
559 
560     private void constructHotLinks() {
561         if (!testSuites.isEmpty()) {
562             sink.paragraph();
563 
564             sink.text("[");
565             link("#Summary", getI18nString("surefire", "label.summary"));
566             sink.text("]");
567 
568             sink.text(" [");
569             link("#Package_List", getI18nString("surefire", "label.packagelist"));
570             sink.text("]");
571 
572             sink.text(" [");
573             link("#Test_Cases", getI18nString("surefire", "label.testcases"));
574             sink.text("]");
575 
576             sink.paragraph_();
577         }
578     }
579 
580     private String toHtmlIdFailure(ReportTestCase testCase) {
581         return testCase.hasError() ? "-error" : "-failure";
582     }
583 
584     private void sinkIcon(String type) {
585         if (type.startsWith("junit.framework") || "skipped".equals(type)) {
586             sink.figureGraphics("images/icon_warning_sml.gif");
587         } else if (type.startsWith("success")) {
588             sink.figureGraphics("images/icon_success_sml.gif");
589         } else {
590             sink.figureGraphics("images/icon_error_sml.gif");
591         }
592     }
593 
594     private void sinkCellAnchor(String text, String anchor) {
595         sink.tableCell();
596         sinkAnchor(anchor);
597         sink.text(text);
598         sink.tableCell_();
599     }
600 
601     private void sinkAnchor(String anchor) {
602         // Dollar '$' for nested classes is not valid character in sink.anchor() and therefore it is ignored
603         // https://issues.apache.org/jira/browse/SUREFIRE-1443
604         sink.unknown(A.toString(), TAG_TYPE_START, new SinkEventAttributeSet(ID, anchor));
605         sink.unknown(A.toString(), TAG_TYPE_END, null);
606     }
607 
608     private void sinkLink(String href) {
609         // The "'" argument in this JavaScript function would be escaped to "&apos;"
610         // sink.link( "javascript:toggleDisplay('" + toHtmlId( testCase.getFullName() ) + "');" );
611         sink.unknown(A.toString(), TAG_TYPE_START, new SinkEventAttributeSet(HREF, href));
612     }
613 
614     @SuppressWarnings("checkstyle:methodname")
615     private void sinkLink_() {
616         sink.unknown(A.toString(), TAG_TYPE_END, null);
617     }
618 
619     private String javascriptToggleDisplayCode() {
620         return "function toggleDisplay(elementId) {" + Markup.EOL
621                 + " var elm = document.getElementById(elementId + '-error');" + Markup.EOL
622                 + " if (elm == null) {" + Markup.EOL
623                 + "  elm = document.getElementById(elementId + '-failure');" + Markup.EOL
624                 + " }" + Markup.EOL
625                 + " if (elm && typeof elm.style != \"undefined\") {" + Markup.EOL
626                 + "  if (elm.style.display == \"none\") {" + Markup.EOL
627                 + "   elm.style.display = \"\";" + Markup.EOL
628                 + "   document.getElementById(elementId + '-off').style.display = \"none\";" + Markup.EOL
629                 + "   document.getElementById(elementId + '-on').style.display = \"inline\";" + Markup.EOL
630                 + "  } else if (elm.style.display == \"\") {"
631                 + "   elm.style.display = \"none\";" + Markup.EOL
632                 + "   document.getElementById(elementId + '-off').style.display = \"inline\";" + Markup.EOL
633                 + "   document.getElementById(elementId + '-on').style.display = \"none\";" + Markup.EOL
634                 + "  }" + Markup.EOL
635                 + " }" + Markup.EOL
636                 + " }";
637     }
638 }