You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cassandra.apache.org by az...@apache.org on 2021/11/15 20:56:51 UTC

[cassandra] 02/04: CASSANDRA-16630. Copied Ant JUnit classes.

This is an automated email from the ASF dual-hosted git repository.

azotcsit pushed a commit to branch cassandra-16630_junit5
in repository https://gitbox.apache.org/repos/asf/cassandra.git

commit 56a32b87b9d91b50d3a228dff6369ea751714435
Author: Aleksei Zotov <az...@gmail.com>
AuthorDate: Sun Nov 14 20:07:36 2021 +0400

    CASSANDRA-16630. Copied Ant JUnit classes.
---
 .../AbstractJUnitResultFormatter.java              | 308 +++++++++++++++
 .../junitlauncher/LegacyBriefResultFormatter.java  |  34 ++
 .../junitlauncher/LegacyPlainResultFormatter.java  | 290 ++++++++++++++
 .../junitlauncher/LegacyXmlResultFormatter.java    | 424 +++++++++++++++++++++
 4 files changed, 1056 insertions(+)

diff --git a/test/unit/org/apache/tools/ant/taskdefs/optional/junitlauncher/AbstractJUnitResultFormatter.java b/test/unit/org/apache/tools/ant/taskdefs/optional/junitlauncher/AbstractJUnitResultFormatter.java
new file mode 100644
index 0000000..221aadb
--- /dev/null
+++ b/test/unit/org/apache/tools/ant/taskdefs/optional/junitlauncher/AbstractJUnitResultFormatter.java
@@ -0,0 +1,308 @@
+/*
+ *  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
+ *
+ *      https://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.tools.ant.taskdefs.optional.junitlauncher;
+
+import org.apache.tools.ant.Project;
+import org.apache.tools.ant.util.FileUtils;
+import org.junit.platform.engine.TestSource;
+import org.junit.platform.engine.support.descriptor.ClassSource;
+import org.junit.platform.launcher.TestIdentifier;
+import org.junit.platform.launcher.TestPlan;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.Closeable;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.io.Writer;
+import java.nio.BufferOverflowException;
+import java.nio.ByteBuffer;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * Contains some common behaviour that's used by our internal {@link TestResultFormatter}s
+ */
+abstract class AbstractJUnitResultFormatter implements TestResultFormatter {
+
+    protected TestExecutionContext context;
+
+    private SysOutErrContentStore sysOutStore;
+    private SysOutErrContentStore sysErrStore;
+
+    @Override
+    public void sysOutAvailable(final byte[] data) {
+        if (this.sysOutStore == null) {
+            this.sysOutStore = new SysOutErrContentStore(context, true);
+        }
+        try {
+            this.sysOutStore.store(data);
+        } catch (IOException e) {
+            handleException(e);
+        }
+    }
+
+    @Override
+    public void sysErrAvailable(final byte[] data) {
+        if (this.sysErrStore == null) {
+            this.sysErrStore = new SysOutErrContentStore(context, false);
+        }
+        try {
+            this.sysErrStore.store(data);
+        } catch (IOException e) {
+            handleException(e);
+        }
+    }
+
+    @Override
+    public void setContext(final TestExecutionContext context) {
+        this.context = context;
+    }
+
+    /**
+     * @return Returns true if there's any stdout data, that was generated during the
+     * tests, is available for use. Else returns false.
+     */
+    boolean hasSysOut() {
+        return this.sysOutStore != null && this.sysOutStore.hasData();
+    }
+
+    /**
+     * @return Returns true if there's any stderr data, that was generated during the
+     * tests, is available for use. Else returns false.
+     */
+    boolean hasSysErr() {
+        return this.sysErrStore != null && this.sysErrStore.hasData();
+    }
+
+    /**
+     * @return Returns a {@link Reader} for reading any stdout data that was generated
+     * during the test execution. It is expected that the {@link #hasSysOut()} be first
+     * called to see if any such data is available and only if there is, then this method
+     * be called
+     * @throws IOException If there's any I/O problem while creating the {@link Reader}
+     */
+    Reader getSysOutReader() throws IOException {
+        return this.sysOutStore.getReader();
+    }
+
+    /**
+     * @return Returns a {@link Reader} for reading any stderr data that was generated
+     * during the test execution. It is expected that the {@link #hasSysErr()} be first
+     * called to see if any such data is available and only if there is, then this method
+     * be called
+     * @throws IOException If there's any I/O problem while creating the {@link Reader}
+     */
+    Reader getSysErrReader() throws IOException {
+        return this.sysErrStore.getReader();
+    }
+
+    /**
+     * Writes out any stdout data that was generated during the
+     * test execution. If there was no such data then this method just returns.
+     *
+     * @param writer The {@link Writer} to use. Cannot be null.
+     * @throws IOException If any I/O problem occurs during writing the data
+     */
+    void writeSysOut(final Writer writer) throws IOException {
+        Objects.requireNonNull(writer, "Writer cannot be null");
+        this.writeFrom(this.sysOutStore, writer);
+    }
+
+    /**
+     * Writes out any stderr data that was generated during the
+     * test execution. If there was no such data then this method just returns.
+     *
+     * @param writer The {@link Writer} to use. Cannot be null.
+     * @throws IOException If any I/O problem occurs during writing the data
+     */
+    void writeSysErr(final Writer writer) throws IOException {
+        Objects.requireNonNull(writer, "Writer cannot be null");
+        this.writeFrom(this.sysErrStore, writer);
+    }
+
+    static Optional<TestIdentifier> traverseAndFindTestClass(final TestPlan testPlan, final TestIdentifier testIdentifier) {
+        if (isTestClass(testIdentifier).isPresent()) {
+            return Optional.of(testIdentifier);
+        }
+        final Optional<TestIdentifier> parent = testPlan.getParent(testIdentifier);
+        return parent.isPresent() ? traverseAndFindTestClass(testPlan, parent.get()) : Optional.empty();
+    }
+
+    static Optional<ClassSource> isTestClass(final TestIdentifier testIdentifier) {
+        if (testIdentifier == null) {
+            return Optional.empty();
+        }
+        final Optional<TestSource> source = testIdentifier.getSource();
+        if (!source.isPresent()) {
+            return Optional.empty();
+        }
+        final TestSource testSource = source.get();
+        if (testSource instanceof ClassSource) {
+            return Optional.of((ClassSource) testSource);
+        }
+        return Optional.empty();
+    }
+
+    private void writeFrom(final SysOutErrContentStore store, final Writer writer) throws IOException {
+        final char[] chars = new char[1024];
+        int numRead = -1;
+        try (final Reader reader = store.getReader()) {
+            while ((numRead = reader.read(chars)) != -1) {
+                writer.write(chars, 0, numRead);
+            }
+        }
+    }
+
+    @Override
+    public void close() throws IOException {
+        FileUtils.close(this.sysOutStore);
+        FileUtils.close(this.sysErrStore);
+    }
+
+    protected void handleException(final Throwable t) {
+        // we currently just log it and move on.
+        this.context.getProject().ifPresent((p) -> p.log("Exception in listener "
+                                                         + AbstractJUnitResultFormatter.this.getClass().getName(), t, Project.MSG_DEBUG));
+    }
+
+
+    /*
+    A "store" for sysout/syserr content that gets sent to the AbstractJUnitResultFormatter.
+    This store first uses a relatively decent sized in-memory buffer for storing the sysout/syserr
+    content. This in-memory buffer will be used as long as it can fit in the new content that
+    keeps coming in. When the size limit is reached, this store switches to a file based store
+    by creating a temporarily file and writing out the already in-memory held buffer content
+    and any new content that keeps arriving to this store. Once the file has been created,
+    the in-memory buffer will never be used any more and in fact is destroyed as soon as the
+    file is created.
+    Instances of this class are not thread-safe and users of this class are expected to use necessary thread
+    safety guarantees, if they want to use an instance of this class by multiple threads.
+    */
+    private static final class SysOutErrContentStore implements Closeable {
+        private static final int DEFAULT_CAPACITY_IN_BYTES = 50 * 1024; // 50 KB
+        private static final Reader EMPTY_READER = new Reader() {
+            @Override
+            public int read(final char[] cbuf, final int off, final int len) throws IOException {
+                return -1;
+            }
+
+            @Override
+            public void close() throws IOException {
+            }
+        };
+
+        private final TestExecutionContext context;
+        private final String tmpFileSuffix;
+        private ByteBuffer inMemoryStore = ByteBuffer.allocate(DEFAULT_CAPACITY_IN_BYTES);
+        private boolean usingFileStore = false;
+        private Path filePath;
+        private OutputStream fileOutputStream;
+
+        private SysOutErrContentStore(final TestExecutionContext context, final boolean isSysOut) {
+            this.context = context;
+            this.tmpFileSuffix = isSysOut ? ".sysout" : ".syserr";
+        }
+
+        private void store(final byte[] data) throws IOException {
+            if (this.usingFileStore) {
+                this.storeToFile(data, 0, data.length);
+                return;
+            }
+            // we haven't yet created a file store and the data can fit in memory,
+            // so we write it in our buffer
+            try {
+                this.inMemoryStore.put(data);
+                return;
+            } catch (BufferOverflowException boe) {
+                // the buffer capacity can't hold this incoming data, so this
+                // incoming data hasn't been transferred to the buffer. let's
+                // now fall back to a file store
+                this.usingFileStore = true;
+            }
+            // since the content couldn't be transferred into in-memory buffer,
+            // we now create a file and transfer already (previously) stored in-memory
+            // content into that file, before finally transferring this new content
+            // into the file too. We then finally discard this in-memory buffer and
+            // just keep using the file store instead
+            this.fileOutputStream = createFileStore();
+            // first the existing in-memory content
+            storeToFile(this.inMemoryStore.array(), 0, this.inMemoryStore.position());
+            storeToFile(data, 0, data.length);
+            // discard the in-memory store
+            this.inMemoryStore = null;
+        }
+
+        private void storeToFile(final byte[] data, final int offset, final int length) throws IOException {
+            if (this.fileOutputStream == null) {
+                // no backing file was created so we can't do anything
+                return;
+            }
+            this.fileOutputStream.write(data, offset, length);
+        }
+
+        private OutputStream createFileStore() throws IOException {
+            this.filePath = FileUtils.getFileUtils()
+                                     .createTempFile(context.getProject().orElse(null), null, this.tmpFileSuffix, null, true, true)
+                                     .toPath();
+            return Files.newOutputStream(this.filePath);
+        }
+
+        /*
+         * Returns a Reader for reading the sysout/syserr content. If there's no data
+         * available in this store, then this returns a Reader which when used for read operations,
+         * will immediately indicate an EOF.
+         */
+        private Reader getReader() throws IOException {
+            if (this.usingFileStore && this.filePath != null) {
+                // we use a FileReader here so that we can use the system default character
+                // encoding for reading the contents on sysout/syserr stream, since that's the
+                // encoding that System.out/System.err uses to write out the messages
+                return new BufferedReader(new FileReader(this.filePath.toFile()));
+            }
+            if (this.inMemoryStore != null) {
+                return new InputStreamReader(new ByteArrayInputStream(this.inMemoryStore.array(), 0, this.inMemoryStore.position()));
+            }
+            // no data to read, so we return an "empty" reader
+            return EMPTY_READER;
+        }
+
+        /*
+         *  Returns true if this store has any data (either in-memory or in a file). Else
+         *  returns false.
+         */
+        private boolean hasData() {
+            if (this.inMemoryStore != null && this.inMemoryStore.position() > 0) {
+                return true;
+            }
+            return this.usingFileStore && this.filePath != null;
+        }
+
+        @Override
+        public void close() throws IOException {
+            this.inMemoryStore = null;
+            FileUtils.close(this.fileOutputStream);
+            FileUtils.delete(this.filePath.toFile());
+        }
+    }
+}
diff --git a/test/unit/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyBriefResultFormatter.java b/test/unit/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyBriefResultFormatter.java
new file mode 100644
index 0000000..7debbf0
--- /dev/null
+++ b/test/unit/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyBriefResultFormatter.java
@@ -0,0 +1,34 @@
+/*
+ *  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
+ *
+ *      https://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.tools.ant.taskdefs.optional.junitlauncher;
+
+import org.junit.platform.engine.TestExecutionResult;
+import org.junit.platform.launcher.TestIdentifier;
+
+/**
+ * A {@link TestResultFormatter} which prints a brief statistic for tests that have
+ * failed, aborted or skipped
+ */
+class LegacyBriefResultFormatter extends LegacyPlainResultFormatter implements TestResultFormatter {
+
+    @Override
+    protected boolean shouldReportExecutionFinished(final TestIdentifier testIdentifier, final TestExecutionResult testExecutionResult) {
+        final TestExecutionResult.Status resultStatus = testExecutionResult.getStatus();
+        return resultStatus == TestExecutionResult.Status.ABORTED || resultStatus == TestExecutionResult.Status.FAILED;
+    }
+}
diff --git a/test/unit/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyPlainResultFormatter.java b/test/unit/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyPlainResultFormatter.java
new file mode 100644
index 0000000..7583d78
--- /dev/null
+++ b/test/unit/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyPlainResultFormatter.java
@@ -0,0 +1,290 @@
+/*
+ *  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
+ *
+ *      https://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.tools.ant.taskdefs.optional.junitlauncher;
+
+import org.junit.platform.engine.TestExecutionResult;
+import org.junit.platform.engine.reporting.ReportEntry;
+import org.junit.platform.engine.support.descriptor.ClassSource;
+import org.junit.platform.launcher.TestIdentifier;
+import org.junit.platform.launcher.TestPlan;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+
+/**
+ * A {@link TestResultFormatter} which prints a short statistic for each of the tests
+ */
+class LegacyPlainResultFormatter extends AbstractJUnitResultFormatter implements TestResultFormatter {
+
+    private OutputStream outputStream;
+    private final Map<TestIdentifier, Stats> testIds = new ConcurrentHashMap<>();
+    private TestPlan testPlan;
+    private BufferedWriter writer;
+    private boolean useLegacyReportingName = true;
+
+    @Override
+    public void testPlanExecutionStarted(final TestPlan testPlan) {
+        this.testPlan = testPlan;
+    }
+
+    @Override
+    public void testPlanExecutionFinished(final TestPlan testPlan) {
+        for (final Map.Entry<TestIdentifier, Stats> entry : this.testIds.entrySet()) {
+            final TestIdentifier testIdentifier = entry.getKey();
+            if (!isTestClass(testIdentifier).isPresent()) {
+                // we are not interested in anything other than a test "class" in this section
+                continue;
+            }
+            final Stats stats = entry.getValue();
+            final StringBuilder sb = new StringBuilder("Tests run: ").append(stats.numTestsRun.get());
+            sb.append(", Failures: ").append(stats.numTestsFailed.get());
+            sb.append(", Skipped: ").append(stats.numTestsSkipped.get());
+            sb.append(", Aborted: ").append(stats.numTestsAborted.get());
+            sb.append(", Time elapsed: ");
+            stats.appendElapsed(sb);
+            try {
+                this.writer.write(sb.toString());
+                this.writer.newLine();
+            } catch (IOException ioe) {
+                handleException(ioe);
+                return;
+            }
+        }
+        // write out sysout and syserr content if any
+        try {
+            if (this.hasSysOut()) {
+                this.writer.write("------------- Standard Output ---------------");
+                this.writer.newLine();
+                writeSysOut(writer);
+                this.writer.write("------------- ---------------- ---------------");
+                this.writer.newLine();
+            }
+            if (this.hasSysErr()) {
+                this.writer.write("------------- Standard Error ---------------");
+                this.writer.newLine();
+                writeSysErr(writer);
+                this.writer.write("------------- ---------------- ---------------");
+                this.writer.newLine();
+            }
+        } catch (IOException ioe) {
+            handleException(ioe);
+        }
+    }
+
+    @Override
+    public void dynamicTestRegistered(final TestIdentifier testIdentifier) {
+        // nothing to do
+    }
+
+    @Override
+    public void executionSkipped(final TestIdentifier testIdentifier, final String reason) {
+        final long currentTime = System.currentTimeMillis();
+        this.testIds.putIfAbsent(testIdentifier, new Stats(testIdentifier, currentTime));
+        final Stats stats = this.testIds.get(testIdentifier);
+        stats.setEndedAt(currentTime);
+        if (testIdentifier.isTest()) {
+            final StringBuilder sb = new StringBuilder();
+            sb.append("Test: ");
+            sb.append(this.useLegacyReportingName ? testIdentifier.getLegacyReportingName() : testIdentifier.getDisplayName());
+            sb.append(" took ");
+            stats.appendElapsed(sb);
+            sb.append(" SKIPPED");
+            if (reason != null && !reason.isEmpty()) {
+                sb.append(": ").append(reason);
+            }
+            try {
+                this.writer.write(sb.toString());
+                this.writer.newLine();
+            } catch (IOException ioe) {
+                handleException(ioe);
+                return;
+            }
+        }
+        // get the parent test class to which this skipped test belongs to
+        final Optional<TestIdentifier> parentTestClass = traverseAndFindTestClass(this.testPlan, testIdentifier);
+        if (!parentTestClass.isPresent()) {
+            return;
+        }
+        final Stats parentClassStats = this.testIds.get(parentTestClass.get());
+        parentClassStats.numTestsSkipped.incrementAndGet();
+    }
+
+    @Override
+    public void executionStarted(final TestIdentifier testIdentifier) {
+        final long currentTime = System.currentTimeMillis();
+        // record this testidentifier's start
+        this.testIds.putIfAbsent(testIdentifier, new Stats(testIdentifier, currentTime));
+        final Optional<ClassSource> testClass = isTestClass(testIdentifier);
+        if (testClass.isPresent()) {
+            // if this is a test class, then print it out
+            try {
+                this.writer.write("Testcase: " + testClass.get().getClassName());
+                this.writer.newLine();
+            } catch (IOException ioe) {
+                handleException(ioe);
+                return;
+            }
+        }
+        // if this is a test (method) then increment the tests run for the test class to which
+        // this test belongs to
+        if (testIdentifier.isTest()) {
+            final Optional<TestIdentifier> parentTestClass = traverseAndFindTestClass(this.testPlan, testIdentifier);
+            if (parentTestClass.isPresent()) {
+                final Stats parentClassStats = this.testIds.get(parentTestClass.get());
+                if (parentClassStats != null) {
+                    parentClassStats.numTestsRun.incrementAndGet();
+                }
+            }
+        }
+    }
+
+    @SuppressWarnings("incomplete-switch")
+    @Override
+    public void executionFinished(final TestIdentifier testIdentifier, final TestExecutionResult testExecutionResult) {
+        final long currentTime = System.currentTimeMillis();
+        final Stats stats = this.testIds.get(testIdentifier);
+        if (stats != null) {
+            stats.setEndedAt(currentTime);
+        }
+        if (testIdentifier.isTest() && shouldReportExecutionFinished(testIdentifier, testExecutionResult)) {
+            final StringBuilder sb = new StringBuilder();
+            sb.append("Test: ");
+            sb.append(this.useLegacyReportingName ? testIdentifier.getLegacyReportingName() : testIdentifier.getDisplayName());
+            if (stats != null) {
+                sb.append(" took ");
+                stats.appendElapsed(sb);
+            }
+            switch (testExecutionResult.getStatus()) {
+                case ABORTED: {
+                    sb.append(" ABORTED");
+                    appendThrowable(sb, testExecutionResult);
+                    break;
+                }
+                case FAILED: {
+                    sb.append(" FAILED");
+                    appendThrowable(sb, testExecutionResult);
+                    break;
+                }
+            }
+            try {
+                this.writer.write(sb.toString());
+                this.writer.newLine();
+            } catch (IOException ioe) {
+                handleException(ioe);
+                return;
+            }
+        }
+        // get the parent test class in which this test completed
+        final Optional<TestIdentifier> parentTestClass = traverseAndFindTestClass(this.testPlan, testIdentifier);
+        if (!parentTestClass.isPresent()) {
+            return;
+        }
+        // update the stats of the parent test class
+        final Stats parentClassStats = this.testIds.get(parentTestClass.get());
+        switch (testExecutionResult.getStatus()) {
+            case ABORTED: {
+                parentClassStats.numTestsAborted.incrementAndGet();
+                break;
+            }
+            case FAILED: {
+                parentClassStats.numTestsFailed.incrementAndGet();
+                break;
+            }
+        }
+    }
+
+    @Override
+    public void reportingEntryPublished(final TestIdentifier testIdentifier, final ReportEntry entry) {
+        // nothing to do
+    }
+
+    @Override
+    public void setDestination(final OutputStream os) {
+        this.outputStream = os;
+        this.writer = new BufferedWriter(new OutputStreamWriter(this.outputStream, StandardCharsets.UTF_8));
+    }
+
+    @Override
+    public void setUseLegacyReportingName(final boolean useLegacyReportingName) {
+        this.useLegacyReportingName = useLegacyReportingName;
+    }
+
+    protected boolean shouldReportExecutionFinished(final TestIdentifier testIdentifier, final TestExecutionResult testExecutionResult) {
+        return true;
+    }
+
+    private static void appendThrowable(final StringBuilder sb, TestExecutionResult result) {
+        if (!result.getThrowable().isPresent()) {
+            return;
+        }
+        final Throwable throwable = result.getThrowable().get();
+        sb.append(String.format(": %s%n", throwable.getMessage()));
+        final StringWriter stacktrace = new StringWriter();
+        throwable.printStackTrace(new PrintWriter(stacktrace));
+        sb.append(stacktrace.toString());
+    }
+
+    @Override
+    public void close() throws IOException {
+        if (this.writer != null) {
+            this.writer.close();
+        }
+        super.close();
+    }
+
+    private final class Stats {
+        @SuppressWarnings("unused")
+        private final TestIdentifier testIdentifier;
+        private final AtomicLong numTestsRun = new AtomicLong(0);
+        private final AtomicLong numTestsFailed = new AtomicLong(0);
+        private final AtomicLong numTestsSkipped = new AtomicLong(0);
+        private final AtomicLong numTestsAborted = new AtomicLong(0);
+        private final long startedAt;
+        private long endedAt;
+
+        private Stats(final TestIdentifier testIdentifier, final long startedAt) {
+            this.testIdentifier = testIdentifier;
+            this.startedAt = startedAt;
+        }
+
+        private void setEndedAt(final long endedAt) {
+            this.endedAt = endedAt;
+        }
+
+        private void appendElapsed(StringBuilder sb) {
+            final long timeElapsed = endedAt - startedAt;
+            if (timeElapsed < 1000) {
+                sb.append(timeElapsed).append(" milli sec(s)");
+            } else {
+                sb.append(TimeUnit.SECONDS.convert(timeElapsed, TimeUnit.MILLISECONDS)).append(" sec(s)");
+            }
+        }
+    }
+}
diff --git a/test/unit/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyXmlResultFormatter.java b/test/unit/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyXmlResultFormatter.java
new file mode 100644
index 0000000..bb9a963
--- /dev/null
+++ b/test/unit/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyXmlResultFormatter.java
@@ -0,0 +1,424 @@
+/*
+ *  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
+ *
+ *      https://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.tools.ant.taskdefs.optional.junitlauncher;
+
+import org.apache.tools.ant.util.DOMElementWriter;
+import org.apache.tools.ant.util.DateUtils;
+import org.apache.tools.ant.util.StringUtils;
+import org.junit.platform.engine.TestExecutionResult;
+import org.junit.platform.engine.TestSource;
+import org.junit.platform.engine.reporting.ReportEntry;
+import org.junit.platform.engine.support.descriptor.ClassSource;
+import org.junit.platform.launcher.TestIdentifier;
+import org.junit.platform.launcher.TestPlan;
+
+import javax.xml.stream.XMLOutputFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.util.Date;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * A {@link TestResultFormatter} which generates an XML report of the tests. The generated XML reports
+ * conforms to the schema of the XML that was generated by the {@code junit} task's XML
+ * report formatter and can be used by the {@code junitreport} task
+ */
+class LegacyXmlResultFormatter extends AbstractJUnitResultFormatter implements TestResultFormatter {
+
+    private static final double ONE_SECOND = 1000.0;
+
+    private OutputStream outputStream;
+    private final Map<TestIdentifier, Stats> testIds = new ConcurrentHashMap<>();
+    private final Map<TestIdentifier, Optional<String>> skipped = new ConcurrentHashMap<>();
+    private final Map<TestIdentifier, Optional<Throwable>> failed = new ConcurrentHashMap<>();
+    private final Map<TestIdentifier, Optional<Throwable>> aborted = new ConcurrentHashMap<>();
+
+    private TestPlan testPlan;
+    private long testPlanStartedAt = -1;
+    private long testPlanEndedAt = -1;
+    private final AtomicLong numTestsRun = new AtomicLong(0);
+    private final AtomicLong numTestsFailed = new AtomicLong(0);
+    private final AtomicLong numTestsSkipped = new AtomicLong(0);
+    private final AtomicLong numTestsAborted = new AtomicLong(0);
+    private boolean useLegacyReportingName = true;
+
+
+    @Override
+    public void testPlanExecutionStarted(final TestPlan testPlan) {
+        this.testPlan = testPlan;
+        this.testPlanStartedAt = System.currentTimeMillis();
+    }
+
+    @Override
+    public void testPlanExecutionFinished(final TestPlan testPlan) {
+        this.testPlanEndedAt = System.currentTimeMillis();
+        // format and print out the result
+        try {
+            new XMLReportWriter().write();
+        } catch (IOException | XMLStreamException e) {
+            handleException(e);
+        }
+    }
+
+    @Override
+    public void dynamicTestRegistered(final TestIdentifier testIdentifier) {
+        // nothing to do
+    }
+
+    @Override
+    public void executionSkipped(final TestIdentifier testIdentifier, final String reason) {
+        final long currentTime = System.currentTimeMillis();
+        this.numTestsSkipped.incrementAndGet();
+        this.skipped.put(testIdentifier, Optional.ofNullable(reason));
+        // a skipped test is considered started and ended now
+        final Stats stats = new Stats(testIdentifier, currentTime);
+        stats.endedAt = currentTime;
+        this.testIds.put(testIdentifier, stats);
+    }
+
+    @Override
+    public void executionStarted(final TestIdentifier testIdentifier) {
+        final long currentTime = System.currentTimeMillis();
+        this.testIds.putIfAbsent(testIdentifier, new Stats(testIdentifier, currentTime));
+        if (testIdentifier.isTest()) {
+            this.numTestsRun.incrementAndGet();
+        }
+    }
+
+    @Override
+    public void executionFinished(final TestIdentifier testIdentifier, final TestExecutionResult testExecutionResult) {
+        final long currentTime = System.currentTimeMillis();
+        final Stats stats = this.testIds.get(testIdentifier);
+        if (stats != null) {
+            stats.endedAt = currentTime;
+        }
+        switch (testExecutionResult.getStatus()) {
+            case SUCCESSFUL: {
+                break;
+            }
+            case ABORTED: {
+                this.numTestsAborted.incrementAndGet();
+                this.aborted.put(testIdentifier, testExecutionResult.getThrowable());
+                break;
+            }
+            case FAILED: {
+                this.numTestsFailed.incrementAndGet();
+                this.failed.put(testIdentifier, testExecutionResult.getThrowable());
+                break;
+            }
+        }
+    }
+
+    @Override
+    public void reportingEntryPublished(final TestIdentifier testIdentifier, final ReportEntry entry) {
+        // nothing to do
+    }
+
+    @Override
+    public void setDestination(final OutputStream os) {
+        this.outputStream = os;
+    }
+
+    @Override
+    public void setUseLegacyReportingName(final boolean useLegacyReportingName) {
+        this.useLegacyReportingName = useLegacyReportingName;
+    }
+
+    private final class Stats {
+        @SuppressWarnings("unused")
+        private final TestIdentifier testIdentifier;
+        private final long startedAt;
+        private long endedAt;
+
+        private Stats(final TestIdentifier testIdentifier, final long startedAt) {
+            this.testIdentifier = testIdentifier;
+            this.startedAt = startedAt;
+        }
+    }
+
+    private final class XMLReportWriter {
+
+        private static final String ELEM_TESTSUITE = "testsuite";
+        private static final String ELEM_PROPERTIES = "properties";
+        private static final String ELEM_PROPERTY = "property";
+        private static final String ELEM_TESTCASE = "testcase";
+        private static final String ELEM_SKIPPED = "skipped";
+        private static final String ELEM_FAILURE = "failure";
+        private static final String ELEM_ABORTED = "aborted";
+        private static final String ELEM_SYSTEM_OUT = "system-out";
+        private static final String ELEM_SYSTEM_ERR = "system-err";
+
+
+        private static final String ATTR_CLASSNAME = "classname";
+        private static final String ATTR_NAME = "name";
+        private static final String ATTR_VALUE = "value";
+        private static final String ATTR_TIME = "time";
+        private static final String ATTR_TIMESTAMP = "timestamp";
+        private static final String ATTR_NUM_ABORTED = "aborted";
+        private static final String ATTR_NUM_FAILURES = "failures";
+        private static final String ATTR_NUM_TESTS = "tests";
+        private static final String ATTR_NUM_SKIPPED = "skipped";
+        private static final String ATTR_MESSAGE = "message";
+        private static final String ATTR_TYPE = "type";
+
+        void write() throws XMLStreamException, IOException {
+            final XMLStreamWriter writer = XMLOutputFactory.newFactory().createXMLStreamWriter(outputStream, "UTF-8");
+            try {
+                writer.writeStartDocument();
+                writeTestSuite(writer);
+                writer.writeEndDocument();
+            } finally {
+                writer.close();
+            }
+        }
+
+        void writeTestSuite(final XMLStreamWriter writer) throws XMLStreamException, IOException {
+            // write the testsuite element
+            writer.writeStartElement(ELEM_TESTSUITE);
+            final String testsuiteName = determineTestSuiteName();
+            writeAttribute(writer, ATTR_NAME, testsuiteName);
+            // time taken for the tests execution
+            writeAttribute(writer, ATTR_TIME, String.valueOf((testPlanEndedAt - testPlanStartedAt) / ONE_SECOND));
+            // add the timestamp of report generation
+            final String timestamp = DateUtils.format(new Date(), DateUtils.ISO8601_DATETIME_PATTERN);
+            writeAttribute(writer, ATTR_TIMESTAMP, timestamp);
+            writeAttribute(writer, ATTR_NUM_TESTS, String.valueOf(numTestsRun.longValue()));
+            writeAttribute(writer, ATTR_NUM_FAILURES, String.valueOf(numTestsFailed.longValue()));
+            writeAttribute(writer, ATTR_NUM_SKIPPED, String.valueOf(numTestsSkipped.longValue()));
+            writeAttribute(writer, ATTR_NUM_ABORTED, String.valueOf(numTestsAborted.longValue()));
+
+            // write the properties
+            writeProperties(writer);
+            // write the tests
+            writeTestCase(writer);
+            writeSysOut(writer);
+            writeSysErr(writer);
+            // end the testsuite
+            writer.writeEndElement();
+        }
+
+        void writeProperties(final XMLStreamWriter writer) throws XMLStreamException {
+            final Properties properties = LegacyXmlResultFormatter.this.context.getProperties();
+            if (properties == null || properties.isEmpty()) {
+                return;
+            }
+            writer.writeStartElement(ELEM_PROPERTIES);
+            for (final String prop : properties.stringPropertyNames()) {
+                writer.writeStartElement(ELEM_PROPERTY);
+                writeAttribute(writer, ATTR_NAME, prop);
+                writeAttribute(writer, ATTR_VALUE, properties.getProperty(prop));
+                writer.writeEndElement();
+            }
+            writer.writeEndElement();
+        }
+
+        void writeTestCase(final XMLStreamWriter writer) throws XMLStreamException {
+            for (final Map.Entry<TestIdentifier, Stats> entry : testIds.entrySet()) {
+                final TestIdentifier testId = entry.getKey();
+                if (!testId.isTest() && !failed.containsKey(testId)) {
+                    // only interested in test methods unless there was a failure,
+                    // in which case we want the exception reported
+                    // (https://bz.apache.org/bugzilla/show_bug.cgi?id=63850)
+                    continue;
+                }
+                // find the associated class of this test
+                final Optional<ClassSource> parentClassSource;
+                if (testId.isTest()) {
+                    parentClassSource = findFirstParentClassSource(testId);
+                }
+                else {
+                    parentClassSource = findFirstClassSource(testId);
+                }
+                if (!parentClassSource.isPresent()) {
+                    continue;
+                }
+                final String classname = (parentClassSource.get()).getClassName();
+                writer.writeStartElement(ELEM_TESTCASE);
+                writeAttribute(writer, ATTR_CLASSNAME, classname);
+                writeAttribute(writer, ATTR_NAME, useLegacyReportingName ? testId.getLegacyReportingName()
+                                                                         : testId.getDisplayName());
+                final Stats stats = entry.getValue();
+                writeAttribute(writer, ATTR_TIME, String.valueOf((stats.endedAt - stats.startedAt) / ONE_SECOND));
+                // skipped element if the test was skipped
+                writeSkipped(writer, testId);
+                // failed element if the test failed
+                writeFailed(writer, testId);
+                // aborted element if the test was aborted
+                writeAborted(writer, testId);
+
+                writer.writeEndElement();
+            }
+        }
+
+        private void writeSkipped(final XMLStreamWriter writer, final TestIdentifier testIdentifier) throws XMLStreamException {
+            if (!skipped.containsKey(testIdentifier)) {
+                return;
+            }
+            writer.writeStartElement(ELEM_SKIPPED);
+            final Optional<String> reason = skipped.get(testIdentifier);
+            if (reason.isPresent()) {
+                writeAttribute(writer, ATTR_MESSAGE, reason.get());
+            }
+            writer.writeEndElement();
+        }
+
+        private void writeFailed(final XMLStreamWriter writer, final TestIdentifier testIdentifier) throws XMLStreamException {
+            if (!failed.containsKey(testIdentifier)) {
+                return;
+            }
+            writer.writeStartElement(ELEM_FAILURE);
+            final Optional<Throwable> cause = failed.get(testIdentifier);
+            if (cause.isPresent()) {
+                final Throwable t = cause.get();
+                final String message = t.getMessage();
+                if (message != null && !message.trim().isEmpty()) {
+                    writeAttribute(writer, ATTR_MESSAGE, message);
+                }
+                writeAttribute(writer, ATTR_TYPE, t.getClass().getName());
+                // write out the stacktrace
+                writer.writeCData(StringUtils.getStackTrace(t));
+            }
+            writer.writeEndElement();
+        }
+
+        private void writeAborted(final XMLStreamWriter writer, final TestIdentifier testIdentifier) throws XMLStreamException {
+            if (!aborted.containsKey(testIdentifier)) {
+                return;
+            }
+            writer.writeStartElement(ELEM_ABORTED);
+            final Optional<Throwable> cause = aborted.get(testIdentifier);
+            if (cause.isPresent()) {
+                final Throwable t = cause.get();
+                final String message = t.getMessage();
+                if (message != null && !message.trim().isEmpty()) {
+                    writeAttribute(writer, ATTR_MESSAGE, message);
+                }
+                writeAttribute(writer, ATTR_TYPE, t.getClass().getName());
+                // write out the stacktrace
+                writer.writeCData(StringUtils.getStackTrace(t));
+            }
+            writer.writeEndElement();
+        }
+
+        private void writeSysOut(final XMLStreamWriter writer) throws XMLStreamException, IOException {
+            if (!LegacyXmlResultFormatter.this.hasSysOut()) {
+                return;
+            }
+            writer.writeStartElement(ELEM_SYSTEM_OUT);
+            try (final Reader reader = LegacyXmlResultFormatter.this.getSysOutReader()) {
+                writeCharactersFrom(reader, writer);
+            }
+            writer.writeEndElement();
+        }
+
+        private void writeSysErr(final XMLStreamWriter writer) throws XMLStreamException, IOException {
+            if (!LegacyXmlResultFormatter.this.hasSysErr()) {
+                return;
+            }
+            writer.writeStartElement(ELEM_SYSTEM_ERR);
+            try (final Reader reader = LegacyXmlResultFormatter.this.getSysErrReader()) {
+                writeCharactersFrom(reader, writer);
+            }
+            writer.writeEndElement();
+        }
+
+        private void writeCharactersFrom(final Reader reader, final XMLStreamWriter writer) throws IOException, XMLStreamException {
+            final char[] chars = new char[1024];
+            int numRead = -1;
+            while ((numRead = reader.read(chars)) != -1) {
+                writer.writeCharacters(encode(new String(chars, 0, numRead)));
+            }
+        }
+
+        private void writeAttribute(final XMLStreamWriter writer, final String name, final String value)
+        throws XMLStreamException {
+            writer.writeAttribute(name, encode(value));
+        }
+
+        private String encode(final String s) {
+            boolean changed = false;
+            final StringBuilder sb = new StringBuilder();
+            for (char c : s.toCharArray()) {
+                if (!DOMElementWriter.isLegalXmlCharacter(c)) {
+                    changed = true;
+                    sb.append("&#").append((int) c).append(';');
+                } else {
+                    sb.append(c);
+                }
+            }
+            return changed ? sb.toString() : s;
+        }
+
+        private String determineTestSuiteName() {
+            // this is really a hack to try and match the expectations of the XML report in JUnit4.x
+            // world. In JUnit5, the TestPlan doesn't have a name and a TestPlan (for which this is a
+            // listener) can have numerous tests within it
+            final Set<TestIdentifier> roots = testPlan.getRoots();
+            if (roots.isEmpty()) {
+                return "UNKNOWN";
+            }
+            for (final TestIdentifier root : roots) {
+                final Optional<ClassSource> classSource = findFirstClassSource(root);
+                if (classSource.isPresent()) {
+                    return classSource.get().getClassName();
+                }
+            }
+            return "UNKNOWN";
+        }
+
+        private Optional<ClassSource> findFirstClassSource(final TestIdentifier root) {
+            if (root.getSource().isPresent()) {
+                final TestSource source = root.getSource().get();
+                if (source instanceof ClassSource) {
+                    return Optional.of((ClassSource) source);
+                }
+            }
+            for (final TestIdentifier child : testPlan.getChildren(root)) {
+                final Optional<ClassSource> classSource = findFirstClassSource(child);
+                if (classSource.isPresent()) {
+                    return classSource;
+                }
+            }
+            return Optional.empty();
+        }
+
+        private Optional<ClassSource> findFirstParentClassSource(final TestIdentifier testId) {
+            final Optional<TestIdentifier> parent = testPlan.getParent(testId);
+            if (!parent.isPresent()) {
+                return Optional.empty();
+            }
+            if (!parent.get().getSource().isPresent()) {
+                // the source of the parent is unknown, so we move up the
+                // hierarchy and try and find a class source
+                return findFirstParentClassSource(parent.get());
+            }
+            final TestSource parentSource = parent.get().getSource().get();
+            return parentSource instanceof ClassSource ? Optional.of((ClassSource) parentSource)
+                                                       : findFirstParentClassSource(parent.get());
+        }
+    }
+
+}

---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@cassandra.apache.org
For additional commands, e-mail: commits-help@cassandra.apache.org