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 createNestedOutErrElements)
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
460
461
462
463 if (createNestedOutErrElements) {
464 ppw.startElement("stackTrace");
465 if (stackTrace != null) {
466 extraEscapeElementValue(stackTrace, outputStreamWriter, ppw, fw);
467 }
468 ppw.endElement();
469
470 createOutErrElements(outputStreamWriter, ppw, report, fw);
471 } else if (stackTrace != null) {
472 extraEscapeElementValue(stackTrace, outputStreamWriter, ppw, fw);
473 }
474
475 ppw.endElement();
476 }
477
478
479 private static void createOutErrElements(
480 OutputStreamWriter outputStreamWriter, XMLWriter ppw, WrappedReportEntry report, OutputStream fw)
481 throws IOException {
482 EncodingOutputStream eos = new EncodingOutputStream(fw);
483 addOutputStreamElement(outputStreamWriter, eos, ppw, report.getStdout(), "system-out");
484 addOutputStreamElement(outputStreamWriter, eos, ppw, report.getStdErr(), "system-err");
485 }
486
487 private static void addOutputStreamElement(
488 OutputStreamWriter outputStreamWriter,
489 EncodingOutputStream eos,
490 XMLWriter xmlWriter,
491 Utf8RecodingDeferredFileOutputStream utf8RecodingDeferredFileOutputStream,
492 String name)
493 throws IOException {
494 if (utf8RecodingDeferredFileOutputStream != null && utf8RecodingDeferredFileOutputStream.getByteCount() > 0) {
495 xmlWriter.startElement(name);
496 xmlWriter.writeText("");
497 outputStreamWriter.flush();
498 eos.getUnderlying().write(ByteConstantsHolder.CDATA_START_BYTES);
499 utf8RecodingDeferredFileOutputStream.writeTo(eos);
500 utf8RecodingDeferredFileOutputStream.free();
501 eos.getUnderlying().write(ByteConstantsHolder.CDATA_END_BYTES);
502 eos.flush();
503 xmlWriter.endElement();
504 }
505 }
506
507
508
509
510
511
512
513 private static void showProperties(XMLWriter xmlWriter, Map<String, String> systemProperties) throws IOException {
514 xmlWriter.startElement("properties");
515 for (final Entry<String, String> entry : systemProperties.entrySet()) {
516 final String key = entry.getKey();
517 String value = entry.getValue();
518
519 if (value == null) {
520 value = "null";
521 }
522
523 xmlWriter.startElement("property");
524
525 xmlWriter.addAttribute("name", key);
526
527 xmlWriter.addAttribute("value", extraEscapeAttribute(value));
528
529 xmlWriter.endElement();
530 }
531 xmlWriter.endElement();
532 }
533
534
535
536
537
538
539
540 private static String extraEscapeAttribute(String message) {
541
542 return containsEscapesIllegalXml10(message) ? escapeXml(message, true) : message;
543 }
544
545
546
547
548
549
550 private static void extraEscapeElementValue(
551 String message, OutputStreamWriter outputStreamWriter, XMLWriter xmlWriter, OutputStream fw)
552 throws IOException {
553
554 if (containsEscapesIllegalXml10(message)) {
555 xmlWriter.writeText(escapeXml(message, false));
556 } else {
557 EncodingOutputStream eos = new EncodingOutputStream(fw);
558 xmlWriter.writeText("");
559 outputStreamWriter.flush();
560 eos.getUnderlying().write(ByteConstantsHolder.CDATA_START_BYTES);
561 eos.write(message.getBytes(UTF_8));
562 eos.getUnderlying().write(ByteConstantsHolder.CDATA_END_BYTES);
563 eos.flush();
564 }
565 }
566
567
568 private static void addCommentElementTestCase(
569 String comment, OutputStreamWriter outputStreamWriter, XMLWriter xmlWriter, OutputStream fw)
570 throws IOException {
571 xmlWriter.writeText("");
572 outputStreamWriter.flush();
573 fw.write(XML_NL.getBytes(UTF_8));
574 fw.write(XML_INDENT.getBytes(UTF_8));
575 fw.write(XML_INDENT.getBytes(UTF_8));
576 fw.write(ByteConstantsHolder.COMMENT_START);
577 fw.write(comment.getBytes(UTF_8));
578 fw.write(ByteConstantsHolder.COMMENT_END);
579 fw.write(XML_NL.getBytes(UTF_8));
580 fw.write(XML_INDENT.getBytes(UTF_8));
581 fw.flush();
582 }
583
584 private static final class EncodingOutputStream extends FilterOutputStream {
585 private int c1;
586
587 private int c2;
588
589 EncodingOutputStream(OutputStream out) {
590 super(out);
591 }
592
593 OutputStream getUnderlying() {
594 return out;
595 }
596
597 private boolean isCdataEndBlock(int c) {
598 return c1 == ']' && c2 == ']' && c == '>';
599 }
600
601 @Override
602 public void write(int b) throws IOException {
603 if (isCdataEndBlock(b)) {
604 out.write(ByteConstantsHolder.CDATA_ESCAPE_STRING_BYTES);
605 } else if (isIllegalEscape(b)) {
606
607
608
609
610
611 out.write(ByteConstantsHolder.AMP_BYTES);
612 out.write(String.valueOf(b).getBytes(UTF_8));
613 out.write(';');
614 } else {
615 out.write(b);
616 }
617 c1 = c2;
618 c2 = b;
619 }
620 }
621
622 private static boolean containsEscapesIllegalXml10(String message) {
623 int size = message.length();
624 for (int i = 0; i < size; i++) {
625 if (isIllegalEscape(message.charAt(i))) {
626 return true;
627 }
628 }
629 return false;
630 }
631
632 private static boolean isIllegalEscape(char c) {
633 return isIllegalEscape((int) c);
634 }
635
636 private static boolean isIllegalEscape(int c) {
637 return c >= 0 && c < 32 && c != '\n' && c != '\r' && c != '\t';
638 }
639
640
641
642
643
644
645
646
647 private static String escapeXml(String text, boolean attribute) {
648 StringBuilder sb = new StringBuilder(text.length() * 2);
649 for (int i = 0; i < text.length(); i++) {
650 char c = text.charAt(i);
651 if (isIllegalEscape(c)) {
652
653
654
655
656
657 sb.append(attribute ? "&#" : "&#")
658 .append((int) c)
659 .append(';');
660 } else {
661 sb.append(c);
662 }
663 }
664 return sb.toString();
665 }
666
667 private static final class ByteConstantsHolder {
668 private static final byte[] CDATA_START_BYTES = "<![CDATA[".getBytes(UTF_8);
669
670 private static final byte[] CDATA_END_BYTES = "]]>".getBytes(UTF_8);
671
672 private static final byte[] CDATA_ESCAPE_STRING_BYTES = "]]><![CDATA[>".getBytes(UTF_8);
673
674 private static final byte[] AMP_BYTES = "&#".getBytes(UTF_8);
675
676 private static final byte[] COMMENT_START = "<!-- ".getBytes(UTF_8);
677
678 private static final byte[] COMMENT_END = " --> ".getBytes(UTF_8);
679 }
680 }