diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/StatelessXmlReporter.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/StatelessXmlReporter.java index eb7554085a..2457f48b9f 100644 --- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/StatelessXmlReporter.java +++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/StatelessXmlReporter.java @@ -1,808 +1,938 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.maven.plugin.surefire.report; - -import java.io.BufferedOutputStream; -import java.io.File; -import java.io.FilterOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.PrintWriter; -import java.nio.file.Files; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Deque; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.StringTokenizer; -import java.util.concurrent.ConcurrentLinkedDeque; - -import org.apache.maven.plugin.surefire.booterclient.output.InPluginProcessDumpSingleton; -import org.apache.maven.surefire.api.report.SafeThrowable; -import org.apache.maven.surefire.extensions.StatelessReportEventListener; -import org.apache.maven.surefire.shared.utils.xml.PrettyPrintXMLWriter; -import org.apache.maven.surefire.shared.utils.xml.XMLWriter; - -import static java.nio.charset.StandardCharsets.UTF_8; -import static org.apache.maven.plugin.surefire.report.DefaultReporterFactory.TestResultType; -import static org.apache.maven.plugin.surefire.report.FileReporterUtils.stripIllegalFilenameChars; -import static org.apache.maven.plugin.surefire.report.ReportEntryType.SKIPPED; -import static org.apache.maven.plugin.surefire.report.ReportEntryType.SUCCESS; -import static org.apache.maven.surefire.shared.utils.StringUtils.isBlank; -import static org.apache.maven.surefire.shared.utils.StringUtils.isNotBlank; - -// CHECKSTYLE_OFF: LineLength -/** - * XML format reporter writing to TEST-reportName[-suffix].xml file like written and read - * by Ant's <junit> and - * <junitreport> tasks, - * then supported by many tools like CI servers. - *
- *
<?xml version="1.0" encoding="UTF-8"?>
- * <testsuite name="suite name" [group="group"] tests="0" failures="0" errors="0" skipped="0" time="{float}">
- *  <properties>
- *    <property name="name" value="value"/>
- *    [...]
- *  </properties>
- *  <testcase time="{float}" name="test name [classname="class name"] [group="group"]"/>
- *  <testcase time="{float}" name="test name [classname="class name"] [group="group"]">
- *    <error message="message" type="exception class name">stacktrace</error>
- *    <system-out>system out content (present only if not empty)</system-out>
- *    <system-err>system err content (present only if not empty)</system-err>
- *  </testcase>
- *  <testcase time="{float}" name="test name [classname="class name"] [group="group"]">
- *    <failure message="message" type="exception class name">stacktrace</failure>
- *    <system-out>system out content (present only if not empty)</system-out>
- *    <system-err>system err content (present only if not empty)</system-err>
- *  </testcase>
- *  <testcase time="{float}" name="test name [classname="class name"] [group="group"]">
- *    <skipped/>
- *  </testcase>
- *  [...]
- * - * @author Kristian Rosenvold - * @see Ant's format enhancement proposal - * (not yet implemented by Ant 1.8.2) - * @see Ant's XMLJUnitResultFormatter - */ -@SuppressWarnings({"javadoc", "checkstyle:javadoctype"}) -// TODO this is no more stateless due to existence of testClassMethodRunHistoryMap since of 2.19. -public class StatelessXmlReporter implements StatelessReportEventListener { - private static final float ONE_SECOND = 1000.0f; - - private static final String XML_INDENT = " "; - - private static final String XML_NL = "\n"; - - private final File reportsDirectory; - - private final String reportNameSuffix; - - private final boolean trimStackTrace; - - private final int rerunFailingTestsCount; - - private final String xsdSchemaLocation; - - private final String xsdVersion; - - // Map between test class name and a map between test method name - // and the list of runs for each test method - private final Map> testClassMethodRunHistoryMap; - - private final boolean phrasedFileName; - - private final boolean phrasedSuiteName; - - private final boolean phrasedClassName; - - private final boolean phrasedMethodName; - - private final boolean enableOutErrElements; - - private final boolean enablePropertiesElement; - - private final boolean reportTestTimestamp; - - /** - * @deprecated Prefer adding a new constructor that accepts a configuration object, e.g. - * {@link org.apache.maven.surefire.extensions.StatelessReportMojoConfiguration}. - */ - @Deprecated - public StatelessXmlReporter( - File reportsDirectory, - String reportNameSuffix, - boolean trimStackTrace, - int rerunFailingTestsCount, - Map> testClassMethodRunHistoryMap, - String xsdSchemaLocation, - String xsdVersion, - boolean phrasedFileName, - boolean phrasedSuiteName, - boolean phrasedClassName, - boolean phrasedMethodName, - boolean enableOutErrElements, - boolean enablePropertiesElement, - boolean reportTestTimestamp) { - this.reportsDirectory = reportsDirectory; - this.reportNameSuffix = reportNameSuffix; - this.trimStackTrace = trimStackTrace; - this.rerunFailingTestsCount = rerunFailingTestsCount; - this.testClassMethodRunHistoryMap = testClassMethodRunHistoryMap; - this.xsdSchemaLocation = xsdSchemaLocation; - this.xsdVersion = xsdVersion; - this.phrasedFileName = phrasedFileName; - this.phrasedSuiteName = phrasedSuiteName; - this.phrasedClassName = phrasedClassName; - this.phrasedMethodName = phrasedMethodName; - this.enableOutErrElements = enableOutErrElements; - this.enablePropertiesElement = enablePropertiesElement; - this.reportTestTimestamp = reportTestTimestamp; - } - - @Override - public void testSetCompleted(WrappedReportEntry testSetReportEntry, TestSetStats testSetStats) { - Map>> classMethodStatistics = - arrangeMethodStatistics(testSetReportEntry, testSetStats); - - // The Java Language Spec: - // "Note that the close methods of resources are called in the opposite order of their creation." - try (OutputStream outputStream = getOutputStream(testSetReportEntry); - OutputStreamWriter fw = getWriter(outputStream)) { - XMLWriter ppw = new PrettyPrintXMLWriter(new PrintWriter(fw), XML_INDENT, XML_NL, UTF_8.name(), null); - - createTestSuiteElement(ppw, testSetReportEntry, classMethodStatistics); // TestSuite - - if (enablePropertiesElement) { - showProperties(ppw, testSetReportEntry.getSystemProperties()); - } else { - boolean hasNonSuccess = false; - for (Map> statistics : classMethodStatistics.values()) { - for (List thisMethodRuns : statistics.values()) { - if (thisMethodRuns.stream() - .anyMatch(entry -> entry.getReportEntryType() != ReportEntryType.SUCCESS)) { - hasNonSuccess = true; - break; - } - } - if (hasNonSuccess) { - break; - } - } - - if (hasNonSuccess) { - showProperties(ppw, testSetReportEntry.getSystemProperties()); - } - } - - for (Entry>> statistics : classMethodStatistics.entrySet()) { - Map> methodStatistics = statistics.getValue(); - for (Entry> thisMethodRuns : methodStatistics.entrySet()) { - serializeTestClass(outputStream, fw, ppw, thisMethodRuns.getValue(), methodStatistics); - } - } - - ppw.endElement(); // TestSuite - } catch (IOException e) { - // It's not a test error. - // This method must be sail-safe and errors are in a dump log. - // The control flow must not be broken in TestSetRunListener#testSetCompleted. - InPluginProcessDumpSingleton.getSingleton().dumpException(e, e.getLocalizedMessage(), reportsDirectory); - } - } - - private Map>> arrangeMethodStatistics( - WrappedReportEntry testSetReportEntry, TestSetStats testSetStats) { - Map>> classMethodStatistics = new LinkedHashMap<>(); - for (WrappedReportEntry methodEntry : aggregateCacheFromMultipleReruns(testSetReportEntry, testSetStats)) { - String testClassName = methodEntry.getSourceName(); - Map> stats = - classMethodStatistics.computeIfAbsent(testClassName, k -> new LinkedHashMap<>()); - String methodName = methodEntry.getName(); - List methodRuns = stats.computeIfAbsent(methodName, k -> new ArrayList<>()); - methodRuns.add(methodEntry); - } - return classMethodStatistics; - } - - private Deque aggregateCacheFromMultipleReruns( - WrappedReportEntry testSetReportEntry, TestSetStats testSetStats) { - String suiteClassName = testSetReportEntry.getSourceName(); - Deque methodRunHistory = getAddMethodRunHistoryMap(suiteClassName); - methodRunHistory.addAll(testSetStats.getReportEntries()); - return methodRunHistory; - } - - private void serializeTestClass( - OutputStream outputStream, - OutputStreamWriter fw, - XMLWriter ppw, - List methodEntries, - Map> methodStatistics) - throws IOException { - if (rerunFailingTestsCount > 0) { - serializeTestClassWithRerun(outputStream, fw, ppw, methodEntries, methodStatistics); - } else { - // rerunFailingTestsCount is smaller than 1, but for some reasons a test could be run - // for more than once - serializeTestClassWithoutRerun(outputStream, fw, ppw, methodEntries); - } - } - - private void serializeTestClassWithoutRerun( - OutputStream outputStream, OutputStreamWriter fw, XMLWriter ppw, List methodEntries) - throws IOException { - for (WrappedReportEntry methodEntry : methodEntries) { - startTestElement(ppw, methodEntry); - if (methodEntry.getReportEntryType() != SUCCESS) { - getTestProblems( - fw, - ppw, - methodEntry, - trimStackTrace, - outputStream, - methodEntry.getReportEntryType().getXmlTag(), - false); - } - if (methodEntry.getReportEntryType() != SUCCESS || enableOutErrElements) { - createOutErrElements(fw, ppw, methodEntry, outputStream); - } - ppw.endElement(); - } - } - - private void serializeTestClassWithRerun( - OutputStream outputStream, - OutputStreamWriter fw, - XMLWriter ppw, - List methodEntries, - Map> methodStatistics) - throws IOException { - WrappedReportEntry firstMethodEntry = methodEntries.get(0); - - TestResultType resultType = - getTestResultTypeWithBeforeAllHandling(firstMethodEntry.getName(), methodEntries, methodStatistics); - - switch (resultType) { - case SUCCESS: - for (WrappedReportEntry methodEntry : methodEntries) { - if (methodEntry.getReportEntryType() == SUCCESS) { - startTestElement(ppw, methodEntry); - ppw.endElement(); - } - } - break; - case ERROR: - case FAILURE: - // When rerunFailingTestsCount is set to larger than 0 - startTestElement(ppw, firstMethodEntry); - boolean firstRun = true; - for (WrappedReportEntry singleRunEntry : methodEntries) { - if (firstRun) { - firstRun = false; - getTestProblems( - fw, - ppw, - singleRunEntry, - trimStackTrace, - outputStream, - singleRunEntry.getReportEntryType().getXmlTag(), - false); - createOutErrElements(fw, ppw, singleRunEntry, outputStream); - } else if (singleRunEntry.getReportEntryType() == SKIPPED) { - // The version 3.1.0 should produce a new XSD schema with version 3.1.0, see SUREFIRE-1986, - // and the XSD schema should add a new element "rerunSkipped" - // then ReportEntryType should update the enum to SKIPPED( "skipped", "", "rerunSkipped" ). - // The teams should be notified - Jenkins reports. - addCommentElementTestCase("a skipped test execution in re-run phase", fw, ppw, outputStream); - } else { - getTestProblems( - fw, - ppw, - singleRunEntry, - trimStackTrace, - outputStream, - singleRunEntry.getReportEntryType().getRerunXmlTag(), - true); - } - } - ppw.endElement(); - break; - case FLAKE: - WrappedReportEntry successful = null; - // Get the run time of the first successful run - for (WrappedReportEntry singleRunEntry : methodEntries) { - if (singleRunEntry.getReportEntryType() == SUCCESS) { - successful = singleRunEntry; - break; - } - } - WrappedReportEntry firstOrSuccessful = successful == null ? methodEntries.get(0) : successful; - startTestElement(ppw, firstOrSuccessful); - for (WrappedReportEntry singleRunEntry : methodEntries) { - if (singleRunEntry.getReportEntryType() != SUCCESS) { - getTestProblems( - fw, - ppw, - singleRunEntry, - trimStackTrace, - outputStream, - singleRunEntry.getReportEntryType().getFlakyXmlTag(), - true); - } - } - ppw.endElement(); - break; - case SKIPPED: - startTestElement(ppw, firstMethodEntry); - getTestProblems( - fw, - ppw, - firstMethodEntry, - trimStackTrace, - outputStream, - firstMethodEntry.getReportEntryType().getXmlTag(), - false); - ppw.endElement(); - break; - default: - throw new IllegalStateException("Get unknown test result type"); - } - } - - /** - * Clean testClassMethodRunHistoryMap. - */ - public void cleanTestHistoryMap() { - testClassMethodRunHistoryMap.clear(); - } - - /** - * Get the result of a test from a list of its runs in WrappedReportEntry. - * - * @param methodEntryList the list of runs for a given test - * @return the TestResultType for the given test - */ - private TestResultType getTestResultType(List methodEntryList) { - List testResultTypeList = new ArrayList<>(); - for (WrappedReportEntry singleRunEntry : methodEntryList) { - testResultTypeList.add(singleRunEntry.getReportEntryType()); - } - - return DefaultReporterFactory.getTestResultType(testResultTypeList, rerunFailingTestsCount); - } - - /** - * Determines the final result type for a test method, applying special handling for @BeforeAll failures. - * If a @BeforeAll fails but any actual test methods succeed, it's classified as a FLAKE. - * - * @param methodName the name of the test method (null or "null" for @BeforeAll) - * @param methodRuns the list of runs for this method - * @param methodStatistics all method statistics for the test class - * @return the final TestResultType - */ - private TestResultType getTestResultTypeWithBeforeAllHandling( - String methodName, - List methodRuns, - Map> methodStatistics) { - TestResultType resultType = getTestResultType(methodRuns); - - // Special handling for @BeforeAll failures (null method name or method name is "null" or "initializationError") - // If @BeforeAll failed but any actual test methods succeeded, treat it as a flake - if ((methodName == null || methodName.equals("null") || methodName.equals("initializationError")) - && (resultType == TestResultType.ERROR || resultType == TestResultType.FAILURE)) { - // Check if any actual test methods (non-null and not "null" names) succeeded - boolean hasSuccessfulTestMethods = methodStatistics.entrySet().stream() - .filter(entry -> - entry.getKey() != null && !entry.getKey().equals("null")) // Only actual test methods - .anyMatch(entry -> entry.getValue().stream() - .anyMatch(reportEntry -> reportEntry.getReportEntryType() == SUCCESS)); - - if (hasSuccessfulTestMethods) { - resultType = TestResultType.FLAKE; - } - } - - return resultType; - } - - private Deque getAddMethodRunHistoryMap(String testClassName) { - Deque methodRunHistory = testClassMethodRunHistoryMap.get(testClassName); - if (methodRunHistory == null) { - methodRunHistory = new ConcurrentLinkedDeque<>(); - testClassMethodRunHistoryMap.put(testClassName == null ? "null" : testClassName, methodRunHistory); - } - return methodRunHistory; - } - - private OutputStream getOutputStream(WrappedReportEntry testSetReportEntry) throws IOException { - File reportFile = getReportFile(testSetReportEntry); - File reportDir = reportFile.getParentFile(); - //noinspection ResultOfMethodCallIgnored - reportFile.delete(); - //noinspection ResultOfMethodCallIgnored - reportDir.mkdirs(); - return new BufferedOutputStream(Files.newOutputStream(reportFile.toPath()), 64 * 1024); - } - - private static OutputStreamWriter getWriter(OutputStream fos) { - return new OutputStreamWriter(fos, UTF_8); - } - - private File getReportFile(WrappedReportEntry report) { - String reportName = "TEST-" + (phrasedFileName ? report.getReportSourceName() : report.getSourceName()); - String customizedReportName = isBlank(reportNameSuffix) ? reportName : reportName + "-" + reportNameSuffix; - return new File(reportsDirectory, stripIllegalFilenameChars(customizedReportName + ".xml")); - } - - private void startTestElement(XMLWriter ppw, WrappedReportEntry report) throws IOException { - ppw.startElement("testcase"); - String name = phrasedMethodName ? report.getReportName() : report.getName(); - ppw.addAttribute("name", name == null ? "" : extraEscapeAttribute(name)); - - if (report.getGroup() != null) { - ppw.addAttribute("group", report.getGroup()); - } - - String className = phrasedClassName - ? report.getReportSourceName(reportNameSuffix) - : report.getSourceQualifiedName(reportNameSuffix); - if (className != null) { - ppw.addAttribute("classname", extraEscapeAttribute(className)); - } - - if (report.getElapsed() != null) { - ppw.addAttribute("time", String.valueOf(report.getElapsed() / ONE_SECOND)); - } - - if (reportTestTimestamp && report.getStartTime() > 0L) { - ppw.addAttribute( - "timestamp", Instant.ofEpochMilli(report.getStartTime()).toString()); - } - } - - private void createTestSuiteElement( - XMLWriter ppw, - WrappedReportEntry report, - Map>> classMethodStatistics) - throws IOException { - ppw.startElement("testsuite"); - - ppw.addAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"); - ppw.addAttribute("xsi:noNamespaceSchemaLocation", xsdSchemaLocation); - ppw.addAttribute("version", xsdVersion); - - String reportName = phrasedSuiteName - ? report.getReportSourceName(reportNameSuffix) - : report.getSourceName(reportNameSuffix); - ppw.addAttribute("name", reportName == null ? "" : extraEscapeAttribute(reportName)); - - if (report.getGroup() != null) { - ppw.addAttribute("group", report.getGroup()); - } - - if (report.getElapsed() != null) { - ppw.addAttribute("time", String.valueOf(report.getElapsed() / ONE_SECOND)); - } - - if (reportTestTimestamp && report.getStartTime() > 0L) { - ppw.addAttribute( - "timestamp", Instant.ofEpochMilli(report.getStartTime()).toString()); - } - - // Count actual unique test methods and their final results from classMethodStatistics (accumulated across - // reruns) - int actualTestCount = 0; - int errors = 0; - int failures = 0; - int skipped = 0; - int flakes = 0; - - for (Map> methodStats : classMethodStatistics.values()) { - actualTestCount += methodStats.size(); - for (Map.Entry> methodEntry : methodStats.entrySet()) { - String methodName = methodEntry.getKey(); - List methodRuns = methodEntry.getValue(); - TestResultType resultType = getTestResultTypeWithBeforeAllHandling(methodName, methodRuns, methodStats); - - switch (resultType) { - case ERROR: - errors++; - break; - case FAILURE: - failures++; - break; - case SKIPPED: - skipped++; - break; - case FLAKE: - flakes++; - break; - case SUCCESS: - default: - break; - } - } - } - - ppw.addAttribute("tests", String.valueOf(actualTestCount)); - ppw.addAttribute("errors", String.valueOf(errors)); - ppw.addAttribute("skipped", String.valueOf(skipped)); - ppw.addAttribute("failures", String.valueOf(failures)); - ppw.addAttribute("flakes", String.valueOf(flakes)); - } - - private static void getTestProblems( - OutputStreamWriter outputStreamWriter, - XMLWriter ppw, - WrappedReportEntry report, - boolean trimStackTrace, - OutputStream fw, - String testErrorType, - boolean createNestedOutErrElements) - throws IOException { - ppw.startElement(testErrorType); - - String stackTrace = report.getStackTrace(trimStackTrace); - - if (report.getMessage() != null && !report.getMessage().isEmpty()) { - ppw.addAttribute("message", extraEscapeAttribute(report.getMessage())); - } - - if (report.getStackTraceWriter() != null) { - //noinspection ThrowableResultOfMethodCallIgnored - SafeThrowable t = report.getStackTraceWriter().getThrowable(); - if (t != null) { - if (t.getMessage() != null) { - int delimiter = stackTrace.indexOf(":"); - String type = delimiter == -1 ? stackTrace : stackTrace.substring(0, delimiter); - ppw.addAttribute("type", type); - } else { - if (isNotBlank(stackTrace)) { - ppw.addAttribute("type", new StringTokenizer(stackTrace).nextToken()); - } - } - } - } - - /* This structure is inconsistent due to bad legacy design choices for the XML schema. - * Ideally, all elements would be complex and strackTrace would have its own element. - * See SUREFIRE-2230 for details to how improve and unify the schema in the future. - */ - if (createNestedOutErrElements) { - ppw.startElement("stackTrace"); - if (stackTrace != null) { - extraEscapeElementValue(stackTrace, outputStreamWriter, ppw, fw); - } - ppw.endElement(); - - createOutErrElements(outputStreamWriter, ppw, report, fw); - } else if (stackTrace != null) { - extraEscapeElementValue(stackTrace, outputStreamWriter, ppw, fw); - } - - ppw.endElement(); // entry type - } - - // Create system-out and system-err elements - private static void createOutErrElements( - OutputStreamWriter outputStreamWriter, XMLWriter ppw, WrappedReportEntry report, OutputStream fw) - throws IOException { - EncodingOutputStream eos = new EncodingOutputStream(fw); - addOutputStreamElement(outputStreamWriter, eos, ppw, report.getStdout(), "system-out"); - addOutputStreamElement(outputStreamWriter, eos, ppw, report.getStdErr(), "system-err"); - } - - private static void addOutputStreamElement( - OutputStreamWriter outputStreamWriter, - EncodingOutputStream eos, - XMLWriter xmlWriter, - Utf8RecodingDeferredFileOutputStream utf8RecodingDeferredFileOutputStream, - String name) - throws IOException { - if (utf8RecodingDeferredFileOutputStream != null && utf8RecodingDeferredFileOutputStream.getByteCount() > 0) { - xmlWriter.startElement(name); - xmlWriter.writeText(""); // Cheat sax to emit element - outputStreamWriter.flush(); - eos.getUnderlying().write(ByteConstantsHolder.CDATA_START_BYTES); // emit cdata - utf8RecodingDeferredFileOutputStream.writeTo(eos); - utf8RecodingDeferredFileOutputStream.free(); - eos.getUnderlying().write(ByteConstantsHolder.CDATA_END_BYTES); - eos.flush(); - xmlWriter.endElement(); - } - } - - /** - * Adds system properties to the XML report. - *
- * - * @param xmlWriter the test suite to report to - */ - private static void showProperties(XMLWriter xmlWriter, Map systemProperties) throws IOException { - xmlWriter.startElement("properties"); - for (final Entry entry : systemProperties.entrySet()) { - final String key = entry.getKey(); - String value = entry.getValue(); - - if (value == null) { - value = "null"; - } - - xmlWriter.startElement("property"); - - xmlWriter.addAttribute("name", key); - - xmlWriter.addAttribute("value", extraEscapeAttribute(value)); - - xmlWriter.endElement(); - } - xmlWriter.endElement(); - } - - /** - * Handle stuff that may pop up in java that is not legal in xml. - * - * @param message the string - * @return the escaped string or returns itself if all characters are legal - */ - private static String extraEscapeAttribute(String message) { - // Someday convert to xml 1.1 which handles everything but 0 inside string - return containsEscapesIllegalXml10(message) ? escapeXml(message, true) : message; - } - - /** - * Writes escaped string or the message within CDATA if all characters are legal. - * - * @param message the string - */ - private static void extraEscapeElementValue( - String message, OutputStreamWriter outputStreamWriter, XMLWriter xmlWriter, OutputStream fw) - throws IOException { - // Someday convert to xml 1.1 which handles everything but 0 inside string - if (containsEscapesIllegalXml10(message)) { - xmlWriter.writeText(escapeXml(message, false)); - } else { - EncodingOutputStream eos = new EncodingOutputStream(fw); - xmlWriter.writeText(""); // Cheat sax to emit element - outputStreamWriter.flush(); - eos.getUnderlying().write(ByteConstantsHolder.CDATA_START_BYTES); - eos.write(message.getBytes(UTF_8)); - eos.getUnderlying().write(ByteConstantsHolder.CDATA_END_BYTES); - eos.flush(); - } - } - - // todo: SUREFIRE-1986 - private static void addCommentElementTestCase( - String comment, OutputStreamWriter outputStreamWriter, XMLWriter xmlWriter, OutputStream fw) - throws IOException { - xmlWriter.writeText(""); // Cheat sax to emit element - outputStreamWriter.flush(); - fw.write(XML_NL.getBytes(UTF_8)); - fw.write(XML_INDENT.getBytes(UTF_8)); - fw.write(XML_INDENT.getBytes(UTF_8)); - fw.write(ByteConstantsHolder.COMMENT_START); - fw.write(comment.getBytes(UTF_8)); - fw.write(ByteConstantsHolder.COMMENT_END); - fw.write(XML_NL.getBytes(UTF_8)); - fw.write(XML_INDENT.getBytes(UTF_8)); - fw.flush(); - } - - private static final class EncodingOutputStream extends FilterOutputStream { - private int c1; - - private int c2; - - EncodingOutputStream(OutputStream out) { - super(out); - } - - OutputStream getUnderlying() { - return out; - } - - private boolean isCdataEndBlock(int c) { - return c1 == ']' && c2 == ']' && c == '>'; - } - - @Override - public void write(int b) throws IOException { - if (isCdataEndBlock(b)) { - out.write(ByteConstantsHolder.CDATA_ESCAPE_STRING_BYTES); - } else if (isIllegalEscape(b)) { - // uh-oh! This character is illegal in XML 1.0! - // http://www.w3.org/TR/1998/REC-xml-19980210#charsets - // we're going to deliberately doubly-XML escape it... - // there's nothing better we can do! :-( - // SUREFIRE-456 - out.write(ByteConstantsHolder.AMP_BYTES); - out.write(String.valueOf(b).getBytes(UTF_8)); - out.write(';'); // & Will be encoded to amp inside xml encodingSHO - } else { - out.write(b); - } - c1 = c2; - c2 = b; - } - } - - private static boolean containsEscapesIllegalXml10(String message) { - int size = message.length(); - for (int i = 0; i < size; i++) { - if (isIllegalEscape(message.charAt(i))) { - return true; - } - } - return false; - } - - private static boolean isIllegalEscape(char c) { - return isIllegalEscape((int) c); - } - - private static boolean isIllegalEscape(int c) { - return c >= 0 && c < 32 && c != '\n' && c != '\r' && c != '\t'; - } - - /** - * Escape for XML 1.0. - * - * @param text the string - * @param attribute true if the escaped value is inside an attribute - * @return the escaped string - */ - private static String escapeXml(String text, boolean attribute) { - StringBuilder sb = new StringBuilder(text.length() * 2); - for (int i = 0; i < text.length(); i++) { - char c = text.charAt(i); - if (isIllegalEscape(c)) { - // uh-oh! This character is illegal in XML 1.0! - // http://www.w3.org/TR/1998/REC-xml-19980210#charsets - // we're going to deliberately doubly-XML escape it... - // there's nothing better we can do! :-( - // SUREFIRE-456 - sb.append(attribute ? "&#" : "&#") - .append((int) c) - .append(';'); // & Will be encoded to amp inside xml encodingSHO - } else { - sb.append(c); - } - } - return sb.toString(); - } - - private static final class ByteConstantsHolder { - private static final byte[] CDATA_START_BYTES = "".getBytes(UTF_8); - - private static final byte[] CDATA_ESCAPE_STRING_BYTES = "]]>".getBytes(UTF_8); - - private static final byte[] AMP_BYTES = "&#".getBytes(UTF_8); - - private static final byte[] COMMENT_START = " ".getBytes(UTF_8); - } -} +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.plugin.surefire.report; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Deque; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.StringTokenizer; +import java.util.concurrent.ConcurrentLinkedDeque; + +import org.apache.maven.plugin.surefire.booterclient.output.InPluginProcessDumpSingleton; +import org.apache.maven.surefire.api.report.SafeThrowable; +import org.apache.maven.surefire.extensions.StatelessReportEventListener; +import org.apache.maven.surefire.shared.utils.xml.PrettyPrintXMLWriter; +import org.apache.maven.surefire.shared.utils.xml.XMLWriter; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.apache.maven.plugin.surefire.report.DefaultReporterFactory.TestResultType; +import static org.apache.maven.plugin.surefire.report.FileReporterUtils.stripIllegalFilenameChars; +import static org.apache.maven.plugin.surefire.report.ReportEntryType.SKIPPED; +import static org.apache.maven.plugin.surefire.report.ReportEntryType.SUCCESS; +import static org.apache.maven.surefire.shared.utils.StringUtils.isBlank; +import static org.apache.maven.surefire.shared.utils.StringUtils.isNotBlank; + +// CHECKSTYLE_OFF: LineLength +/** + * XML format reporter writing to TEST-reportName[-suffix].xml file like written and read + * by Ant's <junit> and + * <junitreport> tasks, + * then supported by many tools like CI servers. + *
+ *
<?xml version="1.0" encoding="UTF-8"?>
+ * <testsuite name="suite name" [group="group"] tests="0" failures="0" errors="0" skipped="0" time="{float}">
+ *  <properties>
+ *    <property name="name" value="value"/>
+ *    [...]
+ *  </properties>
+ *  <testcase time="{float}" name="test name [classname="class name"] [group="group"]"/>
+ *  <testcase time="{float}" name="test name [classname="class name"] [group="group"]">
+ *    <error message="message" type="exception class name">stacktrace</error>
+ *    <system-out>system out content (present only if not empty)</system-out>
+ *    <system-err>system err content (present only if not empty)</system-err>
+ *  </testcase>
+ *  <testcase time="{float}" name="test name [classname="class name"] [group="group"]">
+ *    <failure message="message" type="exception class name">stacktrace</failure>
+ *    <system-out>system out content (present only if not empty)</system-out>
+ *    <system-err>system err content (present only if not empty)</system-err>
+ *  </testcase>
+ *  <testcase time="{float}" name="test name [classname="class name"] [group="group"]">
+ *    <skipped/>
+ *  </testcase>
+ *  [...]
+ * + * @author Kristian Rosenvold + * @see Ant's format enhancement proposal + * (not yet implemented by Ant 1.8.2) + * @see Ant's XMLJUnitResultFormatter + */ +@SuppressWarnings({"javadoc", "checkstyle:javadoctype"}) +// TODO this is no more stateless due to existence of testClassMethodRunHistoryMap since of 2.19. +public class StatelessXmlReporter implements StatelessReportEventListener { + private static final float ONE_SECOND = 1000.0f; + + private static final String XML_INDENT = " "; + + private static final String XML_NL = "\n"; + + private final File reportsDirectory; + + private final String reportNameSuffix; + + private final boolean trimStackTrace; + + private final int rerunFailingTestsCount; + + private final String xsdSchemaLocation; + + private final String xsdVersion; + + // Map between test class name and a map between test method name + // and the list of runs for each test method + private final Map> testClassMethodRunHistoryMap; + + private final boolean phrasedFileName; + + private final boolean phrasedSuiteName; + + private final boolean phrasedClassName; + + private final boolean phrasedMethodName; + + private final boolean enableOutErrElements; + + private final boolean enablePropertiesElement; + + private final boolean reportTestTimestamp; + + /** + * @deprecated Prefer adding a new constructor that accepts a configuration object, e.g. + * {@link org.apache.maven.surefire.extensions.StatelessReportMojoConfiguration}. + */ + @Deprecated + public StatelessXmlReporter( + File reportsDirectory, + String reportNameSuffix, + boolean trimStackTrace, + int rerunFailingTestsCount, + Map> testClassMethodRunHistoryMap, + String xsdSchemaLocation, + String xsdVersion, + boolean phrasedFileName, + boolean phrasedSuiteName, + boolean phrasedClassName, + boolean phrasedMethodName, + boolean enableOutErrElements, + boolean enablePropertiesElement, + boolean reportTestTimestamp) { + this.reportsDirectory = reportsDirectory; + this.reportNameSuffix = reportNameSuffix; + this.trimStackTrace = trimStackTrace; + this.rerunFailingTestsCount = rerunFailingTestsCount; + this.testClassMethodRunHistoryMap = testClassMethodRunHistoryMap; + this.xsdSchemaLocation = xsdSchemaLocation; + this.xsdVersion = xsdVersion; + this.phrasedFileName = phrasedFileName; + this.phrasedSuiteName = phrasedSuiteName; + this.phrasedClassName = phrasedClassName; + this.phrasedMethodName = phrasedMethodName; + this.enableOutErrElements = enableOutErrElements; + this.enablePropertiesElement = enablePropertiesElement; + this.reportTestTimestamp = reportTestTimestamp; + } + + @Override + public void testSetCompleted(WrappedReportEntry testSetReportEntry, TestSetStats testSetStats) { + Map>> classMethodStatistics = + arrangeMethodStatistics(testSetReportEntry, testSetStats); + + // The Java Language Spec: + // "Note that the close methods of resources are called in the opposite order of their creation." + try (OutputStream outputStream = getOutputStream(testSetReportEntry); + OutputStreamWriter fw = getWriter(outputStream)) { + XMLWriter ppw = new PrettyPrintXMLWriter(new PrintWriter(fw), XML_INDENT, XML_NL, UTF_8.name(), null); + + createTestSuiteElement(ppw, testSetReportEntry, classMethodStatistics); // TestSuite + + if (enablePropertiesElement) { + showProperties(ppw, testSetReportEntry.getSystemProperties()); + } else { + boolean hasNonSuccess = false; + for (Map> statistics : classMethodStatistics.values()) { + for (List thisMethodRuns : statistics.values()) { + if (thisMethodRuns.stream() + .anyMatch(entry -> entry.getReportEntryType() != ReportEntryType.SUCCESS)) { + hasNonSuccess = true; + break; + } + } + if (hasNonSuccess) { + break; + } + } + + if (hasNonSuccess) { + showProperties(ppw, testSetReportEntry.getSystemProperties()); + } + } + + for (Entry>> statistics : classMethodStatistics.entrySet()) { + Map> methodStatistics = statistics.getValue(); + for (Entry> thisMethodRuns : methodStatistics.entrySet()) { + serializeTestClass(outputStream, fw, ppw, thisMethodRuns.getValue(), methodStatistics); + } + } + + ppw.endElement(); // TestSuite + } catch (IOException e) { + // It's not a test error. + // This method must be sail-safe and errors are in a dump log. + // The control flow must not be broken in TestSetRunListener#testSetCompleted. + InPluginProcessDumpSingleton.getSingleton().dumpException(e, e.getLocalizedMessage(), reportsDirectory); + } + } + + private Map>> arrangeMethodStatistics( + WrappedReportEntry testSetReportEntry, TestSetStats testSetStats) { + Map>> classMethodStatistics = new LinkedHashMap<>(); + for (WrappedReportEntry methodEntry : aggregateCacheFromMultipleReruns(testSetReportEntry, testSetStats)) { + String testClassName = methodEntry.getSourceName(); + Map> stats = + classMethodStatistics.computeIfAbsent(testClassName, k -> new LinkedHashMap<>()); + String methodName = methodEntry.getName(); + List methodRuns = stats.computeIfAbsent(methodName, k -> new ArrayList<>()); + methodRuns.add(methodEntry); + } + return classMethodStatistics; + } + + private Deque aggregateCacheFromMultipleReruns( + WrappedReportEntry testSetReportEntry, TestSetStats testSetStats) { + String suiteClassName = testSetReportEntry.getSourceName(); + Deque methodRunHistory = getAddMethodRunHistoryMap(suiteClassName); + methodRunHistory.addAll(testSetStats.getReportEntries()); + return methodRunHistory; + } + + private void serializeTestClass( + OutputStream outputStream, + OutputStreamWriter fw, + XMLWriter ppw, + List methodEntries, + Map> methodStatistics) + throws IOException { + if (rerunFailingTestsCount > 0) { + serializeTestClassWithRerun(outputStream, fw, ppw, methodEntries, methodStatistics); + } else { + // rerunFailingTestsCount is smaller than 1, but for some reasons a test could be run + // for more than once + serializeTestClassWithoutRerun(outputStream, fw, ppw, methodEntries); + } + } + + private void serializeTestClassWithoutRerun( + OutputStream outputStream, OutputStreamWriter fw, XMLWriter ppw, List methodEntries) + throws IOException { + for (WrappedReportEntry methodEntry : methodEntries) { + startTestElement(ppw, methodEntry); + if (methodEntry.getReportEntryType() != SUCCESS) { + getTestProblems( + fw, + ppw, + methodEntry, + trimStackTrace, + outputStream, + methodEntry.getReportEntryType().getXmlTag(), + false); + } + if (methodEntry.getReportEntryType() != SUCCESS || enableOutErrElements) { + createOutErrElements(fw, ppw, methodEntry, outputStream); + } + ppw.endElement(); + } + } + + private void serializeTestClassWithRerun( + OutputStream outputStream, + OutputStreamWriter fw, + XMLWriter ppw, + List methodEntries, + Map> methodStatistics) + throws IOException { + WrappedReportEntry firstMethodEntry = methodEntries.get(0); + + TestResultType resultType = + getTestResultTypeWithBeforeAllHandling(firstMethodEntry.getName(), methodEntries, methodStatistics); + + switch (resultType) { + case SUCCESS: + for (WrappedReportEntry methodEntry : methodEntries) { + if (methodEntry.getReportEntryType() == SUCCESS) { + startTestElement(ppw, methodEntry); + ppw.endElement(); + } + } + break; + case ERROR: + case FAILURE: + // When rerunFailingTestsCount is set to larger than 0 + startTestElement(ppw, firstMethodEntry); + boolean firstRun = true; + for (WrappedReportEntry singleRunEntry : methodEntries) { + if (firstRun) { + firstRun = false; + getTestProblems( + fw, + ppw, + singleRunEntry, + trimStackTrace, + outputStream, + singleRunEntry.getReportEntryType().getXmlTag(), + false); + createOutErrElements(fw, ppw, singleRunEntry, outputStream); + } else if (singleRunEntry.getReportEntryType() == SKIPPED) { + // The version 3.1.0 should produce a new XSD schema with version 3.1.0, see SUREFIRE-1986, + // and the XSD schema should add a new element "rerunSkipped" + // then ReportEntryType should update the enum to SKIPPED( "skipped", "", "rerunSkipped" ). + // The teams should be notified - Jenkins reports. + addCommentElementTestCase("a skipped test execution in re-run phase", fw, ppw, outputStream); + } else { + getTestProblems( + fw, + ppw, + singleRunEntry, + trimStackTrace, + outputStream, + singleRunEntry.getReportEntryType().getRerunXmlTag(), + true); + } + } + ppw.endElement(); + break; + case FLAKE: + WrappedReportEntry successful = null; + // Get the run time of the first successful run + for (WrappedReportEntry singleRunEntry : methodEntries) { + if (singleRunEntry.getReportEntryType() == SUCCESS) { + successful = singleRunEntry; + break; + } + } + WrappedReportEntry firstOrSuccessful = successful == null ? methodEntries.get(0) : successful; + startTestElement(ppw, firstOrSuccessful); + for (WrappedReportEntry singleRunEntry : methodEntries) { + if (singleRunEntry.getReportEntryType() != SUCCESS) { + getTestProblems( + fw, + ppw, + singleRunEntry, + trimStackTrace, + outputStream, + singleRunEntry.getReportEntryType().getFlakyXmlTag(), + true); + } + } + ppw.endElement(); + break; + case SKIPPED: + startTestElement(ppw, firstMethodEntry); + getTestProblems( + fw, + ppw, + firstMethodEntry, + trimStackTrace, + outputStream, + firstMethodEntry.getReportEntryType().getXmlTag(), + false); + ppw.endElement(); + break; + default: + throw new IllegalStateException("Get unknown test result type"); + } + } + + /** + * Clean testClassMethodRunHistoryMap. + */ + public void cleanTestHistoryMap() { + testClassMethodRunHistoryMap.clear(); + } + + /** + * Get the result of a test from a list of its runs in WrappedReportEntry. + * + * @param methodEntryList the list of runs for a given test + * @return the TestResultType for the given test + */ + private TestResultType getTestResultType(List methodEntryList) { + List testResultTypeList = new ArrayList<>(); + for (WrappedReportEntry singleRunEntry : methodEntryList) { + testResultTypeList.add(singleRunEntry.getReportEntryType()); + } + + return DefaultReporterFactory.getTestResultType(testResultTypeList, rerunFailingTestsCount); + } + + /** + * Determines the final result type for a test method, applying special handling for @BeforeAll failures. + * If a @BeforeAll fails but any actual test methods succeed, it's classified as a FLAKE. + * + * @param methodName the name of the test method (null or "null" for @BeforeAll) + * @param methodRuns the list of runs for this method + * @param methodStatistics all method statistics for the test class + * @return the final TestResultType + */ + private TestResultType getTestResultTypeWithBeforeAllHandling( + String methodName, + List methodRuns, + Map> methodStatistics) { + TestResultType resultType = getTestResultType(methodRuns); + + // Special handling for @BeforeAll failures (null method name or method name is "null" or "initializationError") + // If @BeforeAll failed but any actual test methods succeeded, treat it as a flake + if ((methodName == null || methodName.equals("null") || methodName.equals("initializationError")) + && (resultType == TestResultType.ERROR || resultType == TestResultType.FAILURE)) { + // Check if any actual test methods (non-null and not "null" names) succeeded + boolean hasSuccessfulTestMethods = methodStatistics.entrySet().stream() + .filter(entry -> + entry.getKey() != null && !entry.getKey().equals("null")) // Only actual test methods + .anyMatch(entry -> entry.getValue().stream() + .anyMatch(reportEntry -> reportEntry.getReportEntryType() == SUCCESS)); + + if (hasSuccessfulTestMethods) { + resultType = TestResultType.FLAKE; + } + } + + return resultType; + } + + private Deque getAddMethodRunHistoryMap(String testClassName) { + Deque methodRunHistory = testClassMethodRunHistoryMap.get(testClassName); + if (methodRunHistory == null) { + methodRunHistory = new ConcurrentLinkedDeque<>(); + testClassMethodRunHistoryMap.put(testClassName == null ? "null" : testClassName, methodRunHistory); + } + return methodRunHistory; + } + + private OutputStream getOutputStream(WrappedReportEntry testSetReportEntry) throws IOException { + File reportFile = getReportFile(testSetReportEntry); + File reportDir = reportFile.getParentFile(); + //noinspection ResultOfMethodCallIgnored + reportFile.delete(); + //noinspection ResultOfMethodCallIgnored + reportDir.mkdirs(); + return new BufferedOutputStream(Files.newOutputStream(reportFile.toPath()), 64 * 1024); + } + + private static OutputStreamWriter getWriter(OutputStream fos) { + return new OutputStreamWriter(fos, UTF_8); + } + + private File getReportFile(WrappedReportEntry report) { + String reportName = "TEST-" + (phrasedFileName ? report.getReportSourceName() : report.getSourceName()); + String customizedReportName = isBlank(reportNameSuffix) ? reportName : reportName + "-" + reportNameSuffix; + return new File(reportsDirectory, stripIllegalFilenameChars(customizedReportName + ".xml")); + } + + private void startTestElement(XMLWriter ppw, WrappedReportEntry report) throws IOException { + ppw.startElement("testcase"); + String name = phrasedMethodName ? report.getReportName() : report.getName(); + ppw.addAttribute("name", name == null ? "" : extraEscapeAttribute(name)); + + if (report.getGroup() != null) { + ppw.addAttribute("group", extraEscapeAttribute(report.getGroup())); + } + + String className = phrasedClassName + ? report.getReportSourceName(reportNameSuffix) + : report.getSourceQualifiedName(reportNameSuffix); + if (className != null) { + ppw.addAttribute("classname", extraEscapeAttribute(className)); + } + + if (report.getElapsed() != null) { + ppw.addAttribute("time", String.valueOf(report.getElapsed() / ONE_SECOND)); + } + + if (reportTestTimestamp && report.getStartTime() > 0L) { + ppw.addAttribute( + "timestamp", Instant.ofEpochMilli(report.getStartTime()).toString()); + } + } + + private void createTestSuiteElement( + XMLWriter ppw, + WrappedReportEntry report, + Map>> classMethodStatistics) + throws IOException { + ppw.startElement("testsuite"); + + ppw.addAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"); + ppw.addAttribute("xsi:noNamespaceSchemaLocation", extraEscapeAttribute(xsdSchemaLocation)); + ppw.addAttribute("version", extraEscapeAttribute(xsdVersion)); + + String reportName = phrasedSuiteName + ? report.getReportSourceName(reportNameSuffix) + : report.getSourceName(reportNameSuffix); + ppw.addAttribute("name", reportName == null ? "" : extraEscapeAttribute(reportName)); + + if (report.getGroup() != null) { + ppw.addAttribute("group", extraEscapeAttribute(report.getGroup())); + } + + if (report.getElapsed() != null) { + ppw.addAttribute("time", String.valueOf(report.getElapsed() / ONE_SECOND)); + } + + if (reportTestTimestamp && report.getStartTime() > 0L) { + ppw.addAttribute( + "timestamp", Instant.ofEpochMilli(report.getStartTime()).toString()); + } + + // Count actual unique test methods and their final results from classMethodStatistics (accumulated across + // reruns) + int actualTestCount = 0; + int errors = 0; + int failures = 0; + int skipped = 0; + int flakes = 0; + + for (Map> methodStats : classMethodStatistics.values()) { + actualTestCount += methodStats.size(); + for (Map.Entry> methodEntry : methodStats.entrySet()) { + String methodName = methodEntry.getKey(); + List methodRuns = methodEntry.getValue(); + TestResultType resultType = getTestResultTypeWithBeforeAllHandling(methodName, methodRuns, methodStats); + + switch (resultType) { + case ERROR: + errors++; + break; + case FAILURE: + failures++; + break; + case SKIPPED: + skipped++; + break; + case FLAKE: + flakes++; + break; + case SUCCESS: + default: + break; + } + } + } + + ppw.addAttribute("tests", String.valueOf(actualTestCount)); + ppw.addAttribute("errors", String.valueOf(errors)); + ppw.addAttribute("skipped", String.valueOf(skipped)); + ppw.addAttribute("failures", String.valueOf(failures)); + ppw.addAttribute("flakes", String.valueOf(flakes)); + } + + private static void getTestProblems( + OutputStreamWriter outputStreamWriter, + XMLWriter ppw, + WrappedReportEntry report, + boolean trimStackTrace, + OutputStream fw, + String testErrorType, + boolean createNestedOutErrElements) + throws IOException { + ppw.startElement(testErrorType); + + String stackTrace = report.getStackTrace(trimStackTrace); + + if (report.getMessage() != null && !report.getMessage().isEmpty()) { + ppw.addAttribute("message", extraEscapeAttribute(report.getMessage())); + } + + if (report.getStackTraceWriter() != null) { + //noinspection ThrowableResultOfMethodCallIgnored + SafeThrowable t = report.getStackTraceWriter().getThrowable(); + if (t != null) { + if (t.getMessage() != null) { + int delimiter = stackTrace.indexOf(":"); + String type = delimiter == -1 ? stackTrace : stackTrace.substring(0, delimiter); + ppw.addAttribute("type", extraEscapeAttribute(type)); + } else { + if (isNotBlank(stackTrace)) { + ppw.addAttribute("type", extraEscapeAttribute(new StringTokenizer(stackTrace).nextToken())); + } + } + } + } + + /* This structure is inconsistent due to bad legacy design choices for the XML schema. + * Ideally, all elements would be complex and strackTrace would have its own element. + * See SUREFIRE-2230 for details to how improve and unify the schema in the future. + */ + if (createNestedOutErrElements) { + ppw.startElement("stackTrace"); + if (stackTrace != null) { + extraEscapeElementValue(stackTrace, outputStreamWriter, ppw, fw); + } + ppw.endElement(); + + createOutErrElements(outputStreamWriter, ppw, report, fw); + } else if (stackTrace != null) { + extraEscapeElementValue(stackTrace, outputStreamWriter, ppw, fw); + } + + ppw.endElement(); // entry type + } + + // Create system-out and system-err elements + private static void createOutErrElements( + OutputStreamWriter outputStreamWriter, XMLWriter ppw, WrappedReportEntry report, OutputStream fw) + throws IOException { + addOutputStreamElement(outputStreamWriter, ppw, report.getStdout(), "system-out", fw); + addOutputStreamElement(outputStreamWriter, ppw, report.getStdErr(), "system-err", fw); + } + + private static void addOutputStreamElement( + OutputStreamWriter outputStreamWriter, + XMLWriter xmlWriter, + Utf8RecodingDeferredFileOutputStream utf8RecodingDeferredFileOutputStream, + String name, + OutputStream fw) + throws IOException { + if (utf8RecodingDeferredFileOutputStream != null && utf8RecodingDeferredFileOutputStream.getByteCount() > 0) { + EncodingOutputStream eos = new EncodingOutputStream(fw); + xmlWriter.startElement(name); + xmlWriter.writeText(""); // Cheat sax to emit element + outputStreamWriter.flush(); + eos.getUnderlying().write(ByteConstantsHolder.CDATA_START_BYTES); // emit cdata + utf8RecodingDeferredFileOutputStream.writeTo(eos); + utf8RecodingDeferredFileOutputStream.free(); + eos.finish(); + eos.getUnderlying().write(ByteConstantsHolder.CDATA_END_BYTES); + eos.flush(); + xmlWriter.endElement(); + } + } + + /** + * Adds system properties to the XML report. + *
+ * + * @param xmlWriter the test suite to report to + */ + private static void showProperties(XMLWriter xmlWriter, Map systemProperties) throws IOException { + xmlWriter.startElement("properties"); + for (final Entry entry : systemProperties.entrySet()) { + final String key = entry.getKey(); + String value = entry.getValue(); + + if (value == null) { + value = "null"; + } + + xmlWriter.startElement("property"); + + xmlWriter.addAttribute("name", extraEscapeAttribute(key)); + + xmlWriter.addAttribute("value", extraEscapeAttribute(value)); + + xmlWriter.endElement(); + } + xmlWriter.endElement(); + } + + /** + * Handle stuff that may pop up in java that is not legal in xml. + * + * @param message the string + * @return the escaped string or returns itself if all characters are legal + */ + private static String extraEscapeAttribute(String message) { + // Someday convert to xml 1.1 which handles everything but 0 inside string + return containsEscapesIllegalXml10(message) ? escapeXml(message, true) : message; + } + + /** + * Writes escaped string or the message within CDATA if all characters are legal. + * + * @param message the string + */ + private static void extraEscapeElementValue( + String message, OutputStreamWriter outputStreamWriter, XMLWriter xmlWriter, OutputStream fw) + throws IOException { + // Someday convert to xml 1.1 which handles everything but 0 inside string + if (containsEscapesIllegalXml10(message)) { + xmlWriter.writeText(escapeXml(message, false)); + } else { + EncodingOutputStream eos = new EncodingOutputStream(fw); + xmlWriter.writeText(""); // Cheat sax to emit element + outputStreamWriter.flush(); + eos.getUnderlying().write(ByteConstantsHolder.CDATA_START_BYTES); + eos.write(message.getBytes(UTF_8)); + eos.finish(); + eos.getUnderlying().write(ByteConstantsHolder.CDATA_END_BYTES); + eos.flush(); + } + } + + // todo: SUREFIRE-1986 + private static void addCommentElementTestCase( + String comment, OutputStreamWriter outputStreamWriter, XMLWriter xmlWriter, OutputStream fw) + throws IOException { + xmlWriter.writeText(""); // Cheat sax to emit element + outputStreamWriter.flush(); + fw.write(XML_NL.getBytes(UTF_8)); + fw.write(XML_INDENT.getBytes(UTF_8)); + fw.write(XML_INDENT.getBytes(UTF_8)); + fw.write(ByteConstantsHolder.COMMENT_START); + fw.write(comment.getBytes(UTF_8)); + fw.write(ByteConstantsHolder.COMMENT_END); + fw.write(XML_NL.getBytes(UTF_8)); + fw.write(XML_INDENT.getBytes(UTF_8)); + fw.flush(); + } + + private static final class EncodingOutputStream extends FilterOutputStream { + private int c1 = -1; + + private int c2 = -1; + + private int codePoint; + + private int expectedBytes; + + private int sequenceLength; + + private int sequenceByte1; + + private int sequenceByte2; + + private int sequenceByte3; + + private int sequenceByte4; + + private int minimumCodePoint; + + EncodingOutputStream(OutputStream out) { + super(out); + } + + OutputStream getUnderlying() { + return out; + } + + private boolean isCdataEndBlock(int c) { + return c1 == ']' && c2 == ']' && c == '>'; + } + + @Override + public void write(int b) throws IOException { + int value = b & 0xFF; + if (expectedBytes == 0) { + if (value <= 0x7F) { + writeCodePoint(value); + } else if (value >= 0xC2 && value <= 0xDF) { + startSequence(value, 2, 0x80); + } else if (value >= 0xE0 && value <= 0xEF) { + startSequence(value, 3, 0x800); + } else if (value >= 0xF0 && value <= 0xF4) { + startSequence(value, 4, 0x10000); + } else { + writeReplacementCharacter(); + } + } else if ((value & 0xC0) == 0x80) { + appendSequenceByte(value); + if (sequenceLength == expectedBytes) { + finishSequence(); + } + } else { + writeReplacementCharacter(); + resetSequence(); + write(value); + } + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + for (int i = off; i < off + len; i++) { + write(b[i] & 0xFF); + } + } + + void finish() throws IOException { + if (expectedBytes != 0) { + writeReplacementCharacter(); + resetSequence(); + } + } + + private void startSequence(int firstByte, int length, int minimumCodePoint) { + expectedBytes = length; + sequenceLength = 1; + sequenceByte1 = firstByte; + codePoint = firstByte & (0x7F >> length); + this.minimumCodePoint = minimumCodePoint; + } + + private void appendSequenceByte(int value) { + sequenceLength++; + if (sequenceLength == 2) { + sequenceByte2 = value; + } else if (sequenceLength == 3) { + sequenceByte3 = value; + } else { + sequenceByte4 = value; + } + codePoint = (codePoint << 6) | (value & 0x3F); + } + + private void finishSequence() throws IOException { + int completedCodePoint = codePoint; + int completedSequenceLength = sequenceLength; + int completedMinimumCodePoint = minimumCodePoint; + int byte1 = sequenceByte1; + int byte2 = sequenceByte2; + int byte3 = sequenceByte3; + int byte4 = sequenceByte4; + resetSequence(); + + if (completedCodePoint < completedMinimumCodePoint + || completedCodePoint > 0x10FFFF + || (completedCodePoint >= 0xD800 && completedCodePoint <= 0xDFFF)) { + writeReplacementCharacter(); + } else if (isIllegalEscape(completedCodePoint)) { + writeEscapedCodePoint(completedCodePoint); + } else { + writeCdataByte(byte1); + if (completedSequenceLength >= 2) { + writeCdataByte(byte2); + } + if (completedSequenceLength >= 3) { + writeCdataByte(byte3); + } + if (completedSequenceLength == 4) { + writeCdataByte(byte4); + } + } + } + + private void resetSequence() { + codePoint = 0; + expectedBytes = 0; + sequenceLength = 0; + minimumCodePoint = 0; + } + + private void writeCodePoint(int codePoint) throws IOException { + if (isIllegalEscape(codePoint)) { + writeEscapedCodePoint(codePoint); + } else { + writeCdataByte(codePoint); + } + } + + private void writeReplacementCharacter() throws IOException { + for (byte b : ByteConstantsHolder.REPLACEMENT_CHARACTER_BYTES) { + writeCdataByte(b & 0xFF); + } + } + + private void writeEscapedCodePoint(int codePoint) throws IOException { + byte[] escaped = ("&#" + codePoint + ";").getBytes(UTF_8); + for (byte b : escaped) { + writeCdataByte(b & 0xFF); + } + } + + private void writeCdataByte(int b) throws IOException { + if (isCdataEndBlock(b)) { + out.write(ByteConstantsHolder.CDATA_ESCAPE_STRING_BYTES); + } else { + out.write(b); + } + c1 = c2; + c2 = b; + } + } + + private static boolean containsEscapesIllegalXml10(String message) { + for (int i = 0; i < message.length(); ) { + int codePoint = message.codePointAt(i); + if (isIllegalEscape(codePoint)) { + return true; + } + i += Character.charCount(codePoint); + } + return false; + } + + private static boolean isIllegalEscape(int c) { + return !(c == 0x9 + || c == 0xA + || c == 0xD + || (c >= 0x20 && c <= 0xD7FF) + || (c >= 0xE000 && c <= 0xFFFD) + || (c >= 0x10000 && c <= 0x10FFFF)); + } + + /** + * Escape for XML 1.0. + * + * @param text the string + * @param attribute true if the escaped value is inside an attribute + * @return the escaped string + */ + private static String escapeXml(String text, boolean attribute) { + StringBuilder sb = new StringBuilder(text.length() * 2); + for (int i = 0; i < text.length(); ) { + int codePoint = text.codePointAt(i); + if (isIllegalEscape(codePoint)) { + // uh-oh! This character is illegal in XML 1.0! + // http://www.w3.org/TR/1998/REC-xml-19980210#charsets + // we're going to deliberately doubly-XML escape it... + // there's nothing better we can do! :-( + // SUREFIRE-456 + sb.append(attribute ? "&#" : "&#") + .append(codePoint) + .append(';'); // & Will be encoded to amp inside xml encodingSHO + } else { + sb.appendCodePoint(codePoint); + } + i += Character.charCount(codePoint); + } + return sb.toString(); + } + + private static final class ByteConstantsHolder { + private static final byte[] CDATA_START_BYTES = "".getBytes(UTF_8); + + private static final byte[] CDATA_ESCAPE_STRING_BYTES = "]]>".getBytes(UTF_8); + + private static final byte[] REPLACEMENT_CHARACTER_BYTES = "\uFFFD".getBytes(UTF_8); + + private static final byte[] COMMENT_START = " ".getBytes(UTF_8); + } +} diff --git a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/report/StatelessXmlReporterTest.java b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/report/StatelessXmlReporterTest.java index 9945133398..8749ada7fe 100644 --- a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/report/StatelessXmlReporterTest.java +++ b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/report/StatelessXmlReporterTest.java @@ -1,726 +1,810 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.maven.plugin.surefire.report; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.RandomAccessFile; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.nio.Buffer; -import java.nio.ByteBuffer; -import java.nio.file.Path; -import java.util.Collections; -import java.util.Deque; -import java.util.HashMap; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicInteger; - -import org.apache.maven.plugin.surefire.booterclient.output.DeserializedStacktraceWriter; -import org.apache.maven.surefire.api.report.ReportEntry; -import org.apache.maven.surefire.api.report.SimpleReportEntry; -import org.apache.maven.surefire.api.report.StackTraceWriter; -import org.apache.maven.surefire.shared.utils.xml.Xpp3Dom; -import org.apache.maven.surefire.shared.utils.xml.Xpp3DomBuilder; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import static java.nio.charset.StandardCharsets.UTF_8; -import static java.nio.file.Files.readAllLines; -import static org.apache.maven.plugin.surefire.report.ReportEntryType.ERROR; -import static org.apache.maven.plugin.surefire.report.ReportEntryType.SKIPPED; -import static org.apache.maven.plugin.surefire.report.ReportEntryType.SUCCESS; -import static org.apache.maven.surefire.api.report.RunMode.NORMAL_RUN; -import static org.apache.maven.surefire.api.report.RunMode.RERUN_TEST_AFTER_FAILURE; -import static org.apache.maven.surefire.api.util.internal.ObjectUtils.systemProps; -import static org.apache.maven.surefire.api.util.internal.StringUtils.NL; -import static org.apache.maven.surefire.shared.utils.StringUtils.isEmpty; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -/** - * - */ -@SuppressWarnings({"ResultOfMethodCallIgnored", "checkstyle:magicnumber"}) -public class StatelessXmlReporterTest { - private static final String XSD = - "https://maven.apache.org/surefire/maven-surefire-plugin/xsd/surefire-test-report.xsd"; - private static final String TEST_ONE = "aTestMethod"; - private static final String TEST_TWO = "bTestMethod"; - private static final String TEST_THREE = "cTestMethod"; - private static final AtomicInteger DIRECTORY_PREFIX = new AtomicInteger(); - - private TestSetStats stats; - private TestSetStats rerunStats; - private File expectedReportFile; - private File reportDir; - - @SuppressWarnings("unchecked") - private static T getInternalState(Object target, String fieldName) { - try { - Class clazz = target.getClass(); - while (clazz != null) { - try { - Field field = clazz.getDeclaredField(fieldName); - field.setAccessible(true); - return (T) field.get(target); - } catch (NoSuchFieldException e) { - clazz = clazz.getSuperclass(); - } - } - throw new NoSuchFieldException(fieldName); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - private static void setInternalState(Object target, String fieldName, Object value) { - try { - Class clazz = target.getClass(); - while (clazz != null) { - try { - Field field = clazz.getDeclaredField(fieldName); - field.setAccessible(true); - field.set(target, value); - return; - } catch (NoSuchFieldException e) { - clazz = clazz.getSuperclass(); - } - } - throw new NoSuchFieldException(fieldName); - } catch (NoSuchFieldException | IllegalAccessException e) { - throw new RuntimeException(e); - } - } - - @SuppressWarnings("unchecked") - private static T invokeMethod(Object target, String methodName, Object... args) throws Exception { - Class clazz = target.getClass(); - while (clazz != null) { - for (Method method : clazz.getDeclaredMethods()) { - if (method.getName().equals(methodName) && method.getParameterCount() == args.length) { - method.setAccessible(true); - return (T) method.invoke(target, args); - } - } - clazz = clazz.getSuperclass(); - } - throw new NoSuchMethodException(methodName); - } - - @BeforeEach - protected void setUp() throws Exception { - stats = new TestSetStats(false, true); - rerunStats = new TestSetStats(false, true); - - File basedir = new File("."); - File target = new File(basedir.getCanonicalFile(), "target"); - target.mkdir(); - String reportRelDir = getClass().getSimpleName() + "-" + DIRECTORY_PREFIX.incrementAndGet(); - reportDir = new File(target, reportRelDir); - reportDir.mkdir(); - } - - @AfterEach - protected void tearDown() { - if (expectedReportFile != null) { - expectedReportFile.delete(); - } - } - - @Test - public void testFileNameWithoutSuffix() { - StatelessXmlReporter reporter = new StatelessXmlReporter( - reportDir, - null, - false, - 0, - new ConcurrentHashMap>(), - XSD, - "3.0.2", - false, - false, - false, - false, - true, - true, - false); - reporter.cleanTestHistoryMap(); - - ReportEntry reportEntry = new SimpleReportEntry( - NORMAL_RUN, 0L, getClass().getName(), null, getClass().getName(), null, 12); - WrappedReportEntry testSetReportEntry = new WrappedReportEntry( - reportEntry, ReportEntryType.SUCCESS, 1771085631L, 12, null, null, systemProps()); - stats.testSucceeded(testSetReportEntry); - reporter.testSetCompleted(testSetReportEntry, stats); - - expectedReportFile = new File(reportDir, "TEST-" + getClass().getName() + ".xml"); - assertTrue( - expectedReportFile.exists(), - "Report file (" + expectedReportFile.getAbsolutePath() + ") doesn't exist"); - } - - @Test - public void testAllFieldsSerialized() throws IOException { - ReportEntry reportEntry = - new SimpleReportEntry(NORMAL_RUN, 0L, getClass().getName(), null, TEST_ONE, null, 12); - WrappedReportEntry testSetReportEntry = - new WrappedReportEntry(reportEntry, SUCCESS, 1771085631L, 12, null, null, systemProps()); - expectedReportFile = new File(reportDir, "TEST-" + getClass().getName() + ".xml"); - - stats.testSucceeded(testSetReportEntry); - StackTraceWriter stackTraceWriter = new DeserializedStacktraceWriter("A fud msg", "trimmed", "fail at foo"); - Utf8RecodingDeferredFileOutputStream stdOut = new Utf8RecodingDeferredFileOutputStream("fds"); - String stdOutPrefix; - String stdErrPrefix; - if (defaultCharsetSupportsSpecialChar()) { - stdErrPrefix = "std-\u0115rr"; - stdOutPrefix = "st]]>d-o\u00DCt"; - } else { - stdErrPrefix = "std-err"; - stdOutPrefix = "st]]>d-out"; - } - - stdOut.write(stdOutPrefix + "!\u0020\u0000\u001F", false, null); - - Utf8RecodingDeferredFileOutputStream stdErr = new Utf8RecodingDeferredFileOutputStream("fds"); - - stdErr.write(stdErrPrefix + "?&-&£\u0020\u0000\u001F", false, null); - WrappedReportEntry t2 = new WrappedReportEntry( - new SimpleReportEntry(NORMAL_RUN, 0L, getClass().getName(), null, TEST_TWO, null, stackTraceWriter, 13), - ReportEntryType.ERROR, - 1771085631L, - 13, - stdOut, - stdErr); - - stats.testSucceeded(t2); - StatelessXmlReporter reporter = new StatelessXmlReporter( - reportDir, - null, - false, - 0, - new ConcurrentHashMap>(), - XSD, - "3.0.2", - false, - false, - false, - false, - true, - true, - false); - reporter.testSetCompleted(testSetReportEntry, stats); - - Xpp3Dom testSuite; - try (FileInputStream fileInputStream = new FileInputStream(expectedReportFile); - InputStreamReader reader = new InputStreamReader(fileInputStream, UTF_8)) { - testSuite = Xpp3DomBuilder.build(reader); - } - assertEquals("testsuite", testSuite.getName()); - Xpp3Dom properties = testSuite.getChild("properties"); - assertEquals(System.getProperties().size(), properties.getChildCount()); - Xpp3Dom child = properties.getChild(1); - assertFalse(isEmpty(child.getAttribute("value"))); - assertFalse(isEmpty(child.getAttribute("name"))); - - Xpp3Dom[] testcase = testSuite.getChildren("testcase"); - Xpp3Dom tca = testcase[0]; - assertEquals(TEST_ONE, tca.getAttribute("name")); - assertEquals("0.012", tca.getAttribute("time")); - assertEquals(getClass().getName(), tca.getAttribute("classname")); - - Xpp3Dom tcb = testcase[1]; - assertEquals(TEST_TWO, tcb.getAttribute("name")); - assertEquals("0.013", tcb.getAttribute("time")); - assertEquals(getClass().getName(), tcb.getAttribute("classname")); - Xpp3Dom errorNode = tcb.getChild("error"); - assertNotNull(errorNode); - assertEquals("A fud msg", errorNode.getAttribute("message")); - assertEquals("fail at foo", errorNode.getAttribute("type")); - assertEquals( - stdOutPrefix + "! &#0;&#31;", - tcb.getChild("system-out").getValue()); - - assertEquals( - stdErrPrefix + "?&-&£ &#0;&#31;", - tcb.getChild("system-err").getValue()); - } - - @Test - public void testOutputRerunFlakyFailure() throws IOException { - WrappedReportEntry testSetReportEntry = new WrappedReportEntry( - new SimpleReportEntry(NORMAL_RUN, 0L, getClass().getName(), null, TEST_ONE, null, 12), - ReportEntryType.SUCCESS, - 1771085631L, - 12, - null, - null, - systemProps()); - expectedReportFile = new File(reportDir, "TEST-" + getClass().getName() + ".xml"); - - stats.testSucceeded(testSetReportEntry); - StackTraceWriter stackTraceWriterOne = new DeserializedStacktraceWriter("A fud msg", "trimmed", "fail at foo"); - StackTraceWriter stackTraceWriterTwo = - new DeserializedStacktraceWriter("A fud msg two", "trimmed two", "fail at foo two"); - - String firstRunOut = "first run out"; - String firstRunErr = "first run err"; - String secondRunOut = "second run out"; - String secondRunErr = "second run err"; - - String cls = getClass().getName(); - WrappedReportEntry testTwoFirstError = new WrappedReportEntry( - new SimpleReportEntry(NORMAL_RUN, 0L, cls, null, TEST_TWO, null, stackTraceWriterOne, 5), - ReportEntryType.ERROR, - 1771085631L, - 5, - createStdOutput(firstRunOut), - createStdOutput(firstRunErr)); - - WrappedReportEntry testTwoSecondError = new WrappedReportEntry( - new SimpleReportEntry(RERUN_TEST_AFTER_FAILURE, 1L, cls, null, TEST_TWO, null, stackTraceWriterTwo, 13), - ReportEntryType.ERROR, - 1771085631L, - 13, - createStdOutput(secondRunOut), - createStdOutput(secondRunErr)); - - WrappedReportEntry testThreeFirstRun = new WrappedReportEntry( - new SimpleReportEntry(NORMAL_RUN, 2L, cls, null, TEST_THREE, null, stackTraceWriterOne, 13), - ReportEntryType.FAILURE, - 1771085631L, - 13, - createStdOutput(firstRunOut), - createStdOutput(firstRunErr)); - - WrappedReportEntry testThreeSecondRun = new WrappedReportEntry( - new SimpleReportEntry( - RERUN_TEST_AFTER_FAILURE, 3L, cls, null, TEST_THREE, null, stackTraceWriterTwo, 2), - ReportEntryType.SUCCESS, - 1771085631L, - 2, - createStdOutput(secondRunOut), - createStdOutput(secondRunErr)); - - stats.testSucceeded(testTwoFirstError); - stats.testSucceeded(testThreeFirstRun); - rerunStats.testSucceeded(testTwoSecondError); - rerunStats.testSucceeded(testThreeSecondRun); - - StatelessXmlReporter reporter = new StatelessXmlReporter( - reportDir, - null, - false, - 1, - new HashMap>(), - XSD, - "3.0.2", - false, - false, - false, - false, - true, - true, - false); - - reporter.testSetCompleted(testSetReportEntry, stats); - reporter.testSetCompleted(testSetReportEntry, rerunStats); - - Xpp3Dom testSuite; - try (FileInputStream fileInputStream = new FileInputStream(expectedReportFile); - InputStreamReader reader = new InputStreamReader(fileInputStream, UTF_8)) { - testSuite = Xpp3DomBuilder.build(reader); - } - assertEquals("testsuite", testSuite.getName()); - assertEquals("0.012", testSuite.getAttribute("time")); - Xpp3Dom properties = testSuite.getChild("properties"); - assertEquals(System.getProperties().size(), properties.getChildCount()); - Xpp3Dom child = properties.getChild(1); - assertFalse(isEmpty(child.getAttribute("value"))); - assertFalse(isEmpty(child.getAttribute("name"))); - - Xpp3Dom[] testcase = testSuite.getChildren("testcase"); - Xpp3Dom testCaseOne = testcase[0]; - assertEquals(TEST_ONE, testCaseOne.getAttribute("name")); - assertEquals("0.012", testCaseOne.getAttribute("time")); - assertEquals(getClass().getName(), testCaseOne.getAttribute("classname")); - - Xpp3Dom testCaseTwo = testcase[1]; - assertEquals(TEST_TWO, testCaseTwo.getAttribute("name")); - // Run time for a rerun failing test is the run time of the first run - assertEquals("0.005", testCaseTwo.getAttribute("time")); - assertEquals(getClass().getName(), testCaseTwo.getAttribute("classname")); - Xpp3Dom errorNode = testCaseTwo.getChild("error"); - Xpp3Dom rerunErrorNode = testCaseTwo.getChild("rerunError"); - assertNotNull(errorNode); - assertNotNull(rerunErrorNode); - - assertEquals("A fud msg", errorNode.getAttribute("message")); - assertEquals("fail at foo", errorNode.getAttribute("type")); - - // Check rerun error node contains all the information - assertEquals(firstRunOut, testCaseTwo.getChild("system-out").getValue()); - assertEquals(firstRunErr, testCaseTwo.getChild("system-err").getValue()); - assertEquals(secondRunOut, rerunErrorNode.getChild("system-out").getValue()); - assertEquals(secondRunErr, rerunErrorNode.getChild("system-err").getValue()); - assertEquals("A fud msg two", rerunErrorNode.getAttribute("message")); - assertEquals("fail at foo two", rerunErrorNode.getAttribute("type")); - - // Check flaky failure node - Xpp3Dom testCaseThree = testcase[2]; - assertEquals(TEST_THREE, testCaseThree.getAttribute("name")); - // Run time for a flaky test is the run time of the first successful run - assertEquals("0.002", testCaseThree.getAttribute("time")); - assertEquals(getClass().getName(), testCaseThree.getAttribute("classname")); - Xpp3Dom flakyFailureNode = testCaseThree.getChild("flakyFailure"); - assertNotNull(flakyFailureNode); - assertEquals(firstRunOut, flakyFailureNode.getChild("system-out").getValue()); - assertEquals(firstRunErr, flakyFailureNode.getChild("system-err").getValue()); - // system-out and system-err should not be present for flaky failures - assertNull(testCaseThree.getChild("system-out")); - assertNull(testCaseThree.getChild("system-err")); - } - - @Test - public void testOutputRerunFlakyAssumption() throws IOException { - expectedReportFile = new File(reportDir, "TEST-" + getClass().getName() + ".xml"); - - StackTraceWriter stackTraceWriterOne = new DeserializedStacktraceWriter("A fud msg", "trimmed", "fail at foo"); - - StackTraceWriter stackTraceWriterTwo = - new DeserializedStacktraceWriter("A fud msg two", "trimmed two", "fail at foo two"); - - String firstRunOut = "first run out"; - String firstRunErr = "first run err"; - String secondRunOut = "second run out"; - String secondRunErr = "second run err"; - - WrappedReportEntry testTwoFirstError = new WrappedReportEntry( - new SimpleReportEntry( - NORMAL_RUN, 1L, getClass().getName(), null, TEST_TWO, null, stackTraceWriterOne, 5), - ERROR, - 1771085631L, - 5, - createStdOutput(firstRunOut), - createStdOutput(firstRunErr)); - - stats.testSucceeded(testTwoFirstError); - - WrappedReportEntry testTwoSecondError = new WrappedReportEntry( - new SimpleReportEntry( - RERUN_TEST_AFTER_FAILURE, - 1L, - getClass().getName(), - null, - TEST_TWO, - null, - stackTraceWriterTwo, - 13), - SKIPPED, - 1771085631L, - 13, - createStdOutput(secondRunOut), - createStdOutput(secondRunErr)); - - rerunStats.testSucceeded(testTwoSecondError); - - StatelessXmlReporter reporter = new StatelessXmlReporter( - reportDir, - null, - false, - 1, - new HashMap<>(), - XSD, - "3.0.2", - false, - false, - false, - false, - true, - true, - false); - - WrappedReportEntry testSetReportEntry = new WrappedReportEntry( - new SimpleReportEntry( - RERUN_TEST_AFTER_FAILURE, 1L, getClass().getName(), null, null, null, stackTraceWriterOne, 5), - ERROR, - 1771085631L, - 20, - createStdOutput(firstRunOut), - createStdOutput(firstRunErr)); - - reporter.testSetCompleted(testSetReportEntry, stats); - reporter.testSetCompleted(testSetReportEntry, rerunStats); - - Xpp3Dom testSuite; - try (FileInputStream fileInputStream = new FileInputStream(expectedReportFile); - InputStreamReader reader = new InputStreamReader(fileInputStream, UTF_8)) { - testSuite = Xpp3DomBuilder.build(reader); - } - assertEquals("testsuite", testSuite.getName()); - assertEquals("0.02", testSuite.getAttribute("time")); - - Xpp3Dom[] testcase = testSuite.getChildren("testcase"); - assertEquals(1, testcase.length); - Xpp3Dom testCaseOne = testcase[0]; - assertEquals(getClass().getName(), testCaseOne.getAttribute("classname")); - assertEquals(TEST_TWO, testCaseOne.getAttribute("name")); - assertEquals("0.005", testCaseOne.getAttribute("time")); - - Xpp3Dom[] testCaseElements = testCaseOne.getChildren(); - assertEquals(3, testCaseElements.length); - assertEquals("error", testCaseElements[0].getName()); - assertEquals("system-out", testCaseElements[1].getName()); - assertEquals("system-err", testCaseElements[2].getName()); - long linesWithComments = readAllLines(expectedReportFile.toPath(), UTF_8).stream() - .filter(line -> line.contains("")) - .count(); - assertEquals(1, linesWithComments); - } - - @Test - public void testNoWritesOnDeferredFile() throws Exception { - Utf8RecodingDeferredFileOutputStream out = new Utf8RecodingDeferredFileOutputStream("test"); - out.free(); - out.write("a", false, null); - assertThat((boolean) getInternalState(out, "isDirty")).isFalse(); - } - - @Test - public void testLengthOnDeferredFile() throws Exception { - Utf8RecodingDeferredFileOutputStream out = new Utf8RecodingDeferredFileOutputStream("test"); - - assertThat(out.getByteCount()).isZero(); - - File f = File.createTempFile("test", "tmp"); - RandomAccessFile storage = new RandomAccessFile(f, "rw"); - setInternalState(out, "storage", storage); - setInternalState(out, "file", f.toPath()); - storage.writeByte(0); - storage.getFD().sync(); - assertThat(out.getByteCount()).isEqualTo(1); - - storage.close(); - assertThat(f.delete()).isTrue(); - assertThat(out.getByteCount()).isZero(); - out.free(); - } - - @Test - @SuppressWarnings("checkstyle:magicnumber") - public void testWritesOnDeferredFile() throws Exception { - Utf8RecodingDeferredFileOutputStream out = new Utf8RecodingDeferredFileOutputStream("test"); - for (int i = 0; i < 33_000; i++) { - out.write("A", false, null); - out.write("B", true, null); - } - out.write(null, false, null); - out.write(null, true, null); - - assertThat(out.getByteCount()).isEqualTo(33_000 * (1 + 1 + NL.length()) + 4 + 4 + NL.length()); - - StringBuilder expectedContent = new StringBuilder(150_000); - for (int i = 0; i < 33_000; i++) { - expectedContent.append('A').append('B').append(NL); - } - expectedContent.append("null").append("null").append(NL); - ByteArrayOutputStream read = new ByteArrayOutputStream(150_000); - out.writeTo(read); - assertThat(read.toString()).isEqualTo(expectedContent.toString()); - - out.free(); - } - - @Test - public void testFreeOnDeferredFile() throws Exception { - Utf8RecodingDeferredFileOutputStream out = new Utf8RecodingDeferredFileOutputStream("test"); - setInternalState(out, "cache", ByteBuffer.allocate(0)); - Path path = mock(Path.class); - File file = mock(File.class); - when(path.toFile()).thenReturn(file); - setInternalState(out, "file", path); - RandomAccessFile storage = mock(RandomAccessFile.class); - doThrow(IOException.class).when(storage).close(); - setInternalState(out, "storage", storage); - out.free(); - assertThat((boolean) getInternalState(out, "closed")).isTrue(); - verify(file, times(1)).deleteOnExit(); - } - - @Test - public void testCacheOnDeferredFile() throws Exception { - Utf8RecodingDeferredFileOutputStream out = new Utf8RecodingDeferredFileOutputStream("test"); - byte[] b1 = invokeMethod(out, "getLargeCache", 1); - byte[] b2 = invokeMethod(out, "getLargeCache", 1); - assertThat(b1).isSameAs(b2); - assertThat(b1).hasSize(1); - - byte[] b3 = invokeMethod(out, "getLargeCache", 2); - assertThat(b3).isNotSameAs(b1); - assertThat(b3).hasSize(2); - - byte[] b4 = invokeMethod(out, "getLargeCache", 1); - assertThat(b4).isSameAs(b3); - assertThat(b3).hasSize(2); - } - - @Test - public void testSyncOnDeferredFile() throws Exception { - Utf8RecodingDeferredFileOutputStream out = new Utf8RecodingDeferredFileOutputStream("test"); - Buffer cache = ByteBuffer.wrap(new byte[] {1, 2, 3}); - cache.position(3); - setInternalState(out, "cache", cache); - assertThat((boolean) getInternalState(out, "isDirty")).isFalse(); - setInternalState(out, "isDirty", true); - File file = new File(reportDir, "test"); - setInternalState(out, "file", file.toPath()); - RandomAccessFile storage = new RandomAccessFile(file, "rw"); - setInternalState(out, "storage", storage); - invokeMethod(out, "sync"); - assertThat((boolean) getInternalState(out, "isDirty")).isFalse(); - storage.seek(0L); - assertThat(storage.read()).isEqualTo(1); - assertThat(storage.read()).isEqualTo(2); - assertThat(storage.read()).isEqualTo(3); - assertThat(storage.read()).isEqualTo(-1); - assertThat(storage.length()).isEqualTo(3L); - assertThat(cache.position()).isEqualTo(0); - assertThat(cache.limit()).isEqualTo(3); - storage.seek(3L); - invokeMethod(out, "sync"); - assertThat((boolean) getInternalState(out, "isDirty")).isFalse(); - assertThat(storage.length()).isEqualTo(3L); - assertThat(out.getByteCount()).isEqualTo(3L); - assertThat((boolean) getInternalState(out, "closed")).isFalse(); - out.free(); - assertThat((boolean) getInternalState(out, "closed")).isTrue(); - // todo assertThat( file ).doesNotExist(); - out.free(); - assertThat((boolean) getInternalState(out, "closed")).isTrue(); - } - - @Test - public void testReporterHandlesATestWithoutMessageAndWithEmptyStackTrace() { - StackTraceWriter stackTraceWriterOne = new DeserializedStacktraceWriter(null, null, ""); - - WrappedReportEntry testReport = new WrappedReportEntry( - new SimpleReportEntry( - NORMAL_RUN, 1L, getClass().getName(), null, "a test name", null, stackTraceWriterOne, 5), - ERROR, - 1771085631L, - 5, - null, - null); - - StatelessXmlReporter reporter = new StatelessXmlReporter( - reportDir, - null, - false, - 1, - new HashMap<>(), - XSD, - "3.0.2", - false, - false, - false, - false, - true, - true, - false); - - reporter.testSetCompleted(testReport, stats); - } - - @Test - public void testClassnameUsesActualClassNameWhenPhrasedClassNameDisabled() throws IOException { - String actualClassName = "MyTest"; - String displayName = "NewName"; - - ReportEntry reportEntry = new SimpleReportEntry( - NORMAL_RUN, - 0L, - actualClassName, - displayName, - actualClassName, - TEST_ONE, - null, - null, - 12, - null, - Collections.emptyMap()); - WrappedReportEntry testSetReportEntry = - new WrappedReportEntry(reportEntry, SUCCESS, 1771085631L, 12, null, null, systemProps()); - expectedReportFile = new File(reportDir, "TEST-" + actualClassName + ".xml"); - - stats.testSucceeded(testSetReportEntry); - - StatelessXmlReporter reporter = new StatelessXmlReporter( - reportDir, - null, - false, - 0, - new ConcurrentHashMap<>(), - XSD, - "3.0.2", - false, - false, - false, // phrasedClassName = false - false, - true, - true, - false); - reporter.testSetCompleted(testSetReportEntry, stats); - - Xpp3Dom testSuite; - try (FileInputStream fileInputStream = new FileInputStream(expectedReportFile); - InputStreamReader reader = new InputStreamReader(fileInputStream, UTF_8)) { - testSuite = Xpp3DomBuilder.build(reader); - } - Xpp3Dom testcase = testSuite.getChildren("testcase")[0]; - assertEquals( - actualClassName, - testcase.getAttribute("classname"), - "classname should be the actual class name, not the @DisplayName value"); - } - - private boolean defaultCharsetSupportsSpecialChar() { - // some charsets are not able to deal with \u0115 on both ways of the conversion - return "\u0115\u00DC".equals(new String("\u0115\u00DC".getBytes())); - } - - private Utf8RecodingDeferredFileOutputStream createStdOutput(String content) throws IOException { - Utf8RecodingDeferredFileOutputStream stdOut = new Utf8RecodingDeferredFileOutputStream("fds2"); - stdOut.write(content, false, null); - return stdOut; - } -} +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.plugin.surefire.report; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.RandomAccessFile; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.nio.file.Path; +import java.util.Collections; +import java.util.Deque; +import java.util.HashMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.maven.plugin.surefire.booterclient.output.DeserializedStacktraceWriter; +import org.apache.maven.surefire.api.report.ReportEntry; +import org.apache.maven.surefire.api.report.SimpleReportEntry; +import org.apache.maven.surefire.api.report.StackTraceWriter; +import org.apache.maven.surefire.shared.utils.xml.Xpp3Dom; +import org.apache.maven.surefire.shared.utils.xml.Xpp3DomBuilder; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.nio.file.Files.readAllLines; +import static org.apache.maven.plugin.surefire.report.ReportEntryType.ERROR; +import static org.apache.maven.plugin.surefire.report.ReportEntryType.SKIPPED; +import static org.apache.maven.plugin.surefire.report.ReportEntryType.SUCCESS; +import static org.apache.maven.surefire.api.report.RunMode.NORMAL_RUN; +import static org.apache.maven.surefire.api.report.RunMode.RERUN_TEST_AFTER_FAILURE; +import static org.apache.maven.surefire.api.util.internal.ObjectUtils.systemProps; +import static org.apache.maven.surefire.api.util.internal.StringUtils.NL; +import static org.apache.maven.surefire.shared.utils.StringUtils.isEmpty; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * + */ +@SuppressWarnings({"ResultOfMethodCallIgnored", "checkstyle:magicnumber"}) +public class StatelessXmlReporterTest { + private static final String XSD = + "https://maven.apache.org/surefire/maven-surefire-plugin/xsd/surefire-test-report.xsd"; + private static final String TEST_ONE = "aTestMethod"; + private static final String TEST_TWO = "bTestMethod"; + private static final String TEST_THREE = "cTestMethod"; + private static final AtomicInteger DIRECTORY_PREFIX = new AtomicInteger(); + + private TestSetStats stats; + private TestSetStats rerunStats; + private File expectedReportFile; + private File reportDir; + + @SuppressWarnings("unchecked") + private static T getInternalState(Object target, String fieldName) { + try { + Class clazz = target.getClass(); + while (clazz != null) { + try { + Field field = clazz.getDeclaredField(fieldName); + field.setAccessible(true); + return (T) field.get(target); + } catch (NoSuchFieldException e) { + clazz = clazz.getSuperclass(); + } + } + throw new NoSuchFieldException(fieldName); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static void setInternalState(Object target, String fieldName, Object value) { + try { + Class clazz = target.getClass(); + while (clazz != null) { + try { + Field field = clazz.getDeclaredField(fieldName); + field.setAccessible(true); + field.set(target, value); + return; + } catch (NoSuchFieldException e) { + clazz = clazz.getSuperclass(); + } + } + throw new NoSuchFieldException(fieldName); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + @SuppressWarnings("unchecked") + private static T invokeMethod(Object target, String methodName, Object... args) throws Exception { + Class clazz = target.getClass(); + while (clazz != null) { + for (Method method : clazz.getDeclaredMethods()) { + if (method.getName().equals(methodName) && method.getParameterCount() == args.length) { + method.setAccessible(true); + return (T) method.invoke(target, args); + } + } + clazz = clazz.getSuperclass(); + } + throw new NoSuchMethodException(methodName); + } + + @BeforeEach + protected void setUp() throws Exception { + stats = new TestSetStats(false, true); + rerunStats = new TestSetStats(false, true); + + File basedir = new File("."); + File target = new File(basedir.getCanonicalFile(), "target"); + target.mkdir(); + String reportRelDir = getClass().getSimpleName() + "-" + DIRECTORY_PREFIX.incrementAndGet(); + reportDir = new File(target, reportRelDir); + reportDir.mkdir(); + } + + @AfterEach + protected void tearDown() { + if (expectedReportFile != null) { + expectedReportFile.delete(); + } + } + + @Test + public void testFileNameWithoutSuffix() { + StatelessXmlReporter reporter = new StatelessXmlReporter( + reportDir, + null, + false, + 0, + new ConcurrentHashMap>(), + XSD, + "3.0.2", + false, + false, + false, + false, + true, + true, + false); + reporter.cleanTestHistoryMap(); + + ReportEntry reportEntry = new SimpleReportEntry( + NORMAL_RUN, 0L, getClass().getName(), null, getClass().getName(), null, 12); + WrappedReportEntry testSetReportEntry = new WrappedReportEntry( + reportEntry, ReportEntryType.SUCCESS, 1771085631L, 12, null, null, systemProps()); + stats.testSucceeded(testSetReportEntry); + reporter.testSetCompleted(testSetReportEntry, stats); + + expectedReportFile = new File(reportDir, "TEST-" + getClass().getName() + ".xml"); + assertTrue( + expectedReportFile.exists(), + "Report file (" + expectedReportFile.getAbsolutePath() + ") doesn't exist"); + } + + @Test + public void testAllFieldsSerialized() throws IOException { + ReportEntry reportEntry = + new SimpleReportEntry(NORMAL_RUN, 0L, getClass().getName(), null, TEST_ONE, null, 12); + WrappedReportEntry testSetReportEntry = + new WrappedReportEntry(reportEntry, SUCCESS, 1771085631L, 12, null, null, systemProps()); + expectedReportFile = new File(reportDir, "TEST-" + getClass().getName() + ".xml"); + + stats.testSucceeded(testSetReportEntry); + StackTraceWriter stackTraceWriter = new DeserializedStacktraceWriter("A fud msg", "trimmed", "fail at foo"); + Utf8RecodingDeferredFileOutputStream stdOut = new Utf8RecodingDeferredFileOutputStream("fds"); + String stdOutPrefix; + String stdErrPrefix; + if (defaultCharsetSupportsSpecialChar()) { + stdErrPrefix = "std-\u0115rr"; + stdOutPrefix = "st]]>d-o\u00DCt"; + } else { + stdErrPrefix = "std-err"; + stdOutPrefix = "st]]>d-out"; + } + + stdOut.write(stdOutPrefix + "!\u0020\u0000\u001F", false, null); + + Utf8RecodingDeferredFileOutputStream stdErr = new Utf8RecodingDeferredFileOutputStream("fds"); + + stdErr.write(stdErrPrefix + "?&-&£\u0020\u0000\u001F", false, null); + WrappedReportEntry t2 = new WrappedReportEntry( + new SimpleReportEntry(NORMAL_RUN, 0L, getClass().getName(), null, TEST_TWO, null, stackTraceWriter, 13), + ReportEntryType.ERROR, + 1771085631L, + 13, + stdOut, + stdErr); + + stats.testSucceeded(t2); + StatelessXmlReporter reporter = new StatelessXmlReporter( + reportDir, + null, + false, + 0, + new ConcurrentHashMap>(), + XSD, + "3.0.2", + false, + false, + false, + false, + true, + true, + false); + reporter.testSetCompleted(testSetReportEntry, stats); + + Xpp3Dom testSuite; + try (FileInputStream fileInputStream = new FileInputStream(expectedReportFile); + InputStreamReader reader = new InputStreamReader(fileInputStream, UTF_8)) { + testSuite = Xpp3DomBuilder.build(reader); + } + assertEquals("testsuite", testSuite.getName()); + Xpp3Dom properties = testSuite.getChild("properties"); + assertEquals(System.getProperties().size(), properties.getChildCount()); + Xpp3Dom child = properties.getChild(1); + assertFalse(isEmpty(child.getAttribute("value"))); + assertFalse(isEmpty(child.getAttribute("name"))); + + Xpp3Dom[] testcase = testSuite.getChildren("testcase"); + Xpp3Dom tca = testcase[0]; + assertEquals(TEST_ONE, tca.getAttribute("name")); + assertEquals("0.012", tca.getAttribute("time")); + assertEquals(getClass().getName(), tca.getAttribute("classname")); + + Xpp3Dom tcb = testcase[1]; + assertEquals(TEST_TWO, tcb.getAttribute("name")); + assertEquals("0.013", tcb.getAttribute("time")); + assertEquals(getClass().getName(), tcb.getAttribute("classname")); + Xpp3Dom errorNode = tcb.getChild("error"); + assertNotNull(errorNode); + assertEquals("A fud msg", errorNode.getAttribute("message")); + assertEquals("fail at foo", errorNode.getAttribute("type")); + assertEquals( + stdOutPrefix + "! &#0;&#31;", + tcb.getChild("system-out").getValue()); + + assertEquals( + stdErrPrefix + "?&-&£ &#0;&#31;", + tcb.getChild("system-err").getValue()); + } + + @Test + public void testInvalidXml10CharactersAreEscaped() throws Exception { + String supplementaryCharacter = new String(Character.toChars(0x1F600)); + StackTraceWriter stackTraceWriter = new DeserializedStacktraceWriter( + "failure\uFFFF", + "trimmed " + supplementaryCharacter, + "stack trace\uFFFE\uFFFF " + supplementaryCharacter); + + Utf8RecodingDeferredFileOutputStream stdOut = new Utf8RecodingDeferredFileOutputStream("fds"); + StringBuilder stdout = new StringBuilder(Utf8RecodingDeferredFileOutputStream.CACHE_SIZE + 16); + for (int i = 0; i < Utf8RecodingDeferredFileOutputStream.CACHE_SIZE - 1; i++) { + stdout.append('a'); + } + stdout.append('\uFFFF').append(" ]]> "); + stdOut.write(stdout.toString(), false, null); + + Utf8RecodingDeferredFileOutputStream stdErr = new Utf8RecodingDeferredFileOutputStream("fds"); + stdErr.write("stderr\uFFFE " + supplementaryCharacter, false, null); + + HashMap systemProperties = new HashMap<>(); + systemProperties.put("property\uFFFF", "value\uFFFE"); + + ReportEntry testSetReport = + new SimpleReportEntry(NORMAL_RUN, 0L, getClass().getName(), null, TEST_ONE, null, systemProperties); + WrappedReportEntry testSetReportEntry = + new WrappedReportEntry(testSetReport, SUCCESS, 1771085631L, 12, null, null, systemProperties); + stats.testSucceeded(testSetReportEntry); + + ReportEntry failingReport = new SimpleReportEntry( + NORMAL_RUN, + 0L, + getClass().getName(), + null, + "test\uFFFF", + null, + stackTraceWriter, + 13, + "message\uFFFE", + systemProperties); + WrappedReportEntry failingReportEntry = + new WrappedReportEntry(failingReport, ERROR, 1771085631L, 13, stdOut, stdErr, systemProperties); + stats.testSucceeded(failingReportEntry); + + expectedReportFile = new File(reportDir, "TEST-" + getClass().getName() + ".xml"); + StatelessXmlReporter reporter = new StatelessXmlReporter( + reportDir, + null, + false, + 0, + new ConcurrentHashMap>(), + XSD, + "3.0.2", + false, + false, + false, + false, + true, + true, + false); + reporter.testSetCompleted(testSetReportEntry, stats); + + String xml = new String(java.nio.file.Files.readAllBytes(expectedReportFile.toPath()), UTF_8); + assertFalse(xml.contains("\uFFFE")); + assertFalse(xml.contains("\uFFFF")); + assertThat(xml).contains("&#65535;").contains("&#65534;"); + + Xpp3Dom testSuite; + try (FileInputStream fileInputStream = new FileInputStream(expectedReportFile); + InputStreamReader reader = new InputStreamReader(fileInputStream, UTF_8)) { + testSuite = Xpp3DomBuilder.build(reader); + } + + Xpp3Dom[] testcases = testSuite.getChildren("testcase"); + Xpp3Dom testcase = testcases[1]; + assertThat(testcase.getAttribute("name")).contains("65535"); + assertThat(testcase.getChild("error").getValue()).contains("65534", supplementaryCharacter); + assertThat(testcase.getChild("system-out").getValue()) + .contains("&#65535;") + .contains("]]>"); + assertThat(testcase.getChild("system-err").getValue()) + .contains("&#65534;") + .contains(supplementaryCharacter); + } + + @Test + public void testOutputRerunFlakyFailure() throws IOException { + WrappedReportEntry testSetReportEntry = new WrappedReportEntry( + new SimpleReportEntry(NORMAL_RUN, 0L, getClass().getName(), null, TEST_ONE, null, 12), + ReportEntryType.SUCCESS, + 1771085631L, + 12, + null, + null, + systemProps()); + expectedReportFile = new File(reportDir, "TEST-" + getClass().getName() + ".xml"); + + stats.testSucceeded(testSetReportEntry); + StackTraceWriter stackTraceWriterOne = new DeserializedStacktraceWriter("A fud msg", "trimmed", "fail at foo"); + StackTraceWriter stackTraceWriterTwo = + new DeserializedStacktraceWriter("A fud msg two", "trimmed two", "fail at foo two"); + + String firstRunOut = "first run out"; + String firstRunErr = "first run err"; + String secondRunOut = "second run out"; + String secondRunErr = "second run err"; + + String cls = getClass().getName(); + WrappedReportEntry testTwoFirstError = new WrappedReportEntry( + new SimpleReportEntry(NORMAL_RUN, 0L, cls, null, TEST_TWO, null, stackTraceWriterOne, 5), + ReportEntryType.ERROR, + 1771085631L, + 5, + createStdOutput(firstRunOut), + createStdOutput(firstRunErr)); + + WrappedReportEntry testTwoSecondError = new WrappedReportEntry( + new SimpleReportEntry(RERUN_TEST_AFTER_FAILURE, 1L, cls, null, TEST_TWO, null, stackTraceWriterTwo, 13), + ReportEntryType.ERROR, + 1771085631L, + 13, + createStdOutput(secondRunOut), + createStdOutput(secondRunErr)); + + WrappedReportEntry testThreeFirstRun = new WrappedReportEntry( + new SimpleReportEntry(NORMAL_RUN, 2L, cls, null, TEST_THREE, null, stackTraceWriterOne, 13), + ReportEntryType.FAILURE, + 1771085631L, + 13, + createStdOutput(firstRunOut), + createStdOutput(firstRunErr)); + + WrappedReportEntry testThreeSecondRun = new WrappedReportEntry( + new SimpleReportEntry( + RERUN_TEST_AFTER_FAILURE, 3L, cls, null, TEST_THREE, null, stackTraceWriterTwo, 2), + ReportEntryType.SUCCESS, + 1771085631L, + 2, + createStdOutput(secondRunOut), + createStdOutput(secondRunErr)); + + stats.testSucceeded(testTwoFirstError); + stats.testSucceeded(testThreeFirstRun); + rerunStats.testSucceeded(testTwoSecondError); + rerunStats.testSucceeded(testThreeSecondRun); + + StatelessXmlReporter reporter = new StatelessXmlReporter( + reportDir, + null, + false, + 1, + new HashMap>(), + XSD, + "3.0.2", + false, + false, + false, + false, + true, + true, + false); + + reporter.testSetCompleted(testSetReportEntry, stats); + reporter.testSetCompleted(testSetReportEntry, rerunStats); + + Xpp3Dom testSuite; + try (FileInputStream fileInputStream = new FileInputStream(expectedReportFile); + InputStreamReader reader = new InputStreamReader(fileInputStream, UTF_8)) { + testSuite = Xpp3DomBuilder.build(reader); + } + assertEquals("testsuite", testSuite.getName()); + assertEquals("0.012", testSuite.getAttribute("time")); + Xpp3Dom properties = testSuite.getChild("properties"); + assertEquals(System.getProperties().size(), properties.getChildCount()); + Xpp3Dom child = properties.getChild(1); + assertFalse(isEmpty(child.getAttribute("value"))); + assertFalse(isEmpty(child.getAttribute("name"))); + + Xpp3Dom[] testcase = testSuite.getChildren("testcase"); + Xpp3Dom testCaseOne = testcase[0]; + assertEquals(TEST_ONE, testCaseOne.getAttribute("name")); + assertEquals("0.012", testCaseOne.getAttribute("time")); + assertEquals(getClass().getName(), testCaseOne.getAttribute("classname")); + + Xpp3Dom testCaseTwo = testcase[1]; + assertEquals(TEST_TWO, testCaseTwo.getAttribute("name")); + // Run time for a rerun failing test is the run time of the first run + assertEquals("0.005", testCaseTwo.getAttribute("time")); + assertEquals(getClass().getName(), testCaseTwo.getAttribute("classname")); + Xpp3Dom errorNode = testCaseTwo.getChild("error"); + Xpp3Dom rerunErrorNode = testCaseTwo.getChild("rerunError"); + assertNotNull(errorNode); + assertNotNull(rerunErrorNode); + + assertEquals("A fud msg", errorNode.getAttribute("message")); + assertEquals("fail at foo", errorNode.getAttribute("type")); + + // Check rerun error node contains all the information + assertEquals(firstRunOut, testCaseTwo.getChild("system-out").getValue()); + assertEquals(firstRunErr, testCaseTwo.getChild("system-err").getValue()); + assertEquals(secondRunOut, rerunErrorNode.getChild("system-out").getValue()); + assertEquals(secondRunErr, rerunErrorNode.getChild("system-err").getValue()); + assertEquals("A fud msg two", rerunErrorNode.getAttribute("message")); + assertEquals("fail at foo two", rerunErrorNode.getAttribute("type")); + + // Check flaky failure node + Xpp3Dom testCaseThree = testcase[2]; + assertEquals(TEST_THREE, testCaseThree.getAttribute("name")); + // Run time for a flaky test is the run time of the first successful run + assertEquals("0.002", testCaseThree.getAttribute("time")); + assertEquals(getClass().getName(), testCaseThree.getAttribute("classname")); + Xpp3Dom flakyFailureNode = testCaseThree.getChild("flakyFailure"); + assertNotNull(flakyFailureNode); + assertEquals(firstRunOut, flakyFailureNode.getChild("system-out").getValue()); + assertEquals(firstRunErr, flakyFailureNode.getChild("system-err").getValue()); + // system-out and system-err should not be present for flaky failures + assertNull(testCaseThree.getChild("system-out")); + assertNull(testCaseThree.getChild("system-err")); + } + + @Test + public void testOutputRerunFlakyAssumption() throws IOException { + expectedReportFile = new File(reportDir, "TEST-" + getClass().getName() + ".xml"); + + StackTraceWriter stackTraceWriterOne = new DeserializedStacktraceWriter("A fud msg", "trimmed", "fail at foo"); + + StackTraceWriter stackTraceWriterTwo = + new DeserializedStacktraceWriter("A fud msg two", "trimmed two", "fail at foo two"); + + String firstRunOut = "first run out"; + String firstRunErr = "first run err"; + String secondRunOut = "second run out"; + String secondRunErr = "second run err"; + + WrappedReportEntry testTwoFirstError = new WrappedReportEntry( + new SimpleReportEntry( + NORMAL_RUN, 1L, getClass().getName(), null, TEST_TWO, null, stackTraceWriterOne, 5), + ERROR, + 1771085631L, + 5, + createStdOutput(firstRunOut), + createStdOutput(firstRunErr)); + + stats.testSucceeded(testTwoFirstError); + + WrappedReportEntry testTwoSecondError = new WrappedReportEntry( + new SimpleReportEntry( + RERUN_TEST_AFTER_FAILURE, + 1L, + getClass().getName(), + null, + TEST_TWO, + null, + stackTraceWriterTwo, + 13), + SKIPPED, + 1771085631L, + 13, + createStdOutput(secondRunOut), + createStdOutput(secondRunErr)); + + rerunStats.testSucceeded(testTwoSecondError); + + StatelessXmlReporter reporter = new StatelessXmlReporter( + reportDir, + null, + false, + 1, + new HashMap<>(), + XSD, + "3.0.2", + false, + false, + false, + false, + true, + true, + false); + + WrappedReportEntry testSetReportEntry = new WrappedReportEntry( + new SimpleReportEntry( + RERUN_TEST_AFTER_FAILURE, 1L, getClass().getName(), null, null, null, stackTraceWriterOne, 5), + ERROR, + 1771085631L, + 20, + createStdOutput(firstRunOut), + createStdOutput(firstRunErr)); + + reporter.testSetCompleted(testSetReportEntry, stats); + reporter.testSetCompleted(testSetReportEntry, rerunStats); + + Xpp3Dom testSuite; + try (FileInputStream fileInputStream = new FileInputStream(expectedReportFile); + InputStreamReader reader = new InputStreamReader(fileInputStream, UTF_8)) { + testSuite = Xpp3DomBuilder.build(reader); + } + assertEquals("testsuite", testSuite.getName()); + assertEquals("0.02", testSuite.getAttribute("time")); + + Xpp3Dom[] testcase = testSuite.getChildren("testcase"); + assertEquals(1, testcase.length); + Xpp3Dom testCaseOne = testcase[0]; + assertEquals(getClass().getName(), testCaseOne.getAttribute("classname")); + assertEquals(TEST_TWO, testCaseOne.getAttribute("name")); + assertEquals("0.005", testCaseOne.getAttribute("time")); + + Xpp3Dom[] testCaseElements = testCaseOne.getChildren(); + assertEquals(3, testCaseElements.length); + assertEquals("error", testCaseElements[0].getName()); + assertEquals("system-out", testCaseElements[1].getName()); + assertEquals("system-err", testCaseElements[2].getName()); + long linesWithComments = readAllLines(expectedReportFile.toPath(), UTF_8).stream() + .filter(line -> line.contains("")) + .count(); + assertEquals(1, linesWithComments); + } + + @Test + public void testNoWritesOnDeferredFile() throws Exception { + Utf8RecodingDeferredFileOutputStream out = new Utf8RecodingDeferredFileOutputStream("test"); + out.free(); + out.write("a", false, null); + assertThat((boolean) getInternalState(out, "isDirty")).isFalse(); + } + + @Test + public void testLengthOnDeferredFile() throws Exception { + Utf8RecodingDeferredFileOutputStream out = new Utf8RecodingDeferredFileOutputStream("test"); + + assertThat(out.getByteCount()).isZero(); + + File f = File.createTempFile("test", "tmp"); + RandomAccessFile storage = new RandomAccessFile(f, "rw"); + setInternalState(out, "storage", storage); + setInternalState(out, "file", f.toPath()); + storage.writeByte(0); + storage.getFD().sync(); + assertThat(out.getByteCount()).isEqualTo(1); + + storage.close(); + assertThat(f.delete()).isTrue(); + assertThat(out.getByteCount()).isZero(); + out.free(); + } + + @Test + @SuppressWarnings("checkstyle:magicnumber") + public void testWritesOnDeferredFile() throws Exception { + Utf8RecodingDeferredFileOutputStream out = new Utf8RecodingDeferredFileOutputStream("test"); + for (int i = 0; i < 33_000; i++) { + out.write("A", false, null); + out.write("B", true, null); + } + out.write(null, false, null); + out.write(null, true, null); + + assertThat(out.getByteCount()).isEqualTo(33_000 * (1 + 1 + NL.length()) + 4 + 4 + NL.length()); + + StringBuilder expectedContent = new StringBuilder(150_000); + for (int i = 0; i < 33_000; i++) { + expectedContent.append('A').append('B').append(NL); + } + expectedContent.append("null").append("null").append(NL); + ByteArrayOutputStream read = new ByteArrayOutputStream(150_000); + out.writeTo(read); + assertThat(read.toString()).isEqualTo(expectedContent.toString()); + + out.free(); + } + + @Test + public void testFreeOnDeferredFile() throws Exception { + Utf8RecodingDeferredFileOutputStream out = new Utf8RecodingDeferredFileOutputStream("test"); + setInternalState(out, "cache", ByteBuffer.allocate(0)); + Path path = mock(Path.class); + File file = mock(File.class); + when(path.toFile()).thenReturn(file); + setInternalState(out, "file", path); + RandomAccessFile storage = mock(RandomAccessFile.class); + doThrow(IOException.class).when(storage).close(); + setInternalState(out, "storage", storage); + out.free(); + assertThat((boolean) getInternalState(out, "closed")).isTrue(); + verify(file, times(1)).deleteOnExit(); + } + + @Test + public void testCacheOnDeferredFile() throws Exception { + Utf8RecodingDeferredFileOutputStream out = new Utf8RecodingDeferredFileOutputStream("test"); + byte[] b1 = invokeMethod(out, "getLargeCache", 1); + byte[] b2 = invokeMethod(out, "getLargeCache", 1); + assertThat(b1).isSameAs(b2); + assertThat(b1).hasSize(1); + + byte[] b3 = invokeMethod(out, "getLargeCache", 2); + assertThat(b3).isNotSameAs(b1); + assertThat(b3).hasSize(2); + + byte[] b4 = invokeMethod(out, "getLargeCache", 1); + assertThat(b4).isSameAs(b3); + assertThat(b3).hasSize(2); + } + + @Test + public void testSyncOnDeferredFile() throws Exception { + Utf8RecodingDeferredFileOutputStream out = new Utf8RecodingDeferredFileOutputStream("test"); + Buffer cache = ByteBuffer.wrap(new byte[] {1, 2, 3}); + cache.position(3); + setInternalState(out, "cache", cache); + assertThat((boolean) getInternalState(out, "isDirty")).isFalse(); + setInternalState(out, "isDirty", true); + File file = new File(reportDir, "test"); + setInternalState(out, "file", file.toPath()); + RandomAccessFile storage = new RandomAccessFile(file, "rw"); + setInternalState(out, "storage", storage); + invokeMethod(out, "sync"); + assertThat((boolean) getInternalState(out, "isDirty")).isFalse(); + storage.seek(0L); + assertThat(storage.read()).isEqualTo(1); + assertThat(storage.read()).isEqualTo(2); + assertThat(storage.read()).isEqualTo(3); + assertThat(storage.read()).isEqualTo(-1); + assertThat(storage.length()).isEqualTo(3L); + assertThat(cache.position()).isEqualTo(0); + assertThat(cache.limit()).isEqualTo(3); + storage.seek(3L); + invokeMethod(out, "sync"); + assertThat((boolean) getInternalState(out, "isDirty")).isFalse(); + assertThat(storage.length()).isEqualTo(3L); + assertThat(out.getByteCount()).isEqualTo(3L); + assertThat((boolean) getInternalState(out, "closed")).isFalse(); + out.free(); + assertThat((boolean) getInternalState(out, "closed")).isTrue(); + // todo assertThat( file ).doesNotExist(); + out.free(); + assertThat((boolean) getInternalState(out, "closed")).isTrue(); + } + + @Test + public void testReporterHandlesATestWithoutMessageAndWithEmptyStackTrace() { + StackTraceWriter stackTraceWriterOne = new DeserializedStacktraceWriter(null, null, ""); + + WrappedReportEntry testReport = new WrappedReportEntry( + new SimpleReportEntry( + NORMAL_RUN, 1L, getClass().getName(), null, "a test name", null, stackTraceWriterOne, 5), + ERROR, + 1771085631L, + 5, + null, + null); + + StatelessXmlReporter reporter = new StatelessXmlReporter( + reportDir, + null, + false, + 1, + new HashMap<>(), + XSD, + "3.0.2", + false, + false, + false, + false, + true, + true, + false); + + reporter.testSetCompleted(testReport, stats); + } + + @Test + public void testClassnameUsesActualClassNameWhenPhrasedClassNameDisabled() throws IOException { + String actualClassName = "MyTest"; + String displayName = "NewName"; + + ReportEntry reportEntry = new SimpleReportEntry( + NORMAL_RUN, + 0L, + actualClassName, + displayName, + actualClassName, + TEST_ONE, + null, + null, + 12, + null, + Collections.emptyMap()); + WrappedReportEntry testSetReportEntry = + new WrappedReportEntry(reportEntry, SUCCESS, 1771085631L, 12, null, null, systemProps()); + expectedReportFile = new File(reportDir, "TEST-" + actualClassName + ".xml"); + + stats.testSucceeded(testSetReportEntry); + + StatelessXmlReporter reporter = new StatelessXmlReporter( + reportDir, + null, + false, + 0, + new ConcurrentHashMap<>(), + XSD, + "3.0.2", + false, + false, + false, // phrasedClassName = false + false, + true, + true, + false); + reporter.testSetCompleted(testSetReportEntry, stats); + + Xpp3Dom testSuite; + try (FileInputStream fileInputStream = new FileInputStream(expectedReportFile); + InputStreamReader reader = new InputStreamReader(fileInputStream, UTF_8)) { + testSuite = Xpp3DomBuilder.build(reader); + } + Xpp3Dom testcase = testSuite.getChildren("testcase")[0]; + assertEquals( + actualClassName, + testcase.getAttribute("classname"), + "classname should be the actual class name, not the @DisplayName value"); + } + + private boolean defaultCharsetSupportsSpecialChar() { + // some charsets are not able to deal with \u0115 on both ways of the conversion + return "\u0115\u00DC".equals(new String("\u0115\u00DC".getBytes())); + } + + private Utf8RecodingDeferredFileOutputStream createStdOutput(String content) throws IOException { + Utf8RecodingDeferredFileOutputStream stdOut = new Utf8RecodingDeferredFileOutputStream("fds2"); + stdOut.write(content, false, null); + return stdOut; + } +}