You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@ant.apache.org by ja...@apache.org on 2018/08/10 04:49:48 UTC

[2/2] ant git commit: Support for fork mode in junitlauncher

Support for fork mode in junitlauncher


Project: http://git-wip-us.apache.org/repos/asf/ant/repo
Commit: http://git-wip-us.apache.org/repos/asf/ant/commit/c9ca84fd
Tree: http://git-wip-us.apache.org/repos/asf/ant/tree/c9ca84fd
Diff: http://git-wip-us.apache.org/repos/asf/ant/diff/c9ca84fd

Branch: refs/heads/master
Commit: c9ca84fd5301aee6d0f58ef0a0907c94ea0cf38b
Parents: 3f36f0b
Author: Jaikiran Pai <ja...@apache.org>
Authored: Wed Jul 25 19:23:00 2018 +0530
Committer: Jaikiran Pai <ja...@apache.org>
Committed: Fri Aug 10 10:18:36 2018 +0530

----------------------------------------------------------------------
 .../taskdefs/optional/junitlauncher.xml         |  12 +
 .../optional/junitlauncher/Constants.java       |  54 ++
 .../optional/junitlauncher/ForkDefinition.java  | 156 ++++++
 .../junitlauncher/JUnitLauncherTask.java        | 552 ++++++-------------
 .../junitlauncher/LaunchDefinition.java         |  75 +++
 .../optional/junitlauncher/LauncherSupport.java | 513 +++++++++++++++++
 .../junitlauncher/ListenerDefinition.java       |  53 ++
 .../optional/junitlauncher/NamedTest.java       |   1 -
 .../optional/junitlauncher/SingleTestClass.java | 100 +++-
 .../junitlauncher/StandaloneLauncher.java       | 259 +++++++++
 .../optional/junitlauncher/TestClasses.java     |  32 +-
 .../optional/junitlauncher/TestDefinition.java  |  22 +-
 .../junitlauncher/JUnitLauncherTaskTest.java    |   9 +
 13 files changed, 1457 insertions(+), 381 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ant/blob/c9ca84fd/src/etc/testcases/taskdefs/optional/junitlauncher.xml
----------------------------------------------------------------------
diff --git a/src/etc/testcases/taskdefs/optional/junitlauncher.xml b/src/etc/testcases/taskdefs/optional/junitlauncher.xml
index ccae7ae..81861a7 100644
--- a/src/etc/testcases/taskdefs/optional/junitlauncher.xml
+++ b/src/etc/testcases/taskdefs/optional/junitlauncher.xml
@@ -30,6 +30,8 @@
 
     <path id="junit.engine.vintage.classpath">
         <fileset dir="../../../../../lib/optional" includes="junit-vintage-engine*.jar"/>
+        <fileset dir="../../../../../lib/optional" includes="junit-*.jar"/>
+        <fileset dir="../../../../../lib/optional" includes="hamcrest*.jar"/>
     </path>
 
     <path id="junit.engine.jupiter.classpath">
@@ -109,5 +111,15 @@
             </testclasses>
         </junitlauncher>
     </target>
+
+    <target name="test-basic-fork" depends="init">
+        <junitlauncher>
+            <classpath refid="test.classpath"/>
+            <test name="org.example.junitlauncher.vintage.JUnit4SampleTest" outputdir="${output.dir}">
+                <fork dir="${basedir}"/>
+                <listener type="legacy-xml" sendSysErr="true" sendSysOut="true"/>
+            </test>
+        </junitlauncher>
+    </target>
 </project>
 

