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 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
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 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
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
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
577
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
584
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 }