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 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
585
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
592
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 }