http://git-wip-us.apache.org/repos/asf/ant/blob/c9ca84fd/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/Constants.java
----------------------------------------------------------------------
diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/Constants.java b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/Constants.java
new file mode 100644
index 0000000..a8b501c
--- /dev/null
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/Constants.java
@@ -0,0 +1,54 @@
+/*
+ *  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;
+
+/**
+ * Constants used within the junitlauncher task
+ */
+final class Constants {
+
+    static final int FORK_EXIT_CODE_SUCCESS = 0;
+    static final int FORK_EXIT_CODE_EXCEPTION = 1;
+    static final int FORK_EXIT_CODE_TESTS_FAILED = 2;
+    static final int FORK_EXIT_CODE_TIMED_OUT = 3;
+
+    static final String ARG_PROPERTIES = "--properties";
+    static final String ARG_LAUNCH_DEFINITION = "--launch-definition";
+
+
+    static final String LD_XML_ELM_LAUNCH_DEF = "launch-definition";
+    static final String LD_XML_ELM_TEST = "test";
+    static final String LD_XML_ELM_TEST_CLASSES = "test-classes";
+    static final String LD_XML_ATTR_HALT_ON_FAILURE = "haltOnFailure";
+    static final String LD_XML_ATTR_OUTPUT_DIRECTORY = "outDir";
+    static final String LD_XML_ATTR_INCLUDE_ENGINES = "includeEngines";
+    static final String LD_XML_ATTR_EXCLUDE_ENGINES = "excludeEngines";
+    static final String LD_XML_ATTR_CLASS_NAME = "classname";
+    static final String LD_XML_ATTR_METHODS = "methods";
+    static final String LD_XML_ATTR_PRINT_SUMMARY = "printSummary";
+    static final String LD_XML_ELM_LISTENER = "listener";
+    static final String LD_XML_ATTR_SEND_SYS_ERR = "sendSysErr";
+    static final String LD_XML_ATTR_SEND_SYS_OUT = "sendSysOut";
+    static final String LD_XML_ATTR_LISTENER_RESULT_FILE = "resultFile";
+
+
+    private Constants() {
+
+    }
+}

