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