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.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  // CHECKSTYLE_OFF: LineLength
53  /**
54   * XML format reporter writing to <code>TEST-<i>reportName</i>[-<i>suffix</i>].xml</code> file like written and read
55   * by Ant's <a href="http://ant.apache.org/manual/Tasks/junit.html"><code>&lt;junit&gt;</code></a> and
56   * <a href="http://ant.apache.org/manual/Tasks/junitreport.html"><code>&lt;junitreport&gt;</code></a> tasks,
57   * then supported by many tools like CI servers.
58   * <br>
59   * <pre>&lt;?xml version="1.0" encoding="UTF-8"?&gt;
60   * &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;
61   *  &lt;properties&gt;
62   *    &lt;property name="<i>name</i>" value="<i>value</i>"/&gt;
63   *    [...]
64   *  &lt;/properties&gt;
65   *  &lt;testcase time="<i>{float}</i>" name="<i>test name</i> [classname="<i>class name</i>"] [group="<i>group</i>"]"/&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;<b>error</b> message="<i>message</i>" type="<i>exception class name</i>"&gt;<i>stacktrace</i>&lt;/error&gt;
68   *    &lt;system-out&gt;<i>system out content (present only if not empty)</i>&lt;/system-out&gt;
69   *    &lt;system-err&gt;<i>system err content (present only if not empty)</i>&lt;/system-err&gt;
70   *  &lt;/testcase&gt;
71   *  &lt;testcase time="<i>{float}</i>" name="<i>test name</i> [classname="<i>class name</i>"] [group="<i>group</i>"]"&gt;
72   *    &lt;<b>failure</b> message="<i>message</i>" type="<i>exception class name</i>"&gt;<i>stacktrace</i>&lt;/failure&gt;
73   *    &lt;system-out&gt;<i>system out content (present only if not empty)</i>&lt;/system-out&gt;
74   *    &lt;system-err&gt;<i>system err content (present only if not empty)</i>&lt;/system-err&gt;
75   *  &lt;/testcase&gt;
76   *  &lt;testcase time="<i>{float}</i>" name="<i>test name</i> [classname="<i>class name</i>"] [group="<i>group</i>"]"&gt;
77   *    &lt;<b>skipped</b>/&gt;
78   *  &lt;/testcase&gt;
79   *  [...]</pre>
80   *
81   * @author Kristian Rosenvold
82   * @see <a href="https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=115528872">Ant's format enhancement proposal</a>
83   *      (not yet implemented by Ant 1.8.2)
84   * @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>
85   */
86  @SuppressWarnings({"javadoc", "checkstyle:javadoctype"})
87  // TODO this is no more stateless due to existence of testClassMethodRunHistoryMap since of 2.19.
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     // Map between test class name and a map between test method name
108     // and the list of runs for each test method
109     private final Map<String, Deque<WrappedReportEntry>> testClassMethodRunHistoryMap;
110 
111     private final boolean phrasedFileName;
112 
113     private final boolean phrasedSuiteName;
114 
115     private final boolean phrasedClassName;
116 
117     private final boolean phrasedMethodName;
118 
119     private final boolean enableOutErrElements;
120 
121     private final boolean enablePropertiesElement;
122 
123     public StatelessXmlReporter(
124             File reportsDirectory,
125             String reportNameSuffix,
126             boolean trimStackTrace,
127             int rerunFailingTestsCount,
128             Map<String, Deque<WrappedReportEntry>> testClassMethodRunHistoryMap,
129             String xsdSchemaLocation,
130             String xsdVersion,
131             boolean phrasedFileName,
132             boolean phrasedSuiteName,
133             boolean phrasedClassName,
134             boolean phrasedMethodName,
135             boolean enableOutErrElements,
136             boolean enablePropertiesElement) {
137         this.reportsDirectory = reportsDirectory;
138         this.reportNameSuffix = reportNameSuffix;
139         this.trimStackTrace = trimStackTrace;
140         this.rerunFailingTestsCount = rerunFailingTestsCount;
141         this.testClassMethodRunHistoryMap = testClassMethodRunHistoryMap;
142         this.xsdSchemaLocation = xsdSchemaLocation;
143         this.xsdVersion = xsdVersion;
144         this.phrasedFileName = phrasedFileName;
145         this.phrasedSuiteName = phrasedSuiteName;
146         this.phrasedClassName = phrasedClassName;
147         this.phrasedMethodName = phrasedMethodName;
148         this.enableOutErrElements = enableOutErrElements;
149         this.enablePropertiesElement = enablePropertiesElement;
150     }
151 
152     @Override
153     public void testSetCompleted(WrappedReportEntry testSetReportEntry, TestSetStats testSetStats) {
154         Map<String, Map<String, List<WrappedReportEntry>>> classMethodStatistics =
155                 arrangeMethodStatistics(testSetReportEntry, testSetStats);
156 
157         // The Java Language Spec:
158         // "Note that the close methods of resources are called in the opposite order of their creation."
159         try (OutputStream outputStream = getOutputStream(testSetReportEntry);
160                 OutputStreamWriter fw = getWriter(outputStream)) {
161             XMLWriter ppw = new PrettyPrintXMLWriter(new PrintWriter(fw), XML_INDENT, XML_NL, UTF_8.name(), null);
162 
163             createTestSuiteElement(ppw, testSetReportEntry, testSetStats); // TestSuite
164 
165             if (enablePropertiesElement) {
166                 showProperties(ppw, testSetReportEntry.getSystemProperties());
167             } else {
168                 boolean hasNonSuccess = false;
169                 for (Map<String, List<WrappedReportEntry>> statistics : classMethodStatistics.values()) {
170                     for (List<WrappedReportEntry> thisMethodRuns : statistics.values()) {
171                         if (thisMethodRuns.stream()
172                                 .anyMatch(entry -> entry.getReportEntryType() != ReportEntryType.SUCCESS)) {
173                             hasNonSuccess = true;
174                             break;
175                         }
176                     }
177                     if (hasNonSuccess) {
178                         break;
179                     }
180                 }
181 
182                 if (hasNonSuccess) {
183                     showProperties(ppw, testSetReportEntry.getSystemProperties());
184                 }
185             }
186 
187             for (Entry<String, Map<String, List<WrappedReportEntry>>> statistics : classMethodStatistics.entrySet()) {
188                 for (Entry<String, List<WrappedReportEntry>> thisMethodRuns :
189                         statistics.getValue().entrySet()) {
190                     serializeTestClass(outputStream, fw, ppw, thisMethodRuns.getValue());
191                 }
192             }
193 
194             ppw.endElement(); // TestSuite
195         } catch (IOException e) {
196             // It's not a test error.
197             // This method must be sail-safe and errors are in a dump log.
198             // The control flow must not be broken in TestSetRunListener#testSetCompleted.
199             InPluginProcessDumpSingleton.getSingleton().dumpException(e, e.getLocalizedMessage(), reportsDirectory);
200         }
201     }
202 
203     private Map<String, Map<String, List<WrappedReportEntry>>> arrangeMethodStatistics(
204             WrappedReportEntry testSetReportEntry, TestSetStats testSetStats) {
205         Map<String, Map<String, List<WrappedReportEntry>>> classMethodStatistics = new LinkedHashMap<>();
206         for (WrappedReportEntry methodEntry : aggregateCacheFromMultipleReruns(testSetReportEntry, testSetStats)) {
207             String testClassName = methodEntry.getSourceName();
208             Map<String, List<WrappedReportEntry>> stats = classMethodStatistics.get(testClassName);
209             if (stats == null) {
210                 stats = new LinkedHashMap<>();
211                 classMethodStatistics.put(testClassName, stats);
212             }
213             String methodName = methodEntry.getName();
214             List<WrappedReportEntry> methodRuns = stats.get(methodName);
215             if (methodRuns == null) {
216                 methodRuns = new ArrayList<>();
217                 stats.put(methodName, methodRuns);
218             }
219             methodRuns.add(methodEntry);
220         }
221         return classMethodStatistics;
222     }
223 
224     private Deque<WrappedReportEntry> aggregateCacheFromMultipleReruns(
225             WrappedReportEntry testSetReportEntry, TestSetStats testSetStats) {
226         String suiteClassName = testSetReportEntry.getSourceName();
227         Deque<WrappedReportEntry> methodRunHistory = getAddMethodRunHistoryMap(suiteClassName);
228         methodRunHistory.addAll(testSetStats.getReportEntries());
229         return methodRunHistory;
230     }
231 
232     private void serializeTestClass(
233             OutputStream outputStream, OutputStreamWriter fw, XMLWriter ppw, List<WrappedReportEntry> methodEntries)
234             throws IOException {
235         if (rerunFailingTestsCount > 0) {
236             serializeTestClassWithRerun(outputStream, fw, ppw, methodEntries);
237         } else {
238             // rerunFailingTestsCount is smaller than 1, but for some reasons a test could be run
239             // for more than once
240             serializeTestClassWithoutRerun(outputStream, fw, ppw, methodEntries);
241         }
242     }
243 
244     private void serializeTestClassWithoutRerun(
245             OutputStream outputStream, OutputStreamWriter fw, XMLWriter ppw, List<WrappedReportEntry> methodEntries)
246             throws IOException {
247         for (WrappedReportEntry methodEntry : methodEntries) {
248             startTestElement(ppw, methodEntry);
249             if (methodEntry.getReportEntryType() != SUCCESS) {
250                 getTestProblems(
251                         fw,
252                         ppw,
253                         methodEntry,
254                         trimStackTrace,
255                         outputStream,
256                         methodEntry.getReportEntryType().getXmlTag(),
257                         false);
258             }
259             if (methodEntry.getReportEntryType() != SUCCESS || enableOutErrElements) {
260                 createOutErrElements(fw, ppw, methodEntry, outputStream);
261             }
262             ppw.endElement();
263         }
264     }
265 
266     private void serializeTestClassWithRerun(
267             OutputStream outputStream, OutputStreamWriter fw, XMLWriter ppw, List<WrappedReportEntry> methodEntries)
268             throws IOException {
269         WrappedReportEntry firstMethodEntry = methodEntries.get(0);
270         switch (getTestResultType(methodEntries)) {
271             case SUCCESS:
272                 for (WrappedReportEntry methodEntry : methodEntries) {
273                     if (methodEntry.getReportEntryType() == SUCCESS) {
274                         startTestElement(ppw, methodEntry);
275                         ppw.endElement();
276                     }
277                 }
278                 break;
279             case ERROR:
280             case FAILURE:
281                 // When rerunFailingTestsCount is set to larger than 0
282                 startTestElement(ppw, firstMethodEntry);
283                 boolean firstRun = true;
284                 for (WrappedReportEntry singleRunEntry : methodEntries) {
285                     if (firstRun) {
286                         firstRun = false;
287                         getTestProblems(
288                                 fw,
289                                 ppw,
290                                 singleRunEntry,
291                                 trimStackTrace,
292                                 outputStream,
293                                 singleRunEntry.getReportEntryType().getXmlTag(),
294                                 false);
295                         createOutErrElements(fw, ppw, singleRunEntry, outputStream);
296                     } else if (singleRunEntry.getReportEntryType() == SKIPPED) {
297                         // The version 3.1.0 should produce a new XSD schema with version 3.1.0, see SUREFIRE-1986,
298                         // and the XSD schema should add a new element "rerunSkipped"
299                         // then ReportEntryType should update the enum to SKIPPED( "skipped", "", "rerunSkipped" ).
300                         // The teams should be notified - Jenkins reports.
301                         addCommentElementTestCase("a skipped test execution in re-run phase", fw, ppw, outputStream);
302                     } else {
303                         getTestProblems(
304                                 fw,
305                                 ppw,
306                                 singleRunEntry,
307                                 trimStackTrace,
308                                 outputStream,
309                                 singleRunEntry.getReportEntryType().getRerunXmlTag(),
310                                 true);
311                     }
312                 }
313                 ppw.endElement();
314                 break;
315             case FLAKE:
316                 WrappedReportEntry successful = null;
317                 // Get the run time of the first successful run
318                 for (WrappedReportEntry singleRunEntry : methodEntries) {
319                     if (singleRunEntry.getReportEntryType() == SUCCESS) {
320                         successful = singleRunEntry;
321                         break;
322                     }
323                 }
324                 WrappedReportEntry firstOrSuccessful = successful == null ? methodEntries.get(0) : successful;
325                 startTestElement(ppw, firstOrSuccessful);
326                 for (WrappedReportEntry singleRunEntry : methodEntries) {
327                     if (singleRunEntry.getReportEntryType() != SUCCESS) {
328                         getTestProblems(
329                                 fw,
330                                 ppw,
331                                 singleRunEntry,
332                                 trimStackTrace,
333                                 outputStream,
334                                 singleRunEntry.getReportEntryType().getFlakyXmlTag(),
335                                 true);
336                     }
337                 }
338                 ppw.endElement();
339                 break;
340             case SKIPPED:
341                 startTestElement(ppw, firstMethodEntry);
342                 getTestProblems(
343                         fw,
344                         ppw,
345                         firstMethodEntry,
346                         trimStackTrace,
347                         outputStream,
348                         firstMethodEntry.getReportEntryType().getXmlTag(),
349                         false);
350                 ppw.endElement();
351                 break;
352             default:
353                 throw new IllegalStateException("Get unknown test result type");
354         }
355     }
356 
357     /**
358      * Clean testClassMethodRunHistoryMap
359      */
360     public void cleanTestHistoryMap() {
361         testClassMethodRunHistoryMap.clear();
362     }
363 
364     /**
365      * Get the result of a test from a list of its runs in WrappedReportEntry
366      *
367      * @param methodEntryList the list of runs for a given test
368      * @return the TestResultType for the given test
369      */
370     private TestResultType getTestResultType(List<WrappedReportEntry> methodEntryList) {
371         List<ReportEntryType> testResultTypeList = new ArrayList<>();
372         for (WrappedReportEntry singleRunEntry : methodEntryList) {
373             testResultTypeList.add(singleRunEntry.getReportEntryType());
374         }
375 
376         return DefaultReporterFactory.getTestResultType(testResultTypeList, rerunFailingTestsCount);
377     }
378 
379     private Deque<WrappedReportEntry> getAddMethodRunHistoryMap(String testClassName) {
380         Deque<WrappedReportEntry> methodRunHistory = testClassMethodRunHistoryMap.get(testClassName);
381         if (methodRunHistory == null) {
382             methodRunHistory = new ConcurrentLinkedDeque<>();
383             testClassMethodRunHistoryMap.put(testClassName == null ? "null" : testClassName, methodRunHistory);
384         }
385         return methodRunHistory;
386     }
387 
388     private OutputStream getOutputStream(WrappedReportEntry testSetReportEntry) throws IOException {
389         File reportFile = getReportFile(testSetReportEntry);
390         File reportDir = reportFile.getParentFile();
391         //noinspection ResultOfMethodCallIgnored
392         reportFile.delete();
393         //noinspection ResultOfMethodCallIgnored
394         reportDir.mkdirs();
395         return new BufferedOutputStream(new FileOutputStream(reportFile), 64 * 1024);
396     }
397 
398     private static OutputStreamWriter getWriter(OutputStream fos) {
399         return new OutputStreamWriter(fos, UTF_8);
400     }
401 
402     private File getReportFile(WrappedReportEntry report) {
403         String reportName = "TEST-" + (phrasedFileName ? report.getReportSourceName() : report.getSourceName());
404         String customizedReportName = isBlank(reportNameSuffix) ? reportName : reportName + "-" + reportNameSuffix;
405         return new File(reportsDirectory, stripIllegalFilenameChars(customizedReportName + ".xml"));
406     }
407 
408     private void startTestElement(XMLWriter ppw, WrappedReportEntry report) throws IOException {
409         ppw.startElement("testcase");
410         String name = phrasedMethodName ? report.getReportName() : report.getName();
411         ppw.addAttribute("name", name == null ? "" : extraEscapeAttribute(name));
412 
413         if (report.getGroup() != null) {
414             ppw.addAttribute("group", report.getGroup());
415         }
416 
417         String className = phrasedClassName
418                 ? report.getReportSourceName(reportNameSuffix)
419                 : report.getSourceName(reportNameSuffix);
420         if (className != null) {
421             ppw.addAttribute("classname", extraEscapeAttribute(className));
422         }
423 
424         if (report.getElapsed() != null) {
425             ppw.addAttribute("time", String.valueOf(report.getElapsed() / ONE_SECOND));
426         }
427     }
428 
429     private void createTestSuiteElement(XMLWriter ppw, WrappedReportEntry report, TestSetStats testSetStats)
430             throws IOException {
431         ppw.startElement("testsuite");
432 
433         ppw.addAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
434         ppw.addAttribute("xsi:noNamespaceSchemaLocation", xsdSchemaLocation);
435         ppw.addAttribute("version", xsdVersion);
436 
437         String reportName = phrasedSuiteName
438                 ? report.getReportSourceName(reportNameSuffix)
439                 : report.getSourceName(reportNameSuffix);
440         ppw.addAttribute("name", reportName == null ? "" : extraEscapeAttribute(reportName));
441 
442         if (report.getGroup() != null) {
443             ppw.addAttribute("group", report.getGroup());
444         }
445 
446         if (report.getElapsed() != null) {
447             ppw.addAttribute("time", String.valueOf(report.getElapsed() / ONE_SECOND));
448         }
449 
450         ppw.addAttribute("tests", String.valueOf(testSetStats.getCompletedCount()));
451 
452         ppw.addAttribute("errors", String.valueOf(testSetStats.getErrors()));
453 
454         ppw.addAttribute("skipped", String.valueOf(testSetStats.getSkipped()));
455 
456         ppw.addAttribute("failures", String.valueOf(testSetStats.getFailures()));
457     }
458 
459     private static void getTestProblems(
460             OutputStreamWriter outputStreamWriter,
461             XMLWriter ppw,
462             WrappedReportEntry report,
463             boolean trimStackTrace,
464             OutputStream fw,
465             String testErrorType,
466             boolean createNestedOutErrElements)
467             throws IOException {
468         ppw.startElement(testErrorType);
469 
470         String stackTrace = report.getStackTrace(trimStackTrace);
471 
472         if (report.getMessage() != null && !report.getMessage().isEmpty()) {
473             ppw.addAttribute("message", extraEscapeAttribute(report.getMessage()));
474         }
475 
476         if (report.getStackTraceWriter() != null) {
477             //noinspection ThrowableResultOfMethodCallIgnored
478             SafeThrowable t = report.getStackTraceWriter().getThrowable();
479             if (t != null) {
480                 if (t.getMessage() != null) {
481                     int delimiter = stackTrace.indexOf(":");
482                     String type = delimiter == -1 ? stackTrace : stackTrace.substring(0, delimiter);
483                     ppw.addAttribute("type", type);
484                 } else {
485                     if (isNotBlank(stackTrace)) {
486                         ppw.addAttribute("type", new StringTokenizer(stackTrace).nextToken());
487                     }
488                 }
489             }
490         }
491 
492         /* This structure is inconsistent due to bad legacy design choices for the XML schema.
493          * Ideally, all elements would be complex and strackTrace would have its own element.
494          * See SUREFIRE-2230 for details to how improve and unify the schema in the future.
495          */
496         if (createNestedOutErrElements) {
497             ppw.startElement("stackTrace");
498             if (stackTrace != null) {
499                 extraEscapeElementValue(stackTrace, outputStreamWriter, ppw, fw);
500             }
501             ppw.endElement();
502 
503             createOutErrElements(outputStreamWriter, ppw, report, fw);
504         } else if (stackTrace != null) {
505             extraEscapeElementValue(stackTrace, outputStreamWriter, ppw, fw);
506         }
507 
508         ppw.endElement(); // entry type
509     }
510 
511     // Create system-out and system-err elements
512     private static void createOutErrElements(
513             OutputStreamWriter outputStreamWriter, XMLWriter ppw, WrappedReportEntry report, OutputStream fw)
514             throws IOException {
515         EncodingOutputStream eos = new EncodingOutputStream(fw);
516         addOutputStreamElement(outputStreamWriter, eos, ppw, report.getStdout(), "system-out");
517         addOutputStreamElement(outputStreamWriter, eos, ppw, report.getStdErr(), "system-err");
518     }
519 
520     private static void addOutputStreamElement(
521             OutputStreamWriter outputStreamWriter,
522             EncodingOutputStream eos,
523             XMLWriter xmlWriter,
524             Utf8RecodingDeferredFileOutputStream utf8RecodingDeferredFileOutputStream,
525             String name)
526             throws IOException {
527         if (utf8RecodingDeferredFileOutputStream != null && utf8RecodingDeferredFileOutputStream.getByteCount() > 0) {
528             xmlWriter.startElement(name);
529             xmlWriter.writeText(""); // Cheat sax to emit element
530             outputStreamWriter.flush();
531             eos.getUnderlying().write(ByteConstantsHolder.CDATA_START_BYTES); // emit cdata
532             utf8RecodingDeferredFileOutputStream.writeTo(eos);
533             utf8RecodingDeferredFileOutputStream.free();
534             eos.getUnderlying().write(ByteConstantsHolder.CDATA_END_BYTES);
535             eos.flush();
536             xmlWriter.endElement();
537         }
538     }
539 
540     /**
541      * Adds system properties to the XML report.
542      * <br>
543      *
544      * @param xmlWriter The test suite to report to
545      */
546     private static void showProperties(XMLWriter xmlWriter, Map<String, String> systemProperties) throws IOException {
547         xmlWriter.startElement("properties");
548         for (final Entry<String, String> entry : systemProperties.entrySet()) {
549             final String key = entry.getKey();
550             String value = entry.getValue();
551 
552             if (value == null) {
553                 value = "null";
554             }
555 
556             xmlWriter.startElement("property");
557 
558             xmlWriter.addAttribute("name", key);
559 
560             xmlWriter.addAttribute("value", extraEscapeAttribute(value));
561 
562             xmlWriter.endElement();
563         }
564         xmlWriter.endElement();
565     }
566 
567     /**
568      * Handle stuff that may pop up in java that is not legal in xml.
569      *
570      * @param message   The string
571      * @return The escaped string or returns itself if all characters are legal
572      */
573     private static String extraEscapeAttribute(String message) {
574         // Someday convert to xml 1.1 which handles everything but 0 inside string
575         return containsEscapesIllegalXml10(message) ? escapeXml(message, true) : message;
576     }
577 
578     /**
579      * Writes escaped string or the message within CDATA if all characters are legal.
580      *
581      * @param message   The string
582      */
583     private static void extraEscapeElementValue(
584             String message, OutputStreamWriter outputStreamWriter, XMLWriter xmlWriter, OutputStream fw)
585             throws IOException {
586         // Someday convert to xml 1.1 which handles everything but 0 inside string
587         if (containsEscapesIllegalXml10(message)) {
588             xmlWriter.writeText(escapeXml(message, false));
589         } else {
590             EncodingOutputStream eos = new EncodingOutputStream(fw);
591             xmlWriter.writeText(""); // Cheat sax to emit element
592             outputStreamWriter.flush();
593             eos.getUnderlying().write(ByteConstantsHolder.CDATA_START_BYTES);
594             eos.write(message.getBytes(UTF_8));
595             eos.getUnderlying().write(ByteConstantsHolder.CDATA_END_BYTES);
596             eos.flush();
597         }
598     }
599 
600     // todo: SUREFIRE-1986
601     private static void addCommentElementTestCase(
602             String comment, OutputStreamWriter outputStreamWriter, XMLWriter xmlWriter, OutputStream fw)
603             throws IOException {
604         xmlWriter.writeText(""); // Cheat sax to emit element
605         outputStreamWriter.flush();
606         fw.write(XML_NL.getBytes(UTF_8));
607         fw.write(XML_INDENT.getBytes(UTF_8));
608         fw.write(XML_INDENT.getBytes(UTF_8));
609         fw.write(ByteConstantsHolder.COMMENT_START);
610         fw.write(comment.getBytes(UTF_8));
611         fw.write(ByteConstantsHolder.COMMENT_END);
612         fw.write(XML_NL.getBytes(UTF_8));
613         fw.write(XML_INDENT.getBytes(UTF_8));
614         fw.flush();
615     }
616 
617     private static final class EncodingOutputStream extends FilterOutputStream {
618         private int c1;
619 
620         private int c2;
621 
622         EncodingOutputStream(OutputStream out) {
623             super(out);
624         }
625 
626         OutputStream getUnderlying() {
627             return out;
628         }
629 
630         private boolean isCdataEndBlock(int c) {
631             return c1 == ']' && c2 == ']' && c == '>';
632         }
633 
634         @Override
635         public void write(int b) throws IOException {
636             if (isCdataEndBlock(b)) {
637                 out.write(ByteConstantsHolder.CDATA_ESCAPE_STRING_BYTES);
638             } else if (isIllegalEscape(b)) {
639                 // uh-oh!  This character is illegal in XML 1.0!
640                 // http://www.w3.org/TR/1998/REC-xml-19980210#charsets
641                 // we're going to deliberately doubly-XML escape it...
642                 // there's nothing better we can do! :-(
643                 // SUREFIRE-456
644                 out.write(ByteConstantsHolder.AMP_BYTES);
645                 out.write(String.valueOf(b).getBytes(UTF_8));
646                 out.write(';'); // & Will be encoded to amp inside xml encodingSHO
647             } else {
648                 out.write(b);
649             }
650             c1 = c2;
651             c2 = b;
652         }
653     }
654 
655     private static boolean containsEscapesIllegalXml10(String message) {
656         int size = message.length();
657         for (int i = 0; i < size; i++) {
658             if (isIllegalEscape(message.charAt(i))) {
659                 return true;
660             }
661         }
662         return false;
663     }
664 
665     private static boolean isIllegalEscape(char c) {
666         return isIllegalEscape((int) c);
667     }
668 
669     private static boolean isIllegalEscape(int c) {
670         return c >= 0 && c < 32 && c != '\n' && c != '\r' && c != '\t';
671     }
672 
673     /**
674      * escape for XML 1.0
675      *
676      * @param text      The string
677      * @param attribute true if the escaped value is inside an attribute
678      * @return The escaped string
679      */
680     private static String escapeXml(String text, boolean attribute) {
681         StringBuilder sb = new StringBuilder(text.length() * 2);
682         for (int i = 0; i < text.length(); i++) {
683             char c = text.charAt(i);
684             if (isIllegalEscape(c)) {
685                 // uh-oh!  This character is illegal in XML 1.0!
686                 // http://www.w3.org/TR/1998/REC-xml-19980210#charsets
687                 // we're going to deliberately doubly-XML escape it...
688                 // there's nothing better we can do! :-(
689                 // SUREFIRE-456
690                 sb.append(attribute ? "&#" : "&amp#")
691                         .append((int) c)
692                         .append(';'); // & Will be encoded to amp inside xml encodingSHO
693             } else {
694                 sb.append(c);
695             }
696         }
697         return sb.toString();
698     }
699 
700     private static final class ByteConstantsHolder {
701         private static final byte[] CDATA_START_BYTES = "<![CDATA[".getBytes(UTF_8);
702 
703         private static final byte[] CDATA_END_BYTES = "]]>".getBytes(UTF_8);
704 
705         private static final byte[] CDATA_ESCAPE_STRING_BYTES = "]]><![CDATA[>".getBytes(UTF_8);
706 
707         private static final byte[] AMP_BYTES = "&amp#".getBytes(UTF_8);
708 
709         private static final byte[] COMMENT_START = "<!-- ".getBytes(UTF_8);
710 
711         private static final byte[] COMMENT_END = " --> ".getBytes(UTF_8);
712     }
713 }