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