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