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