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;
+ }
+}