http://git-wip-us.apache.org/repos/asf/ant/blob/c9ca84fd/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/ForkDefinition.java
----------------------------------------------------------------------
diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/ForkDefinition.java b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/ForkDefinition.java
new file mode 100644
index 0000000..bda3381
--- /dev/null
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/ForkDefinition.java
@@ -0,0 +1,156 @@
+/*
+ *  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.Task;
+import org.apache.tools.ant.launch.AntMain;
+import org.apache.tools.ant.types.Commandline;
+import org.apache.tools.ant.types.CommandlineJava;
+import org.apache.tools.ant.types.Environment;
+import org.apache.tools.ant.types.Path;
+import org.apache.tools.ant.types.PropertySet;
+import org.apache.tools.ant.util.LoaderUtils;
+import org.junit.platform.commons.annotation.Testable;
+import org.junit.platform.engine.TestEngine;
+import org.junit.platform.launcher.core.LauncherFactory;
+
+import java.io.File;
+
+/**
+ * Represents the {@code fork} element within test definitions of the
+ * {@code junitlauncher} task
+ */
+public class ForkDefinition {
+
+    private boolean includeAntRuntimeLibraries = true;
+    private boolean includeJunitPlatformLibraries = true;
+
+    private final CommandlineJava commandLineJava;
+    private final Environment env = new Environment();
+
+    private String dir;
+    private long timeout = -1;
+
+    ForkDefinition() {
+        this.commandLineJava = new CommandlineJava();
+    }
+
+    public void setDir(final String dir) {
+        this.dir = dir;
+    }
+
+    String getDir() {
+        return this.dir;
+    }
+
+    public void setTimeout(final long timeout) {
+        this.timeout = timeout;
+    }
+
+    long getTimeout() {
+        return this.timeout;
+    }
+
+    public Commandline.Argument createJvmArg() {
+        return this.commandLineJava.createVmArgument();
+    }
+
+    public void addConfiguredSysProperty(final Environment.Variable sysProp) {
+        // validate that key/value are present
+        sysProp.validate();
+        this.commandLineJava.addSysproperty(sysProp);
+    }
+
+    public void addConfiguredSysPropertySet(final PropertySet propertySet) {
+        this.commandLineJava.addSyspropertyset(propertySet);
+    }
+
+    public void addConfiguredEnv(final Environment.Variable var) {
+        this.env.addVariable(var);
+    }
+
+    public void addConfiguredModulePath(final Path modulePath) {
+        this.commandLineJava.createModulepath(modulePath.getProject()).add(modulePath);
+    }
+
+    public void addConfiguredUpgradeModulePath(final Path upgradeModulePath) {
+        this.commandLineJava.createUpgrademodulepath(upgradeModulePath.getProject()).add(upgradeModulePath);
+    }
+
+    Environment getEnv() {
+        return this.env;
+    }
+
+    /**
+     * Generates a new {@link CommandlineJava} constructed out of the configurations set on this
+     * {@link ForkDefinition}
+     *
+     * @param task The junitlaunchertask for which this is a fork definition
+     * @return
+     */
+    CommandlineJava generateCommandLine(final JUnitLauncherTask task) {
+        final CommandlineJava cmdLine;
+        try {
+            cmdLine = (CommandlineJava) this.commandLineJava.clone();
+        } catch (CloneNotSupportedException e) {
+            throw new BuildException(e);
+        }
+        cmdLine.setClassname(StandaloneLauncher.class.getName());
+        // VM arguments
+        final Project project = task.getProject();
+        final Path antRuntimeResourceSources = new Path(project);
+        if (this.includeAntRuntimeLibraries) {
+            addAntRuntimeResourceSource(antRuntimeResourceSources, task, toResourceName(AntMain.class));
+            addAntRuntimeResourceSource(antRuntimeResourceSources, task, toResourceName(Task.class));
+            addAntRuntimeResourceSource(antRuntimeResourceSources, task, toResourceName(JUnitLauncherTask.class));
+        }
+
+        if (this.includeJunitPlatformLibraries) {
+            // platform-engine
+            addAntRuntimeResourceSource(antRuntimeResourceSources, task, toResourceName(TestEngine.class));
+            // platform-launcher
+            addAntRuntimeResourceSource(antRuntimeResourceSources, task, toResourceName(LauncherFactory.class));
+            // platform-commons
+            addAntRuntimeResourceSource(antRuntimeResourceSources, task, toResourceName(Testable.class));
+        }
+        final Path classPath = cmdLine.createClasspath(project);
+        classPath.createPath().append(antRuntimeResourceSources);
+
+        return cmdLine;
+    }
+
+    private static boolean addAntRuntimeResourceSource(final Path path, final JUnitLauncherTask task, final String resource) {
+        final File f = LoaderUtils.getResourceSource(task.getClass().getClassLoader(), resource);
+        if (f == null) {
+            task.log("Could not locate source of resource " + resource);
+            return false;
+        }
+        task.log("Found source " + f + " of resource " + resource);
+        path.createPath().setLocation(f);
+        return true;
+    }
+
+    private static String toResourceName(final Class klass) {
+        final String name = klass.getName();
+        return name.replaceAll("\\.", "/") + ".class";
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/ant/blob/c9ca84fd/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/JUnitLauncherTask.java
----------------------------------------------------------------------
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
index a6423ca..a328e4b 100644
--- a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/JUnitLauncherTask.java
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/JUnitLauncherTask.java
@@ -21,37 +21,32 @@ 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.taskdefs.Execute;
+import org.apache.tools.ant.taskdefs.ExecuteWatchdog;
+import org.apache.tools.ant.taskdefs.LogOutputStream;
+import org.apache.tools.ant.taskdefs.PumpStreamHandler;
+import org.apache.tools.ant.types.CommandlineJava;
+import org.apache.tools.ant.types.Environment;
 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 javax.xml.stream.XMLOutputFactory;
+import javax.xml.stream.XMLStreamWriter;
 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.charset.StandardCharsets;
 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.Hashtable;
 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;
+import java.util.concurrent.TimeoutException;
+
+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;
 
 /**
  * An Ant {@link Task} responsible for launching the JUnit platform for running tests.
@@ -84,55 +79,22 @@ public class JUnitLauncherTask extends Task {
 
     @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<TestRequest> requests = buildTestRequests();
-            for (final TestRequest testRequest : requests) {
-                try {
-                    final TestDefinition test = testRequest.getOwner();
-                    final LauncherDiscoveryRequest request = testRequest.getDiscoveryRequest().build();
-                    final List<TestExecutionListener> 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);
-                    }
-                }
+        if (this.tests.isEmpty()) {
+            return;
+        }
+        final Project project = getProject();
+        for (final TestDefinition test : this.tests) {
+            if (!test.shouldRun(project)) {
+                log("Excluding test " + test + " since it's considered not to run " +
+                        "in context of project " + project, Project.MSG_DEBUG);
+                continue;
+            }
+            if (test.getForkDefinition() != null) {
+                forkTest(test);
+            } else {
+                final LauncherSupport launcherSupport = new LauncherSupport(new InVMLaunch(Collections.singletonList(test)));
+                launcherSupport.launch();
             }
-        } finally {
-            Thread.currentThread().setContextClassLoader(previousClassLoader);
         }
     }
 
@@ -204,360 +166,212 @@ public class JUnitLauncherTask extends Task {
         }
     }
 
-    private List<TestRequest> buildTestRequests() {
-        if (this.tests.isEmpty()) {
-            return Collections.emptyList();
-        }
-        final List<TestRequest> requests = new ArrayList<>();
-        for (final TestDefinition test : this.tests) {
-            final List<TestRequest> testRequests = test.createTestRequests(this);
-            if (testRequests == null || testRequests.isEmpty()) {
-                continue;
-            }
-            requests.addAll(testRequests);
+    private ClassLoader createClassLoaderForTestExecution() {
+        if (this.classPath == null) {
+            return this.getClass().getClassLoader();
         }
-        return requests;
+        return new AntClassLoader(this.getClass().getClassLoader(), getProject(), this.classPath, true);
     }
 
-    private List<TestExecutionListener> getListeners(final TestRequest testRequest, final ClassLoader classLoader) {
-        final TestDefinition test = testRequest.getOwner();
-        final List<ListenerDefinition> applicableListenerElements = test.getListeners().isEmpty() ? this.listeners : test.getListeners();
-        final List<TestExecutionListener> 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 java.nio.file.Path dumpProjectProperties() throws IOException {
+        final java.nio.file.Path propsPath = Files.createTempFile(null, "properties");
+        propsPath.toFile().deleteOnExit();
+        final Hashtable<String, Object> props = this.getProject().getProperties();
+        final Properties projProperties = new Properties();
+        projProperties.putAll(props);
+        try (final OutputStream os = Files.newOutputStream(propsPath)) {
+            // TODO: Is it always UTF-8?
+            projProperties.store(os, StandardCharsets.UTF_8.name());
         }
+        return propsPath;
     }
 
-    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");
+    private void forkTest(final TestDefinition test) {
+        // create launch command
+        final ForkDefinition forkDefinition = test.getForkDefinition();
+        final CommandlineJava commandlineJava = forkDefinition.generateCommandLine(this);
+        if (this.classPath != null) {
+            commandlineJava.createClasspath(getProject()).createPath().append(this.classPath);
         }
-        final Class<?> klass;
+        final java.nio.file.Path projectPropsPath;
         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);
+            projectPropsPath = dumpProjectProperties();
+        } catch (IOException e) {
+            throw new BuildException("Could not create the necessary properties file while forking a process" +
+                    " for a test", e);
         }
-    }
+        // --properties <path-to-properties-file>
+        commandlineJava.createArgument().setValue(Constants.ARG_PROPERTIES);
+        commandlineJava.createArgument().setValue(projectPropsPath.toAbsolutePath().toString());
 
-    private void handleTestExecutionCompletion(final TestDefinition test, final TestExecutionSummary summary) {
-        if (printSummary) {
-            // print the summary to System.out
-            summary.printTo(new PrintWriter(System.out, true));
-        }
-        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)";
+        final java.nio.file.Path launchDefXmlPath = newLaunchDefinitionXml();
+        try (final OutputStream os = Files.newOutputStream(launchDefXmlPath)) {
+            final XMLStreamWriter writer = XMLOutputFactory.newFactory().createXMLStreamWriter(os, "UTF-8");
+            try {
+                writer.writeStartDocument();
+                writer.writeStartElement(LD_XML_ELM_LAUNCH_DEF);
+                if (this.printSummary) {
+                    writer.writeAttribute(LD_XML_ATTR_PRINT_SUMMARY, "true");
                 }
-                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);
-    }
-
-    @SuppressWarnings("resource")
-    private Optional<SwitchedStreamHandle> trySwitchSysOutErr(final TestRequest testRequest, final StreamType streamType) {
-        switch (streamType) {
-            case SYS_OUT: {
-                if (!testRequest.interestedInSysOut()) {
-                    return Optional.empty();
+                if (this.haltOnFailure) {
+                    writer.writeAttribute(LD_XML_ATTR_HALT_ON_FAILURE, "true");
                 }
-                break;
-            }
-            case SYS_ERR: {
-                if (!testRequest.interestedInSysErr()) {
-                    return Optional.empty();
+                // task level listeners
+                for (final ListenerDefinition listenerDef : this.listeners) {
+                    if (!listenerDef.shouldUse(getProject())) {
+                        continue;
+                    }
+                    // construct the listener definition argument
+                    listenerDef.toForkedRepresentation(writer);
                 }
-                break;
-            }
-            default: {
-                // unknown, but no need to error out, just be lenient
-                // and return back
-                return Optional.empty();
+                // test definition as XML
+                test.toForkedRepresentation(this, writer);
+                writer.writeEndElement();
+                writer.writeEndDocument();
+            } finally {
+                writer.close();
             }
+        } catch (Exception e) {
+            throw new BuildException("Failed to construct command line for test", e);
         }
-        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();
+        // --launch-definition <xml-file-path>
+        commandlineJava.createArgument().setValue(Constants.ARG_LAUNCH_DEFINITION);
+        commandlineJava.createArgument().setValue(launchDefXmlPath.toAbsolutePath().toString());
+
+        // launch the process and wait for process to complete
+        final int exitCode = executeForkedTest(forkDefinition, commandlineJava);
+        switch (exitCode) {
+            case Constants.FORK_EXIT_CODE_SUCCESS: {
+                // success
                 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();
+            case Constants.FORK_EXIT_CODE_EXCEPTION: {
+                // process failed with some exception
+                throw new BuildException("Forked test(s) failed with an exception");
+            }
+            case Constants.FORK_EXIT_CODE_TESTS_FAILED: {
+                // test has failure(s)
+                try {
+                    if (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
+                        this.getProject().setNewProperty(test.getFailureProperty(), "true");
+                    }
+                } finally {
+                    if (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 failure(s)";
+                        } else {
+                            errorMessage = "Some test(s) have failure(s)";
+                        }
+                        throw new BuildException(errorMessage);
+                    }
+                }
                 break;
             }
-            default: {
-                return Optional.empty();
+            case Constants.FORK_EXIT_CODE_TIMED_OUT: {
+                throw new BuildException(new TimeoutException("Forked test(s) timed out"));
             }
         }
-        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<TestResultFormatter> resultFormatters;
-        private volatile SysOutErrContentDeliverer contentDeliverer;
-
-        SysOutErrStreamReader(final JUnitLauncherTask task, final InputStream source, final StreamType streamType, final Collection<TestResultFormatter> resultFormatters) {
-            this.task = task;
-            this.sourceStream = source;
-            this.streamType = streamType;
-            this.resultFormatters = resultFormatters;
+    private int executeForkedTest(final ForkDefinition forkDefinition, final CommandlineJava commandlineJava) {
+        final LogOutputStream outStream = new LogOutputStream(this, Project.MSG_INFO);
+        final LogOutputStream errStream = new LogOutputStream(this, Project.MSG_WARN);
+        final ExecuteWatchdog watchdog = forkDefinition.getTimeout() > 0 ? new ExecuteWatchdog(forkDefinition.getTimeout()) : null;
+        final Execute execute = new Execute(new PumpStreamHandler(outStream, errStream), watchdog);
+        execute.setCommandline(commandlineJava.getCommandline());
+        execute.setAntRun(getProject());
+        if (forkDefinition.getDir() != null) {
+            execute.setWorkingDirectory(Paths.get(forkDefinition.getDir()).toFile());
         }
+        final Environment env = forkDefinition.getEnv();
+        if (env != null && env.getVariables() != null) {
+            execute.setEnvironment(env.getVariables());
+        }
+        log(commandlineJava.describeCommand(), Project.MSG_VERBOSE);
+        int exitCode;
+        try {
+            exitCode = execute.execute();
+        } catch (IOException e) {
+            throw new BuildException("Process fork failed", e, getLocation());
+        }
+        return (watchdog != null && watchdog.killedProcess()) ? Constants.FORK_EXIT_CODE_TIMED_OUT : exitCode;
+    }
 
-        @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);
-            } 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 java.nio.file.Path newLaunchDefinitionXml() {
+        final java.nio.file.Path xmlFilePath;
+        try {
+            xmlFilePath = Files.createTempFile(null, ".xml");
+        } catch (IOException e) {
+            throw new BuildException("Failed to construct command line for test", e);
         }
+        xmlFilePath.toFile().deleteOnExit();
+        return xmlFilePath;
     }
 
-    private static final class SysOutErrContentDeliverer implements Runnable {
-        private volatile boolean stop;
-        private final Collection<TestResultFormatter> resultFormatters;
-        private final StreamType streamType;
-        private final BlockingQueue<byte[]> availableData = new LinkedBlockingQueue<>();
-        private final CountDownLatch completionLatch = new CountDownLatch(1);
+    private final class InVMExecution implements TestExecutionContext {
+
+        private final Properties props;
 
-        SysOutErrContentDeliverer(final StreamType streamType, final Collection<TestResultFormatter> resultFormatters) {
-            this.streamType = streamType;
-            this.resultFormatters = resultFormatters;
+        InVMExecution() {
+            this.props = new Properties();
+            this.props.putAll(JUnitLauncherTask.this.getProject().getProperties());
         }
 
         @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<byte[]> remaining = new ArrayList<>();
-                this.availableData.drainTo(remaining);
-                if (!remaining.isEmpty()) {
-                    for (final byte[] data : remaining) {
-                        deliver(data);
-                    }
-                }
-            } finally {
-                this.completionLatch.countDown();
-            }
+        public Properties getProperties() {
+            return this.props;
         }
 
-        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;
-                    }
-                }
-            }
+        @Override
+        public Optional<Project> getProject() {
+            return Optional.of(JUnitLauncherTask.this.getProject());
         }
     }
 
-    private final class SwitchedStreamHandle {
-        private final PipedOutputStream outputStream;
-        private final SysOutErrStreamReader streamReader;
+    private final class InVMLaunch implements LaunchDefinition {
 
-        SwitchedStreamHandle(final PipedOutputStream outputStream, final SysOutErrStreamReader streamReader) {
-            this.streamReader = streamReader;
-            this.outputStream = outputStream;
-        }
-    }
+        private final TestExecutionContext testExecutionContext = new InVMExecution();
+        private final List<TestDefinition> inVMTests;
+        private final ClassLoader executionCL;
 
-    private final class Listener extends SummaryGeneratingListener {
-        private Optional<SwitchedStreamHandle> switchedSysOutHandle;
-        private Optional<SwitchedStreamHandle> switchedSysErrHandle;
+        private InVMLaunch(final List<TestDefinition> inVMTests) {
+            this.inVMTests = inVMTests;
+            this.executionCL = createClassLoaderForTestExecution();
+        }
 
         @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();
-                }
-            }
+        public List<TestDefinition> getTests() {
+            return this.inVMTests;
         }
 
-        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);
+        @Override
+        public List<ListenerDefinition> getListeners() {
+            return listeners;
         }
-    }
 
-    private final class InVMExecution implements TestExecutionContext {
-
-        private final Properties props;
+        @Override
+        public boolean isPrintSummary() {
+            return printSummary;
+        }
 
-        InVMExecution() {
-            this.props = new Properties();
-            this.props.putAll(JUnitLauncherTask.this.getProject().getProperties());
+        @Override
+        public boolean isHaltOnFailure() {
+            return haltOnFailure;
         }
 
         @Override
-        public Properties getProperties() {
-            return this.props;
+        public ClassLoader getClassLoader() {
+            return this.executionCL;
         }
 
         @Override
-        public Optional<Project> getProject() {
-            return Optional.of(JUnitLauncherTask.this.getProject());
+        public TestExecutionContext getTestExecutionContext() {
+            return this.testExecutionContext;
         }
     }
 }

http://git-wip-us.apache.org/repos/asf/ant/blob/c9ca84fd/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LaunchDefinition.java
----------------------------------------------------------------------
diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LaunchDefinition.java b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LaunchDefinition.java
new file mode 100644
index 0000000..d3e5ae3
--- /dev/null
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LaunchDefinition.java
@@ -0,0 +1,75 @@
+/*
+ *  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 java.util.List;
+
+/**
+ * Defines the necessary context for launching the JUnit platform for running
+ * tests.
+ */
+public interface LaunchDefinition {
+
+    /**
+     * Returns the {@link TestDefinition tests} that have to be launched
+     *
+     * @return
+     */
+    List<TestDefinition> getTests();
+
+    /**
+     * Returns the default {@link ListenerDefinition listeners} that will be used
+     * for the tests, if the {@link #getTests() tests} themselves don't specify any
+     *
+     * @return
+     */
+    List<ListenerDefinition> 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();
+}

http://git-wip-us.apache.org/repos/asf/ant/blob/c9ca84fd/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LauncherSupport.java
----------------------------------------------------------------------
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 0000000..6a8027b
--- /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.
+ * <p>
+ * This class relies on a {@link LaunchDefinition} for setting up the launch of the
+ * JUnit platform.
+ * <p>
+ * 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.
+ * <p>
+ * 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<TestRequest> requests = buildTestRequests();
+            for (final TestRequest testRequest : requests) {
+                try {
+                    final TestDefinition test = testRequest.getOwner();
+                    final LauncherDiscoveryRequest request = testRequest.getDiscoveryRequest().build();
+                    final List<TestExecutionListener> 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<TestRequest> buildTestRequests() {
+        final List<TestDefinition> tests = this.launchDefinition.getTests();
+        if (tests.isEmpty()) {
+            return Collections.emptyList();
+        }
+        final List<TestRequest> requests = new ArrayList<>();
+        for (final TestDefinition test : tests) {
+            final List<TestRequest> testRequests = test.createTestRequests();
+            if (testRequests == null || testRequests.isEmpty()) {
+                continue;
+            }
+            requests.addAll(testRequests);
+        }
+        return requests;
+    }
+
+    private List<TestExecutionListener> getListeners(final TestRequest testRequest, final ClassLoader classLoader) {
+        final TestDefinition test = testRequest.getOwner();
+        final List<ListenerDefinition> applicableListenerElements = test.getListeners().isEmpty()
+                ? this.launchDefinition.getListeners() : test.getListeners();
+        final List<TestExecutionListener> listeners = new ArrayList<>();
+        final Optional<Project> 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<SwitchedStreamHandle> 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<TestResultFormatter> resultFormatters;
+        private volatile SysOutErrContentDeliverer contentDeliverer;
+
+        SysOutErrStreamReader(final LauncherSupport launchManager, final InputStream source, final StreamType streamType, final Collection<TestResultFormatter> 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<TestResultFormatter> resultFormatters;
+        private final StreamType streamType;
+        private final BlockingQueue<byte[]> availableData = new LinkedBlockingQueue<>();
+        private final CountDownLatch completionLatch = new CountDownLatch(1);
+
+        SysOutErrContentDeliverer(final StreamType streamType, final Collection<TestResultFormatter> 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<byte[]> 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<SwitchedStreamHandle> switchedSysOutHandle;
+        private Optional<SwitchedStreamHandle> 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);
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/ant/blob/c9ca84fd/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/ListenerDefinition.java
----------------------------------------------------------------------
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 6b50ce2..c24e872 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 &lt;listener&gt;} element within the {@code &lt;junitlauncher&gt;}
  * 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());
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/ant/blob/c9ca84fd/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/NamedTest.java
----------------------------------------------------------------------
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 01c23cb..07039a6 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();

http://git-wip-us.apache.org/repos/asf/ant/blob/c9ca84fd/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/SingleTestClass.java
----------------------------------------------------------------------
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 a950f85..3744a81 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<TestRequest> 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<TestRequest> 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());
+    }
 }