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