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 String tmpFileSuffix;
+ private ByteBuffer inMemoryStore = ByteBuffer.allocate(DEFAULT_CAPACITY_IN_BYTES);
+ private boolean usingFileStore = false;
+ private Path filePath;
+ private FileOutputStream fileOutputStream;
+
+ private SysOutErrContentStore(final boolean isSysOut) {
+ 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 FileOutputStream createFileStore() throws IOException {
+ this.filePath = Files.createTempFile(null, this.tmpFileSuffix);
+ this.filePath.toFile().deleteOnExit();
+ return new FileOutputStream(this.filePath.toFile());
+ }
+
+ /*
+ * 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;
+ }
+ if (this.usingFileStore && this.filePath != null) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void close() throws IOException {
+ this.inMemoryStore = null;
+ FileUtils.close(this.fileOutputStream);
+ FileUtils.delete(this.filePath.toFile());
+ }
+ }
+}
diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/JUnitLauncherTask.java b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/JUnitLauncherTask.java
new file mode 100644
index 000000000..ac4ef44c5
--- /dev/null
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/JUnitLauncherTask.java
@@ -0,0 +1,537 @@
+package org.apache.tools.ant.taskdefs.optional.junitlauncher;
+
+import org.apache.tools.ant.AntClassLoader;
+import org.apache.tools.ant.BuildException;
+import org.apache.tools.ant.Project;
+import org.apache.tools.ant.Task;
+import org.apache.tools.ant.types.Path;
+import org.apache.tools.ant.util.FileUtils;
+import org.apache.tools.ant.util.KeepAliveOutputStream;
+import org.junit.platform.launcher.Launcher;
+import org.junit.platform.launcher.LauncherDiscoveryRequest;
+import org.junit.platform.launcher.TestExecutionListener;
+import org.junit.platform.launcher.TestPlan;
+import org.junit.platform.launcher.core.LauncherFactory;
+import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
+import org.junit.platform.launcher.listeners.TestExecutionSummary;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.io.PrintStream;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.Properties;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * An Ant {@link Task} responsible for launching the JUnit platform for running tests.
+ * This requires a minimum of JUnit 5, since that's the version in which the JUnit platform launcher
+ * APIs were introduced.
+ *
+ * This task in itself doesn't run the JUnit tests, instead the sole responsibility of
+ * this task is to setup the JUnit platform launcher, build requests, launch those requests and then parse the
+ * result of the execution to present in a way that's been configured on this Ant task.
+ *
+ *
+ * Furthermore, this task allows users control over which classes to select for passing on to the JUnit 5
+ * platform for test execution. It however, is solely the JUnit 5 platform, backed by test engines that
+ * decide and execute the tests.
+ *
+ * @see JUnit 5 documentation for more details
+ * on how JUnit manages the platform and the test engines.
+ */
+public class JUnitLauncherTask extends Task {
+
+ private Path classPath;
+ private boolean haltOnFailure;
+ private String failureProperty;
+ private final List tests = new ArrayList<>();
+ private final List listeners = new ArrayList<>();
+
+ public JUnitLauncherTask() {
+ }
+
+ @Override
+ public void execute() throws BuildException {
+ final ClassLoader previousClassLoader = Thread.currentThread().getContextClassLoader();
+ try {
+ final ClassLoader executionCL = createClassLoaderForTestExecution();
+ Thread.currentThread().setContextClassLoader(executionCL);
+ final Launcher launcher = LauncherFactory.create();
+ final List requests = buildTestRequests();
+ for (final TestRequest testRequest : requests) {
+ try {
+ final TestDefinition test = testRequest.getOwner();
+ final LauncherDiscoveryRequest request = testRequest.getDiscoveryRequest().build();
+ final List testExecutionListeners = new ArrayList<>();
+ // a listener that we always put at the front of list of listeners
+ // for this request.
+ final Listener firstListener = new Listener();
+ // we always enroll the summary generating listener, to the request, so that we
+ // get to use some of the details of the summary for our further decision making
+ testExecutionListeners.add(firstListener);
+ testExecutionListeners.addAll(getListeners(testRequest, executionCL));
+ final PrintStream originalSysOut = System.out;
+ final PrintStream originalSysErr = System.err;
+ try {
+ firstListener.switchedSysOutHandle = trySwitchSysOutErr(testRequest, StreamType.SYS_OUT);
+ firstListener.switchedSysErrHandle = trySwitchSysOutErr(testRequest, StreamType.SYS_ERR);
+ launcher.execute(request, testExecutionListeners.toArray(new TestExecutionListener[testExecutionListeners.size()]));
+ } finally {
+ // switch back sysout/syserr to the original
+ try {
+ System.setOut(originalSysOut);
+ } catch (Exception e) {
+ // ignore
+ }
+ try {
+ System.setErr(originalSysErr);
+ } catch (Exception e) {
+ // ignore
+ }
+ }
+ handleTestExecutionCompletion(test, firstListener.getSummary());
+ } finally {
+ try {
+ testRequest.close();
+ } catch (Exception e) {
+ // log and move on
+ log("Failed to cleanly close test request", e, Project.MSG_DEBUG);
+ }
+ }
+ }
+ } finally {
+ Thread.currentThread().setContextClassLoader(previousClassLoader);
+ }
+ }
+
+ /**
+ * Adds the {@link Path} to the classpath which will be used for execution of the tests
+ *
+ * @param path The classpath
+ */
+ public void addConfiguredClassPath(final Path path) {
+ if (this.classPath == null) {
+ // create a "wrapper" path which can hold on to multiple
+ // paths that get passed to this method (if at all the task in the build is
+ // configured with multiple classpaht elements)
+ this.classPath = new Path(getProject());
+ }
+ this.classPath.add(path);
+ }
+
+ /**
+ * Adds a {@link SingleTestClass} that will be passed on to the underlying JUnit platform
+ * for possible execution of the test
+ *
+ * @param test The test
+ */
+ public void addConfiguredTest(final SingleTestClass test) {
+ this.preConfigure(test);
+ this.tests.add(test);
+ }
+
+ /**
+ * Adds {@link TestClasses} that will be passed on to the underlying JUnit platform for
+ * possible execution of the tests
+ *
+ * @param testClasses The test classes
+ */
+ public void addConfiguredTestClasses(final TestClasses testClasses) {
+ this.preConfigure(testClasses);
+ this.tests.add(testClasses);
+ }
+
+ /**
+ * Adds a {@link ListenerDefinition listener} which will be enrolled for listening to test
+ * execution events
+ *
+ * @param listener The listener
+ */
+ public void addConfiguredListener(final ListenerDefinition listener) {
+ this.listeners.add(listener);
+ }
+
+ public void setHaltonfailure(final boolean haltonfailure) {
+ this.haltOnFailure = haltonfailure;
+ }
+
+ public void setFailureProperty(final String failureProperty) {
+ this.failureProperty = failureProperty;
+ }
+
+ private void preConfigure(final TestDefinition test) {
+ if (test.getHaltOnFailure() == null) {
+ test.setHaltOnFailure(this.haltOnFailure);
+ }
+ if (test.getFailureProperty() == null) {
+ test.setFailureProperty(this.failureProperty);
+ }
+ }
+
+ private List buildTestRequests() {
+ if (this.tests.isEmpty()) {
+ return Collections.emptyList();
+ }
+ final List requests = new ArrayList<>();
+ for (final TestDefinition test : this.tests) {
+ final List testRequests = test.createTestRequests(this);
+ if (testRequests == null || testRequests.isEmpty()) {
+ continue;
+ }
+ requests.addAll(testRequests);
+ }
+ return requests;
+ }
+
+ private List getListeners(final TestRequest testRequest, final ClassLoader classLoader) {
+ final TestDefinition test = testRequest.getOwner();
+ final List applicableListenerElements = test.getListeners().isEmpty() ? this.listeners : test.getListeners();
+ final List listeners = new ArrayList<>();
+ final Project project = getProject();
+ for (final ListenerDefinition applicableListener : applicableListenerElements) {
+ if (!applicableListener.shouldUse(project)) {
+ log("Excluding listener " + applicableListener.getClassName() + " since it's not applicable" +
+ " in the context of project " + project, Project.MSG_DEBUG);
+ continue;
+ }
+ final TestExecutionListener listener = requireTestExecutionListener(applicableListener, classLoader);
+ if (listener instanceof TestResultFormatter) {
+ // setup/configure the result formatter
+ setupResultFormatter(testRequest, applicableListener, (TestResultFormatter) listener);
+ }
+ listeners.add(listener);
+ }
+ return listeners;
+ }
+
+ private void setupResultFormatter(final TestRequest testRequest, final ListenerDefinition formatterDefinition,
+ final TestResultFormatter resultFormatter) {
+
+ testRequest.closeUponCompletion(resultFormatter);
+ // set the execution context
+ resultFormatter.setContext(new InVMExecution());
+ // set the destination output stream for writing out the formatted result
+ final TestDefinition test = testRequest.getOwner();
+ final java.nio.file.Path outputDir = test.getOutputDir() != null ? Paths.get(test.getOutputDir()) : getProject().getBaseDir().toPath();
+ final String filename = formatterDefinition.requireResultFile(test);
+ final java.nio.file.Path resultOutputFile = Paths.get(outputDir.toString(), filename);
+ try {
+ final OutputStream resultOutputStream = Files.newOutputStream(resultOutputFile);
+ // enroll the output stream to be closed when the execution of the TestRequest completes
+ testRequest.closeUponCompletion(resultOutputStream);
+ resultFormatter.setDestination(new KeepAliveOutputStream(resultOutputStream));
+ } catch (IOException e) {
+ throw new BuildException(e);
+ }
+ // check if system.out/system.err content needs to be passed on to the listener
+ if (formatterDefinition.shouldSendSysOut()) {
+ testRequest.addSysOutInterest(resultFormatter);
+ }
+ if (formatterDefinition.shouldSendSysErr()) {
+ testRequest.addSysErrInterest(resultFormatter);
+ }
+ }
+
+ private TestExecutionListener requireTestExecutionListener(final ListenerDefinition listener, final ClassLoader classLoader) {
+ final String className = listener.getClassName();
+ if (className == null || className.trim().isEmpty()) {
+ throw new BuildException("classname attribute value is missing on listener element");
+ }
+ final Class> klass;
+ try {
+ klass = Class.forName(className, false, classLoader);
+ } catch (ClassNotFoundException e) {
+ throw new BuildException("Failed to load listener class " + className, e);
+ }
+ if (!TestExecutionListener.class.isAssignableFrom(klass)) {
+ throw new BuildException("Listener class " + className + " is not of type " + TestExecutionListener.class.getName());
+ }
+ try {
+ return TestExecutionListener.class.cast(klass.newInstance());
+ } catch (Exception e) {
+ throw new BuildException("Failed to create an instance of listener " + className, e);
+ }
+ }
+
+ private void handleTestExecutionCompletion(final TestDefinition test, final TestExecutionSummary summary) {
+ final boolean hasTestFailures = summary.getTestsFailedCount() != 0;
+ try {
+ if (hasTestFailures && test.getFailureProperty() != null) {
+ // if there are test failures and the test is configured to set a property in case
+ // of failure, then set the property to true
+ getProject().setNewProperty(test.getFailureProperty(), "true");
+ }
+ } finally {
+ if (hasTestFailures && test.isHaltOnFailure()) {
+ // if the test is configured to halt on test failures, throw a build error
+ final String errorMessage;
+ if (test instanceof NamedTest) {
+ errorMessage = "Test " + ((NamedTest) test).getName() + " has " + summary.getTestsFailedCount() + " failure(s)";
+ } else {
+ errorMessage = "Some test(s) have failure(s)";
+ }
+ throw new BuildException(errorMessage);
+ }
+ }
+ }
+
+ private ClassLoader createClassLoaderForTestExecution() {
+ if (this.classPath == null) {
+ return this.getClass().getClassLoader();
+ }
+ return new AntClassLoader(this.getClass().getClassLoader(), getProject(), this.classPath, true);
+ }
+
+ private Optional trySwitchSysOutErr(final TestRequest testRequest, final StreamType streamType) {
+ switch (streamType) {
+ case SYS_OUT: {
+ if (!testRequest.interestedInSysOut()) {
+ return Optional.empty();
+ }
+ break;
+ }
+ case SYS_ERR: {
+ if (!testRequest.interestedInSysErr()) {
+ return Optional.empty();
+ }
+ break;
+ }
+ default: {
+ // unknown, but no need to error out, just be lenient
+ // and return back
+ return Optional.empty();
+ }
+ }
+ final PipedOutputStream pipedOutputStream = new PipedOutputStream();
+ final PipedInputStream pipedInputStream;
+ try {
+ pipedInputStream = new PipedInputStream(pipedOutputStream);
+ } catch (IOException ioe) {
+ // log and return
+ return Optional.empty();
+ }
+ final PrintStream printStream = new PrintStream(pipedOutputStream, true);
+ final SysOutErrStreamReader streamer;
+ switch (streamType) {
+ case SYS_OUT: {
+ System.setOut(new PrintStream(printStream));
+ streamer = new SysOutErrStreamReader(this, pipedInputStream,
+ StreamType.SYS_OUT, testRequest.getSysOutInterests());
+ final Thread sysOutStreamer = new Thread(streamer);
+ sysOutStreamer.setDaemon(true);
+ sysOutStreamer.setName("junitlauncher-sysout-stream-reader");
+ sysOutStreamer.setUncaughtExceptionHandler((t, e) -> this.log("Failed in sysout streaming", e, Project.MSG_INFO));
+ sysOutStreamer.start();
+ break;
+ }
+ case SYS_ERR: {
+ System.setErr(new PrintStream(printStream));
+ streamer = new SysOutErrStreamReader(this, pipedInputStream,
+ StreamType.SYS_ERR, testRequest.getSysErrInterests());
+ final Thread sysErrStreamer = new Thread(streamer);
+ sysErrStreamer.setDaemon(true);
+ sysErrStreamer.setName("junitlauncher-syserr-stream-reader");
+ sysErrStreamer.setUncaughtExceptionHandler((t, e) -> this.log("Failed in syserr streaming", e, Project.MSG_INFO));
+ sysErrStreamer.start();
+ break;
+ }
+ default: {
+ return Optional.empty();
+ }
+ }
+ return Optional.of(new SwitchedStreamHandle(pipedOutputStream, streamer));
+ }
+
+ private enum StreamType {
+ SYS_OUT,
+ SYS_ERR
+ }
+
+ private static final class SysOutErrStreamReader implements Runnable {
+ private static final byte[] EMPTY = new byte[0];
+
+ private final JUnitLauncherTask task;
+ private final InputStream sourceStream;
+ private final StreamType streamType;
+ private final Collection resultFormatters;
+ private volatile SysOutErrContentDeliverer contentDeliverer;
+
+ SysOutErrStreamReader(final JUnitLauncherTask task, final InputStream source, final StreamType streamType, final Collection resultFormatters) {
+ this.task = task;
+ this.sourceStream = source;
+ this.streamType = streamType;
+ this.resultFormatters = resultFormatters;
+ }
+
+ @Override
+ public void run() {
+ final SysOutErrContentDeliverer streamContentDeliver = new SysOutErrContentDeliverer(this.streamType, this.resultFormatters);
+ final Thread deliveryThread = new Thread(streamContentDeliver);
+ deliveryThread.setName("junitlauncher-" + (this.streamType == StreamType.SYS_OUT ? "sysout" : "syserr") + "-stream-deliverer");
+ deliveryThread.setDaemon(true);
+ deliveryThread.start();
+ this.contentDeliverer = streamContentDeliver;
+ int numRead = -1;
+ final byte[] data = new byte[1024];
+ try {
+ while ((numRead = this.sourceStream.read(data)) != -1) {
+ final byte[] copy = Arrays.copyOf(data, numRead);
+ streamContentDeliver.availableData.offer(copy);
+ }
+ } catch (IOException e) {
+ task.log("Failed while streaming " + (this.streamType == StreamType.SYS_OUT ? "sysout" : "syserr") + " data",
+ e, Project.MSG_INFO);
+ return;
+ } finally {
+ streamContentDeliver.stop = true;
+ // just "wakeup" the delivery thread, to take into account
+ // those race conditions, where that other thread didn't yet
+ // notice that it was asked to stop and has now gone into a
+ // X amount of wait, waiting for any new data
+ streamContentDeliver.availableData.offer(EMPTY);
+ }
+ }
+ }
+
+ private static final class SysOutErrContentDeliverer implements Runnable {
+ private volatile boolean stop;
+ private final Collection resultFormatters;
+ private final StreamType streamType;
+ private final BlockingQueue availableData = new LinkedBlockingQueue<>();
+ private final CountDownLatch completionLatch = new CountDownLatch(1);
+
+ SysOutErrContentDeliverer(final StreamType streamType, final Collection resultFormatters) {
+ this.streamType = streamType;
+ this.resultFormatters = resultFormatters;
+ }
+
+ @Override
+ public void run() {
+ try {
+ while (!this.stop) {
+ final byte[] streamData;
+ try {
+ streamData = this.availableData.poll(2, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return;
+ }
+ if (streamData != null) {
+ deliver(streamData);
+ }
+ }
+ // drain it
+ final List remaining = new ArrayList<>();
+ this.availableData.drainTo(remaining);
+ if (!remaining.isEmpty()) {
+ for (final byte[] data : remaining) {
+ deliver(data);
+ }
+ }
+ } finally {
+ this.completionLatch.countDown();
+ }
+ }
+
+ private void deliver(final byte[] data) {
+ if (data == null || data.length == 0) {
+ return;
+ }
+ for (final TestResultFormatter resultFormatter : this.resultFormatters) {
+ // send it to the formatter
+ switch (streamType) {
+ case SYS_OUT: {
+ resultFormatter.sysOutAvailable(data);
+ break;
+ }
+ case SYS_ERR: {
+ resultFormatter.sysErrAvailable(data);
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ private final class SwitchedStreamHandle {
+ private final PipedOutputStream outputStream;
+ private final SysOutErrStreamReader streamReader;
+
+ SwitchedStreamHandle(final PipedOutputStream outputStream, final SysOutErrStreamReader streamReader) {
+ this.streamReader = streamReader;
+ this.outputStream = outputStream;
+ }
+ }
+
+ private final class Listener extends SummaryGeneratingListener {
+ private Optional switchedSysOutHandle;
+ private Optional switchedSysErrHandle;
+
+ @Override
+ public void testPlanExecutionFinished(final TestPlan testPlan) {
+ super.testPlanExecutionFinished(testPlan);
+ // now that the test plan execution is finished, close the switched sysout/syserr output streams
+ // and wait for the sysout and syserr content delivery, to result formatters, to finish
+ if (this.switchedSysOutHandle.isPresent()) {
+ final SwitchedStreamHandle sysOut = this.switchedSysOutHandle.get();
+ try {
+ closeAndWait(sysOut);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return;
+ }
+ }
+ if (this.switchedSysErrHandle.isPresent()) {
+ final SwitchedStreamHandle sysErr = this.switchedSysErrHandle.get();
+ try {
+ closeAndWait(sysErr);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return;
+ }
+ }
+ }
+
+ private void closeAndWait(final SwitchedStreamHandle handle) throws InterruptedException {
+ FileUtils.close(handle.outputStream);
+ if (handle.streamReader.contentDeliverer == null) {
+ return;
+ }
+ // wait for a few seconds
+ handle.streamReader.contentDeliverer.completionLatch.await(2, TimeUnit.SECONDS);
+ }
+ }
+
+ private final class InVMExecution implements TestExecutionContext {
+
+ private final Properties props;
+
+ InVMExecution() {
+ this.props = new Properties();
+ this.props.putAll(JUnitLauncherTask.this.getProject().getProperties());
+ }
+
+ @Override
+ public Properties getProperties() {
+ return this.props;
+ }
+
+ @Override
+ public Optional getProject() {
+ return Optional.of(JUnitLauncherTask.this.getProject());
+ }
+ }
+}
diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyBriefResultFormatter.java b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyBriefResultFormatter.java
new file mode 100644
index 000000000..d5d46705c
--- /dev/null
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyBriefResultFormatter.java
@@ -0,0 +1,17 @@
+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/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyPlainResultFormatter.java b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyPlainResultFormatter.java
new file mode 100644
index 000000000..49ce7e387
--- /dev/null
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyPlainResultFormatter.java
@@ -0,0 +1,294 @@
+/*
+ * 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.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.io.UnsupportedEncodingException;
+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 testIds = new ConcurrentHashMap<>();
+ private TestPlan testPlan;
+ private BufferedWriter writer;
+
+ @Override
+ public void testPlanExecutionStarted(final TestPlan testPlan) {
+ this.testPlan = testPlan;
+ }
+
+ @Override
+ public void testPlanExecutionFinished(final TestPlan testPlan) {
+ for (final Map.Entry 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());
+ final long timeElapsed = stats.endedAt - stats.startedAt;
+ sb.append(", Time elapsed: ");
+ if (timeElapsed < 1000) {
+ sb.append(timeElapsed).append(" milli sec(s)");
+ } else {
+ sb.append(TimeUnit.SECONDS.convert(timeElapsed, TimeUnit.MILLISECONDS)).append(" sec(s)");
+ }
+ 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);
+ return;
+ }
+ }
+
+ @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(testIdentifier.getLegacyReportingName());
+ final long timeElapsed = stats.endedAt - stats.startedAt;
+ sb.append(" took ");
+ if (timeElapsed < 1000) {
+ sb.append(timeElapsed).append(" milli sec(s)");
+ } else {
+ sb.append(TimeUnit.SECONDS.convert(timeElapsed, TimeUnit.MILLISECONDS)).append(" sec(s)");
+ }
+ 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 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 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 parentTestClass = traverseAndFindTestClass(this.testPlan, testIdentifier);
+ if (parentTestClass.isPresent()) {
+ final Stats parentClassStats = this.testIds.get(parentTestClass.get());
+ if (parentClassStats != null) {
+ parentClassStats.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.setEndedAt(currentTime);
+ }
+ if (testIdentifier.isTest() && shouldReportExecutionFinished(testIdentifier, testExecutionResult)) {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("Test: ");
+ sb.append(testIdentifier.getLegacyReportingName());
+ if (stats != null) {
+ final long timeElapsed = stats.endedAt - stats.startedAt;
+ sb.append(" took ");
+ if (timeElapsed < 1000) {
+ sb.append(timeElapsed).append(" milli sec(s)");
+ } else {
+ sb.append(TimeUnit.SECONDS.convert(timeElapsed, TimeUnit.MILLISECONDS)).append(" sec(s)");
+ }
+ }
+ 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 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;
+ try {
+ this.writer = new BufferedWriter(new OutputStreamWriter(this.outputStream, "UTF-8"));
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException("Failed to create a writer", e);
+ }
+ }
+
+ 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(": ").append(throwable.getMessage());
+ sb.append(NEW_LINE);
+ 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 {
+ 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;
+ }
+ }
+}
diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyXmlResultFormatter.java b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyXmlResultFormatter.java
new file mode 100644
index 000000000..3a94a80d5
--- /dev/null
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyXmlResultFormatter.java
@@ -0,0 +1,363 @@
+package org.apache.tools.ant.taskdefs.optional.junitlauncher;
+
+import org.apache.tools.ant.util.DOMElementWriter;
+import org.apache.tools.ant.util.DateUtils;
+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 testIds = new ConcurrentHashMap<>();
+ private final Map> skipped = new ConcurrentHashMap<>();
+ private final Map> failed = new ConcurrentHashMap<>();
+ private final Map> 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);
+
+
+ @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);
+ return;
+ }
+ }
+
+ @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;
+ }
+
+ private final class Stats {
+ 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();
+ writer.writeAttribute(ATTR_NAME, testsuiteName);
+ // time taken for the tests execution
+ writer.writeAttribute(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);
+ writer.writeAttribute(ATTR_TIMESTAMP, timestamp);
+ writer.writeAttribute(ATTR_NUM_TESTS, String.valueOf(numTestsRun.longValue()));
+ writer.writeAttribute(ATTR_NUM_FAILURES, String.valueOf(numTestsFailed.longValue()));
+ writer.writeAttribute(ATTR_NUM_SKIPPED, String.valueOf(numTestsSkipped.longValue()));
+ writer.writeAttribute(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);
+ writer.writeAttribute(ATTR_NAME, prop);
+ writer.writeAttribute(ATTR_VALUE, properties.getProperty(prop));
+ writer.writeEndElement();
+ }
+ writer.writeEndElement();
+ }
+
+ void writeTestCase(final XMLStreamWriter writer) throws XMLStreamException {
+ for (final Map.Entry entry : testIds.entrySet()) {
+ final TestIdentifier testId = entry.getKey();
+ if (!testId.isTest()) {
+ // only interested in test methods
+ continue;
+ }
+ // find the parent class of this test method
+ final Optional parent = testPlan.getParent(testId);
+ if (!parent.isPresent() || !parent.get().getSource().isPresent()) {
+ // we need to know the parent test class, else we aren't interested
+ continue;
+ }
+ final TestSource parentSource = parent.get().getSource().get();
+ if (!(parentSource instanceof ClassSource)) {
+ continue;
+ }
+ final String classname = ((ClassSource) parentSource).getClassName();
+ writer.writeStartElement(ELEM_TESTCASE);
+ writer.writeAttribute(ATTR_CLASSNAME, classname);
+ writer.writeAttribute(ATTR_NAME, testId.getDisplayName());
+ final Stats stats = entry.getValue();
+ writer.writeAttribute(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 reason = skipped.get(testIdentifier);
+ if (reason.isPresent()) {
+ writer.writeAttribute(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 cause = failed.get(testIdentifier);
+ if (cause.isPresent()) {
+ final Throwable t = cause.get();
+ final String message = t.getMessage();
+ if (message != null && !message.trim().isEmpty()) {
+ writer.writeAttribute(ATTR_MESSAGE, message);
+ }
+ writer.writeAttribute(ATTR_TYPE, t.getClass().getName());
+ }
+ writer.writeEndElement();
+ }
+
+ private void writeAborted(final XMLStreamWriter writer, final TestIdentifier testIdentifier) throws XMLStreamException {
+ if (!aborted.containsKey(testIdentifier)) {
+ return;
+ }
+ writer.writeStartElement(ELEM_ABORTED);
+ final Optional cause = aborted.get(testIdentifier);
+ if (cause.isPresent()) {
+ final Throwable t = cause.get();
+ final String message = t.getMessage();
+ if (message != null && !message.trim().isEmpty()) {
+ writer.writeAttribute(ATTR_MESSAGE, message);
+ }
+ writer.writeAttribute(ATTR_TYPE, t.getClass().getName());
+ }
+ 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) {
+ // although it's called a DOMElementWriter, the encode method is just a
+ // straight forward XML util method which doesn't concern about whether
+ // DOM, SAX, StAX semantics.
+ // TODO: Perhaps make it a static method
+ final String encoded = new DOMElementWriter().encode(new String(chars, 0, numRead));
+ writer.writeCharacters(encoded);
+ }
+ }
+
+ 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 roots = testPlan.getRoots();
+ if (roots.isEmpty()) {
+ return "UNKNOWN";
+ }
+ for (final TestIdentifier root : roots) {
+ final Optional classSource = findFirstClassSource(root);
+ if (classSource.isPresent()) {
+ return classSource.get().getClassName();
+ }
+ }
+ return "UNKNOWN";
+ }
+
+ private Optional 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 = findFirstClassSource(child);
+ if (classSource.isPresent()) {
+ return classSource;
+ }
+ }
+ return Optional.empty();
+ }
+ }
+
+}
diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/ListenerDefinition.java b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/ListenerDefinition.java
new file mode 100644
index 000000000..afbc16161
--- /dev/null
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/ListenerDefinition.java
@@ -0,0 +1,121 @@
+package org.apache.tools.ant.taskdefs.optional.junitlauncher;
+
+import org.apache.tools.ant.Project;
+import org.apache.tools.ant.PropertyHelper;
+import org.apache.tools.ant.types.EnumeratedAttribute;
+
+/**
+ * Represents the {@code <listener>} element within the {@code <junitlauncher>}
+ * task
+ */
+public class ListenerDefinition {
+
+ private static final String LEGACY_PLAIN = "legacy-plain";
+ private static final String LEGACY_BRIEF = "legacy-brief";
+ private static final String LEGACY_XML = "legacy-xml";
+
+ private String ifProperty;
+ private String unlessProperty;
+ private String className;
+ private String resultFile;
+ private boolean sendSysOut;
+ private boolean sendSysErr;
+
+ private String defaultResultFileSuffix = "txt";
+
+ public ListenerDefinition() {
+
+ }
+
+ public void setClassName(final String className) {
+ this.className = className;
+ }
+
+ String getClassName() {
+ return this.className;
+ }
+
+ String getIfProperty() {
+ return ifProperty;
+ }
+
+ public void setIf(final String ifProperty) {
+ this.ifProperty = ifProperty;
+ }
+
+ String getUnlessProperty() {
+ return unlessProperty;
+ }
+
+ public void setUnless(final String unlessProperty) {
+ this.unlessProperty = unlessProperty;
+ }
+
+ public void setType(final ListenerType type) {
+ switch (type.getValue()) {
+ case LEGACY_PLAIN: {
+ this.setClassName("org.apache.tools.ant.taskdefs.optional.junitlauncher.LegacyPlainResultFormatter");
+ this.defaultResultFileSuffix = "txt";
+ break;
+ }
+ case LEGACY_BRIEF: {
+ this.setClassName("org.apache.tools.ant.taskdefs.optional.junitlauncher.LegacyBriefResultFormatter");
+ this.defaultResultFileSuffix = "txt";
+ break;
+ }
+ case LEGACY_XML: {
+ this.setClassName("org.apache.tools.ant.taskdefs.optional.junitlauncher.LegacyXmlResultFormatter");
+ this.defaultResultFileSuffix = "xml";
+ break;
+ }
+ }
+ }
+
+ public void setResultFile(final String filename) {
+ this.resultFile = filename;
+ }
+
+ String requireResultFile(final TestDefinition test) {
+ if (this.resultFile != null) {
+ return this.resultFile;
+ }
+ final StringBuilder sb = new StringBuilder("TEST-");
+ if (test instanceof NamedTest) {
+ sb.append(((NamedTest) test).getName());
+ } else {
+ sb.append("unknown");
+ }
+ sb.append(".").append(this.defaultResultFileSuffix);
+ return sb.toString();
+ }
+
+ public void setSendSysOut(final boolean sendSysOut) {
+ this.sendSysOut = sendSysOut;
+ }
+
+ boolean shouldSendSysOut() {
+ return this.sendSysOut;
+ }
+
+ public void setSendSysErr(final boolean sendSysErr) {
+ this.sendSysErr = sendSysErr;
+ }
+
+ boolean shouldSendSysErr() {
+ return this.sendSysErr;
+ }
+
+ protected boolean shouldUse(final Project project) {
+ final PropertyHelper propertyHelper = PropertyHelper.getPropertyHelper(project);
+ return propertyHelper.testIfCondition(this.ifProperty) && propertyHelper.testUnlessCondition(this.unlessProperty);
+ }
+
+ public static class ListenerType extends EnumeratedAttribute {
+
+ @Override
+ public String[] getValues() {
+ return new String[]{LEGACY_PLAIN, LEGACY_BRIEF, LEGACY_XML};
+ }
+ }
+
+}
diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/NamedTest.java b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/NamedTest.java
new file mode 100644
index 000000000..5061c314a
--- /dev/null
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/NamedTest.java
@@ -0,0 +1,14 @@
+package org.apache.tools.ant.taskdefs.optional.junitlauncher;
+
+/**
+ * A test that has a name associated with it
+ */
+public interface NamedTest {
+
+ /**
+ * Returns the name of the test
+ *
+ * @return
+ */
+ String getName();
+}
diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/SingleTestClass.java b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/SingleTestClass.java
new file mode 100644
index 000000000..f044e7e8a
--- /dev/null
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/SingleTestClass.java
@@ -0,0 +1,101 @@
+package org.apache.tools.ant.taskdefs.optional.junitlauncher;
+
+import org.apache.tools.ant.Project;
+import org.junit.Test;
+import org.junit.platform.engine.discovery.DiscoverySelectors;
+import org.junit.platform.launcher.EngineFilter;
+import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
+
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.StringTokenizer;
+
+/**
+ * Represents the single {@code test} (class) that's configured to be launched by the {@link JUnitLauncherTask}
+ */
+public class SingleTestClass extends TestDefinition implements NamedTest {
+
+ private String testClass;
+ private Set testMethods;
+
+ public SingleTestClass() {
+
+ }
+
+ public void setName(final String test) {
+ if (test == null || test.trim().isEmpty()) {
+ throw new IllegalArgumentException("Test name cannot be null or empty string");
+ }
+ this.testClass = test;
+ }
+
+ @Test
+ public String getName() {
+ return this.testClass;
+ }
+
+ public void setMethods(final String methods) {
+ // parse the comma separated set of methods
+ if (methods == null || methods.trim().isEmpty()) {
+ this.testMethods = Collections.emptySet();
+ return;
+ }
+ final StringTokenizer tokenizer = new StringTokenizer(methods, ",");
+ if (!tokenizer.hasMoreTokens()) {
+ this.testMethods = Collections.emptySet();
+ return;
+ }
+ // maintain specified ordering
+ this.testMethods = new LinkedHashSet<>();
+ while (tokenizer.hasMoreTokens()) {
+ final String method = tokenizer.nextToken().trim();
+ if (method.isEmpty()) {
+ continue;
+ }
+ this.testMethods.add(method);
+ }
+ }
+
+ boolean hasMethodsSpecified() {
+ return this.testMethods != null && !this.testMethods.isEmpty();
+ }
+
+ String[] getMethods() {
+ if (!hasMethodsSpecified()) {
+ return null;
+ }
+ return this.testMethods.toArray(new String[this.testMethods.size()]);
+ }
+
+ @Override
+ List createTestRequests(final JUnitLauncherTask launcherTask) {
+ final Project project = launcherTask.getProject();
+ if (!shouldRun(project)) {
+ launcherTask.log("Excluding test " + this.testClass + " since it's considered not to run " +
+ "in context of project " + project, Project.MSG_DEBUG);
+ return Collections.emptyList();
+ }
+ final LauncherDiscoveryRequestBuilder requestBuilder = LauncherDiscoveryRequestBuilder.request();
+ if (!this.hasMethodsSpecified()) {
+ requestBuilder.selectors(DiscoverySelectors.selectClass(this.testClass));
+ } else {
+ // add specific methods
+ final String[] methods = this.getMethods();
+ for (final String method : methods) {
+ requestBuilder.selectors(DiscoverySelectors.selectMethod(this.testClass, method));
+ }
+ }
+ // add any engine filters
+ final String[] enginesToInclude = this.getIncludeEngines();
+ if (enginesToInclude != null && enginesToInclude.length > 0) {
+ requestBuilder.filters(EngineFilter.includeEngines(enginesToInclude));
+ }
+ final String[] enginesToExclude = this.getExcludeEngines();
+ if (enginesToExclude != null && enginesToExclude.length > 0) {
+ requestBuilder.filters(EngineFilter.excludeEngines(enginesToExclude));
+ }
+ return Collections.singletonList(new TestRequest(this, requestBuilder));
+ }
+}
diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestClasses.java b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestClasses.java
new file mode 100644
index 000000000..451e79f1a
--- /dev/null
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestClasses.java
@@ -0,0 +1,112 @@
+package org.apache.tools.ant.taskdefs.optional.junitlauncher;
+
+import org.apache.tools.ant.types.Resource;
+import org.apache.tools.ant.types.ResourceCollection;
+import org.apache.tools.ant.types.resources.Resources;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Represents a {@code testclasses} that's configured to be launched by the {@link JUnitLauncherTask}
+ */
+public class TestClasses extends TestDefinition {
+
+ private final Resources resources = new Resources();
+
+ public TestClasses() {
+
+ }
+
+ public void add(final ResourceCollection resourceCollection) {
+ this.resources.add(resourceCollection);
+ }
+
+ @Override
+ List createTestRequests(final JUnitLauncherTask launcherTask) {
+ final List tests = this.getTests();
+ if (tests.isEmpty()) {
+ return Collections.emptyList();
+ }
+ final List requests = new ArrayList<>();
+ for (final SingleTestClass test : tests) {
+ requests.addAll(test.createTestRequests(launcherTask));
+ }
+ return requests;
+ }
+
+ private List getTests() {
+ if (this.resources.isEmpty()) {
+ return Collections.emptyList();
+ }
+ final List tests = new ArrayList<>();
+ for (final Resource resource : resources) {
+ if (!resource.isExists()) {
+ continue;
+ }
+ final String name = resource.getName();
+ // we only consider .class files
+ if (!name.endsWith(".class")) {
+ continue;
+ }
+ final String className = name.substring(0, name.lastIndexOf('.'));
+ final BatchSourcedSingleTest test = new BatchSourcedSingleTest(className.replace(File.separatorChar, '.').replace('/', '.').replace('\\', '.'));
+ tests.add(test);
+ }
+ return tests;
+ }
+
+ /**
+ * A {@link BatchSourcedSingleTest} is similar to a {@link SingleTestClass} except that
+ * some of the characteristics of the test (like whether to halt on failure) are borrowed
+ * from the {@link TestClasses batch} to which this test belongs to
+ */
+ private final class BatchSourcedSingleTest extends SingleTestClass {
+
+ private BatchSourcedSingleTest(final String testClassName) {
+ this.setName(testClassName);
+ }
+
+ @Override
+ String getIfProperty() {
+ return TestClasses.this.getIfProperty();
+ }
+
+ @Override
+ String getUnlessProperty() {
+ return TestClasses.this.getUnlessProperty();
+ }
+
+ @Override
+ boolean isHaltOnFailure() {
+ return TestClasses.this.isHaltOnFailure();
+ }
+
+ @Override
+ String getFailureProperty() {
+ return TestClasses.this.getFailureProperty();
+ }
+
+ @Override
+ List getListeners() {
+ return TestClasses.this.getListeners();
+ }
+
+ @Override
+ String getOutputDir() {
+ return TestClasses.this.getOutputDir();
+ }
+
+ @Override
+ String[] getIncludeEngines() {
+ return TestClasses.this.getIncludeEngines();
+ }
+
+ @Override
+ String[] getExcludeEngines() {
+ return TestClasses.this.getExcludeEngines();
+ }
+ }
+}
diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestDefinition.java b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestDefinition.java
new file mode 100644
index 000000000..d05ae3e9f
--- /dev/null
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestDefinition.java
@@ -0,0 +1,113 @@
+package org.apache.tools.ant.taskdefs.optional.junitlauncher;
+
+import org.apache.tools.ant.Project;
+import org.apache.tools.ant.PropertyHelper;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Represents the configuration details of a test that needs to be launched by the {@link JUnitLauncherTask}
+ */
+abstract class TestDefinition {
+ protected String ifProperty;
+ protected String unlessProperty;
+ protected Boolean haltOnFailure;
+ protected String failureProperty;
+ protected String outputDir;
+ protected String includeEngines;
+ protected String excludeEngines;
+
+ protected List listeners = new ArrayList<>();
+
+ String getIfProperty() {
+ return ifProperty;
+ }
+
+ public void setIf(final String ifProperty) {
+ this.ifProperty = ifProperty;
+ }
+
+ String getUnlessProperty() {
+ return unlessProperty;
+ }
+
+ public void setUnless(final String unlessProperty) {
+ this.unlessProperty = unlessProperty;
+ }
+
+ boolean isHaltOnFailure() {
+ return this.haltOnFailure == null ? false : this.haltOnFailure;
+ }
+
+ Boolean getHaltOnFailure() {
+ return this.haltOnFailure;
+ }
+
+ public void setHaltOnFailure(final boolean haltonfailure) {
+ this.haltOnFailure = haltonfailure;
+ }
+
+ String getFailureProperty() {
+ return failureProperty;
+ }
+
+ public void setFailureProperty(final String failureProperty) {
+ this.failureProperty = failureProperty;
+ }
+
+ public void addConfiguredListener(final ListenerDefinition listener) {
+ this.listeners.add(listener);
+ }
+
+ List getListeners() {
+ return Collections.unmodifiableList(this.listeners);
+ }
+
+ public void setOutputDir(final String dir) {
+ this.outputDir = dir;
+ }
+
+ String getOutputDir() {
+ return this.outputDir;
+ }
+
+ abstract List createTestRequests(final JUnitLauncherTask launcherTask);
+
+ protected boolean shouldRun(final Project project) {
+ final PropertyHelper propertyHelper = PropertyHelper.getPropertyHelper(project);
+ return propertyHelper.testIfCondition(this.ifProperty) && propertyHelper.testUnlessCondition(this.unlessProperty);
+ }
+
+ String[] getIncludeEngines() {
+ return includeEngines == null ? new String[0] : split(this.includeEngines, ",");
+ }
+
+ public void setIncludeEngines(final String includeEngines) {
+ this.includeEngines = includeEngines;
+ }
+
+ String[] getExcludeEngines() {
+ return excludeEngines == null ? new String[0] : split(this.excludeEngines, ",");
+ }
+
+ public void setExcludeEngines(final String excludeEngines) {
+ this.excludeEngines = excludeEngines;
+ }
+
+ private static String[] split(final String value, final String delimiter) {
+ if (value == null) {
+ return new String[0];
+ }
+ final List parts = new ArrayList<>();
+ for (final String part : value.split(delimiter)) {
+ if (part.trim().isEmpty()) {
+ // skip it
+ continue;
+ }
+ parts.add(part);
+ }
+ return parts.toArray(new String[parts.size()]);
+ }
+}
diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestExecutionContext.java b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestExecutionContext.java
new file mode 100644
index 000000000..fd08a1baa
--- /dev/null
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestExecutionContext.java
@@ -0,0 +1,28 @@
+package org.apache.tools.ant.taskdefs.optional.junitlauncher;
+
+import org.apache.tools.ant.Project;
+
+import java.util.Optional;
+import java.util.Properties;
+
+/**
+ * A {@link TestExecutionContext} represents the execution context for a test
+ * that has been launched by the {@link JUnitLauncherTask} and provides any necessary
+ * contextual information about such tests.
+ */
+public interface TestExecutionContext {
+
+ /**
+ * @return Returns the properties that were used for the execution of the test
+ */
+ Properties getProperties();
+
+
+ /**
+ * @return Returns the {@link Project} in whose context the test is being executed.
+ * The {@code Project} is sometimes not available, like in the case where
+ * the test is being run in a forked mode, in such cases this method returns
+ * {@link Optional#empty() an empty value}
+ */
+ Optional getProject();
+}
diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestRequest.java b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestRequest.java
new file mode 100644
index 000000000..a56b3d240
--- /dev/null
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestRequest.java
@@ -0,0 +1,74 @@
+package org.apache.tools.ant.taskdefs.optional.junitlauncher;
+
+import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
+
+import java.io.Closeable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Holds together the necessary details about a request that will be launched by the {@link JUnitLauncherTask}
+ */
+final class TestRequest implements AutoCloseable {
+ private final TestDefinition ownerTest;
+ private final LauncherDiscoveryRequestBuilder discoveryRequest;
+ private final List closables = new ArrayList<>();
+ private final List interestedInSysOut = new ArrayList<>();
+ private final List interestedInSysErr = new ArrayList<>();
+
+
+ TestRequest(final TestDefinition ownerTest, final LauncherDiscoveryRequestBuilder discoveryRequest) {
+ this.ownerTest = ownerTest;
+ this.discoveryRequest = discoveryRequest;
+ }
+
+ TestDefinition getOwner() {
+ return ownerTest;
+ }
+
+ LauncherDiscoveryRequestBuilder getDiscoveryRequest() {
+ return discoveryRequest;
+ }
+
+ void closeUponCompletion(final Closeable closeable) {
+ if (closeable == null) {
+ return;
+ }
+ this.closables.add(closeable);
+ }
+
+ void addSysOutInterest(final TestResultFormatter out) {
+ this.interestedInSysOut.add(out);
+ }
+
+ boolean interestedInSysOut() {
+ return !this.interestedInSysOut.isEmpty();
+ }
+
+ Collection getSysOutInterests() {
+ return Collections.unmodifiableList(this.interestedInSysOut);
+ }
+
+ void addSysErrInterest(final TestResultFormatter err) {
+ this.interestedInSysErr.add(err);
+ }
+
+ boolean interestedInSysErr() {
+ return !this.interestedInSysErr.isEmpty();
+ }
+
+ Collection getSysErrInterests() {
+ return Collections.unmodifiableList(this.interestedInSysErr);
+ }
+
+ public void close() throws Exception {
+ if (this.closables.isEmpty()) {
+ return;
+ }
+ for (final Closeable closeable : closables) {
+ closeable.close();
+ }
+ }
+}
diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestResultFormatter.java b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestResultFormatter.java
new file mode 100644
index 000000000..ad1e4f36a
--- /dev/null
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestResultFormatter.java
@@ -0,0 +1,58 @@
+package org.apache.tools.ant.taskdefs.optional.junitlauncher;
+
+import org.junit.platform.launcher.TestExecutionListener;
+
+import java.io.Closeable;
+import java.io.OutputStream;
+
+/**
+ * A {@link TestExecutionListener} which lets implementing classes format and write out
+ * the test execution results.
+ */
+public interface TestResultFormatter extends TestExecutionListener, Closeable {
+
+ /**
+ * This method will be invoked by the junitlauncher
and will be passed the
+ * {@link OutputStream} to a file, to which the formatted result is expected to be written
+ * to.
+ *
+ * This method will be called once, early on, during the initialization of this
+ * {@link TestResultFormatter}, typically before the test execution itself has started.
+ *
+ *
+ * @param os The output stream to which to write out the result
+ */
+ void setDestination(OutputStream os);
+
+ /**
+ * This method will be invoked by the junitlauncher
and will be passed a
+ * {@link TestExecutionContext}. This allows the {@link TestResultFormatter} to have access
+ * to any additional contextual information to use in the test reports.
+ *
+ * @param context The context of the execution of the test
+ */
+ void setContext(TestExecutionContext context);
+
+ /**
+ * This method will be invoked by the junitlauncher
, regularly/multiple times,
+ * as and when any content is generated on the standard output stream during the test execution.
+ * This method will be only be called if the sendSysOut
attribute of the listener
,
+ * to which this {@link TestResultFormatter} is configured for, is enabled
+ *
+ * @param data The content generated on standard output stream
+ */
+ default void sysOutAvailable(byte[] data) {
+ }
+
+ /**
+ * This method will be invoked by the junitlauncher
, regularly/multiple times,
+ * as and when any content is generated on the standard error stream during the test execution.
+ * This method will be only be called if the sendSysErr
attribute of the listener
,
+ * to which this {@link TestResultFormatter} is configured for, is enabled
+ *
+ * @param data The content generated on standard error stream
+ */
+ default void sysErrAvailable(byte[] data) {
+ }
+
+}
diff --git a/src/tests/junit/org/apache/tools/ant/taskdefs/optional/junitlauncher/JUnitLauncherTaskTest.java b/src/tests/junit/org/apache/tools/ant/taskdefs/optional/junitlauncher/JUnitLauncherTaskTest.java
new file mode 100644
index 000000000..84d4875ce
--- /dev/null
+++ b/src/tests/junit/org/apache/tools/ant/taskdefs/optional/junitlauncher/JUnitLauncherTaskTest.java
@@ -0,0 +1,127 @@
+package org.apache.tools.ant.taskdefs.optional.junitlauncher;
+
+import org.apache.tools.ant.BuildEvent;
+import org.apache.tools.ant.BuildException;
+import org.apache.tools.ant.BuildListener;
+import org.apache.tools.ant.Project;
+import org.apache.tools.ant.ProjectHelper;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.File;
+
+/**
+ * Tests the {@link JUnitLauncherTask}
+ */
+public class JUnitLauncherTaskTest {
+
+ private Project project;
+
+ /**
+ * The JUnit setup method.
+ */
+ @Before
+ public void setUp() {
+ File antFile = new File(System.getProperty("root"), "src/etc/testcases/taskdefs/optional/junitlauncher.xml");
+ this.project = new Project();
+ this.project.init();
+ ProjectHelper.configureProject(project, antFile);
+ project.addBuildListener(new BuildListener() {
+ @Override
+ public void buildStarted(final BuildEvent event) {
+
+ }
+
+ @Override
+ public void buildFinished(final BuildEvent event) {
+
+ }
+
+ @Override
+ public void targetStarted(final BuildEvent event) {
+
+ }
+
+ @Override
+ public void targetFinished(final BuildEvent event) {
+
+ }
+
+ @Override
+ public void taskStarted(final BuildEvent event) {
+
+ }
+
+ @Override
+ public void taskFinished(final BuildEvent event) {
+
+ }
+
+ @Override
+ public void messageLogged(final BuildEvent event) {
+ if (event.getPriority() <= Project.MSG_INFO) {
+ System.out.println(event.getMessage());
+ }
+ }
+ });
+ }
+
+ /**
+ * Tests that when a test, that's configured with {@code haltOnFailure=true}, stops the build, when the
+ * test fails
+ */
+ @Test
+ public void testFailureStopsBuild() {
+ try {
+ project.executeTarget("test-failure-stops-build");
+ Assert.fail("Test execution failure was expected to stop the build but didn't");
+ } catch (BuildException be) {
+ // expected
+ }
+ }
+
+ /**
+ * Tests that when a test, that's isn't configured with {@code haltOnFailure=true}, continues the
+ * build even when there are test failures
+ */
+ @Test
+ public void testFailureContinuesBuild() {
+ project.executeTarget("test-failure-continues-build");
+ }
+
+
+ /**
+ * Tests the execution of test that's expected to succeed
+ */
+ @Test
+ public void testSuccessfulTests() {
+ project.executeTarget("test-success");
+ }
+
+ /**
+ * Tests execution of a test which is configured to execute only a particular set of test methods
+ */
+ @Test
+ public void testSpecificMethodTest() {
+ project.executeTarget("test-one-specific-method");
+ project.executeTarget("test-multiple-specific-methods");
+ }
+
+ /**
+ * Tests the execution of more than one {@code <test>} elements in the {@code <junitlauncher>} task
+ */
+ @Test
+ public void testMultipleIndividualTests() {
+ project.executeTarget("test-multiple-individual");
+ }
+
+ /**
+ * Tests execution of tests, that have been configured using the {@code <testclasses>} nested element
+ * of the {@code <junitlauncher>} task
+ */
+ @Test
+ public void testTestClasses() {
+ project.executeTarget("test-batch");
+ }
+}
diff --git a/src/tests/junit/org/apache/tools/ant/taskdefs/optional/junitlauncher/example/jupiter/JupiterSampleTest.java b/src/tests/junit/org/apache/tools/ant/taskdefs/optional/junitlauncher/example/jupiter/JupiterSampleTest.java
new file mode 100644
index 000000000..38b49199e
--- /dev/null
+++ b/src/tests/junit/org/apache/tools/ant/taskdefs/optional/junitlauncher/example/jupiter/JupiterSampleTest.java
@@ -0,0 +1,50 @@
+package org.apache.tools.ant.taskdefs.optional.junitlauncher.example.jupiter;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ *
+ */
+public class JupiterSampleTest {
+
+ private static final String message = "The quick brown fox jumps over the lazy dog";
+
+ @BeforeAll
+ static void beforeAll() {
+ }
+
+ @BeforeEach
+ void beforeEach() {
+ }
+
+ @Test
+ void testSucceeds() {
+ System.out.println(message);
+ System.out.print("Hello world! ");
+ }
+
+ @Test
+ void testFails() {
+ fail("intentionally failing");
+ }
+
+ @Test
+ @Disabled("intentionally skipped")
+ void testSkipped() {
+ }
+
+ @AfterEach
+ void afterEach() {
+ }
+
+ @AfterAll
+ static void afterAll() {
+ }
+}
diff --git a/src/tests/junit/org/example/junitlauncher/vintage/AlwaysFailingJUnit4Test.java b/src/tests/junit/org/example/junitlauncher/vintage/AlwaysFailingJUnit4Test.java
new file mode 100644
index 000000000..ce4f1620a
--- /dev/null
+++ b/src/tests/junit/org/example/junitlauncher/vintage/AlwaysFailingJUnit4Test.java
@@ -0,0 +1,16 @@
+package org.example.junitlauncher.vintage;
+
+import org.junit.Assert;
+import org.junit.Ignore;
+import org.junit.Test;
+
+/**
+ *
+ */
+public class AlwaysFailingJUnit4Test {
+
+ @Test
+ public void testWillFail() throws Exception {
+ Assert.assertEquals("Values weren't equal", 3, 4);
+ }
+}
diff --git a/src/tests/junit/org/example/junitlauncher/vintage/JUnit4SampleTest.java b/src/tests/junit/org/example/junitlauncher/vintage/JUnit4SampleTest.java
new file mode 100644
index 000000000..bf606a27d
--- /dev/null
+++ b/src/tests/junit/org/example/junitlauncher/vintage/JUnit4SampleTest.java
@@ -0,0 +1,25 @@
+package org.example.junitlauncher.vintage;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ *
+ */
+public class JUnit4SampleTest {
+
+ @Test
+ public void testFoo() {
+ Assert.assertEquals(1, 1);
+ }
+
+ @Test
+ public void testBar() throws Exception {
+ Assert.assertTrue(true);
+ }
+
+ @Test
+ public void testFooBar() {
+ Assert.assertFalse(false);
+ }
+}