1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
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
83
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
95
96
97
98 private String getI18nString(String section, String key) {
99 return i18n.getString("surefire-report", locale, "report." + section + '.' + key);
100 }
101
102
103
104
105
106
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
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
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
603
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
610
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 }