getListeners();
+
+ /**
+ * Returns true if a summary needs to be printed out after the execution of the
+ * tests. False otherwise.
+ *
+ * @return
+ */
+ boolean isPrintSummary();
+
+ /**
+ * Returns true if any remaining tests launch need to be stopped if any test execution
+ * failed. False otherwise.
+ *
+ * @return
+ */
+ boolean isHaltOnFailure();
+
+ /**
+ * Returns the {@link ClassLoader} that has to be used for launching and execution of the
+ * tests
+ *
+ * @return
+ */
+ ClassLoader getClassLoader();
+
+ /**
+ * Returns the {@link TestExecutionContext} that will be passed to {@link TestResultFormatter#setContext(TestExecutionContext)
+ * result formatters} which are applicable during the execution of the tests.
+ *
+ * @return
+ */
+ TestExecutionContext getTestExecutionContext();
+}
diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LauncherSupport.java b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LauncherSupport.java
new file mode 100644
index 000000000..6a8027b3e
--- /dev/null
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LauncherSupport.java
@@ -0,0 +1,513 @@
+/*
+ * 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.apache.tools.ant.BuildException;
+import org.apache.tools.ant.Project;
+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.io.PrintWriter;
+import java.nio.file.Files;
+import java.nio.file.Path;
+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.concurrent.BlockingQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Responsible for doing the real work involved in launching the JUnit platform
+ * and passing it the relevant tests that need to be executed by the JUnit platform.
+ *
+ * This class relies on a {@link LaunchDefinition} for setting up the launch of the
+ * JUnit platform.
+ *
+ * The {@code LauncherSupport} isn't concerned with whether or not
+ * it's being executed in the same JVM as the build in which the {@code junitlauncher}
+ * was triggered or if it's running as part of a forked JVM. Instead it just relies
+ * on the {@code LaunchDefinition} to do whatever decisions need to be done before and
+ * after launching the tests.
+ *
+ * This class is not thread-safe and isn't expected to be used for launching from
+ * multiple different threads simultaneously.
+ */
+class LauncherSupport {
+
+ private final LaunchDefinition launchDefinition;
+
+ private boolean testsFailed;
+
+ /**
+ * Create a {@link LauncherSupport} for the passed {@link LaunchDefinition}
+ *
+ * @param definition The launch definition which will be used for launching the tests
+ */
+ LauncherSupport(final LaunchDefinition definition) {
+ if (definition == null) {
+ throw new IllegalArgumentException("Launch definition cannot be null");
+ }
+ this.launchDefinition = definition;
+ }
+
+ /**
+ * Launches the tests defined in the {@link LaunchDefinition}
+ *
+ * @throws BuildException If any tests failed and the launch definition was configured to throw
+ * an exception, or if any other exception occurred before or after launching
+ * the tests
+ */
+ void launch() throws BuildException {
+ final ClassLoader previousClassLoader = Thread.currentThread().getContextClassLoader();
+ try {
+ Thread.currentThread().setContextClassLoader(this.launchDefinition.getClassLoader());
+ 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, this.launchDefinition.getClassLoader()));
+ 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);
+ }
+ }
+
+ /**
+ * Returns true if there were any test failures, when this {@link LauncherSupport} was used
+ * to {@link #launch()} tests. False otherwise.
+ *
+ * @return
+ */
+ boolean hasTestFailures() {
+ return this.testsFailed;
+ }
+
+ private List buildTestRequests() {
+ final List tests = this.launchDefinition.getTests();
+ if (tests.isEmpty()) {
+ return Collections.emptyList();
+ }
+ final List requests = new ArrayList<>();
+ for (final TestDefinition test : tests) {
+ final List testRequests = test.createTestRequests();
+ 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.launchDefinition.getListeners() : test.getListeners();
+ final List listeners = new ArrayList<>();
+ final Optional project = this.launchDefinition.getTestExecutionContext().getProject();
+ for (final ListenerDefinition applicableListener : applicableListenerElements) {
+ if (project.isPresent() && !applicableListener.shouldUse(project.get())) {
+ log("Excluding listener " + applicableListener.getClassName() + " since it's not applicable" +
+ " in the context of project", null, 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(this.launchDefinition.getTestExecutionContext());
+ // set the destination output stream for writing out the formatted result
+ final TestDefinition test = testRequest.getOwner();
+ final TestExecutionContext testExecutionContext = this.launchDefinition.getTestExecutionContext();
+ final Path baseDir = testExecutionContext.getProject().isPresent()
+ ? testExecutionContext.getProject().get().getBaseDir().toPath() : Paths.get(System.getProperty("user.dir"));
+ final java.nio.file.Path outputDir = test.getOutputDir() != null ? Paths.get(test.getOutputDir()) : baseDir;
+ 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) {
+ if (this.launchDefinition.isPrintSummary()) {
+ // print the summary to System.out
+ summary.printTo(new PrintWriter(System.out, true));
+ }
+ final boolean hasTestFailures = summary.getTestsFailedCount() != 0;
+ if (hasTestFailures) {
+ // keep track of the test failure(s) for the entire launched instance
+ this.testsFailed = true;
+ }
+ 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
+ final TestExecutionContext testExecutionContext = this.launchDefinition.getTestExecutionContext();
+ if (testExecutionContext.getProject().isPresent()) {
+ final Project project = testExecutionContext.getProject().get();
+ project.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 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 void log(final String message, final Throwable t, final int level) {
+ final TestExecutionContext testExecutionContext = this.launchDefinition.getTestExecutionContext();
+ if (testExecutionContext.getProject().isPresent()) {
+ testExecutionContext.getProject().get().log(message, t, level);
+ return;
+ }
+ if (t == null) {
+ System.out.println(message);
+ } else {
+ System.err.println(message);
+ t.printStackTrace();
+ }
+ }
+
+ private enum StreamType {
+ SYS_OUT,
+ SYS_ERR
+ }
+
+ private static final class SysOutErrStreamReader implements Runnable {
+ private static final byte[] EMPTY = new byte[0];
+
+ private final LauncherSupport launchManager;
+ private final InputStream sourceStream;
+ private final StreamType streamType;
+ private final Collection resultFormatters;
+ private volatile SysOutErrContentDeliverer contentDeliverer;
+
+ SysOutErrStreamReader(final LauncherSupport launchManager, final InputStream source, final StreamType streamType, final Collection resultFormatters) {
+ this.launchManager = launchManager;
+ 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) {
+ this.launchManager.log("Failed while streaming " + (this.streamType == StreamType.SYS_OUT ? "sysout" : "syserr") + " data",
+ e, Project.MSG_INFO);
+ } 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();
+ }
+ }
+ }
+
+ 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);
+ }
+ }
+
+}
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
index 6b50ce267..c24e87240 100644
--- a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/ListenerDefinition.java
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/ListenerDefinition.java
@@ -21,12 +21,24 @@ import org.apache.tools.ant.Project;
import org.apache.tools.ant.PropertyHelper;
import org.apache.tools.ant.types.EnumeratedAttribute;
+import javax.xml.stream.XMLStreamConstants;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+import javax.xml.stream.XMLStreamWriter;
+
+import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ATTR_CLASS_NAME;
+import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ATTR_LISTENER_RESULT_FILE;
+import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ATTR_SEND_SYS_ERR;
+import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ATTR_SEND_SYS_OUT;
+import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ELM_LISTENER;
+
/**
* 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";
@@ -135,4 +147,45 @@ public class ListenerDefinition {
}
}
+ void toForkedRepresentation(final XMLStreamWriter writer) throws XMLStreamException {
+ writer.writeStartElement(LD_XML_ELM_LISTENER);
+ writer.writeAttribute(LD_XML_ATTR_CLASS_NAME, this.className);
+ writer.writeAttribute(LD_XML_ATTR_SEND_SYS_ERR, Boolean.toString(this.sendSysErr));
+ writer.writeAttribute(LD_XML_ATTR_SEND_SYS_OUT, Boolean.toString(this.sendSysOut));
+ if (this.resultFile != null) {
+ writer.writeAttribute(LD_XML_ATTR_LISTENER_RESULT_FILE, this.resultFile);
+ }
+ writer.writeEndElement();
+ }
+
+ static ListenerDefinition fromForkedRepresentation(final XMLStreamReader reader) throws XMLStreamException {
+ reader.require(XMLStreamConstants.START_ELEMENT, null, LD_XML_ELM_LISTENER);
+ final ListenerDefinition listenerDef = new ListenerDefinition();
+ final String className = requireAttributeValue(reader, LD_XML_ATTR_CLASS_NAME);
+ listenerDef.setClassName(className);
+ final String sendSysErr = reader.getAttributeValue(null, LD_XML_ATTR_SEND_SYS_ERR);
+ if (sendSysErr != null) {
+ listenerDef.setSendSysErr(Boolean.parseBoolean(sendSysErr));
+ }
+ final String sendSysOut = reader.getAttributeValue(null, LD_XML_ATTR_SEND_SYS_OUT);
+ if (sendSysOut != null) {
+ listenerDef.setSendSysOut(Boolean.parseBoolean(sendSysOut));
+ }
+ final String resultFile = reader.getAttributeValue(null, LD_XML_ATTR_LISTENER_RESULT_FILE);
+ if (resultFile != null) {
+ listenerDef.setResultFile(resultFile);
+ }
+ reader.nextTag();
+ reader.require(XMLStreamConstants.END_ELEMENT, null, LD_XML_ELM_LISTENER);
+ return listenerDef;
+ }
+
+ private static String requireAttributeValue(final XMLStreamReader reader, final String attrName) throws XMLStreamException {
+ final String val = reader.getAttributeValue(null, attrName);
+ if (val != null) {
+ return val;
+ }
+ throw new XMLStreamException("Attribute " + attrName + " is missing at " + reader.getLocation());
+ }
+
}
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
index 01c23cb39..07039a6bd 100644
--- a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/NamedTest.java
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/NamedTest.java
@@ -23,7 +23,6 @@ package org.apache.tools.ant.taskdefs.optional.junitlauncher;
public interface NamedTest {
/**
- *
* @return Returns the name of the test
*/
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
index a950f85ca..3744a817e 100644
--- a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/SingleTestClass.java
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/SingleTestClass.java
@@ -17,17 +17,28 @@
*/
package org.apache.tools.ant.taskdefs.optional.junitlauncher;
-import org.apache.tools.ant.Project;
import org.junit.platform.engine.discovery.DiscoverySelectors;
import org.junit.platform.launcher.EngineFilter;
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
+import javax.xml.stream.XMLStreamConstants;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+import javax.xml.stream.XMLStreamWriter;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;
+import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ATTR_CLASS_NAME;
+import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ATTR_EXCLUDE_ENGINES;
+import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ATTR_HALT_ON_FAILURE;
+import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ATTR_INCLUDE_ENGINES;
+import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ATTR_METHODS;
+import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ATTR_OUTPUT_DIRECTORY;
+import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ELM_TEST;
+
/**
* Represents the single {@code test} (class) that's configured to be launched by the {@link JUnitLauncherTask}
*/
@@ -85,13 +96,7 @@ public class SingleTestClass extends TestDefinition implements NamedTest {
}
@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();
- }
+ List createTestRequests() {
final LauncherDiscoveryRequestBuilder requestBuilder = LauncherDiscoveryRequestBuilder.request();
if (!this.hasMethodsSpecified()) {
requestBuilder.selectors(DiscoverySelectors.selectClass(this.testClass));
@@ -112,4 +117,83 @@ public class SingleTestClass extends TestDefinition implements NamedTest {
}
return Collections.singletonList(new TestRequest(this, requestBuilder));
}
+
+ @Override
+ protected void toForkedRepresentation(final JUnitLauncherTask task, final XMLStreamWriter writer) throws XMLStreamException {
+ writer.writeStartElement(LD_XML_ELM_TEST);
+ writer.writeAttribute(LD_XML_ATTR_CLASS_NAME, testClass);
+ if (testMethods != null) {
+ final StringBuilder sb = new StringBuilder();
+ for (final String method : testMethods) {
+ if (sb.length() != 0) {
+ sb.append(",");
+ }
+ sb.append(method);
+ }
+ writer.writeAttribute(LD_XML_ATTR_METHODS, sb.toString());
+ }
+ if (haltOnFailure != null) {
+ writer.writeAttribute(LD_XML_ATTR_HALT_ON_FAILURE, haltOnFailure.toString());
+ }
+ if (outputDir != null) {
+ writer.writeAttribute(LD_XML_ATTR_OUTPUT_DIRECTORY, outputDir);
+ }
+ if (includeEngines != null) {
+ writer.writeAttribute(LD_XML_ATTR_INCLUDE_ENGINES, includeEngines);
+ }
+ if (excludeEngines != null) {
+ writer.writeAttribute(LD_XML_ATTR_EXCLUDE_ENGINES, excludeEngines);
+ }
+ // listeners for this test
+ if (listeners != null) {
+ for (final ListenerDefinition listenerDef : getListeners()) {
+ if (!listenerDef.shouldUse(task.getProject())) {
+ // not applicable
+ continue;
+ }
+ listenerDef.toForkedRepresentation(writer);
+ }
+ }
+ writer.writeEndElement();
+ }
+
+ static TestDefinition fromForkedRepresentation(final XMLStreamReader reader) throws XMLStreamException {
+ reader.require(XMLStreamConstants.START_ELEMENT, null, LD_XML_ELM_TEST);
+ final SingleTestClass testDefinition = new SingleTestClass();
+ final String testClassName = requireAttributeValue(reader, LD_XML_ATTR_CLASS_NAME);
+ testDefinition.setName(testClassName);
+ final String methodNames = reader.getAttributeValue(null, LD_XML_ATTR_METHODS);
+ if (methodNames != null) {
+ testDefinition.setMethods(methodNames);
+ }
+ final String halt = reader.getAttributeValue(null, LD_XML_ATTR_HALT_ON_FAILURE);
+ if (halt != null) {
+ testDefinition.setHaltOnFailure(Boolean.parseBoolean(halt));
+ }
+ final String outDir = reader.getAttributeValue(null, LD_XML_ATTR_OUTPUT_DIRECTORY);
+ if (outDir != null) {
+ testDefinition.setOutputDir(outDir);
+ }
+ final String includeEngs = reader.getAttributeValue(null, LD_XML_ATTR_INCLUDE_ENGINES);
+ if (includeEngs != null) {
+ testDefinition.setIncludeEngines(includeEngs);
+ }
+ final String excludeEngs = reader.getAttributeValue(null, LD_XML_ATTR_EXCLUDE_ENGINES);
+ if (excludeEngs != null) {
+ testDefinition.setExcludeEngines(excludeEngs);
+ }
+ while (reader.nextTag() != XMLStreamConstants.END_ELEMENT) {
+ reader.require(XMLStreamConstants.START_ELEMENT, null, Constants.LD_XML_ELM_LISTENER);
+ testDefinition.addConfiguredListener(ListenerDefinition.fromForkedRepresentation(reader));
+ }
+ return testDefinition;
+ }
+
+ private static String requireAttributeValue(final XMLStreamReader reader, final String attrName) throws XMLStreamException {
+ final String val = reader.getAttributeValue(null, attrName);
+ if (val != null) {
+ return val;
+ }
+ throw new XMLStreamException("Attribute " + attrName + " is missing at " + reader.getLocation());
+ }
}
diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/StandaloneLauncher.java b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/StandaloneLauncher.java
new file mode 100644
index 000000000..7e342cc80
--- /dev/null
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/StandaloneLauncher.java
@@ -0,0 +1,259 @@
+/*
+ * 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.apache.tools.ant.BuildException;
+import org.apache.tools.ant.Project;
+
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLStreamReader;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.Properties;
+
+import static javax.xml.stream.XMLStreamConstants.END_DOCUMENT;
+import static javax.xml.stream.XMLStreamConstants.END_ELEMENT;
+import static javax.xml.stream.XMLStreamConstants.START_DOCUMENT;
+import static javax.xml.stream.XMLStreamConstants.START_ELEMENT;
+import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ATTR_HALT_ON_FAILURE;
+import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ATTR_PRINT_SUMMARY;
+import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ELM_LAUNCH_DEF;
+import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ELM_LISTENER;
+import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ELM_TEST;
+import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ELM_TEST_CLASSES;
+
+/**
+ * Used for launching forked tests from the {@link JUnitLauncherTask}.
+ *
+ * Although this class is public, this isn't meant for external use. The contract of what
+ * program arguments {@link #main(String[]) the main method} accepts and how it interprets it,
+ * is also an internal detail and can change across Ant releases.
+ *
+ * @since Ant 1.10.6
+ */
+public class StandaloneLauncher {
+
+ /**
+ * Entry point to launching the forked test.
+ *
+ * @param args The arguments passed to this program for launching the tests
+ * @throws Exception
+ */
+ public static void main(final String[] args) throws Exception {
+ // The main responsibility of this entry point is to create a LaunchDefinition,
+ // by parsing the passed arguments and then use the LauncherSupport to
+ // LauncherSupport#launch the tests
+ try {
+ ForkedLaunch launchDefinition = null;
+ final ForkedExecution forkedExecution = new ForkedExecution();
+ for (int i = 0; i < args.length; ) {
+ final String arg = args[i];
+ int numArgsConsumed = 1;
+ switch (arg) {
+ case Constants.ARG_PROPERTIES: {
+ final Path propsPath = Paths.get(args[i + 1]);
+ if (!Files.isRegularFile(propsPath)) {
+ throw new IllegalArgumentException(propsPath + " does not point to a properties file");
+ }
+ final Properties properties = new Properties();
+ try (final InputStream is = Files.newInputStream(propsPath)) {
+ properties.load(is);
+ }
+ forkedExecution.setProperties(properties);
+ numArgsConsumed = 2;
+ break;
+ }
+ case Constants.ARG_LAUNCH_DEFINITION: {
+ final Path launchDefXmlPath = Paths.get(args[i + 1]);
+ if (!Files.isRegularFile(launchDefXmlPath)) {
+ throw new IllegalArgumentException(launchDefXmlPath + " does not point to a launch definition file");
+ }
+ launchDefinition = parseLaunchDefinition(launchDefXmlPath);
+ numArgsConsumed = 2;
+ break;
+ }
+ }
+ i = i + numArgsConsumed;
+ }
+
+
+ launchDefinition.setTestExecutionContext(forkedExecution);
+ final LauncherSupport launcherSupport = new LauncherSupport(launchDefinition);
+ try {
+ launcherSupport.launch();
+ } catch (Throwable t) {
+ if (launcherSupport.hasTestFailures()) {
+ System.exit(Constants.FORK_EXIT_CODE_TESTS_FAILED);
+ throw t;
+ }
+ }
+ if (launcherSupport.hasTestFailures()) {
+ System.exit(Constants.FORK_EXIT_CODE_TESTS_FAILED);
+ return;
+ }
+ System.exit(Constants.FORK_EXIT_CODE_SUCCESS);
+ return;
+ } catch (Throwable t) {
+ t.printStackTrace();
+ throw t;
+ }
+ }
+
+ private static ForkedLaunch parseLaunchDefinition(final Path pathToLaunchDefXml) {
+ if (pathToLaunchDefXml == null || !Files.isRegularFile(pathToLaunchDefXml)) {
+ throw new IllegalArgumentException(pathToLaunchDefXml + " is not a file");
+ }
+ final ForkedLaunch forkedLaunch = new ForkedLaunch();
+ try (final InputStream is = Files.newInputStream(pathToLaunchDefXml)) {
+ final XMLStreamReader reader = XMLInputFactory.newFactory().createXMLStreamReader(is);
+ reader.require(START_DOCUMENT, null, null);
+ reader.nextTag();
+ reader.require(START_ELEMENT, null, LD_XML_ELM_LAUNCH_DEF);
+ final String haltOnfFailure = reader.getAttributeValue(null, LD_XML_ATTR_HALT_ON_FAILURE);
+ if (haltOnfFailure != null) {
+ forkedLaunch.setHaltOnFailure(Boolean.parseBoolean(haltOnfFailure));
+ }
+ final String printSummary = reader.getAttributeValue(null, LD_XML_ATTR_PRINT_SUMMARY);
+ if (printSummary != null) {
+ forkedLaunch.setPrintSummary(Boolean.parseBoolean(printSummary));
+ }
+ if (haltOnfFailure != null) {
+ forkedLaunch.setHaltOnFailure(Boolean.parseBoolean(haltOnfFailure));
+ }
+ reader.nextTag();
+ reader.require(START_ELEMENT, null, null);
+ final String elementName = reader.getLocalName();
+ switch (elementName) {
+ case LD_XML_ELM_TEST: {
+ forkedLaunch.addTests(Collections.singletonList(SingleTestClass.fromForkedRepresentation(reader)));
+ break;
+ }
+ case LD_XML_ELM_TEST_CLASSES: {
+ forkedLaunch.addTests(TestClasses.fromForkedRepresentation(reader));
+ break;
+ }
+ case LD_XML_ELM_LISTENER: {
+ forkedLaunch.addListener(ListenerDefinition.fromForkedRepresentation(reader));
+ break;
+ }
+ }
+ reader.nextTag();
+ reader.require(END_ELEMENT, null, LD_XML_ELM_LAUNCH_DEF);
+ reader.next();
+ reader.require(END_DOCUMENT, null, null);
+ return forkedLaunch;
+ } catch (Exception e) {
+ throw new BuildException("Failed to construct definition from forked representation", e);
+ }
+ }
+
+
+ private static final class ForkedExecution implements TestExecutionContext {
+ private Properties properties = new Properties();
+
+ private ForkedExecution() {
+ }
+
+ private ForkedExecution setProperties(final Properties properties) {
+ this.properties = properties;
+ return this;
+ }
+
+ @Override
+ public Properties getProperties() {
+ return this.properties;
+ }
+
+ @Override
+ public Optional getProject() {
+ // forked execution won't have access to the Ant Project
+ return Optional.empty();
+ }
+ }
+
+ private static final class ForkedLaunch implements LaunchDefinition {
+
+ private boolean printSummary;
+ private boolean haltOnFailure;
+ private TestExecutionContext testExecutionContext;
+ private List tests = new ArrayList<>();
+ private List listeners = new ArrayList<>();
+
+ @Override
+ public List getTests() {
+ return this.tests;
+ }
+
+ ForkedLaunch addTests(final List tests) {
+ this.tests.addAll(tests);
+ return this;
+ }
+
+ @Override
+ public List getListeners() {
+ return this.listeners;
+ }
+
+ ForkedLaunch addListener(final ListenerDefinition listener) {
+ this.listeners.add(listener);
+ return this;
+ }
+
+ @Override
+ public boolean isPrintSummary() {
+ return this.printSummary;
+ }
+
+ private ForkedLaunch setPrintSummary(final boolean printSummary) {
+ this.printSummary = printSummary;
+ return this;
+ }
+
+ @Override
+ public boolean isHaltOnFailure() {
+ return this.haltOnFailure;
+ }
+
+ public ForkedLaunch setHaltOnFailure(final boolean haltOnFailure) {
+ this.haltOnFailure = haltOnFailure;
+ return this;
+ }
+
+ public ForkedLaunch setTestExecutionContext(final TestExecutionContext testExecutionContext) {
+ this.testExecutionContext = testExecutionContext;
+ return this;
+ }
+
+ @Override
+ public ClassLoader getClassLoader() {
+ return this.getClass().getClassLoader();
+ }
+
+ @Override
+ public TestExecutionContext getTestExecutionContext() {
+ return this.testExecutionContext;
+ }
+ }
+}
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
index 45d8bf056..cbba48497 100644
--- a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestClasses.java
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestClasses.java
@@ -21,11 +21,17 @@ import org.apache.tools.ant.types.Resource;
import org.apache.tools.ant.types.ResourceCollection;
import org.apache.tools.ant.types.resources.Resources;
+import javax.xml.stream.XMLStreamConstants;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+import javax.xml.stream.XMLStreamWriter;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ELM_TEST_CLASSES;
+
/**
* Represents a {@code testclasses} that's configured to be launched by the {@link JUnitLauncherTask}
*/
@@ -42,14 +48,14 @@ public class TestClasses extends TestDefinition {
}
@Override
- List createTestRequests(final JUnitLauncherTask launcherTask) {
+ List createTestRequests() {
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));
+ requests.addAll(test.createTestRequests());
}
return requests;
}
@@ -126,4 +132,26 @@ public class TestClasses extends TestDefinition {
return TestClasses.this.getExcludeEngines();
}
}
+
+ @Override
+ protected void toForkedRepresentation(final JUnitLauncherTask task, final XMLStreamWriter writer) throws XMLStreamException {
+ writer.writeStartElement(LD_XML_ELM_TEST_CLASSES);
+ // write out as multiple SingleTestClass representations
+ for (final SingleTestClass singleTestClass : getTests()) {
+ singleTestClass.toForkedRepresentation(task, writer);
+ }
+ writer.writeEndElement();
+ }
+
+ static List fromForkedRepresentation(final XMLStreamReader reader) throws XMLStreamException {
+ reader.require(XMLStreamConstants.START_ELEMENT, null, LD_XML_ELM_TEST_CLASSES);
+ final List testDefinitions = new ArrayList<>();
+ // read out as multiple SingleTestClass representations
+ while (reader.nextTag() != XMLStreamConstants.END_ELEMENT) {
+ reader.require(XMLStreamConstants.START_ELEMENT, null, Constants.LD_XML_ELM_TEST);
+ testDefinitions.add(SingleTestClass.fromForkedRepresentation(reader));
+ }
+ reader.require(XMLStreamConstants.END_ELEMENT, null, LD_XML_ELM_TEST_CLASSES);
+ return testDefinitions;
+ }
}
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
index 7ca449952..9fe452f17 100644
--- a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestDefinition.java
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestDefinition.java
@@ -17,9 +17,12 @@
*/
package org.apache.tools.ant.taskdefs.optional.junitlauncher;
+import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.PropertyHelper;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -28,6 +31,7 @@ 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;
@@ -35,6 +39,7 @@ abstract class TestDefinition {
protected String outputDir;
protected String includeEngines;
protected String excludeEngines;
+ protected ForkDefinition forkDefinition;
protected List listeners = new ArrayList<>();
@@ -90,7 +95,19 @@ abstract class TestDefinition {
return this.outputDir;
}
- abstract List createTestRequests(final JUnitLauncherTask launcherTask);
+ public ForkDefinition createFork() {
+ if (this.forkDefinition != null) {
+ throw new BuildException("Test definition cannot have more than one fork elements");
+ }
+ this.forkDefinition = new ForkDefinition();
+ return this.forkDefinition;
+ }
+
+ ForkDefinition getForkDefinition() {
+ return this.forkDefinition;
+ }
+
+ abstract List createTestRequests();
protected boolean shouldRun(final Project project) {
final PropertyHelper propertyHelper = PropertyHelper.getPropertyHelper(project);
@@ -127,4 +144,7 @@ abstract class TestDefinition {
}
return parts.toArray(new String[parts.size()]);
}
+
+ protected abstract void toForkedRepresentation(JUnitLauncherTask task, XMLStreamWriter writer) throws XMLStreamException;
+
}
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
index 90754b578..c17ca57b3 100644
--- 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
@@ -125,4 +125,13 @@ public class JUnitLauncherTaskTest {
public void testTestClasses() {
buildRule.executeTarget("test-batch");
}
+
+ /**
+ * Tests the execution of a forked test
+ */
+ @Test
+ public void testBasicFork() {
+ buildRule.executeTarget("test-basic-fork");
+
+ }
}