View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
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.time.Instant;
30  import java.util.ArrayList;
31  import java.util.Deque;
32  import java.util.LinkedHashMap;
33  import java.util.List;
34  import java.util.Map;
35  import java.util.Map.Entry;
36  import java.util.StringTokenizer;
37  import java.util.concurrent.ConcurrentLinkedDeque;
38  
39  import org.apache.maven.plugin.surefire.booterclient.output.InPluginProcessDumpSingleton;
40  import org.apache.maven.surefire.api.report.SafeThrowable;
41  import org.apache.maven.surefire.extensions.StatelessReportEventListener;
42  import org.apache.maven.surefire.shared.utils.xml.PrettyPrintXMLWriter;
43  import org.apache.maven.surefire.shared.utils.xml.XMLWriter;
44  
45  import static java.nio.charset.StandardCharsets.UTF_8;
46  import static org.apache.maven.plugin.surefire.report.DefaultReporterFactory.TestResultType;
47  import static org.apache.maven.plugin.surefire.report.FileReporterUtils.stripIllegalFilenameChars;
48  import static org.apache.maven.plugin.surefire.report.ReportEntryType.SKIPPED;
49  import static org.apache.maven.plugin.surefire.report.ReportEntryType.SUCCESS;
50  import static org.apache.maven.surefire.shared.utils.StringUtils.isBlank;
51  import static org.apache.maven.surefire.shared.utils.StringUtils.isNotBlank;
52  
53  // CHECKSTYLE_OFF: LineLength
54  /**
55   * XML format reporter writing to <code>TEST-<i>reportName</i>[-<i>suffix</i>].xml</code> file like written and read
56   * by Ant's <a href="http://ant.apache.org/manual/Tasks/junit.html"><code>&lt;junit&gt;</code></a> and
57   * <a href="http://ant.apache.org/manual/Tasks/junitreport.html"><code>&lt;junitreport&gt;</code></a> tasks,
58   * then supported by many tools like CI servers.
59   * <br>
60   * <pre>&lt;?xml version="1.0" encoding="UTF-8"?&gt;
61   * &lt;testsuite name="<i>suite name</i>" [group="<i>group</i>"] tests="<i>0</i>" failures="<i>0</i>" errors="<i>0</i>" skipped="<i>0</i>" time="<i>{float}</i>"&gt;
62   *  &lt;properties&gt;
63   *    &lt;property name="<i>name</i>" value="<i>value</i>"/&gt;
64   *    [...]
65   *  &lt;/properties&gt;
66   *  &lt;testcase time="<i>{float}</i>" name="<i>test name</i> [classname="<i>class name</i>"] [group="<i>group</i>"]"/&gt;
67   *  &lt;testcase time="<i>{float}</i>" name="<i>test name</i> [classname="<i>class name</i>"] [group="<i>group</i>"]"&gt;
68   *    &lt;<b>error</b> message="<i>message</i>" type="<i>exception class name</i>"&gt;<i>stacktrace</i>&lt;/error&gt;
69   *    &lt;system-out&gt;<i>system out content (present only if not empty)</i>&lt;/system-out&gt;
70   *    &lt;system-err&gt;<i>system err content (present only if not empty)</i>&lt;/system-err&gt;
71   *  &lt;/testcase&gt;
72   *  &lt;testcase time="<i>{float}</i>" name="<i>test name</i> [classname="<i>class name</i>"] [group="<i>group</i>"]"&gt;
73   *    &lt;<b>failure</b> message="<i>message</i>" type="<i>exception class name</i>"&gt;<i>stacktrace</i>&lt;/failure&gt;
74   *    &lt;system-out&gt;<i>system out content (present only if not empty)</i>&lt;/system-out&gt;
75   *    &lt;system-err&gt;<i>system err content (present only if not empty)</i>&lt;/system-err&gt;
76   *  &lt;/testcase&gt;
77   *  &lt;testcase time="<i>{float}</i>" name="<i>test name</i> [classname="<i>class name</i>"] [group="<i>group</i>"]"&gt;
78   *    &lt;<b>skipped</b>/&gt;
79   *  &lt;/testcase&gt;
80   *  [...]</pre>
81   *
82   * @author Kristian Rosenvold
83   * @see <a href="https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=115528872">Ant's format enhancement proposal</a>
84   *      (not yet implemented by Ant 1.8.2)
85   * @see <a href="https://github.com/apache/ant/blob/6f68fbdefad521741b37304cfdfc6d8646cf1839/src/main/org/apache/tools/ant/taskdefs/optional/junit/XMLJUnitResultFormatter.java">Ant's <code>XMLJUnitResultFormatter</code></a>
86   */
87  @SuppressWarnings({"javadoc", "checkstyle:javadoctype"})
88  // TODO this is no more stateless due to existence of testClassMethodRunHistoryMap since of 2.19.
89  public class StatelessXmlReporter implements StatelessReportEventListener<WrappedReportEntry, TestSetStats> {
90      private static final float ONE_SECOND = 1000.0f;
91  
92      private static final String XML_INDENT = "  ";
93  
94      private static final String XML_NL = "\n";
95  
96      private final File reportsDirectory;
97  
98      private final String reportNameSuffix;
99  
100     private final boolean trimStackTrace;
101 
102     private final int rerunFailingTestsCount;
103 
104     private final String xsdSchemaLocation;
105 
106     private final String xsdVersion;
107 
108     // Map between test class name and a map between test method name
109     // and the list of runs for each test method
110     private final Map<String, Deque<WrappedReportEntry>> testClassMethodRunHistoryMap;
111 
112     private final boolean phrasedFileName;
113 
114     private final boolean phrasedSuiteName;
115 
116     private final boolean phrasedClassName;
117 
118     private final boolean phrasedMethodName;
119 
120     private final boolean enableOutErrElements;
121 
122     private final boolean enablePropertiesElement;
123 
124     private final boolean reportTestTimestamp;
125 
126     /**
127      * @deprecated Prefer adding a new constructor that accepts a configuration object, e.g.
128      *             {@link org.apache.maven.surefire.extensions.StatelessReportMojoConfiguration}.
129      */
130     @Deprecated
131     public StatelessXmlReporter(
132             File reportsDirectory,
133             String reportNameSuffix,
134             boolean trimStackTrace,
135             int rerunFailingTestsCount,
136             Map<String, Deque<WrappedReportEntry>> testClassMethodRunHistoryMap,
137             String xsdSchemaLocation,
138             String xsdVersion,
139             boolean phrasedFileName,
140             boolean phrasedSuiteName,
141             boolean phrasedClassName,
142             boolean phrasedMethodName,
143             boolean enableOutErrElements,
144             boolean enablePropertiesElement,
145             boolean reportTestTimestamp) {
146         this.reportsDirectory = reportsDirectory;
147         this.reportNameSuffix = reportNameSuffix;
148         this.trimStackTrace = trimStackTrace;
149         this.rerunFailingTestsCount = rerunFailingTestsCount;
150         this.testClassMethodRunHistoryMap = testClassMethodRunHistoryMap;
151         this.xsdSchemaLocation = xsdSchemaLocation;
152         this.xsdVersion = xsdVersion;
153         this.phrasedFileName = phrasedFileName;
154         this.phrasedSuiteName = phrasedSuiteName;
155         this.phrasedClassName = phrasedClassName;
156         this.phrasedMethodName = phrasedMethodName;
157         this.enableOutErrElements = enableOutErrElements;
158         this.enablePropertiesElement = enablePropertiesElement;
159         this.reportTestTimestamp = reportTestTimestamp;
160     }
161 
162     @Override
163     public void testSetCompleted(WrappedReportEntry testSetReportEntry, TestSetStats testSetStats) {
164         Map<String, Map<String, List<WrappedReportEntry>>> classMethodStatistics =
165                 arrangeMethodStatistics(testSetReportEntry, testSetStats);
166 
167         // The Java Language Spec:
168         // "Note that the close methods of resources are called in the opposite order of their creation."
169         try (OutputStream outputStream = getOutputStream(testSetReportEntry);
170                 OutputStreamWriter fw = getWriter(outputStream)) {
171             XMLWriter ppw = new PrettyPrintXMLWriter(new PrintWriter(fw), XML_INDENT, XML_NL, UTF_8.name(), null);
172 
173             createTestSuiteElement(ppw, testSetReportEntry, classMethodStatistics); // TestSuite
174 
175             if (enablePropertiesElement) {
176                 showProperties(ppw, testSetReportEntry.getSystemProperties());
177             } else {
178                 boolean hasNonSuccess = false;
179                 for (Map<String, List<WrappedReportEntry>> statistics : classMethodStatistics.values()) {
180                     for (List<WrappedReportEntry> thisMethodRuns : statistics.values()) {
181                         if (thisMethodRuns.stream()
182                                 .anyMatch(entry -> entry.getReportEntryType() != ReportEntryType.SUCCESS)) {
183                             hasNonSuccess = true;
184                             break;
185                         }
186                     }
187                     if (hasNonSuccess) {
188                         break;
189                     }
190                 }
191 
192                 if (hasNonSuccess) {
193                     showProperties(ppw, testSetReportEntry.getSystemProperties());
194                 }
195             }
196 
197             for (Entry<String, Map<String, List<WrappedReportEntry>>> statistics : classMethodStatistics.entrySet()) {
198                 Map<String, List<WrappedReportEntry>> methodStatistics = statistics.getValue();
199                 for (Entry<String, List<WrappedReportEntry>> thisMethodRuns : methodStatistics.entrySet()) {
200                     serializeTestClass(outputStream, fw, ppw, thisMethodRuns.getValue(), methodStatistics);
201                 }
202             }
203 
204             ppw.endElement(); // TestSuite
205         } catch (IOException e) {
206             // It's not a test error.
207             // This method must be sail-safe and errors are in a dump log.
208             // The control flow must not be broken in TestSetRunListener#testSetCompleted.
209             InPluginProcessDumpSingleton.getSingleton().dumpException(e, e.getLocalizedMessage(), reportsDirectory);
210         }
211     }
212 
213     private Map<String, Map<String, List<WrappedReportEntry>>> arrangeMethodStatistics(
214             WrappedReportEntry testSetReportEntry, TestSetStats testSetStats) {
215         Map<String, Map<String, List<WrappedReportEntry>>> classMethodStatistics = new LinkedHashMap<>();
216         for (WrappedReportEntry methodEntry : aggregateCacheFromMultipleReruns(testSetReportEntry, testSetStats)) {
217             String testClassName = methodEntry.getSourceName();
218             Map<String, List<WrappedReportEntry>> stats =
219                     classMethodStatistics.computeIfAbsent(testClassName, k -> new LinkedHashMap<>());
220             String methodName = methodEntry.getName();
221             List<WrappedReportEntry> methodRuns = stats.computeIfAbsent(methodName, k -> new ArrayList<>());
222             methodRuns.add(methodEntry);
223         }
224         return classMethodStatistics;
225     }
226 
227     private Deque<WrappedReportEntry> aggregateCacheFromMultipleReruns(
228             WrappedReportEntry testSetReportEntry, TestSetStats testSetStats) {
229         String suiteClassName = testSetReportEntry.getSourceName();
230         Deque<WrappedReportEntry> methodRunHistory = getAddMethodRunHistoryMap(suiteClassName);
231         methodRunHistory.addAll(testSetStats.getReportEntries());
232         return methodRunHistory;
233     }
234 
235     private void serializeTestClass(
236             OutputStream outputStream,
237             OutputStreamWriter fw,
238             XMLWriter ppw,
239             List<WrappedReportEntry> methodEntries,
240             Map<String, List<WrappedReportEntry>> methodStatistics)
241             throws IOException {
242         if (rerunFailingTestsCount > 0) {
243             serializeTestClassWithRerun(outputStream, fw, ppw, methodEntries, methodStatistics);
244         } else {
245             // rerunFailingTestsCount is smaller than 1, but for some reasons a test could be run
246             // for more than once
247             serializeTestClassWithoutRerun(outputStream, fw, ppw, methodEntries);
248         }
249     }
250 
251     private void serializeTestClassWithoutRerun(
252             OutputStream outputStream, OutputStreamWriter fw, XMLWriter ppw, List<WrappedReportEntry> methodEntries)
253             throws IOException {
254         for (WrappedReportEntry methodEntry : methodEntries) {
255             startTestElement(ppw, methodEntry);
256             if (methodEntry.getReportEntryType() != SUCCESS) {
257                 getTestProblems(
258                         fw,
259                         ppw,
260                         methodEntry,
261                         trimStackTrace,
262                         outputStream,
263                         methodEntry.getReportEntryType().getXmlTag(),
264                         false);
265             }
266             if (methodEntry.getReportEntryType() != SUCCESS || enableOutErrElements) {
267                 createOutErrElements(fw, ppw, methodEntry, outputStream);
268             }
269             ppw.endElement();
270         }
271     }
272 
273     private void serializeTestClassWithRerun(
274             OutputStream outputStream,
275             OutputStreamWriter fw,
276             XMLWriter ppw,
277             List<WrappedReportEntry> methodEntries,
278             Map<String, List<WrappedReportEntry>> methodStatistics)
279             throws IOException {
280         WrappedReportEntry firstMethodEntry = methodEntries.get(0);
281 
282         TestResultType resultType =
283                 getTestResultTypeWithBeforeAllHandling(firstMethodEntry.getName(), methodEntries, methodStatistics);
284 
285         switch (resultType) {
286             case SUCCESS:
287                 for (WrappedReportEntry methodEntry : methodEntries) {
288                     if (methodEntry.getReportEntryType() == SUCCESS) {
289                         startTestElement(ppw, methodEntry);
290                         ppw.endElement();
291                     }
292                 }
293                 break;
294             case ERROR:
295             case FAILURE:
296                 // When rerunFailingTestsCount is set to larger than 0
297                 startTestElement(ppw, firstMethodEntry);
298                 boolean firstRun = true;
299                 for (WrappedReportEntry singleRunEntry : methodEntries) {
300                     if (firstRun) {
301                         firstRun = false;
302                         getTestProblems(
303                                 fw,
304                                 ppw,
305                                 singleRunEntry,
306                                 trimStackTrace,
307                                 outputStream,
308                                 singleRunEntry.getReportEntryType().getXmlTag(),
309                                 false);
310                         createOutErrElements(fw, ppw, singleRunEntry, outputStream);
311                     } else if (singleRunEntry.getReportEntryType() == SKIPPED) {
312                         // The version 3.1.0 should produce a new XSD schema with version 3.1.0, see SUREFIRE-1986,
313                         // and the XSD schema should add a new element "rerunSkipped"
314                         // then ReportEntryType should update the enum to SKIPPED( "skipped", "", "rerunSkipped" ).
315                         // The teams should be notified - Jenkins reports.
316                         addCommentElementTestCase("a skipped test execution in re-run phase", fw, ppw, outputStream);
317                     } else {
318                         getTestProblems(
319                                 fw,
320                                 ppw,
321                                 singleRunEntry,
322                                 trimStackTrace,
323                                 outputStream,
324                                 singleRunEntry.getReportEntryType().getRerunXmlTag(),
325                                 true);
326                     }
327                 }
328                 ppw.endElement();
329                 break;
330             case FLAKE:
331                 WrappedReportEntry successful = null;
332                 // Get the run time of the first successful run
333                 for (WrappedReportEntry singleRunEntry : methodEntries) {
334                     if (singleRunEntry.getReportEntryType() == SUCCESS) {
335                         successful = singleRunEntry;
336                         break;
337                     }
338                 }
339                 WrappedReportEntry firstOrSuccessful = successful == null ? methodEntries.get(0) : successful;
340                 startTestElement(ppw, firstOrSuccessful);
341                 for (WrappedReportEntry singleRunEntry : methodEntries) {
342                     if (singleRunEntry.getReportEntryType() != SUCCESS) {
343                         getTestProblems(
344                                 fw,
345                                 ppw,
346                                 singleRunEntry,
347                                 trimStackTrace,
348                                 outputStream,
349                                 singleRunEntry.getReportEntryType().getFlakyXmlTag(),
350                                 true);
351                     }
352                 }
353                 ppw.endElement();
354                 break;
355             case SKIPPED:
356                 startTestElement(ppw, firstMethodEntry);
357                 getTestProblems(
358                         fw,
359                         ppw,
360                         firstMethodEntry,
361                         trimStackTrace,
362                         outputStream,
363                         firstMethodEntry.getReportEntryType().getXmlTag(),
364                         false);
365                 ppw.endElement();
366                 break;
367             default:
368                 throw new IllegalStateException("Get unknown test result type");
369         }
370     }
371 
372     /**
373      * Clean testClassMethodRunHistoryMap.
374      */
375     public void cleanTestHistoryMap() {
376         testClassMethodRunHistoryMap.clear();
377     }
378 
379     /**
380      * Get the result of a test from a list of its runs in WrappedReportEntry.
381      *
382      * @param methodEntryList the list of runs for a given test
383      * @return the TestResultType for the given test
384      */
385     private TestResultType getTestResultType(List<WrappedReportEntry> methodEntryList) {
386         List<ReportEntryType> testResultTypeList = new ArrayList<>();
387         for (WrappedReportEntry singleRunEntry : methodEntryList) {
388             testResultTypeList.add(singleRunEntry.getReportEntryType());
389         }
390 
391         return DefaultReporterFactory.getTestResultType(testResultTypeList, rerunFailingTestsCount);
392     }
393 
394     /**
395      * Determines the final result type for a test method, applying special handling for @BeforeAll failures.
396      * If a @BeforeAll fails but any actual test methods succeed, it's classified as a FLAKE.
397      *
398      * @param methodName the name of the test method (null or "null" for @BeforeAll)
399      * @param methodRuns the list of runs for this method
400      * @param methodStatistics all method statistics for the test class
401      * @return the final TestResultType
402      */
403     private TestResultType getTestResultTypeWithBeforeAllHandling(
404             String methodName,
405             List<WrappedReportEntry> methodRuns,
406             Map<String, List<WrappedReportEntry>> methodStatistics) {
407         TestResultType resultType = getTestResultType(methodRuns);
408 
409         // Special handling for @BeforeAll failures (null method name or method name is "null")
410         // If @BeforeAll failed but any actual test methods succeeded, treat it as a flake
411         if ((methodName == null || methodName.equals("null"))
412                 && (resultType == TestResultType.ERROR || resultType == TestResultType.FAILURE)) {
413             // Check if any actual test methods (non-null and not "null" names) succeeded
414             boolean hasSuccessfulTestMethods = methodStatistics.entrySet().stream()
415                     .filter(entry ->
416                             entry.getKey() != null && !entry.getKey().equals("null")) // Only actual test methods
417                     .anyMatch(entry -> entry.getValue().stream()
418                             .anyMatch(reportEntry -> reportEntry.getReportEntryType() == SUCCESS));
419 
420             if (hasSuccessfulTestMethods) {
421                 resultType = TestResultType.FLAKE;
422             }
423         }
424 
425         return resultType;
426     }
427 
428     private Deque<WrappedReportEntry> getAddMethodRunHistoryMap(String testClassName) {
429         Deque<WrappedReportEntry> methodRunHistory = testClassMethodRunHistoryMap.get(testClassName);
430         if (methodRunHistory == null) {
431             methodRunHistory = new ConcurrentLinkedDeque<>();
432             testClassMethodRunHistoryMap.put(testClassName == null ? "null" : testClassName, methodRunHistory);
433         }
434         return methodRunHistory;
435     }
436 
437     private OutputStream getOutputStream(WrappedReportEntry testSetReportEntry) throws IOException {
438         File reportFile = getReportFile(testSetReportEntry);
439         File reportDir = reportFile.getParentFile();
440         //noinspection ResultOfMethodCallIgnored
441         reportFile.delete();
442         //noinspection ResultOfMethodCallIgnored
443         reportDir.mkdirs();
444         return new BufferedOutputStream(Files.newOutputStream(reportFile.toPath()), 64 * 1024);
445     }
446 
447     private static OutputStreamWriter getWriter(OutputStream fos) {
448         return new OutputStreamWriter(fos, UTF_8);
449     }
450 
451     private File getReportFile(WrappedReportEntry report) {
452         String reportName = "TEST-" + (phrasedFileName ? report.getReportSourceName() : report.getSourceName());
453         String customizedReportName = isBlank(reportNameSuffix) ? reportName : reportName + "-" + reportNameSuffix;
454         return new File(reportsDirectory, stripIllegalFilenameChars(customizedReportName + ".xml"));
455     }
456 
457     private void startTestElement(XMLWriter ppw, WrappedReportEntry report) throws IOException {
458         ppw.startElement("testcase");
459         String name = phrasedMethodName ? report.getReportName() : report.getName();
460         ppw.addAttribute("name", name == null ? "" : extraEscapeAttribute(name));
461 
462         if (report.getGroup() != null) {
463             ppw.addAttribute("group", report.getGroup());
464         }
465 
466         String className = phrasedClassName
467                 ? report.getReportSourceName(reportNameSuffix)
468                 : report.getSourceText() != null ? report.getSourceText() : report.getSourceName(reportNameSuffix);
469         if (className != null) {
470             ppw.addAttribute("classname", extraEscapeAttribute(className));
471         }
472 
473         if (report.getElapsed() != null) {
474             ppw.addAttribute("time", String.valueOf(report.getElapsed() / ONE_SECOND));
475         }
476 
477         if (reportTestTimestamp && report.getStartTime() > 0L) {
478             ppw.addAttribute(
479                     "timestamp", Instant.ofEpochMilli(report.getStartTime()).toString());
480         }
481     }
482 
483     private void createTestSuiteElement(
484             XMLWriter ppw,
485             WrappedReportEntry report,
486             Map<String, Map<String, List<WrappedReportEntry>>> classMethodStatistics)
487             throws IOException {
488         ppw.startElement("testsuite");
489 
490         ppw.addAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
491         ppw.addAttribute("xsi:noNamespaceSchemaLocation", xsdSchemaLocation);
492         ppw.addAttribute("version", xsdVersion);
493 
494         String reportName = phrasedSuiteName
495                 ? report.getReportSourceName(reportNameSuffix)
496                 : report.getSourceName(reportNameSuffix);
497         ppw.addAttribute("name", reportName == null ? "" : extraEscapeAttribute(reportName));
498 
499         if (report.getGroup() != null) {
500             ppw.addAttribute("group", report.getGroup());
501         }
502 
503         if (report.getElapsed() != null) {
504             ppw.addAttribute("time", String.valueOf(report.getElapsed() / ONE_SECOND));
505         }
506 
507         if (reportTestTimestamp && report.getStartTime() > 0L) {
508             ppw.addAttribute(
509                     "timestamp", Instant.ofEpochMilli(report.getStartTime()).toString());
510         }
511 
512         // Count actual unique test methods and their final results from classMethodStatistics (accumulated across
513         // reruns)
514         int actualTestCount = 0;
515         int errors = 0;
516         int failures = 0;
517         int skipped = 0;
518         int flakes = 0;
519 
520         for (Map<String, List<WrappedReportEntry>> methodStats : classMethodStatistics.values()) {
521             actualTestCount += methodStats.size();
522             for (Map.Entry<String, List<WrappedReportEntry>> methodEntry : methodStats.entrySet()) {
523                 String methodName = methodEntry.getKey();
524                 List<WrappedReportEntry> methodRuns = methodEntry.getValue();
525                 TestResultType resultType = getTestResultTypeWithBeforeAllHandling(methodName, methodRuns, methodStats);
526 
527                 switch (resultType) {
528                     case ERROR:
529                         errors++;
530                         break;
531                     case FAILURE:
532                         failures++;
533                         break;
534                     case SKIPPED:
535                         skipped++;
536                         break;
537                     case FLAKE:
538                         flakes++;
539                         break;
540                     case SUCCESS:
541                     default:
542                         break;
543                 }
544             }
545         }
546 
547         ppw.addAttribute("tests", String.valueOf(actualTestCount));
548         ppw.addAttribute("errors", String.valueOf(errors));
549         ppw.addAttribute("skipped", String.valueOf(skipped));
550         ppw.addAttribute("failures", String.valueOf(failures));
551         ppw.addAttribute("flakes", String.valueOf(flakes));
552     }
553 
554     private static void getTestProblems(
555             OutputStreamWriter outputStreamWriter,
556             XMLWriter ppw,
557             WrappedReportEntry report,
558             boolean trimStackTrace,
559             OutputStream fw,
560             String testErrorType,
561             boolean createNestedOutErrElements)
562             throws IOException {
563         ppw.startElement(testErrorType);
564 
565         String stackTrace = report.getStackTrace(trimStackTrace);
566 
567         if (report.getMessage() != null && !report.getMessage().isEmpty()) {
568             ppw.addAttribute("message", extraEscapeAttribute(report.getMessage()));
569         }
570 
571         if (report.getStackTraceWriter() != null) {
572             //noinspection ThrowableResultOfMethodCallIgnored
573             SafeThrowable t = report.getStackTraceWriter().getThrowable();
574             if (t != null) {
575                 if (t.getMessage() != null) {
576                     int delimiter = stackTrace.indexOf(":");
577                     String type = delimiter == -1 ? stackTrace : stackTrace.substring(0, delimiter);
578                     ppw.addAttribute("type", type);
579                 } else {
580                     if (isNotBlank(stackTrace)) {
581                         ppw.addAttribute("type", new StringTokenizer(stackTrace).nextToken());
582                     }
583                 }
584             }
585         }
586 
587         /* This structure is inconsistent due to bad legacy design choices for the XML schema.
588          * Ideally, all elements would be complex and strackTrace would have its own element.
589          * See SUREFIRE-2230 for details to how improve and unify the schema in the future.
590          */
591         if (createNestedOutErrElements) {
592             ppw.startElement("stackTrace");
593             if (stackTrace != null) {
594                 extraEscapeElementValue(stackTrace, outputStreamWriter, ppw, fw);
595             }
596             ppw.endElement();
597 
598             createOutErrElements(outputStreamWriter, ppw, report, fw);
599         } else if (stackTrace != null) {
600             extraEscapeElementValue(stackTrace, outputStreamWriter, ppw, fw);
601         }
602 
603         ppw.endElement(); // entry type
604     }
605 
606     // Create system-out and system-err elements
607     private static void createOutErrElements(
608             OutputStreamWriter outputStreamWriter, XMLWriter ppw, WrappedReportEntry report, OutputStream fw)
609             throws IOException {
610         EncodingOutputStream eos = new EncodingOutputStream(fw);
611         addOutputStreamElement(outputStreamWriter, eos, ppw, report.getStdout(), "system-out");
612         addOutputStreamElement(outputStreamWriter, eos, ppw, report.getStdErr(), "system-err");
613     }
614 
615     private static void addOutputStreamElement(
616             OutputStreamWriter outputStreamWriter,
617             EncodingOutputStream eos,
618             XMLWriter xmlWriter,
619             Utf8RecodingDeferredFileOutputStream utf8RecodingDeferredFileOutputStream,
620             String name)
621             throws IOException {
622         if (utf8RecodingDeferredFileOutputStream != null && utf8RecodingDeferredFileOutputStream.getByteCount() > 0) {
623             xmlWriter.startElement(name);
624             xmlWriter.writeText(""); // Cheat sax to emit element
625             outputStreamWriter.flush();
626             eos.getUnderlying().write(ByteConstantsHolder.CDATA_START_BYTES); // emit cdata
627             utf8RecodingDeferredFileOutputStream.writeTo(eos);
628             utf8RecodingDeferredFileOutputStream.free();
629             eos.getUnderlying().write(ByteConstantsHolder.CDATA_END_BYTES);
630             eos.flush();
631             xmlWriter.endElement();
632         }
633     }
634 
635     /**
636      * Adds system properties to the XML report.
637      * <br>
638      *
639      * @param xmlWriter the test suite to report to
640      */
641     private static void showProperties(XMLWriter xmlWriter, Map<String, String> systemProperties) throws IOException {
642         xmlWriter.startElement("properties");
643         for (final Entry<String, String> entry : systemProperties.entrySet()) {
644             final String key = entry.getKey();
645             String value = entry.getValue();
646 
647             if (value == null) {
648                 value = "null";
649             }
650 
651             xmlWriter.startElement("property");
652 
653             xmlWriter.addAttribute("name", key);
654 
655             xmlWriter.addAttribute("value", extraEscapeAttribute(value));
656 
657             xmlWriter.endElement();
658         }
659         xmlWriter.endElement();
660     }
661 
662     /**
663      * Handle stuff that may pop up in java that is not legal in xml.
664      *
665      * @param message   the string
666      * @return the escaped string or returns itself if all characters are legal
667      */
668     private static String extraEscapeAttribute(String message) {
669         // Someday convert to xml 1.1 which handles everything but 0 inside string
670         return containsEscapesIllegalXml10(message) ? escapeXml(message, true) : message;
671     }
672 
673     /**
674      * Writes escaped string or the message within CDATA if all characters are legal.
675      *
676      * @param message   the string
677      */
678     private static void extraEscapeElementValue(
679             String message, OutputStreamWriter outputStreamWriter, XMLWriter xmlWriter, OutputStream fw)
680             throws IOException {
681         // Someday convert to xml 1.1 which handles everything but 0 inside string
682         if (containsEscapesIllegalXml10(message)) {
683             xmlWriter.writeText(escapeXml(message, false));
684         } else {
685             EncodingOutputStream eos = new EncodingOutputStream(fw);
686             xmlWriter.writeText(""); // Cheat sax to emit element
687             outputStreamWriter.flush();
688             eos.getUnderlying().write(ByteConstantsHolder.CDATA_START_BYTES);
689             eos.write(message.getBytes(UTF_8));
690             eos.getUnderlying().write(ByteConstantsHolder.CDATA_END_BYTES);
691             eos.flush();
692         }
693     }
694 
695     // todo: SUREFIRE-1986
696     private static void addCommentElementTestCase(
697             String comment, OutputStreamWriter outputStreamWriter, XMLWriter xmlWriter, OutputStream fw)
698             throws IOException {
699         xmlWriter.writeText(""); // Cheat sax to emit element
700         outputStreamWriter.flush();
701         fw.write(XML_NL.getBytes(UTF_8));
702         fw.write(XML_INDENT.getBytes(UTF_8));
703         fw.write(XML_INDENT.getBytes(UTF_8));
704         fw.write(ByteConstantsHolder.COMMENT_START);
705         fw.write(comment.getBytes(UTF_8));
706         fw.write(ByteConstantsHolder.COMMENT_END);
707         fw.write(XML_NL.getBytes(UTF_8));
708         fw.write(XML_INDENT.getBytes(UTF_8));
709         fw.flush();
710     }
711 
712     private static final class EncodingOutputStream extends FilterOutputStream {
713         private int c1;
714 
715         private int c2;
716 
717         EncodingOutputStream(OutputStream out) {
718             super(out);
719         }
720 
721         OutputStream getUnderlying() {
722             return out;
723         }
724 
725         private boolean isCdataEndBlock(int c) {
726             return c1 == ']' && c2 == ']' && c == '>';
727         }
728 
729         @Override
730         public void write(int b) throws IOException {
731             if (isCdataEndBlock(b)) {
732                 out.write(ByteConstantsHolder.CDATA_ESCAPE_STRING_BYTES);
733             } else if (isIllegalEscape(b)) {
734                 // uh-oh!  This character is illegal in XML 1.0!
735                 // http://www.w3.org/TR/1998/REC-xml-19980210#charsets
736                 // we're going to deliberately doubly-XML escape it...
737                 // there's nothing better we can do! :-(
738                 // SUREFIRE-456
739                 out.write(ByteConstantsHolder.AMP_BYTES);
740                 out.write(String.valueOf(b).getBytes(UTF_8));
741                 out.write(';'); // & Will be encoded to amp inside xml encodingSHO
742             } else {
743                 out.write(b);
744             }
745             c1 = c2;
746             c2 = b;
747         }
748     }
749 
750     private static boolean containsEscapesIllegalXml10(String message) {
751         int size = message.length();
752         for (int i = 0; i < size; i++) {
753             if (isIllegalEscape(message.charAt(i))) {
754                 return true;
755             }
756         }
757         return false;
758     }
759 
760     private static boolean isIllegalEscape(char c) {
761         return isIllegalEscape((int) c);
762     }
763 
764     private static boolean isIllegalEscape(int c) {
765         return c >= 0 && c < 32 && c != '\n' && c != '\r' && c != '\t';
766     }
767 
768     /**
769      * Escape for XML 1.0.
770      *
771      * @param text      the string
772      * @param attribute true if the escaped value is inside an attribute
773      * @return the escaped string
774      */
775     private static String escapeXml(String text, boolean attribute) {
776         StringBuilder sb = new StringBuilder(text.length() * 2);
777         for (int i = 0; i < text.length(); i++) {
778             char c = text.charAt(i);
779             if (isIllegalEscape(c)) {
780                 // uh-oh!  This character is illegal in XML 1.0!
781                 // http://www.w3.org/TR/1998/REC-xml-19980210#charsets
782                 // we're going to deliberately doubly-XML escape it...
783                 // there's nothing better we can do! :-(
784                 // SUREFIRE-456
785                 sb.append(attribute ? "&#" : "&amp#")
786                         .append((int) c)
787                         .append(';'); // & Will be encoded to amp inside xml encodingSHO
788             } else {
789                 sb.append(c);
790             }
791         }
792         return sb.toString();
793     }
794 
795     private static final class ByteConstantsHolder {
796         private static final byte[] CDATA_START_BYTES = "<![CDATA[".getBytes(UTF_8);
797 
798         private static final byte[] CDATA_END_BYTES = "]]>".getBytes(UTF_8);
799 
800         private static final byte[] CDATA_ESCAPE_STRING_BYTES = "]]><![CDATA[>".getBytes(UTF_8);
801 
802         private static final byte[] AMP_BYTES = "&amp#".getBytes(UTF_8);
803 
804         private static final byte[] COMMENT_START = "<!-- ".getBytes(UTF_8);
805 
806         private static final byte[] COMMENT_END = " --> ".getBytes(UTF_8);
807     }
808 }