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                 tableRow(new String[] {"", message, ""});
434             }
435 
436             String detail = testCase.getFailureDetail();
437             if (detail != null) {
438                 SinkEventAttributeSet atts = new SinkEventAttributeSet();
439                 atts.addAttribute(ID, toHtmlId(testCase.getFullName()) + toHtmlIdFailure(testCase));
440                 atts.addAttribute(STYLE, "display:none;");
441                 sink.tableRow(atts);
442 
443                 tableCell("");
444 
445                 sink.tableCell();
446 
447                 verbatimText(detail);
448 
449                 sink.tableCell_();
450 
451                 tableCell("");
452 
453                 sink.tableRow_();
454             }
455         }
456     }
457 
458     private String toHtmlId(String id) {
459         return DoxiaUtils.isValidId(id) ? id : DoxiaUtils.encodeId(id, true);
460     }
461 
462     private void renderSectionFailureDetails() {
463         List<ReportTestCase> failures = parser.getFailureDetails(testSuites);
464         if (failures.isEmpty()) {
465             return;
466         }
467 
468         sink.section1();
469         sinkAnchor("Failure_Details");
470         sink.sectionTitle1();
471         sink.text(getI18nString("surefire", "label.failuredetails"));
472         sink.sectionTitle1_();
473 
474         constructHotLinks();
475 
476         sink.lineBreak();
477 
478         startTable();
479 
480         for (ReportTestCase testCase : failures) {
481             sink.tableRow();
482 
483             sink.tableCell();
484 
485             String type = testCase.getFailureType();
486 
487             sinkIcon(type);
488 
489             sink.tableCell_();
490 
491             sinkCellAnchor(testCase.getName(), toHtmlId(testCase.getFullName()));
492 
493             sink.tableRow_();
494 
495             String message = testCase.getFailureMessage();
496 
497             sink.tableRow();
498 
499             tableCell("");
500 
501             tableCell(message == null ? type : type + ": " + message);
502 
503             sink.tableRow_();
504 
505             String detail = testCase.getFailureDetail();
506             if (detail != null) {
507                 sink.tableRow();
508 
509                 tableCell("");
510 
511                 sink.tableCell();
512                 SinkEventAttributeSet atts = new SinkEventAttributeSet();
513                 atts.addAttribute(ID, testCase.getName() + toHtmlIdFailure(testCase));
514                 sink.unknown("div", TAG_TYPE_START, atts);
515 
516                 String fullClassName = testCase.getFullClassName();
517                 String errorLineNumber = testCase.getFailureErrorLine();
518                 if (xrefLocation != null) {
519                     String path = fullClassName.replace('.', '/');
520                     sink.link(xrefLocation + "/" + path + ".html#L" + errorLineNumber);
521                 }
522                 sink.text(fullClassName + ":" + errorLineNumber);
523 
524                 if (xrefLocation != null) {
525                     sink.link_();
526                 }
527                 sink.unknown("div", TAG_TYPE_END, null);
528 
529                 sink.tableCell_();
530 
531                 sink.tableRow_();
532             }
533         }
534 
535         endTable();
536 
537         sink.lineBreak();
538 
539         sink.section1_();
540     }
541 
542     private void constructHotLinks() {
543         if (!testSuites.isEmpty()) {
544             sink.paragraph();
545 
546             sink.text("[");
547             link("#Summary", getI18nString("surefire", "label.summary"));
548             sink.text("]");
549 
550             sink.text(" [");
551             link("#Package_List", getI18nString("surefire", "label.packagelist"));
552             sink.text("]");
553 
554             sink.text(" [");
555             link("#Test_Cases", getI18nString("surefire", "label.testcases"));
556             sink.text("]");
557 
558             sink.paragraph_();
559         }
560     }
561 
562     private String toHtmlIdFailure(ReportTestCase testCase) {
563         return testCase.hasError() ? "-error" : "-failure";
564     }
565 
566     private void sinkIcon(String type) {
567         if (type.startsWith("junit.framework") || "skipped".equals(type)) {
568             sink.figureGraphics("images/icon_warning_sml.gif");
569         } else if (type.startsWith("success")) {
570             sink.figureGraphics("images/icon_success_sml.gif");
571         } else {
572             sink.figureGraphics("images/icon_error_sml.gif");
573         }
574     }
575 
576     private void sinkCellAnchor(String text, String anchor) {
577         sink.tableCell();
578         sinkAnchor(anchor);
579         sink.text(text);
580         sink.tableCell_();
581     }
582 
583     private void sinkAnchor(String anchor) {
584         // Dollar '$' for nested classes is not valid character in sink.anchor() and therefore it is ignored
585         // https://issues.apache.org/jira/browse/SUREFIRE-1443
586         sink.unknown(A.toString(), TAG_TYPE_START, new SinkEventAttributeSet(ID, anchor));
587         sink.unknown(A.toString(), TAG_TYPE_END, null);
588     }
589 
590     private void sinkLink(String href) {
591         // The "'" argument in this JavaScript function would be escaped to "&apos;"
592         // sink.link( "javascript:toggleDisplay('" + toHtmlId( testCase.getFullName() ) + "');" );
593         sink.unknown(A.toString(), TAG_TYPE_START, new SinkEventAttributeSet(HREF, href));
594     }
595 
596     @SuppressWarnings("checkstyle:methodname")
597     private void sinkLink_() {
598         sink.unknown(A.toString(), TAG_TYPE_END, null);
599     }
600 
601     private String javascriptToggleDisplayCode() {
602         return "function toggleDisplay(elementId) {" + Markup.EOL
603                 + " var elm = document.getElementById(elementId + '-error');" + Markup.EOL
604                 + " if (elm == null) {" + Markup.EOL
605                 + "  elm = document.getElementById(elementId + '-failure');" + Markup.EOL
606                 + " }" + Markup.EOL
607                 + " if (elm && typeof elm.style != \"undefined\") {" + Markup.EOL
608                 + "  if (elm.style.display == \"none\") {" + Markup.EOL
609                 + "   elm.style.display = \"\";" + Markup.EOL
610                 + "   document.getElementById(elementId + '-off').style.display = \"none\";" + Markup.EOL
611                 + "   document.getElementById(elementId + '-on').style.display = \"inline\";" + Markup.EOL
612                 + "  } else if (elm.style.display == \"\") {"
613                 + "   elm.style.display = \"none\";" + Markup.EOL
614                 + "   document.getElementById(elementId + '-off').style.display = \"inline\";" + Markup.EOL
615                 + "   document.getElementById(elementId + '-on').style.display = \"none\";" + Markup.EOL
616                 + "  }" + Markup.EOL
617                 + " }" + Markup.EOL
618                 + " }";
619     }
620 }