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