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.text.NumberFormat;
23 import java.util.List;
24 import java.util.Locale;
25 import java.util.Map;
26
27 import org.apache.maven.doxia.markup.HtmlMarkup;
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.MavenReportException;
33
34 import static org.apache.maven.doxia.markup.HtmlMarkup.A;
35 import static org.apache.maven.doxia.sink.Sink.JUSTIFY_LEFT;
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 import static org.apache.maven.doxia.sink.SinkEventAttributes.TYPE;
41
42
43
44
45 public final class SurefireReportGenerator {
46 private static final int LEFT = JUSTIFY_LEFT;
47 private static final Object[] TAG_TYPE_START = {HtmlMarkup.TAG_TYPE_START};
48 private static final Object[] TAG_TYPE_END = {HtmlMarkup.TAG_TYPE_END};
49
50 private final SurefireReportParser report;
51 private final boolean showSuccess;
52 private final String xrefLocation;
53 private List<ReportTestSuite> testSuites;
54
55 public SurefireReportGenerator(
56 List<File> reportsDirectories,
57 Locale locale,
58 boolean showSuccess,
59 String xrefLocation,
60 ConsoleLogger consoleLogger) {
61 report = new SurefireReportParser(reportsDirectories, locale, consoleLogger);
62 this.showSuccess = showSuccess;
63 this.xrefLocation = xrefLocation;
64 }
65
66 public void doGenerateReport(LocalizedProperties bundle, Sink sink) throws MavenReportException {
67 testSuites = report.parseXMLReportFiles();
68
69 sink.head();
70
71 sink.title();
72 sink.text(bundle.getReportHeader());
73 sink.title_();
74
75 sink.head_();
76
77 sink.body();
78
79 SinkEventAttributeSet atts = new SinkEventAttributeSet();
80 atts.addAttribute(TYPE, "application/javascript");
81 sink.unknown("script", new Object[] {HtmlMarkup.TAG_TYPE_START}, atts);
82 sink.unknown("cdata", new Object[] {HtmlMarkup.CDATA_TYPE, javascriptToggleDisplayCode()}, null);
83 sink.unknown("script", new Object[] {HtmlMarkup.TAG_TYPE_END}, null);
84
85 sink.section1();
86 sink.sectionTitle1();
87 sink.text(bundle.getReportHeader());
88 sink.sectionTitle1_();
89 sink.section1_();
90
91 constructSummarySection(bundle, sink);
92
93 Map<String, List<ReportTestSuite>> suitePackages = report.getSuitesGroupByPackage(testSuites);
94 if (!suitePackages.isEmpty()) {
95 constructPackagesSection(bundle, sink, suitePackages);
96 }
97
98 if (!testSuites.isEmpty()) {
99 constructTestCasesSection(bundle, sink);
100 }
101
102 List<ReportTestCase> failureList = report.getFailureDetails(testSuites);
103 if (!failureList.isEmpty()) {
104 constructFailureDetails(sink, bundle, failureList);
105 }
106
107 sink.body_();
108
109 sink.flush();
110
111 sink.close();
112 }
113
114 private void constructSummarySection(LocalizedProperties bundle, Sink sink) {
115 Map<String, String> summary = report.getSummary(testSuites);
116
117 sink.section1();
118 sinkAnchor(sink, "Summary");
119 sink.sectionTitle1();
120 sink.text(bundle.getReportLabelSummary());
121 sink.sectionTitle1_();
122
123 constructHotLinks(sink, bundle);
124
125 sinkLineBreak(sink);
126
127 sink.table();
128
129 sink.tableRows(new int[] {LEFT, LEFT, LEFT, LEFT, LEFT, LEFT}, false);
130
131 sink.tableRow();
132
133 sinkHeader(sink, bundle.getReportLabelTests());
134
135 sinkHeader(sink, bundle.getReportLabelErrors());
136
137 sinkHeader(sink, bundle.getReportLabelFailures());
138
139 sinkHeader(sink, bundle.getReportLabelSkipped());
140
141 sinkHeader(sink, bundle.getReportLabelSuccessRate());
142
143 sinkHeader(sink, bundle.getReportLabelTime());
144
145 sink.tableRow_();
146
147 sink.tableRow();
148
149 sinkCell(sink, summary.get("totalTests"));
150
151 sinkCell(sink, summary.get("totalErrors"));
152
153 sinkCell(sink, summary.get("totalFailures"));
154
155 sinkCell(sink, summary.get("totalSkipped"));
156
157 sinkCell(sink, summary.get("totalPercentage") + "%");
158
159 sinkCell(sink, summary.get("totalElapsedTime"));
160
161 sink.tableRow_();
162
163 sink.tableRows_();
164
165 sink.table_();
166
167 sink.lineBreak();
168
169 sink.paragraph();
170 sink.text(bundle.getReportTextNode1());
171 sink.paragraph_();
172
173 sinkLineBreak(sink);
174
175 sink.section1_();
176 }
177
178 private void constructPackagesSection(
179 LocalizedProperties bundle, Sink sink, Map<String, List<ReportTestSuite>> suitePackages) {
180 NumberFormat numberFormat = report.getNumberFormat();
181
182 sink.section1();
183 sinkAnchor(sink, "Package_List");
184 sink.sectionTitle1();
185 sink.text(bundle.getReportLabelPackageList());
186 sink.sectionTitle1_();
187
188 constructHotLinks(sink, bundle);
189
190 sinkLineBreak(sink);
191
192 sink.table();
193
194 sink.tableRows(new int[] {LEFT, LEFT, LEFT, LEFT, LEFT, LEFT, LEFT}, false);
195
196 sink.tableRow();
197
198 sinkHeader(sink, bundle.getReportLabelPackage());
199
200 sinkHeader(sink, bundle.getReportLabelTests());
201
202 sinkHeader(sink, bundle.getReportLabelErrors());
203
204 sinkHeader(sink, bundle.getReportLabelFailures());
205
206 sinkHeader(sink, bundle.getReportLabelSkipped());
207
208 sinkHeader(sink, bundle.getReportLabelSuccessRate());
209
210 sinkHeader(sink, bundle.getReportLabelTime());
211
212 sink.tableRow_();
213
214 for (Map.Entry<String, List<ReportTestSuite>> entry : suitePackages.entrySet()) {
215 sink.tableRow();
216
217 String packageName = entry.getKey();
218
219 List<ReportTestSuite> testSuiteList = entry.getValue();
220
221 Map<String, String> packageSummary = report.getSummary(testSuiteList);
222
223 sinkCellLink(sink, packageName, "#" + packageName);
224
225 sinkCell(sink, packageSummary.get("totalTests"));
226
227 sinkCell(sink, packageSummary.get("totalErrors"));
228
229 sinkCell(sink, packageSummary.get("totalFailures"));
230
231 sinkCell(sink, packageSummary.get("totalSkipped"));
232
233 sinkCell(sink, packageSummary.get("totalPercentage") + "%");
234
235 sinkCell(sink, packageSummary.get("totalElapsedTime"));
236
237 sink.tableRow_();
238 }
239
240 sink.tableRows_();
241
242 sink.table_();
243
244 sink.lineBreak();
245
246 sink.paragraph();
247 sink.text(bundle.getReportTextNode2());
248 sink.paragraph_();
249
250 for (Map.Entry<String, List<ReportTestSuite>> entry : suitePackages.entrySet()) {
251 String packageName = entry.getKey();
252
253 List<ReportTestSuite> testSuiteList = entry.getValue();
254
255 sink.section2();
256 sinkAnchor(sink, packageName);
257 sink.sectionTitle2();
258 sink.text(packageName);
259 sink.sectionTitle2_();
260
261 boolean showTable = false;
262
263 for (ReportTestSuite suite : testSuiteList) {
264 if (showSuccess || suite.getNumberOfErrors() != 0 || suite.getNumberOfFailures() != 0) {
265 showTable = true;
266
267 break;
268 }
269 }
270
271 if (showTable) {
272 sink.table();
273
274 sink.tableRows(new int[] {LEFT, LEFT, LEFT, LEFT, LEFT, LEFT, LEFT, LEFT}, false);
275
276 sink.tableRow();
277
278 sinkHeader(sink, "");
279
280 sinkHeader(sink, bundle.getReportLabelClass());
281
282 sinkHeader(sink, bundle.getReportLabelTests());
283
284 sinkHeader(sink, bundle.getReportLabelErrors());
285
286 sinkHeader(sink, bundle.getReportLabelFailures());
287
288 sinkHeader(sink, bundle.getReportLabelSkipped());
289
290 sinkHeader(sink, bundle.getReportLabelSuccessRate());
291
292 sinkHeader(sink, bundle.getReportLabelTime());
293
294 sink.tableRow_();
295
296 for (ReportTestSuite suite : testSuiteList) {
297 if (showSuccess || suite.getNumberOfErrors() != 0 || suite.getNumberOfFailures() != 0) {
298 constructTestSuiteSection(sink, numberFormat, suite);
299 }
300 }
301
302 sink.tableRows_();
303
304 sink.table_();
305 }
306
307 sink.section2_();
308 }
309
310 sinkLineBreak(sink);
311
312 sink.section1_();
313 }
314
315 private void constructTestSuiteSection(Sink sink, NumberFormat numberFormat, ReportTestSuite suite) {
316 sink.tableRow();
317
318 sink.tableCell();
319
320 sink.link("#" + suite.getPackageName() + '.' + suite.getName());
321
322 if (suite.getNumberOfErrors() > 0) {
323 sinkIcon("error", sink);
324 } else if (suite.getNumberOfFailures() > 0) {
325 sinkIcon("junit.framework", sink);
326 } else if (suite.getNumberOfSkipped() > 0) {
327 sinkIcon("skipped", sink);
328 } else {
329 sinkIcon("success", sink);
330 }
331
332 sink.link_();
333
334 sink.tableCell_();
335
336 sinkCellLink(sink, suite.getName(), "#" + suite.getPackageName() + '.' + suite.getName());
337
338 sinkCell(sink, Integer.toString(suite.getNumberOfTests()));
339
340 sinkCell(sink, Integer.toString(suite.getNumberOfErrors()));
341
342 sinkCell(sink, Integer.toString(suite.getNumberOfFailures()));
343
344 sinkCell(sink, Integer.toString(suite.getNumberOfSkipped()));
345
346 String percentage = report.computePercentage(
347 suite.getNumberOfTests(), suite.getNumberOfErrors(),
348 suite.getNumberOfFailures(), suite.getNumberOfSkipped());
349 sinkCell(sink, percentage + "%");
350
351 sinkCell(sink, numberFormat.format(suite.getTimeElapsed()));
352
353 sink.tableRow_();
354 }
355
356 private void constructTestCasesSection(LocalizedProperties bundle, Sink sink) {
357 NumberFormat numberFormat = report.getNumberFormat();
358
359 sink.section1();
360 sinkAnchor(sink, "Test_Cases");
361 sink.sectionTitle1();
362 sink.text(bundle.getReportLabelTestCases());
363 sink.sectionTitle1_();
364
365 constructHotLinks(sink, bundle);
366
367 for (ReportTestSuite suite : testSuites) {
368 List<ReportTestCase> testCases = suite.getTestCases();
369
370 if (!testCases.isEmpty()) {
371 sink.section2();
372 sinkAnchor(sink, suite.getPackageName() + '.' + suite.getName());
373 sink.sectionTitle2();
374 sink.text(suite.getName());
375 sink.sectionTitle2_();
376
377 boolean showTable = false;
378
379 for (ReportTestCase testCase : testCases) {
380 if (!testCase.isSuccessful() || showSuccess) {
381 showTable = true;
382
383 break;
384 }
385 }
386
387 if (showTable) {
388 sink.table();
389
390 sink.tableRows(new int[] {LEFT, LEFT, LEFT}, false);
391
392 for (ReportTestCase testCase : testCases) {
393 if (!testCase.isSuccessful() || showSuccess) {
394 constructTestCaseSection(sink, numberFormat, testCase);
395 }
396 }
397
398 sink.tableRows_();
399
400 sink.table_();
401 }
402
403 sink.section2_();
404 }
405 }
406
407 sinkLineBreak(sink);
408
409 sink.section1_();
410 }
411
412 private static void constructTestCaseSection(Sink sink, NumberFormat numberFormat, ReportTestCase testCase) {
413 sink.tableRow();
414
415 sink.tableCell();
416
417 if (testCase.getFailureType() != null) {
418 sink.link("#" + toHtmlId(testCase.getFullName()));
419
420 sinkIcon(testCase.getFailureType(), sink);
421
422 sink.link_();
423 } else {
424 sinkIcon("success", sink);
425 }
426
427 sink.tableCell_();
428
429 if (!testCase.isSuccessful()) {
430 sink.tableCell();
431 sinkAnchor(sink, "TC_" + toHtmlId(testCase.getFullName()));
432
433 sinkLink(sink, testCase.getName(), "#" + toHtmlId(testCase.getFullName()));
434
435 SinkEventAttributeSet atts = new SinkEventAttributeSet();
436 atts.addAttribute(CLASS, "detailToggle");
437 atts.addAttribute(STYLE, "display:inline");
438 sink.unknown("div", TAG_TYPE_START, atts);
439
440 sinkLink(sink, "javascript:toggleDisplay('" + toHtmlId(testCase.getFullName()) + "');");
441
442 atts = new SinkEventAttributeSet();
443 atts.addAttribute(STYLE, "display:inline;");
444 atts.addAttribute(ID, toHtmlId(testCase.getFullName()) + "-off");
445 sink.unknown("span", TAG_TYPE_START, atts);
446 sink.text(" + ");
447 sink.unknown("span", TAG_TYPE_END, null);
448
449 atts = new SinkEventAttributeSet();
450 atts.addAttribute(STYLE, "display:none;");
451 atts.addAttribute(ID, toHtmlId(testCase.getFullName()) + "-on");
452 sink.unknown("span", TAG_TYPE_START, atts);
453 sink.text(" - ");
454 sink.unknown("span", TAG_TYPE_END, null);
455
456 sink.text("[ Detail ]");
457 sinkLink_(sink);
458
459 sink.unknown("div", TAG_TYPE_END, null);
460
461 sink.tableCell_();
462 } else {
463 sinkCellAnchor(sink, testCase.getName(), "TC_" + toHtmlId(testCase.getFullName()));
464 }
465
466 sinkCell(sink, numberFormat.format(testCase.getTime()));
467
468 sink.tableRow_();
469
470 if (!testCase.isSuccessful()) {
471 sink.tableRow();
472
473 sinkCell(sink, "");
474 sinkCell(sink, testCase.getFailureMessage());
475 sinkCell(sink, "");
476 sink.tableRow_();
477
478 String detail = testCase.getFailureDetail();
479 if (detail != null) {
480 sink.tableRow();
481 sinkCell(sink, "");
482
483 sink.tableCell();
484 SinkEventAttributeSet atts = new SinkEventAttributeSet();
485 atts.addAttribute(ID, toHtmlId(testCase.getFullName()) + toHtmlIdFailure(testCase));
486 atts.addAttribute(STYLE, "display:none;");
487 sink.unknown("div", TAG_TYPE_START, atts);
488
489 sink.verbatim(null);
490 sink.text(detail);
491 sink.verbatim_();
492
493 sink.unknown("div", TAG_TYPE_END, null);
494 sink.tableCell_();
495
496 sinkCell(sink, "");
497
498 sink.tableRow_();
499 }
500 }
501 }
502
503 private static String toHtmlId(String id) {
504 return DoxiaUtils.isValidId(id) ? id : DoxiaUtils.encodeId(id, true);
505 }
506
507 private void constructFailureDetails(Sink sink, LocalizedProperties bundle, List<ReportTestCase> failures) {
508 sink.section1();
509 sinkAnchor(sink, "Failure_Details");
510 sink.sectionTitle1();
511 sink.text(bundle.getReportLabelFailureDetails());
512 sink.sectionTitle1_();
513
514 constructHotLinks(sink, bundle);
515
516 sinkLineBreak(sink);
517
518 sink.table();
519
520 sink.tableRows(new int[] {LEFT, LEFT}, false);
521
522 for (ReportTestCase tCase : failures) {
523 sink.tableRow();
524
525 sink.tableCell();
526
527 String type = tCase.getFailureType();
528
529 sinkIcon(type, sink);
530
531 sink.tableCell_();
532
533 sinkCellAnchor(sink, tCase.getName(), toHtmlId(tCase.getFullName()));
534
535 sink.tableRow_();
536
537 String message = tCase.getFailureMessage();
538
539 sink.tableRow();
540
541 sinkCell(sink, "");
542
543 sinkCell(sink, message == null ? type : type + ": " + message);
544
545 sink.tableRow_();
546
547 String detail = tCase.getFailureDetail();
548 if (detail != null) {
549 sink.tableRow();
550
551 sinkCell(sink, "");
552
553 sink.tableCell();
554 SinkEventAttributeSet atts = new SinkEventAttributeSet();
555 atts.addAttribute(ID, tCase.getName() + toHtmlIdFailure(tCase));
556 sink.unknown("div", TAG_TYPE_START, atts);
557
558 String fullClassName = tCase.getFullClassName();
559 String errorLineNumber = tCase.getFailureErrorLine();
560 if (xrefLocation != null) {
561 String path = fullClassName.replace('.', '/');
562 sink.link(xrefLocation + "/" + path + ".html#L" + errorLineNumber);
563 }
564 sink.text(fullClassName + ":" + errorLineNumber);
565
566 if (xrefLocation != null) {
567 sink.link_();
568 }
569 sink.unknown("div", TAG_TYPE_END, null);
570
571 sink.tableCell_();
572
573 sink.tableRow_();
574 }
575 }
576
577 sink.tableRows_();
578
579 sink.table_();
580
581 sinkLineBreak(sink);
582
583 sink.section1_();
584 }
585
586 private void constructHotLinks(Sink sink, LocalizedProperties bundle) {
587 if (!testSuites.isEmpty()) {
588 sink.paragraph();
589
590 sink.text("[");
591 sinkLink(sink, bundle.getReportLabelSummary(), "#Summary");
592 sink.text("]");
593
594 sink.text(" [");
595 sinkLink(sink, bundle.getReportLabelPackageList(), "#Package_List");
596 sink.text("]");
597
598 sink.text(" [");
599 sinkLink(sink, bundle.getReportLabelTestCases(), "#Test_Cases");
600 sink.text("]");
601
602 sink.paragraph_();
603 }
604 }
605
606 private static String toHtmlIdFailure(ReportTestCase tCase) {
607 return tCase.hasError() ? "-error" : "-failure";
608 }
609
610 private static void sinkLineBreak(Sink sink) {
611 sink.lineBreak();
612 }
613
614 private static void sinkIcon(String type, Sink sink) {
615 if (type.startsWith("junit.framework") || "skipped".equals(type)) {
616 sink.figureGraphics("images/icon_warning_sml.gif");
617 } else if (type.startsWith("success")) {
618 sink.figureGraphics("images/icon_success_sml.gif");
619 } else {
620 sink.figureGraphics("images/icon_error_sml.gif");
621 }
622 }
623
624 private static void sinkHeader(Sink sink, String header) {
625 sink.tableHeaderCell();
626 sink.text(header);
627 sink.tableHeaderCell_();
628 }
629
630 private static void sinkCell(Sink sink, String text) {
631 sink.tableCell();
632 sink.text(text);
633 sink.tableCell_();
634 }
635
636 private static void sinkLink(Sink sink, String text, String link) {
637 sink.link(link);
638 sink.text(text);
639 sink.link_();
640 }
641
642 private static void sinkCellLink(Sink sink, String text, String link) {
643 sink.tableCell();
644 sinkLink(sink, text, link);
645 sink.tableCell_();
646 }
647
648 private static void sinkCellAnchor(Sink sink, String text, String anchor) {
649 sink.tableCell();
650 sinkAnchor(sink, anchor);
651 sink.text(text);
652 sink.tableCell_();
653 }
654
655 private static void sinkAnchor(Sink sink, String anchor) {
656
657
658 sink.unknown(A.toString(), TAG_TYPE_START, new SinkEventAttributeSet(ID, anchor));
659 sink.unknown(A.toString(), TAG_TYPE_END, null);
660 }
661
662 private static void sinkLink(Sink sink, String href) {
663
664
665 sink.unknown(A.toString(), TAG_TYPE_START, new SinkEventAttributeSet(HREF, href));
666 }
667
668 @SuppressWarnings("checkstyle:methodname")
669 private static void sinkLink_(Sink sink) {
670 sink.unknown(A.toString(), TAG_TYPE_END, null);
671 }
672
673 private static String javascriptToggleDisplayCode() {
674
675
676
677
678 return "\n" + "function toggleDisplay(elementId) {\n"
679 + " var elm = document.getElementById(elementId + '-error');\n"
680 + " if (elm == null) {\n"
681 + " elm = document.getElementById(elementId + '-failure');\n"
682 + " }\n"
683 + " if (elm && typeof elm.style != \"undefined\") {\n"
684 + " if (elm.style.display == \"none\") {\n"
685 + " elm.style.display = \"\";\n"
686 + " document.getElementById(elementId + '-off').style.display = \"none\";\n"
687 + " document.getElementById(elementId + '-on').style.display = \"inline\";\n"
688 + " } else if (elm.style.display == \"\") {"
689 + " elm.style.display = \"none\";\n"
690 + " document.getElementById(elementId + '-off').style.display = \"inline\";\n"
691 + " document.getElementById(elementId + '-on').style.display = \"none\";\n"
692 + " } \n"
693 + " } \n"
694 + " }\n"
695 + "//";
696 }
697 }