1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.maven.plugin.surefire.report;
20
21 import java.io.BufferedOutputStream;
22 import java.io.File;
23 import java.io.FileOutputStream;
24 import java.io.FilterOutputStream;
25 import java.io.IOException;
26 import java.io.OutputStream;
27 import java.io.OutputStreamWriter;
28 import java.io.PrintWriter;
29 import java.util.ArrayList;
30 import java.util.Deque;
31 import java.util.LinkedHashMap;
32 import java.util.List;
33 import java.util.Map;
34 import java.util.Map.Entry;
35 import java.util.StringTokenizer;
36 import java.util.concurrent.ConcurrentLinkedDeque;
37
38 import org.apache.maven.plugin.surefire.booterclient.output.InPluginProcessDumpSingleton;
39 import org.apache.maven.surefire.api.report.SafeThrowable;
40 import org.apache.maven.surefire.extensions.StatelessReportEventListener;
41 import org.apache.maven.surefire.shared.utils.xml.PrettyPrintXMLWriter;
42 import org.apache.maven.surefire.shared.utils.xml.XMLWriter;
43
44 import static java.nio.charset.StandardCharsets.UTF_8;
45 import static org.apache.maven.plugin.surefire.report.DefaultReporterFactory.TestResultType;
46 import static org.apache.maven.plugin.surefire.report.FileReporterUtils.stripIllegalFilenameChars;
47 import static org.apache.maven.plugin.surefire.report.ReportEntryType.SKIPPED;
48 import static org.apache.maven.plugin.surefire.report.ReportEntryType.SUCCESS;
49 import static org.apache.maven.surefire.shared.utils.StringUtils.isBlank;
50
51 @SuppressWarnings({"javadoc", "checkstyle:javadoctype"})
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86 public class StatelessXmlReporter implements StatelessReportEventListener<WrappedReportEntry, TestSetStats> {
87 private static final String XML_INDENT = " ";
88
89 private static final String XML_NL = "\n";
90
91 private final File reportsDirectory;
92
93 private final String reportNameSuffix;
94
95 private final boolean trimStackTrace;
96
97 private final int rerunFailingTestsCount;
98
99 private final String xsdSchemaLocation;
100
101 private final String xsdVersion;
102
103
104
105 private final Map<String, Deque<WrappedReportEntry>> testClassMethodRunHistoryMap;
106
107 private final boolean phrasedFileName;
108
109 private final boolean phrasedSuiteName;
110
111 private final boolean phrasedClassName;
112
113 private final boolean phrasedMethodName;
114
115 public StatelessXmlReporter(
116 File reportsDirectory,
117 String reportNameSuffix,
118 boolean trimStackTrace,
119 int rerunFailingTestsCount,
120 Map<String, Deque<WrappedReportEntry>> testClassMethodRunHistoryMap,
121 String xsdSchemaLocation,
122 String xsdVersion,
123 boolean phrasedFileName,
124 boolean phrasedSuiteName,
125 boolean phrasedClassName,
126 boolean phrasedMethodName) {
127 this.reportsDirectory = reportsDirectory;
128 this.reportNameSuffix = reportNameSuffix;
129 this.trimStackTrace = trimStackTrace;
130 this.rerunFailingTestsCount = rerunFailingTestsCount;
131 this.testClassMethodRunHistoryMap = testClassMethodRunHistoryMap;
132 this.xsdSchemaLocation = xsdSchemaLocation;
133 this.xsdVersion = xsdVersion;
134 this.phrasedFileName = phrasedFileName;
135 this.phrasedSuiteName = phrasedSuiteName;
136 this.phrasedClassName = phrasedClassName;
137 this.phrasedMethodName = phrasedMethodName;
138 }
139
140 @Override
141 public void testSetCompleted(WrappedReportEntry testSetReportEntry, TestSetStats testSetStats) {
142 Map<String, Map<String, List<WrappedReportEntry>>> classMethodStatistics =
143 arrangeMethodStatistics(testSetReportEntry, testSetStats);
144
145
146
147 try (OutputStream outputStream = getOutputStream(testSetReportEntry);
148 OutputStreamWriter fw = getWriter(outputStream)) {
149 XMLWriter ppw = new PrettyPrintXMLWriter(new PrintWriter(fw), XML_INDENT, XML_NL, UTF_8.name(), null);
150
151 createTestSuiteElement(ppw, testSetReportEntry, testSetStats);
152
153 showProperties(ppw, testSetReportEntry.getSystemProperties());
154
155 for (Entry<String, Map<String, List<WrappedReportEntry>>> statistics : classMethodStatistics.entrySet()) {
156 for (Entry<String, List<WrappedReportEntry>> thisMethodRuns :
157 statistics.getValue().entrySet()) {
158 serializeTestClass(outputStream, fw, ppw, thisMethodRuns.getValue());
159 }
160 }
161
162 ppw.endElement();
163 } catch (IOException e) {
164
165
166
167 InPluginProcessDumpSingleton.getSingleton().dumpException(e, e.getLocalizedMessage(), reportsDirectory);
168 }
169 }
170
171 private Map<String, Map<String, List<WrappedReportEntry>>> arrangeMethodStatistics(
172 WrappedReportEntry testSetReportEntry, TestSetStats testSetStats) {
173 Map<String, Map<String, List<WrappedReportEntry>>> classMethodStatistics = new LinkedHashMap<>();
174 for (WrappedReportEntry methodEntry : aggregateCacheFromMultipleReruns(testSetReportEntry, testSetStats)) {
175 String testClassName = methodEntry.getSourceName();
176 Map<String, List<WrappedReportEntry>> stats = classMethodStatistics.get(testClassName);
177 if (stats == null) {
178 stats = new LinkedHashMap<>();
179 classMethodStatistics.put(testClassName, stats);
180 }
181 String methodName = methodEntry.getName();
182 List<WrappedReportEntry> methodRuns = stats.get(methodName);
183 if (methodRuns == null) {
184 methodRuns = new ArrayList<>();
185 stats.put(methodName, methodRuns);
186 }
187 methodRuns.add(methodEntry);
188 }
189 return classMethodStatistics;
190 }
191
192 private Deque<WrappedReportEntry> aggregateCacheFromMultipleReruns(
193 WrappedReportEntry testSetReportEntry, TestSetStats testSetStats) {
194 String suiteClassName = testSetReportEntry.getSourceName();
195 Deque<WrappedReportEntry> methodRunHistory = getAddMethodRunHistoryMap(suiteClassName);
196 methodRunHistory.addAll(testSetStats.getReportEntries());
197 return methodRunHistory;
198 }
199
200 private void serializeTestClass(
201 OutputStream outputStream, OutputStreamWriter fw, XMLWriter ppw, List<WrappedReportEntry> methodEntries)
202 throws IOException {
203 if (rerunFailingTestsCount > 0) {
204 serializeTestClassWithRerun(outputStream, fw, ppw, methodEntries);
205 } else {
206
207
208 serializeTestClassWithoutRerun(outputStream, fw, ppw, methodEntries);
209 }
210 }
211
212 private void serializeTestClassWithoutRerun(
213 OutputStream outputStream, OutputStreamWriter fw, XMLWriter ppw, List<WrappedReportEntry> methodEntries)
214 throws IOException {
215 for (WrappedReportEntry methodEntry : methodEntries) {
216 startTestElement(ppw, methodEntry);
217 if (methodEntry.getReportEntryType() != SUCCESS) {
218 getTestProblems(
219 fw,
220 ppw,
221 methodEntry,
222 trimStackTrace,
223 outputStream,
224 methodEntry.getReportEntryType().getXmlTag(),
225 false);
226 }
227 createOutErrElements(fw, ppw, methodEntry, outputStream);
228 ppw.endElement();
229 }
230 }
231
232 private void serializeTestClassWithRerun(
233 OutputStream outputStream, OutputStreamWriter fw, XMLWriter ppw, List<WrappedReportEntry> methodEntries)
234 throws IOException {
235 WrappedReportEntry firstMethodEntry = methodEntries.get(0);
236 switch (getTestResultType(methodEntries)) {
237 case success:
238 for (WrappedReportEntry methodEntry : methodEntries) {
239 if (methodEntry.getReportEntryType() == SUCCESS) {
240 startTestElement(ppw, methodEntry);
241 ppw.endElement();
242 }
243 }
244 break;
245 case error:
246 case failure:
247
248 startTestElement(ppw, firstMethodEntry);
249 boolean firstRun = true;
250 for (WrappedReportEntry singleRunEntry : methodEntries) {
251 if (firstRun) {
252 firstRun = false;
253 getTestProblems(
254 fw,
255 ppw,
256 singleRunEntry,
257 trimStackTrace,
258 outputStream,
259 singleRunEntry.getReportEntryType().getXmlTag(),
260 false);
261 createOutErrElements(fw, ppw, singleRunEntry, outputStream);
262 } else if (singleRunEntry.getReportEntryType() == SKIPPED) {
263
264
265
266
267 addCommentElementTestCase("a skipped test execution in re-run phase", fw, ppw, outputStream);
268 } else {
269 getTestProblems(
270 fw,
271 ppw,
272 singleRunEntry,
273 trimStackTrace,
274 outputStream,
275 singleRunEntry.getReportEntryType().getRerunXmlTag(),
276 true);
277 }
278 }
279 ppw.endElement();
280 break;
281 case flake:
282 WrappedReportEntry successful = null;
283
284 for (WrappedReportEntry singleRunEntry : methodEntries) {
285 if (singleRunEntry.getReportEntryType() == SUCCESS) {
286 successful = singleRunEntry;
287 break;
288 }
289 }
290 WrappedReportEntry firstOrSuccessful = successful == null ? methodEntries.get(0) : successful;
291 startTestElement(ppw, firstOrSuccessful);
292 for (WrappedReportEntry singleRunEntry : methodEntries) {
293 if (singleRunEntry.getReportEntryType() != SUCCESS) {
294 getTestProblems(
295 fw,
296 ppw,
297 singleRunEntry,
298 trimStackTrace,
299 outputStream,
300 singleRunEntry.getReportEntryType().getFlakyXmlTag(),
301 true);
302 }
303 }
304 ppw.endElement();
305 break;
306 case skipped:
307 startTestElement(ppw, firstMethodEntry);
308 getTestProblems(
309 fw,
310 ppw,
311 firstMethodEntry,
312 trimStackTrace,
313 outputStream,
314 firstMethodEntry.getReportEntryType().getXmlTag(),
315 false);
316 ppw.endElement();
317 break;
318 default:
319 throw new IllegalStateException("Get unknown test result type");
320 }
321 }
322
323
324
325
326 public void cleanTestHistoryMap() {
327 testClassMethodRunHistoryMap.clear();
328 }
329
330
331
332
333
334
335
336 private TestResultType getTestResultType(List<WrappedReportEntry> methodEntryList) {
337 List<ReportEntryType> testResultTypeList = new ArrayList<>();
338 for (WrappedReportEntry singleRunEntry : methodEntryList) {
339 testResultTypeList.add(singleRunEntry.getReportEntryType());
340 }
341
342 return DefaultReporterFactory.getTestResultType(testResultTypeList, rerunFailingTestsCount);
343 }
344
345 private Deque<WrappedReportEntry> getAddMethodRunHistoryMap(String testClassName) {
346 Deque<WrappedReportEntry> methodRunHistory = testClassMethodRunHistoryMap.get(testClassName);
347 if (methodRunHistory == null) {
348 methodRunHistory = new ConcurrentLinkedDeque<>();
349 testClassMethodRunHistoryMap.put(testClassName == null ? "null" : testClassName, methodRunHistory);
350 }
351 return methodRunHistory;
352 }
353
354 private OutputStream getOutputStream(WrappedReportEntry testSetReportEntry) throws IOException {
355 File reportFile = getReportFile(testSetReportEntry);
356 File reportDir = reportFile.getParentFile();
357
358 reportFile.delete();
359
360 reportDir.mkdirs();
361 return new BufferedOutputStream(new FileOutputStream(reportFile), 64 * 1024);
362 }
363
364 private static OutputStreamWriter getWriter(OutputStream fos) {
365 return new OutputStreamWriter(fos, UTF_8);
366 }
367
368 private File getReportFile(WrappedReportEntry report) {
369 String reportName = "TEST-" + (phrasedFileName ? report.getReportSourceName() : report.getSourceName());
370 String customizedReportName = isBlank(reportNameSuffix) ? reportName : reportName + "-" + reportNameSuffix;
371 return new File(reportsDirectory, stripIllegalFilenameChars(customizedReportName + ".xml"));
372 }
373
374 private void startTestElement(XMLWriter ppw, WrappedReportEntry report) throws IOException {
375 ppw.startElement("testcase");
376 String name = phrasedMethodName ? report.getReportName() : report.getName();
377 ppw.addAttribute("name", name == null ? "" : extraEscapeAttribute(name));
378
379 if (report.getGroup() != null) {
380 ppw.addAttribute("group", report.getGroup());
381 }
382
383 String className = phrasedClassName
384 ? report.getReportSourceName(reportNameSuffix)
385 : report.getSourceName(reportNameSuffix);
386 if (className != null) {
387 ppw.addAttribute("classname", extraEscapeAttribute(className));
388 }
389
390 ppw.addAttribute("time", report.elapsedTimeAsString());
391 }
392
393 private void createTestSuiteElement(XMLWriter ppw, WrappedReportEntry report, TestSetStats testSetStats)
394 throws IOException {
395 ppw.startElement("testsuite");
396
397 ppw.addAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
398 ppw.addAttribute("xsi:noNamespaceSchemaLocation", xsdSchemaLocation);
399 ppw.addAttribute("version", xsdVersion);
400
401 String reportName = phrasedSuiteName
402 ? report.getReportSourceName(reportNameSuffix)
403 : report.getSourceName(reportNameSuffix);
404 ppw.addAttribute("name", reportName == null ? "" : extraEscapeAttribute(reportName));
405
406 if (report.getGroup() != null) {
407 ppw.addAttribute("group", report.getGroup());
408 }
409
410 ppw.addAttribute("time", report.elapsedTimeAsString());
411
412 ppw.addAttribute("tests", String.valueOf(testSetStats.getCompletedCount()));
413
414 ppw.addAttribute("errors", String.valueOf(testSetStats.getErrors()));
415
416 ppw.addAttribute("skipped", String.valueOf(testSetStats.getSkipped()));
417
418 ppw.addAttribute("failures", String.valueOf(testSetStats.getFailures()));
419 }
420
421 private static void getTestProblems(
422 OutputStreamWriter outputStreamWriter,
423 XMLWriter ppw,
424 WrappedReportEntry report,
425 boolean trimStackTrace,
426 OutputStream fw,
427 String testErrorType,
428 boolean createOutErrElementsInside)
429 throws IOException {
430 ppw.startElement(testErrorType);
431
432 String stackTrace = report.getStackTrace(trimStackTrace);
433
434 if (report.getMessage() != null && !report.getMessage().isEmpty()) {
435 ppw.addAttribute("message", extraEscapeAttribute(report.getMessage()));
436 }
437
438 if (report.getStackTraceWriter() != null) {
439
440 SafeThrowable t = report.getStackTraceWriter().getThrowable();
441 if (t != null) {
442 if (t.getMessage() != null) {
443 int delimiter = stackTrace.indexOf(":");
444 String type = delimiter == -1 ? stackTrace : stackTrace.substring(0, delimiter);
445 ppw.addAttribute("type", type);
446 } else {
447 ppw.addAttribute("type", new StringTokenizer(stackTrace).nextToken());
448 }
449 }
450 }
451
452 boolean hasNestedElements = createOutErrElementsInside & stackTrace != null;
453
454 if (stackTrace != null) {
455 if (hasNestedElements) {
456 ppw.startElement("stackTrace");
457 }
458
459 extraEscapeElementValue(stackTrace, outputStreamWriter, ppw, fw);
460
461 if (hasNestedElements) {
462 ppw.endElement();
463 }
464 }
465
466 if (createOutErrElementsInside) {
467 createOutErrElements(outputStreamWriter, ppw, report, fw);
468 }
469
470 ppw.endElement();
471 }
472
473
474 private static void createOutErrElements(
475 OutputStreamWriter outputStreamWriter, XMLWriter ppw, WrappedReportEntry report, OutputStream fw)
476 throws IOException {
477 EncodingOutputStream eos = new EncodingOutputStream(fw);
478 addOutputStreamElement(outputStreamWriter, eos, ppw, report.getStdout(), "system-out");
479 addOutputStreamElement(outputStreamWriter, eos, ppw, report.getStdErr(), "system-err");
480 }
481
482 private static void addOutputStreamElement(
483 OutputStreamWriter outputStreamWriter,
484 EncodingOutputStream eos,
485 XMLWriter xmlWriter,
486 Utf8RecodingDeferredFileOutputStream utf8RecodingDeferredFileOutputStream,
487 String name)
488 throws IOException {
489 if (utf8RecodingDeferredFileOutputStream != null && utf8RecodingDeferredFileOutputStream.getByteCount() > 0) {
490 xmlWriter.startElement(name);
491 xmlWriter.writeText("");
492 outputStreamWriter.flush();
493 eos.getUnderlying().write(ByteConstantsHolder.CDATA_START_BYTES);
494 utf8RecodingDeferredFileOutputStream.writeTo(eos);
495 utf8RecodingDeferredFileOutputStream.free();
496 eos.getUnderlying().write(ByteConstantsHolder.CDATA_END_BYTES);
497 eos.flush();
498 xmlWriter.endElement();
499 }
500 }
501
502
503
504
505
506
507
508 private static void showProperties(XMLWriter xmlWriter, Map<String, String> systemProperties) throws IOException {
509 xmlWriter.startElement("properties");
510 for (final Entry<String, String> entry : systemProperties.entrySet()) {
511 final String key = entry.getKey();
512 String value = entry.getValue();
513
514 if (value == null) {
515 value = "null";
516 }
517
518 xmlWriter.startElement("property");
519
520 xmlWriter.addAttribute("name", key);
521
522 xmlWriter.addAttribute("value", extraEscapeAttribute(value));
523
524 xmlWriter.endElement();
525 }
526 xmlWriter.endElement();
527 }
528
529
530
531
532
533
534
535 private static String extraEscapeAttribute(String message) {
536
537 return containsEscapesIllegalXml10(message) ? escapeXml(message, true) : message;
538 }
539
540
541
542
543
544
545 private static void extraEscapeElementValue(
546 String message, OutputStreamWriter outputStreamWriter, XMLWriter xmlWriter, OutputStream fw)
547 throws IOException {
548
549 if (containsEscapesIllegalXml10(message)) {
550 xmlWriter.writeText(escapeXml(message, false));
551 } else {
552 EncodingOutputStream eos = new EncodingOutputStream(fw);
553 xmlWriter.writeText("");
554 outputStreamWriter.flush();
555 eos.getUnderlying().write(ByteConstantsHolder.CDATA_START_BYTES);
556 eos.write(message.getBytes(UTF_8));
557 eos.getUnderlying().write(ByteConstantsHolder.CDATA_END_BYTES);
558 eos.flush();
559 }
560 }
561
562
563 private static void addCommentElementTestCase(
564 String comment, OutputStreamWriter outputStreamWriter, XMLWriter xmlWriter, OutputStream fw)
565 throws IOException {
566 xmlWriter.writeText("");
567 outputStreamWriter.flush();
568 fw.write(XML_NL.getBytes(UTF_8));
569 fw.write(XML_INDENT.getBytes(UTF_8));
570 fw.write(XML_INDENT.getBytes(UTF_8));
571 fw.write(ByteConstantsHolder.COMMENT_START);
572 fw.write(comment.getBytes(UTF_8));
573 fw.write(ByteConstantsHolder.COMMENT_END);
574 fw.write(XML_NL.getBytes(UTF_8));
575 fw.write(XML_INDENT.getBytes(UTF_8));
576 fw.flush();
577 }
578
579 private static final class EncodingOutputStream extends FilterOutputStream {
580 private int c1;
581
582 private int c2;
583
584 EncodingOutputStream(OutputStream out) {
585 super(out);
586 }
587
588 OutputStream getUnderlying() {
589 return out;
590 }
591
592 private boolean isCdataEndBlock(int c) {
593 return c1 == ']' && c2 == ']' && c == '>';
594 }
595
596 @Override
597 public void write(int b) throws IOException {
598 if (isCdataEndBlock(b)) {
599 out.write(ByteConstantsHolder.CDATA_ESCAPE_STRING_BYTES);
600 } else if (isIllegalEscape(b)) {
601
602
603
604
605
606 out.write(ByteConstantsHolder.AMP_BYTES);
607 out.write(String.valueOf(b).getBytes(UTF_8));
608 out.write(';');
609 } else {
610 out.write(b);
611 }
612 c1 = c2;
613 c2 = b;
614 }
615 }
616
617 private static boolean containsEscapesIllegalXml10(String message) {
618 int size = message.length();
619 for (int i = 0; i < size; i++) {
620 if (isIllegalEscape(message.charAt(i))) {
621 return true;
622 }
623 }
624 return false;
625 }
626
627 private static boolean isIllegalEscape(char c) {
628 return isIllegalEscape((int) c);
629 }
630
631 private static boolean isIllegalEscape(int c) {
632 return c >= 0 && c < 32 && c != '\n' && c != '\r' && c != '\t';
633 }
634
635
636
637
638
639
640
641
642 private static String escapeXml(String text, boolean attribute) {
643 StringBuilder sb = new StringBuilder(text.length() * 2);
644 for (int i = 0; i < text.length(); i++) {
645 char c = text.charAt(i);
646 if (isIllegalEscape(c)) {
647
648
649
650
651
652 sb.append(attribute ? "&#" : "&#")
653 .append((int) c)
654 .append(';');
655 } else {
656 sb.append(c);
657 }
658 }
659 return sb.toString();
660 }
661
662 private static final class ByteConstantsHolder {
663 private static final byte[] CDATA_START_BYTES = "<![CDATA[".getBytes(UTF_8);
664
665 private static final byte[] CDATA_END_BYTES = "]]>".getBytes(UTF_8);
666
667 private static final byte[] CDATA_ESCAPE_STRING_BYTES = "]]><![CDATA[>".getBytes(UTF_8);
668
669 private static final byte[] AMP_BYTES = "&#".getBytes(UTF_8);
670
671 private static final byte[] COMMENT_START = "<!-- ".getBytes(UTF_8);
672
673 private static final byte[] COMMENT_END = " --> ".getBytes(UTF_8);
674 }
675 }