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/03/17 10:54:06 UTC

[2/2] ant git commit: JUnit 5 support - A new junitlauncher task

JUnit 5 support - A new junitlauncher task


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

Branch: refs/heads/master
Commit: 063e60813af955e6db6353ede79249a7bab9d63e
Parents: c5e87fe
Author: Jaikiran Pai <ja...@apache.org>
Authored: Wed Dec 13 19:07:41 2017 +0530
Committer: Jaikiran Pai <ja...@apache.org>
Committed: Sat Mar 17 16:23:44 2018 +0530

----------------------------------------------------------------------
 WHATSNEW                                        |   3 +
 build.xml                                       |  29 +-
 fetch.xml                                       |  21 +-
 lib/libraries.properties                        |   5 +
 manual/Tasks/junitlauncher.html                 | 481 +++++++++++++++++
 .../taskdefs/optional/junitlauncher.xml         | 113 ++++
 .../tools/ant/taskdefs/defaults.properties      |   1 +
 .../AbstractJUnitResultFormatter.java           | 295 ++++++++++
 .../junitlauncher/JUnitLauncherTask.java        | 537 +++++++++++++++++++
 .../LegacyBriefResultFormatter.java             |  17 +
 .../LegacyPlainResultFormatter.java             | 294 ++++++++++
 .../junitlauncher/LegacyXmlResultFormatter.java | 363 +++++++++++++
 .../junitlauncher/ListenerDefinition.java       | 121 +++++
 .../optional/junitlauncher/NamedTest.java       |  14 +
 .../optional/junitlauncher/SingleTestClass.java | 101 ++++
 .../optional/junitlauncher/TestClasses.java     | 112 ++++
 .../optional/junitlauncher/TestDefinition.java  | 113 ++++
 .../junitlauncher/TestExecutionContext.java     |  28 +
 .../optional/junitlauncher/TestRequest.java     |  74 +++
 .../junitlauncher/TestResultFormatter.java      |  58 ++
 .../junitlauncher/JUnitLauncherTaskTest.java    | 127 +++++
 .../example/jupiter/JupiterSampleTest.java      |  50 ++
 .../vintage/AlwaysFailingJUnit4Test.java        |  16 +
 .../junitlauncher/vintage/JUnit4SampleTest.java |  25 +
 24 files changed, 2996 insertions(+), 2 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ant/blob/063e6081/WHATSNEW
----------------------------------------------------------------------
diff --git a/WHATSNEW b/WHATSNEW
index cd960d2..0415592 100644
--- a/WHATSNEW
+++ b/WHATSNEW
@@ -37,6 +37,9 @@ Other changes:
    requested. Java11 removes support for CORBA and the switches have
    been removed from the rmic tool.
 
+ * A new junitlauncher task which support JUnit 5 test framework.
+   Bugzilla Report 61796
+
 Changes from Ant 1.10.1 TO Ant 1.10.2
 =====================================
 

http://git-wip-us.apache.org/repos/asf/ant/blob/063e6081/build.xml
----------------------------------------------------------------------
diff --git a/build.xml b/build.xml
index e2fe0ae..108c0a2 100644
--- a/build.xml
+++ b/build.xml
@@ -208,6 +208,20 @@
       </or>
   </selector>
 
+  <selector id="needs.junitlauncher">
+    <filename name="${optional.package}/junitlauncher/"/>
+  </selector>
+
+    <selector id="needs.junit.engine.vintage">
+        <!-- we need JUnit vintage engine only in tests where we test the junitlauncher task -->
+        <filename name="${src.junit}/org/apache/tools/ant/taskdefs/optional/junitlauncher/**/*"/>
+    </selector>
+
+    <selector id="needs.junit.engine.jupiter">
+        <!-- we need JUnit jupiter engine only in tests where we test the junitlauncher task -->
+        <filename name="${src.junit}/org/apache/tools/ant/taskdefs/optional/junitlauncher/**/*"/>
+    </selector>
+
   <selector id="needs.apache-regexp">
     <filename name="${regexp.package}/JakartaRegexp*"/>
   </selector>
@@ -322,6 +336,7 @@
         <selector refid="needs.jsch"/>
         <selector refid="needs.junit"/>
         <selector refid="needs.junit4"/>
+        <selector refid="needs.junitlauncher"/>
         <selector refid="needs.netrexx"/>
         <selector refid="needs.swing"/>
         <selector refid="needs.xz"/>
@@ -405,6 +420,15 @@
     <available property="junit4.present"
                classname="org.junit.Test"
                classpathref="classpath" ignoresystemclasses="${ignoresystemclasses}"/>
+    <available property="junitlauncher.present"
+               classname="org.junit.platform.launcher.Launcher"
+               classpathref="classpath" ignoresystemclasses="${ignoresystemclasses}"/>
+    <available property="junit.engine.vintage.present"
+               classname="org.junit.vintage.engine.VintageTestEngine"
+               classpathref="classpath" ignoresystemclasses="${ignoresystemclasses}"/>
+    <available property="junit.engine.jupiter.present"
+               classname="org.junit.jupiter.engine.JupiterTestEngine"
+               classpathref="classpath" ignoresystemclasses="${ignoresystemclasses}"/>
     <available property="antunit.present"
                classname="org.apache.ant.antunit.AntUnit"
                classpathref="classpath" ignoresystemclasses="${ignoresystemclasses}"/>
@@ -562,10 +586,12 @@
         <not>
           <or>
             <selector refid="not.in.kaffe" if="kaffe"/>
-
             <selector refid="needs.apache-resolver" unless="apache.resolver.present"/>
             <selector refid="needs.junit" unless="junit.present"/> <!-- TODO should perhaps use -source 1.4? -->
             <selector refid="needs.junit4" unless="junit4.present"/>
+            <selector refid="needs.junitlauncher" unless="junitlauncher.present"/>
+            <selector refid="needs.junit.engine.vintage" unless="junit.engine.vintage.present"/>
+            <selector refid="needs.junit.engine.jupiter" unless="junit.engine.jupiter.present"/>
             <selector refid="needs.apache-regexp" unless="apache.regexp.present"/>
             <selector refid="needs.apache-oro" unless="apache.oro.present"/>
             <selector refid="needs.apache-bcel" unless="bcel.present"/>
@@ -733,6 +759,7 @@
     <optional-jar dep="apache-resolver"/>
     <optional-jar dep="junit"/>
     <optional-jar dep="junit4"/>
+    <optional-jar dep="junitlauncher"/>
     <optional-jar dep="apache-regexp"/>
     <optional-jar dep="apache-oro"/>
     <optional-jar dep="apache-bcel"/>

http://git-wip-us.apache.org/repos/asf/ant/blob/063e6081/fetch.xml
----------------------------------------------------------------------
diff --git a/fetch.xml b/fetch.xml
index 166a2bb..9a98699 100644
--- a/fetch.xml
+++ b/fetch.xml
@@ -232,6 +232,24 @@ Set -Ddest=LOCATION on the command line
     <f2 project="org.hamcrest" archive="hamcrest-library"/>
   </target>
 
+  <target name="junitlauncher"
+    description="load junitlauncher libraries"
+    depends="init">
+    <f2 project="org.junit.platform" archive="junit-platform-launcher" />
+  </target>
+
+  <target name="junit-engine-jupiter"
+          description="load junit jupiter engine libraries (necessary only for internal Ant project tests)"
+          depends="init">
+    <f2 project="org.junit.jupiter" archive="junit-jupiter-engine" />
+  </target>
+
+  <target name="junit-engine-vintage"
+          description="load junit vintage engine libraries (necessary only for internal Ant project tests)"
+          depends="init">
+    <f2 project="org.junit.vintage" archive="junit-vintage-engine" />
+  </target>
+
   <target name="xml"
           description="load full XML libraries (Xalan and xml-resolver)"
           depends="init">
@@ -367,5 +385,6 @@ Set -Ddest=LOCATION on the command line
 
   <target name="all"
     description="load all the libraries (except jython)"
-    depends="antunit,ivy,logging,junit,xml,networking,regexp,antlr,bcel,jdepend,bsf,debugging,script,javamail,jspc,jai,xz,netrexx"/>
+    depends="antunit,ivy,logging,junit,junitlauncher,xml,networking,regexp,antlr,bcel,jdepend,bsf,debugging,script,
+      javamail,jspc,jai,xz,netrexx,junit-engine-vintage,junit-engine-jupiter"/>
 </project>

http://git-wip-us.apache.org/repos/asf/ant/blob/063e6081/lib/libraries.properties
----------------------------------------------------------------------
diff --git a/lib/libraries.properties b/lib/libraries.properties
index 3d4f4a1..cf8e930 100644
--- a/lib/libraries.properties
+++ b/lib/libraries.properties
@@ -53,6 +53,11 @@ jdepend.version=2.9.1
 jruby.version=1.6.8
 junit.version=4.12
 rhino.version=1.7.8
+junit-platform-launcher.version=1.1.0
+# Only used for internal tests in Ant project
+junit-vintage-engine.version=5.1.0
+# Only used for internal tests in Ant project
+junit-jupiter-engine.version=5.1.0
 jsch.version=0.1.54
 jython.version=2.7.0
 # log4j 1.2.15 requires JMS and a few other Sun jars that are not in the m2 repo

http://git-wip-us.apache.org/repos/asf/ant/blob/063e6081/manual/Tasks/junitlauncher.html
----------------------------------------------------------------------
diff --git a/manual/Tasks/junitlauncher.html b/manual/Tasks/junitlauncher.html
new file mode 100644
index 0000000..8603f59
--- /dev/null
+++ b/manual/Tasks/junitlauncher.html
@@ -0,0 +1,481 @@
+<!--
+   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.
+-->
+<html>
+<head>
+    <link rel="stylesheet" type="text/css" href="../stylesheets/style.css">
+    <title>JUnitLauncher Task</title>
+</head>
+<body>
+
+<h2 id="junitlauncher">JUnitLauncher</h2>
+<h3>Description</h3>
+
+<p>
+    This task allows tests to be launched and run using the JUnit 5 framework.
+</p>
+<p>
+    JUnit 5 introduced a newer set of APIs to write and launch tests. It also introduced
+    the concept of test engines. Test engines decide which classes are considered as testcases
+    and how they are executed. JUnit 5 supports running tests that have been written using
+    JUnit 4 constructs as well as tests that have been written using JUnit 5 constructs.
+    For more details about JUnit 5 itself, please refer to the JUnit 5 project's documentation at
+    <a href="https://junit.org/junit5/">https://junit.org/junit5/</a>.
+</p>
+<p>
+    The goal of this <code>junitlauncher</code> task is to allow launching the JUnit 5
+    test launcher and building the test requests so that the selected tests can then be parsed
+    and executed by the test engine(s) supported by JUnit 5. This task in itself does <i>not</i>
+    understand what a test case is nor does it execute the tests itself.
+</p>
+<p>
+    <strong>Note</strong>: This task depends on external libraries not included
+    in the Apache Ant distribution. See <a href="../install.html#librarydependencies">
+    Library Dependencies</a> for more information.
+</p>
+<p>
+    <strong>Note</strong>:
+    You must have the necessary JUnit 5 libraries in the classpath of the tests. At the time of
+    writing this documentation, the list of JUnit 5 platform libraries that are necessary to run the tests
+    are:
+<ul>
+    <li>
+        junit-platform-commons.jar
+    </li>
+    <li>
+        junit-platform-engine.jar
+    </li>
+    <li>
+        junit-platform-launcher.jar
+    </li>
+</ul>
+</p>
+<p>
+    Depending on the test engine(s) that you want to use in your tests, you will further need the following
+    libraries in the classpath
+</p>
+
+<p>
+    For <code>junit-vintage</code> engine:
+<ul>
+    <li>
+        junit-vintage-engine.jar
+    </li>
+    <li>
+        junit.jar (JUnit 4.x version)
+    </li>
+</ul>
+</p>
+<p>
+    For <code>junit-jupiter</code> engine:
+<ul>
+    <li>
+        junit-jupiter-api.jar
+    </li>
+    <li>
+        junit-jupiter-engine.jar
+    </li>
+    <li>
+        opentest4j.jar
+    </li>
+</ul>
+
+</p>
+<p>
+    To have these in the test classpath, you can follow <i>either</i> of the following approaches:
+<ul>
+    <li>Put all these relevant jars along with the <code>ant-junitlauncher.jar</code> in <code>ANT_HOME/lib</code>
+        directory
+    </li>
+    <li>OR Leave <code>ant-junitlauncher.jar</code> in the <code>ANT_HOME/lib</code> directory and include all
+        other relevant jars in the classpath by passing them as a <code>-lib</code> option, while invoking Ant
+    </li>
+</ul>
+</p>
+
+<p>
+    Tests are defined by nested elements like <code>test</code>,
+    <code>testclasses</code> tags (see <a href="#nested">nested
+    elements</a>).</p>
+
+<h3>Parameters</h3>
+<table>
+    <tr>
+        <td valign="top"><b>Attribute</b></td>
+        <td valign="top"><b>Description</b></td>
+        <td valign="top"><b>Required</b></td>
+    </tr>
+    <tr>
+        <td valign="top">haltOnFailure</td>
+        <td valign="top">A value of <code>true</code> implies that build has to stop
+            if any failure occurs in any of the tests. JUnit 5 classifies failures
+            as both assertion failures as well as exceptions that get thrown during
+            test execution. As such, this task too considers both these cases as
+            failures and doesn't distinguish one from another.
+        </td>
+        <td align="center" valign="top">No; default is <code>false</code>.</td>
+    </tr>
+    <tr>
+        <td valign="top">failureProperty</td>
+        <td valign="top">The name of a property to set in the event of a failure
+            (exceptions in tests are considered failures as well).
+        </td>
+        <td align="center" valign="top">No.</td>
+    </tr>
+</table>
+
+<h3 id="nested">Nested Elements</h3>
+
+<h4>classpath</h4>
+<p>
+    The nested <code>&lt;classpath&gt;</code> element that represents a
+    <a href="../using.html#path">PATH like structure</a> can be used to configure
+    the task to use this classpath for finding and running the tests. This classpath
+    will be used for:
+<ul>
+    <li>Finding the test classes to execute</li>
+    <li>Finding the JUnit 5 framework libraries (which include the API jars and test engine jars). The complete
+        set of jars that are relevant in JUnit 5 framework are listed in the <a href="#junit5deps">dependecies</a>
+        section
+    </li>
+</ul>
+If the <code>classpath</code> element isn't configured for the task, then the classpath of
+Ant itself will be used for finding the test classes and JUnit 5 libraries.
+
+</p>
+
+<h4>listener</h4>
+
+<p>
+    The <code>junitlauncher</code> task can be configured with <code>listener</code>(s) to listen
+    to test execution events (such as a test execution starting, completing etc...). The listener
+    is expected to be a class which implements the <code>org.junit.platform.launcher.TestExecutionListener</code>.
+    This <code>TestExecutionListener</code> interface is an API exposed by the JUnit 5 platform APIs and isn't
+    specific to Ant. As such, you can use any existing implementation of <code>TestExecutionListener</code> in
+    this task.
+</p>
+
+<h5>Test result formatter</h5>
+<p>
+    <code>junitlauncher</code> provides a way where the test execution results can be formatted and presented
+    in a way that's customizable. The task allows for configuring test result formatters, through the use of
+    <code>listener</code> element. As noted previously, the <code>listener</code> element expects the listener
+    to implement the <code>org.junit.platform.launcher.TestExecutionListener</code> interface. Typically, result
+    formatters need a bit more configuration details to be fed to them, during the test execution - details
+    like where to write out the formatted result. Any such listener can optionally implement
+    the <code>org.apache.tools.ant.taskdefs.optional.junitlauncher.TestResultFormatter</code> interface. This interface
+    is specific to Ant <code>junitlauncher</code> task and it extends the <code>org.junit.platform.launcher.TestExecutionListener</code>
+    interface
+</p>
+<p>
+    The <code>junitlauncher</code> task comes with the following pre-defined test result formatter types:
+<ul>
+    <li>
+        <code>legacy-plain</code> : This formatter prints a short statistics line for all test cases.
+    </li>
+    <li>
+        <code>legacy-brief</code> : This formatter prints information for tests that failed or were skipped.
+    </li>
+    <li>
+        <code>legacy-xml</code> : This formatter prints statistics for the tests in xml format.
+    </li>
+</ul>
+<em>NOTE:</em> Each of these formatters, that are named "legacy" try, and format the results to be almost similar to
+what the <code>junit</code> task's formatters used to do. Furthermore, the <code>legacy-xml</code> formatters
+generates the XML to comply with the same schema that the <code>junit</code> task's XML formatter used to follow.
+As a result, the XML generated by this formatter, can be used as-is by the <code>junitreport</code> task.
+
+</p>
+
+The <code>listener</code> element supports the following attributes:
+
+<table>
+    <tr>
+        <td valign="top"><b>Attribute</b></td>
+        <td valign="top"><b>Description</b></td>
+        <td valign="top"><b>Required</b></td>
+    </tr>
+    <tr>
+        <td valign="top">type</td>
+        <td valign="top">Use a predefined formatter (either
+            <code>legacy-xml</code>, <code>legacy-plain</code> or <code>legacy-brief</code>).
+        </td>
+        <td align="center" rowspan="2">Exactly one of these</td>
+    </tr>
+    <tr>
+        <td valign="top">classname</td>
+        <td valign="top">Name of a listener class which implements <code>org.junit.platform.launcher.TestExecutionListener</code>
+            or the <code>org.apache.tools.ant.taskdefs.optional.junitlauncher.TestResultFormatter</code> interface
+        </td>
+    </tr>
+    <tr>
+        <td valign="top">resultFile</td>
+        <td valign="top">The file name to which the formatted result needs to be written to. This attribute is only
+            relevant
+            when the listener class implements the <code>org.apache.tools.ant.taskdefs.optional.junitlauncher.TestResultFormatter</code>
+            interface.
+            <p> If no value is specified for this attribute and the listener implements the
+                <code>org.apache.tools.ant.taskdefs.optional.junitlauncher.TestResultFormatter</code> then the file name
+                will be defaulted
+                to and will be of the form <code>TEST-&lt;testname&gt;.&lt;formatter-specific-extension&gt;</code>
+                (ex: TEST-org.myapp.SomeTest.xml for the <code>legacy-xml</code> type formatter)
+            </p>
+        </td>
+        <td align="center">No</td>
+    </tr>
+    <tr>
+        <td valign="top">sendSysOut</td>
+        <td valign="top">If set to <code>true</code> then the listener will be passed the <code>stdout</code> content
+            generated by the test(s). This attribute is relevant only if the listener
+            class implements the <code>org.apache.tools.ant.taskdefs.optional.junitlauncher.TestResultFormatter</code>
+            interface.
+        </td>
+        <td align="center">No; defaults to <code>false</code></td>
+    </tr>
+    <tr>
+        <td valign="top">sendSysErr</td>
+        <td valign="top">If set to <code>true</code> then the listener will be passed the <code>stderr</code> content
+            generated by the test(s). This attribute is relevant only if the listener
+            class implements the <code>org.apache.tools.ant.taskdefs.optional.junitlauncher.TestResultFormatter</code>
+            interface.
+        </td>
+        <td align="center">No; defaults to <code>false</code></td>
+    </tr>
+    <tr>
+        <td valign="top">if</td>
+        <td valign="top">Only use this listener <a href="../properties.html#if+unless">if the named property is set</a>.
+        </td>
+        <td align="center">No</td>
+    </tr>
+    <tr>
+        <td valign="top">unless</td>
+        <td valign="top">Only use this listener <a href="../properties.html#if+unless">if the named property is
+            <b>not</b>
+            set</a>.
+        </td>
+        <td align="center">No</td>
+    </tr>
+</table>
+
+<h4>test</h4>
+
+<p>Defines a single test class.</p>
+
+<table>
+    <tr>
+        <td valign="top"><b>Attribute</b></td>
+        <td valign="top"><b>Description</b></td>
+        <td valign="top"><b>Required</b></td>
+    </tr>
+    <tr>
+        <td valign="top">name</td>
+        <td valign="top">Fully qualified name of the test class.</td>
+        <td align="center">Yes</td>
+    </tr>
+    <tr>
+        <td valign="top">methods</td>
+        <td valign="top">Comma-separated list of names of test case methods to execute.
+            If this is specified, then only these test methods from the test class will be
+            executed.
+        </td>
+        <td align="center">No</td>
+    </tr>
+    <tr>
+        <td valign="top">haltOnFailure</td>
+        <td valign="top">Stop the build process if a failure occurs during the test
+            run (exceptions are considered as failures too).
+            Overrides value set on <code>junitlauncher</code> element.
+        </td>
+        <td align="center" valign="top">No</td>
+    </tr>
+    <tr>
+        <td valign="top">failureProperty</td>
+        <td valign="top">The name of a property to set in the event of a failure
+            (exceptions are considered failures as well). Overrides value set on
+            <code>junitlauncher</code> element.
+        </td>
+        <td align="center" valign="top">No</td>
+    </tr>
+    <tr>
+        <td valign="top">outputDir</td>
+        <td valign="top">Directory to write the reports to.</td>
+        <td align="center" valign="top">No; default is the base directory of the project.</td>
+    </tr>
+    <tr>
+        <td valign="top">if</td>
+        <td valign="top">Only run this test <a href="../properties.html#if+unless">if the named property is set</a>.
+        </td>
+        <td align="center" valign="top">No</td>
+    </tr>
+    <tr>
+        <td valign="top">unless</td>
+        <td valign="top">Only run this test <a href="../properties.html#if+unless">if the named property is <b>not</b>
+            set</a>.
+        </td>
+        <td align="center" valign="top">No</td>
+    </tr>
+</table>
+
+<p>
+    Tests can define their own listeners via nested <code>listener</code> elements.
+</p>
+
+<h4>testclasses</h4>
+
+<p>Define a number of tests based on pattern matching.</p>
+
+<p>
+    <code>testclasses</code> collects the included <a href="../Types/resources.html">resources</a> from any number
+    of nested <a
+        href="../Types/resources.html#collection">Resource Collection</a>s. It then
+    selects each resource whose name ends in <code>.class</code>. These classes are then passed on to the
+    JUnit 5 platform for it to decide and run them as tests.
+</p>
+
+<table>
+    <tr>
+        <td valign="top"><b>Attribute</b></td>
+        <td valign="top"><b>Description</b></td>
+        <td valign="top"><b>Required</b></td>
+    </tr>
+    <tr>
+        <td valign="top">haltOnFailure</td>
+        <td valign="top">Stop the build process if a failure occurs during the test
+            run (exceptions are considered as failures too).
+            Overrides value set on <code>junitlauncher</code> element.
+        </td>
+        <td align="center" valign="top">No</td>
+    </tr>
+    <tr>
+        <td valign="top">failureProperty</td>
+        <td valign="top">The name of a property to set in the event of a failure
+            (exceptions are considered failures as well). Overrides value set on
+            <code>junitlauncher</code> element.
+        </td>
+        <td align="center" valign="top">No</td>
+    </tr>
+    <tr>
+        <td valign="top">outputDir</td>
+        <td valign="top">Directory to write the reports to.</td>
+        <td align="center" valign="top">No; default is the base directory of the project.</td>
+    </tr>
+    <tr>
+        <td valign="top">if</td>
+        <td valign="top">Only run the tests <a href="../properties.html#if+unless">if the named property is set</a>.
+        </td>
+        <td align="center" valign="top">No</td>
+    </tr>
+    <tr>
+        <td valign="top">unless</td>
+        <td valign="top">Only run the tests <a href="../properties.html#if+unless">if the named property is <b>not</b>
+            set</a>.
+        </td>
+        <td align="center" valign="top">No</td>
+    </tr>
+</table>
+
+<p>
+    <code>testclasses</code> can define their own listeners via nested <code>listener</code> elements.
+</p>
+
+<h3>Examples</h3>
+
+<pre>
+&lt;path id="test.classpath"&gt;
+    ...
+&lt;/path&gt;
+
+&lt;junitlauncher&gt;
+    &lt;classpath refid="test.classpath"/&gt;
+    &lt;test name="org.myapp.SimpleTest"/&gt;
+&lt;/junitlauncher&gt;
+
+</pre>
+
+<p>
+    Launches the JUnit 5 platform to run the <code>org.myapp.SimpleTest</code> test
+</p>
+
+<pre>
+&lt;junitlauncher&gt;
+    &lt;classpath refid="test.classpath"/&gt;
+    &lt;test name="org.myapp.SimpleTest" haltOnFailure="true"/&gt;
+    &lt;test name="org.myapp.AnotherTest"/&gt;
+&lt;/junitlauncher&gt;
+</pre>
+
+<p>
+    Launches the JUnit 5 platform to run the <code>org.myapp.SimpleTest</code> and the
+    <code>org.myapp.AnotherTest</code> tests. The build process will be stopped if any
+    test, in the <code>org.myapp.SimpleTest</code>, fails.
+</p>
+
+<pre>
+&lt;junitlauncher&gt;
+    &lt;classpath refid="test.classpath"/&gt;
+    &lt;test name="org.myapp.SimpleTest" methods="testFoo, testBar"/&gt;
+&lt;/junitlauncher&gt;
+</pre>
+<p>
+    Launches the JUnit 5 platform to run only the <code>testFoo</code> and <code>testBar</code> methods of the
+    <code>org.myapp.SimpleTest</code> test class.
+</p>
+
+<pre>
+&lt;junitlauncher&gt;
+    &lt;classpath refid="test.classpath"/&gt;
+
+    &lt;testclasses outputdir="${output.dir}"&gt;
+        &lt;fileset dir="${build.classes.dir}"&gt;
+            &lt;include name="org/example/**/tests/**/"/&gt;
+        &lt;/fileset&gt;
+    &lt;/testclasses&gt;
+&lt;/junitlauncher&gt;
+</pre>
+
+<p>
+    Selects any <code>.class</code> files that match the <code>org/example/**/tests/**/</code> <code>fileset</code>
+    filter, under the <code>${build.classes.dir}</code> and passes those classes to the JUnit 5 platform for
+    execution as tests.
+</p>
+
+<pre>
+&lt;junitlauncher&gt;
+    &lt;classpath refid="test.classpath"/&gt;
+
+    &lt;testclasses outputdir="${output.dir}"&gt;
+        &lt;fileset dir="${build.classes.dir}"&gt;
+            &lt;include name="org/example/**/tests/**/"/&gt;
+        &lt;/fileset&gt;
+        &lt;listener type="legacy-xml" sendSysOut="true" sendSysErr="true"/&gt;
+        &lt;listener type="legacy-plain" sendSysOut="true" /&gt;
+    &lt;/testclasses&gt;
+&lt;/junitlauncher&gt;
+</pre>
+<p>
+    Selects any <code>.class</code> files that match the <code>org/example/**/tests/**/</code> <code>fileset</code>
+    filter, under the <code>${build.classes.dir}</code> and passes those classes to the JUnit 5 platform for
+    execution as tests. Test results will be written out to the <code>${output.dir}</code> by the
+    <code>legacy-xml</code> and <code>legacy-plain</code> formatters, in separate files.
+    Furthermore, both the <code>legacy-xml</code> and the <code>legacy-plain</code>
+    listeners, above, are configured to receive the standard output content generated by the tests.
+    The <code>legacy-xml</code> listener is configured to receive standard error content as well.
+
+</p>
+
+
+</body>
+</html>

http://git-wip-us.apache.org/repos/asf/ant/blob/063e6081/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
new file mode 100644
index 0000000..ccae7ae
--- /dev/null
+++ b/src/etc/testcases/taskdefs/optional/junitlauncher.xml
@@ -0,0 +1,113 @@
+<?xml version="1.0"?>
+<!--
+  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.
+-->
+
+<project name="junitlauncher-test" basedir=".">
+
+    <property name="output.dir" location="${java.io.tmpdir}"/>
+    <property name="build.classes.dir" value="../../../../../build/testcases"/>
+    <target name="init">
+        <mkdir dir="${output.dir}"/>
+    </target>
+
+    <path id="junit.platform.classpath">
+        <fileset dir="../../../../../lib/optional" includes="junit-platform*.jar"/>
+    </path>
+
+    <path id="junit.engine.vintage.classpath">
+        <fileset dir="../../../../../lib/optional" includes="junit-vintage-engine*.jar"/>
+    </path>
+
+    <path id="junit.engine.jupiter.classpath">
+        <fileset dir="../../../../../lib/optional">
+            <include name="junit-jupiter*.jar"/>
+            <include name="opentest4j*.jar"/>
+        </fileset>
+    </path>
+
+    <path id="test.classpath">
+        <pathelement location="${build.classes.dir}"/>
+        <path refid="junit.platform.classpath"/>
+        <path refid="junit.engine.vintage.classpath"/>
+        <path refid="junit.engine.jupiter.classpath"/>
+    </path>
+
+    <target name="test-failure-stops-build" depends="init">
+        <junitlauncher>
+            <!-- A specific test meant to fail -->
+            <test name="org.example.junitlauncher.vintage.AlwaysFailingJUnit4Test" haltOnFailure="true"/>
+            <!-- classpath to be used for the tests -->
+            <classpath refid="test.classpath"/>
+        </junitlauncher>
+    </target>
+
+    <target name="test-failure-continues-build" depends="init">
+        <junitlauncher>
+            <!-- A specific test meant to fail -->
+            <test name="org.example.junitlauncher.vintage.AlwaysFailingJUnit4Test"/>
+            <classpath refid="test.classpath"/>
+        </junitlauncher>
+    </target>
+
+    <target name="test-success" depends="init">
+        <junitlauncher>
+            <!-- A specific test meant to pass -->
+            <test name="org.example.junitlauncher.vintage.JUnit4SampleTest"/>
+            <classpath refid="test.classpath"/>
+        </junitlauncher>
+    </target>
+
+    <target name="test-one-specific-method" depends="init">
+        <junitlauncher>
+            <test name="org.example.junitlauncher.vintage.JUnit4SampleTest" methods="testBar" haltonfailure="true"/>
+            <classpath refid="test.classpath"/>
+        </junitlauncher>
+    </target>
+
+    <target name="test-multiple-specific-methods" depends="init">
+        <junitlauncher>
+            <test name="org.example.junitlauncher.vintage.JUnit4SampleTest" methods=" testFoo, testFooBar "
+                  haltonfailure="true"/>
+            <classpath refid="test.classpath"/>
+        </junitlauncher>
+    </target>
+
+    <target name="test-multiple-individual" depends="init">
+        <junitlauncher>
+            <test name="org.example.junitlauncher.vintage.AlwaysFailingJUnit4Test"/>
+            <test name="org.example.junitlauncher.vintage.JUnit4SampleTest"/>
+            <classpath refid="test.classpath"/>
+        </junitlauncher>
+    </target>
+
+    <target name="test-batch" depends="init">
+        <junitlauncher>
+            <classpath refid="test.classpath"/>
+            <testclasses outputdir="${output.dir}">
+                <fileset dir="${build.classes.dir}">
+                    <include name="org/example/**/junitlauncher/**/"/>
+                </fileset>
+                <fileset dir="${build.classes.dir}">
+                    <include name="org/apache/tools/ant/taskdefs/optional/junitlauncher/example/**/"/>
+                </fileset>
+                <listener type="legacy-brief" sendSysOut="true"/>
+                <listener type="legacy-xml" sendSysErr="true" sendSysOut="true"/>
+            </testclasses>
+        </junitlauncher>
+    </target>
+</project>
+

http://git-wip-us.apache.org/repos/asf/ant/blob/063e6081/src/main/org/apache/tools/ant/taskdefs/defaults.properties
----------------------------------------------------------------------
diff --git a/src/main/org/apache/tools/ant/taskdefs/defaults.properties b/src/main/org/apache/tools/ant/taskdefs/defaults.properties
index 8db1ebc..7b4781c 100644
--- a/src/main/org/apache/tools/ant/taskdefs/defaults.properties
+++ b/src/main/org/apache/tools/ant/taskdefs/defaults.properties
@@ -160,6 +160,7 @@ jjdoc=org.apache.tools.ant.taskdefs.optional.javacc.JJDoc
 jjtree=org.apache.tools.ant.taskdefs.optional.javacc.JJTree
 junit=org.apache.tools.ant.taskdefs.optional.junit.JUnitTask
 junitreport=org.apache.tools.ant.taskdefs.optional.junit.XMLResultAggregator
+junitlauncher=org.apache.tools.ant.taskdefs.optional.junitlauncher.JUnitLauncherTask
 native2ascii=org.apache.tools.ant.taskdefs.optional.Native2Ascii
 netrexxc=org.apache.tools.ant.taskdefs.optional.NetRexxC
 propertyfile=org.apache.tools.ant.taskdefs.optional.PropertyFile

http://git-wip-us.apache.org/repos/asf/ant/blob/063e6081/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/AbstractJUnitResultFormatter.java
----------------------------------------------------------------------
diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/AbstractJUnitResultFormatter.java b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/AbstractJUnitResultFormatter.java
new file mode 100644
index 0000000..4d1aa117
--- /dev/null
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/AbstractJUnitResultFormatter.java
@@ -0,0 +1,295 @@
+package org.apache.tools.ant.taskdefs.optional.junitlauncher;
+
+import org.apache.tools.ant.Project;
+import org.apache.tools.ant.util.FileUtils;
+import org.junit.platform.engine.TestSource;
+import org.junit.platform.engine.support.descriptor.ClassSource;
+import org.junit.platform.launcher.TestIdentifier;
+import org.junit.platform.launcher.TestPlan;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.Closeable;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.Writer;
+import java.nio.BufferOverflowException;
+import java.nio.ByteBuffer;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * Contains some common behaviour that's used by our internal {@link TestResultFormatter}s
+ */
+abstract class AbstractJUnitResultFormatter implements TestResultFormatter {
+
+
+    protected static String NEW_LINE = System.getProperty("line.separator");
+    protected TestExecutionContext context;
+
+    private SysOutErrContentStore sysOutStore;
+    private SysOutErrContentStore sysErrStore;
+
+    @Override
+    public void sysOutAvailable(final byte[] data) {
+        if (this.sysOutStore == null) {
+            this.sysOutStore = new SysOutErrContentStore(true);
+        }
+        try {
+            this.sysOutStore.store(data);
+        } catch (IOException e) {
+            handleException(e);
+            return;
+        }
+    }
+
+    @Override
+    public void sysErrAvailable(final byte[] data) {
+        if (this.sysErrStore == null) {
+            this.sysErrStore = new SysOutErrContentStore(false);
+        }
+        try {
+            this.sysErrStore.store(data);
+        } catch (IOException e) {
+            handleException(e);
+            return;
+        }
+    }
+
+    @Override
+    public void setContext(final TestExecutionContext context) {
+        this.context = context;
+    }
+
+    /**
+     * @return Returns true if there's any stdout data, that was generated during the
+     * tests, is available for use. Else returns false.
+     */
+    boolean hasSysOut() {
+        return this.sysOutStore != null && this.sysOutStore.hasData();
+    }
+
+    /**
+     * @return Returns true if there's any stderr data, that was generated during the
+     * tests, is available for use. Else returns false.
+     */
+    boolean hasSysErr() {
+        return this.sysErrStore != null && this.sysErrStore.hasData();
+    }
+
+    /**
+     * @return Returns a {@link Reader} for reading any stdout data that was generated
+     * during the test execution. It is expected that the {@link #hasSysOut()} be first
+     * called to see if any such data is available and only if there is, then this method
+     * be called
+     * @throws IOException If there's any I/O problem while creating the {@link Reader}
+     */
+    Reader getSysOutReader() throws IOException {
+        return this.sysOutStore.getReader();
+    }
+
+    /**
+     * @return Returns a {@link Reader} for reading any stderr data that was generated
+     * during the test execution. It is expected that the {@link #hasSysErr()} be first
+     * called to see if any such data is available and only if there is, then this method
+     * be called
+     * @throws IOException If there's any I/O problem while creating the {@link Reader}
+     */
+    Reader getSysErrReader() throws IOException {
+        return this.sysErrStore.getReader();
+    }
+
+    /**
+     * Writes out any stdout data that was generated during the
+     * test execution. If there was no such data then this method just returns.
+     *
+     * @param writer The {@link Writer} to use. Cannot be null.
+     * @throws IOException If any I/O problem occurs during writing the data
+     */
+    void writeSysOut(final Writer writer) throws IOException {
+        Objects.requireNonNull(writer, "Writer cannot be null");
+        this.writeFrom(this.sysOutStore, writer);
+    }
+
+    /**
+     * Writes out any stderr data that was generated during the
+     * test execution. If there was no such data then this method just returns.
+     *
+     * @param writer The {@link Writer} to use. Cannot be null.
+     * @throws IOException If any I/O problem occurs during writing the data
+     */
+    void writeSysErr(final Writer writer) throws IOException {
+        Objects.requireNonNull(writer, "Writer cannot be null");
+        this.writeFrom(this.sysErrStore, writer);
+    }
+
+    static Optional<TestIdentifier> traverseAndFindTestClass(final TestPlan testPlan, final TestIdentifier testIdentifier) {
+        if (isTestClass(testIdentifier).isPresent()) {
+            return Optional.of(testIdentifier);
+        }
+        final Optional<TestIdentifier> parent = testPlan.getParent(testIdentifier);
+        return parent.isPresent() ? traverseAndFindTestClass(testPlan, parent.get()) : Optional.empty();
+    }
+
+    static Optional<ClassSource> isTestClass(final TestIdentifier testIdentifier) {
+        if (testIdentifier == null) {
+            return Optional.empty();
+        }
+        final Optional<TestSource> source = testIdentifier.getSource();
+        if (!source.isPresent()) {
+            return Optional.empty();
+        }
+        final TestSource testSource = source.get();
+        if (testSource instanceof ClassSource) {
+            return Optional.of((ClassSource) testSource);
+        }
+        return Optional.empty();
+    }
+
+    private void writeFrom(final SysOutErrContentStore store, final Writer writer) throws IOException {
+        final char[] chars = new char[1024];
+        int numRead = -1;
+        try (final Reader reader = store.getReader()) {
+            while ((numRead = reader.read(chars)) != -1) {
+                writer.write(chars, 0, numRead);
+            }
+        }
+    }
+
+    @Override
+    public void close() throws IOException {
+        FileUtils.close(this.sysOutStore);
+        FileUtils.close(this.sysErrStore);
+    }
+
+    protected void handleException(final Throwable t) {
+        // we currently just log it and move on.
+        this.context.getProject().ifPresent((p) -> p.log("Exception in listener "
+                + AbstractJUnitResultFormatter.this.getClass().getName(), t, Project.MSG_DEBUG));
+    }
+
+
+    /*
+    A "store" for sysout/syserr content that gets sent to the AbstractJUnitResultFormatter.
+    This store first uses a relatively decent sized in-memory buffer for storing the sysout/syserr
+    content. This in-memory buffer will be used as long as it can fit in the new content that
+    keeps coming in. When the size limit is reached, this store switches to a file based store
+    by creating a temporarily file and writing out the already in-memory held buffer content
+    and any new content that keeps arriving to this store. Once the file has been created,
+    the in-memory buffer will never be used any more and in fact is destroyed as soon as the
+    file is created.
+    Instances of this class are not thread-safe and users of this class are expected to use necessary thread
+    safety guarantees, if they want to use an instance of this class by multiple threads.
+    */
+    private static final class SysOutErrContentStore implements Closeable {
+        private static final int DEFAULT_CAPACITY_IN_BYTES = 50 * 1024; // 50 KB
+        private static final Reader EMPTY_READER = new Reader() {
+            @Override
+            public int read(final char[] cbuf, final int off, final int len) throws IOException {
+                return -1;
+            }
+
+            @Override
+            public void close() throws IOException {
+            }
+        };
+
+        private final String tmpFileSuffix;
+        private ByteBuffer inMemoryStore = ByteBuffer.allocate(DEFAULT_CAPACITY_IN_BYTES);
+        private boolean usingFileStore = false;
+        private Path filePath;
+        private FileOutputStream fileOutputStream;
+
+        private SysOutErrContentStore(final boolean isSysOut) {
+            this.tmpFileSuffix = isSysOut ? ".sysout" : ".syserr";
+        }
+
+        private void store(final byte[] data) throws IOException {
+            if (this.usingFileStore) {
+                this.storeToFile(data, 0, data.length);
+                return;
+            }
+            // we haven't yet created a file store and the data can fit in memory,
+            // so we write it in our buffer
+            try {
+                this.inMemoryStore.put(data);
+                return;
+            } catch (BufferOverflowException boe) {
+                // the buffer capacity can't hold this incoming data, so this
+                // incoming data hasn't been transferred to the buffer. let's
+                // now fall back to a file store
+                this.usingFileStore = true;
+            }
+            // since the content couldn't be transferred into in-memory buffer,
+            // we now create a file and transfer already (previously) stored in-memory
+            // content into that file, before finally transferring this new content
+            // into the file too. We then finally discard this in-memory buffer and
+            // just keep using the file store instead
+            this.fileOutputStream = createFileStore();
+            // first the existing in-memory content
+            storeToFile(this.inMemoryStore.array(), 0, this.inMemoryStore.position());
+            storeToFile(data, 0, data.length);
+            // discard the in-memory store
+            this.inMemoryStore = null;
+        }
+
+        private void storeToFile(final byte[] data, final int offset, final int length) throws IOException {
+            if (this.fileOutputStream == null) {
+                // no backing file was created so we can't do anything
+                return;
+            }
+            this.fileOutputStream.write(data, offset, length);
+        }
+
+        private FileOutputStream createFileStore() throws IOException {
+            this.filePath = Files.createTempFile(null, this.tmpFileSuffix);
+            this.filePath.toFile().deleteOnExit();
+            return new FileOutputStream(this.filePath.toFile());
+        }
+
+        /*
+         * Returns a Reader for reading the sysout/syserr content. If there's no data
+         * available in this store, then this returns a Reader which when used for read operations,
+         * will immediately indicate an EOF.
+         */
+        private Reader getReader() throws IOException {
+            if (this.usingFileStore && this.filePath != null) {
+                // we use a FileReader here so that we can use the system default character
+                // encoding for reading the contents on sysout/syserr stream, since that's the
+                // encoding that System.out/System.err uses to write out the messages
+                return new BufferedReader(new FileReader(this.filePath.toFile()));
+            }
+            if (this.inMemoryStore != null) {
+                return new InputStreamReader(new ByteArrayInputStream(this.inMemoryStore.array(), 0, this.inMemoryStore.position()));
+            }
+            // no data to read, so we return an "empty" reader
+            return EMPTY_READER;
+        }
+
+        /*
+         *  Returns true if this store has any data (either in-memory or in a file). Else
+         *  returns false.
+         */
+        private boolean hasData() {
+            if (this.inMemoryStore != null && this.inMemoryStore.position() > 0) {
+                return true;
+            }
+            if (this.usingFileStore && this.filePath != null) {
+                return true;
+            }
+            return false;
+        }
+
+        @Override
+        public void close() throws IOException {
+            this.inMemoryStore = null;
+            FileUtils.close(this.fileOutputStream);
+            FileUtils.delete(this.filePath.toFile());
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/ant/blob/063e6081/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
new file mode 100644
index 0000000..ac4ef44
--- /dev/null
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/JUnitLauncherTask.java
@@ -0,0 +1,537 @@
+package org.apache.tools.ant.taskdefs.optional.junitlauncher;
+
+import org.apache.tools.ant.AntClassLoader;
+import org.apache.tools.ant.BuildException;
+import org.apache.tools.ant.Project;
+import org.apache.tools.ant.Task;
+import org.apache.tools.ant.types.Path;
+import org.apache.tools.ant.util.FileUtils;
+import org.apache.tools.ant.util.KeepAliveOutputStream;
+import org.junit.platform.launcher.Launcher;
+import org.junit.platform.launcher.LauncherDiscoveryRequest;
+import org.junit.platform.launcher.TestExecutionListener;
+import org.junit.platform.launcher.TestPlan;
+import org.junit.platform.launcher.core.LauncherFactory;
+import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
+import org.junit.platform.launcher.listeners.TestExecutionSummary;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.io.PrintStream;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.Properties;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * An Ant {@link Task} responsible for launching the JUnit platform for running tests.
+ * This requires a minimum of JUnit 5, since that's the version in which the JUnit platform launcher
+ * APIs were introduced.
+ * <p>
+ * This task in itself doesn't run the JUnit tests, instead the sole responsibility of
+ * this task is to setup the JUnit platform launcher, build requests, launch those requests and then parse the
+ * result of the execution to present in a way that's been configured on this Ant task.
+ * </p>
+ * <p>
+ * Furthermore, this task allows users control over which classes to select for passing on to the JUnit 5
+ * platform for test execution. It however, is solely the JUnit 5 platform, backed by test engines that
+ * decide and execute the tests.
+ *
+ * @see <a href="https://junit.org/junit5/">JUnit 5 documentation</a> for more details
+ * on how JUnit manages the platform and the test engines.
+ */
+public class JUnitLauncherTask extends Task {
+
+    private Path classPath;
+    private boolean haltOnFailure;
+    private String failureProperty;
+    private final List<TestDefinition> tests = new ArrayList<>();
+    private final List<ListenerDefinition> listeners = new ArrayList<>();
+
+    public JUnitLauncherTask() {
+    }
+
+    @Override
+    public void execute() throws BuildException {
+        final ClassLoader previousClassLoader = Thread.currentThread().getContextClassLoader();
+        try {
+            final ClassLoader executionCL = createClassLoaderForTestExecution();
+            Thread.currentThread().setContextClassLoader(executionCL);
+            final Launcher launcher = LauncherFactory.create();
+            final List<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);
+                    }
+                }
+            }
+        } finally {
+            Thread.currentThread().setContextClassLoader(previousClassLoader);
+        }
+    }
+
+    /**
+     * Adds the {@link Path} to the classpath which will be used for execution of the tests
+     *
+     * @param path The classpath
+     */
+    public void addConfiguredClassPath(final Path path) {
+        if (this.classPath == null) {
+            // create a "wrapper" path which can hold on to multiple
+            // paths that get passed to this method (if at all the task in the build is
+            // configured with multiple classpaht elements)
+            this.classPath = new Path(getProject());
+        }
+        this.classPath.add(path);
+    }
+
+    /**
+     * Adds a {@link SingleTestClass} that will be passed on to the underlying JUnit platform
+     * for possible execution of the test
+     *
+     * @param test The test
+     */
+    public void addConfiguredTest(final SingleTestClass test) {
+        this.preConfigure(test);
+        this.tests.add(test);
+    }
+
+    /**
+     * Adds {@link TestClasses} that will be passed on to the underlying JUnit platform for
+     * possible execution of the tests
+     *
+     * @param testClasses The test classes
+     */
+    public void addConfiguredTestClasses(final TestClasses testClasses) {
+        this.preConfigure(testClasses);
+        this.tests.add(testClasses);
+    }
+
+    /**
+     * Adds a {@link ListenerDefinition listener} which will be enrolled for listening to test
+     * execution events
+     *
+     * @param listener The listener
+     */
+    public void addConfiguredListener(final ListenerDefinition listener) {
+        this.listeners.add(listener);
+    }
+
+    public void setHaltonfailure(final boolean haltonfailure) {
+        this.haltOnFailure = haltonfailure;
+    }
+
+    public void setFailureProperty(final String failureProperty) {
+        this.failureProperty = failureProperty;
+    }
+
+    private void preConfigure(final TestDefinition test) {
+        if (test.getHaltOnFailure() == null) {
+            test.setHaltOnFailure(this.haltOnFailure);
+        }
+        if (test.getFailureProperty() == null) {
+            test.setFailureProperty(this.failureProperty);
+        }
+    }
+
+    private List<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);
+        }
+        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.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 TestExecutionListener requireTestExecutionListener(final ListenerDefinition listener, final ClassLoader classLoader) {
+        final String className = listener.getClassName();
+        if (className == null || className.trim().isEmpty()) {
+            throw new BuildException("classname attribute value is missing on listener element");
+        }
+        final Class<?> klass;
+        try {
+            klass = Class.forName(className, false, classLoader);
+        } catch (ClassNotFoundException e) {
+            throw new BuildException("Failed to load listener class " + className, e);
+        }
+        if (!TestExecutionListener.class.isAssignableFrom(klass)) {
+            throw new BuildException("Listener class " + className + " is not of type " + TestExecutionListener.class.getName());
+        }
+        try {
+            return TestExecutionListener.class.cast(klass.newInstance());
+        } catch (Exception e) {
+            throw new BuildException("Failed to create an instance of listener " + className, e);
+        }
+    }
+
+    private void handleTestExecutionCompletion(final TestDefinition test, final TestExecutionSummary summary) {
+        final boolean hasTestFailures = summary.getTestsFailedCount() != 0;
+        try {
+            if (hasTestFailures && test.getFailureProperty() != null) {
+                // if there are test failures and the test is configured to set a property in case
+                // of failure, then set the property to true
+                getProject().setNewProperty(test.getFailureProperty(), "true");
+            }
+        } finally {
+            if (hasTestFailures && test.isHaltOnFailure()) {
+                // if the test is configured to halt on test failures, throw a build error
+                final String errorMessage;
+                if (test instanceof NamedTest) {
+                    errorMessage = "Test " + ((NamedTest) test).getName() + " has " + summary.getTestsFailedCount() + " failure(s)";
+                } else {
+                    errorMessage = "Some test(s) have failure(s)";
+                }
+                throw new BuildException(errorMessage);
+            }
+        }
+    }
+
+    private ClassLoader createClassLoaderForTestExecution() {
+        if (this.classPath == null) {
+            return this.getClass().getClassLoader();
+        }
+        return new AntClassLoader(this.getClass().getClassLoader(), getProject(), this.classPath, true);
+    }
+
+    private Optional<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 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;
+        }
+
+        @Override
+        public void run() {
+            final SysOutErrContentDeliverer streamContentDeliver = new SysOutErrContentDeliverer(this.streamType, this.resultFormatters);
+            final Thread deliveryThread = new Thread(streamContentDeliver);
+            deliveryThread.setName("junitlauncher-" + (this.streamType == StreamType.SYS_OUT ? "sysout" : "syserr") + "-stream-deliverer");
+            deliveryThread.setDaemon(true);
+            deliveryThread.start();
+            this.contentDeliverer = streamContentDeliver;
+            int numRead = -1;
+            final byte[] data = new byte[1024];
+            try {
+                while ((numRead = this.sourceStream.read(data)) != -1) {
+                    final byte[] copy = Arrays.copyOf(data, numRead);
+                    streamContentDeliver.availableData.offer(copy);
+                }
+            } catch (IOException e) {
+                task.log("Failed while streaming " + (this.streamType == StreamType.SYS_OUT ? "sysout" : "syserr") + " data",
+                        e, Project.MSG_INFO);
+                return;
+            } finally {
+                streamContentDeliver.stop = true;
+                // just "wakeup" the delivery thread, to take into account
+                // those race conditions, where that other thread didn't yet
+                // notice that it was asked to stop and has now gone into a
+                // X amount of wait, waiting for any new data
+                streamContentDeliver.availableData.offer(EMPTY);
+            }
+        }
+    }
+
+    private static final class SysOutErrContentDeliverer implements Runnable {
+        private volatile boolean stop;
+        private final Collection<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();
+                    return;
+                }
+            }
+        }
+
+        private void closeAndWait(final SwitchedStreamHandle handle) throws InterruptedException {
+            FileUtils.close(handle.outputStream);
+            if (handle.streamReader.contentDeliverer == null) {
+                return;
+            }
+            // wait for a few seconds
+            handle.streamReader.contentDeliverer.completionLatch.await(2, TimeUnit.SECONDS);
+        }
+    }
+
+    private final class InVMExecution implements TestExecutionContext {
+
+        private final Properties props;
+
+        InVMExecution() {
+            this.props = new Properties();
+            this.props.putAll(JUnitLauncherTask.this.getProject().getProperties());
+        }
+
+        @Override
+        public Properties getProperties() {
+            return this.props;
+        }
+
+        @Override
+        public Optional<Project> getProject() {
+            return Optional.of(JUnitLauncherTask.this.getProject());
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/ant/blob/063e6081/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyBriefResultFormatter.java
----------------------------------------------------------------------
diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyBriefResultFormatter.java b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyBriefResultFormatter.java
new file mode 100644
index 0000000..d5d4670
--- /dev/null
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyBriefResultFormatter.java
@@ -0,0 +1,17 @@
+package org.apache.tools.ant.taskdefs.optional.junitlauncher;
+
+import org.junit.platform.engine.TestExecutionResult;
+import org.junit.platform.launcher.TestIdentifier;
+
+/**
+ * A {@link TestResultFormatter} which prints a brief statistic for tests that have
+ * failed, aborted or skipped
+ */
+class LegacyBriefResultFormatter extends LegacyPlainResultFormatter implements TestResultFormatter {
+
+    @Override
+    protected boolean shouldReportExecutionFinished(final TestIdentifier testIdentifier, final TestExecutionResult testExecutionResult) {
+        final TestExecutionResult.Status resultStatus = testExecutionResult.getStatus();
+        return resultStatus == TestExecutionResult.Status.ABORTED || resultStatus == TestExecutionResult.Status.FAILED;
+    }
+}

http://git-wip-us.apache.org/repos/asf/ant/blob/063e6081/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyPlainResultFormatter.java
----------------------------------------------------------------------
diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyPlainResultFormatter.java b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyPlainResultFormatter.java
new file mode 100644
index 0000000..49ce7e3
--- /dev/null
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyPlainResultFormatter.java
@@ -0,0 +1,294 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ */
+
+package org.apache.tools.ant.taskdefs.optional.junitlauncher;
+
+import org.junit.platform.engine.TestExecutionResult;
+import org.junit.platform.engine.reporting.ReportEntry;
+import org.junit.platform.engine.support.descriptor.ClassSource;
+import org.junit.platform.launcher.TestIdentifier;
+import org.junit.platform.launcher.TestPlan;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.io.UnsupportedEncodingException;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+
+/**
+ * A {@link TestResultFormatter} which prints a short statistic for each of the tests
+ */
+class LegacyPlainResultFormatter extends AbstractJUnitResultFormatter implements TestResultFormatter {
+
+    private OutputStream outputStream;
+    private final Map<TestIdentifier, Stats> testIds = new ConcurrentHashMap<>();
+    private TestPlan testPlan;
+    private BufferedWriter writer;
+
+    @Override
+    public void testPlanExecutionStarted(final TestPlan testPlan) {
+        this.testPlan = testPlan;
+    }
+
+    @Override
+    public void testPlanExecutionFinished(final TestPlan testPlan) {
+        for (final Map.Entry<TestIdentifier, Stats> entry : this.testIds.entrySet()) {
+            final TestIdentifier testIdentifier = entry.getKey();
+            if (!isTestClass(testIdentifier).isPresent()) {
+                // we are not interested in anything other than a test "class" in this section
+                continue;
+            }
+            final Stats stats = entry.getValue();
+            final StringBuilder sb = new StringBuilder("Tests run: ").append(stats.numTestsRun.get());
+            sb.append(", Failures: ").append(stats.numTestsFailed.get());
+            sb.append(", Skipped: ").append(stats.numTestsSkipped.get());
+            sb.append(", Aborted: ").append(stats.numTestsAborted.get());
+            final long timeElapsed = stats.endedAt - stats.startedAt;
+            sb.append(", Time elapsed: ");
+            if (timeElapsed < 1000) {
+                sb.append(timeElapsed).append(" milli sec(s)");
+            } else {
+                sb.append(TimeUnit.SECONDS.convert(timeElapsed, TimeUnit.MILLISECONDS)).append(" sec(s)");
+            }
+            try {
+                this.writer.write(sb.toString());
+                this.writer.newLine();
+            } catch (IOException ioe) {
+                handleException(ioe);
+                return;
+            }
+        }
+        // write out sysout and syserr content if any
+        try {
+            if (this.hasSysOut()) {
+                this.writer.write("------------- Standard Output ---------------");
+                this.writer.newLine();
+                writeSysOut(writer);
+                this.writer.write("------------- ---------------- ---------------");
+                this.writer.newLine();
+            }
+            if (this.hasSysErr()) {
+                this.writer.write("------------- Standard Error ---------------");
+                this.writer.newLine();
+                writeSysErr(writer);
+                this.writer.write("------------- ---------------- ---------------");
+                this.writer.newLine();
+            }
+        } catch (IOException ioe) {
+            handleException(ioe);
+            return;
+        }
+    }
+
+    @Override
+    public void dynamicTestRegistered(final TestIdentifier testIdentifier) {
+        // nothing to do
+    }
+
+    @Override
+    public void executionSkipped(final TestIdentifier testIdentifier, final String reason) {
+        final long currentTime = System.currentTimeMillis();
+        this.testIds.putIfAbsent(testIdentifier, new Stats(testIdentifier, currentTime));
+        final Stats stats = this.testIds.get(testIdentifier);
+        stats.setEndedAt(currentTime);
+        if (testIdentifier.isTest()) {
+            final StringBuilder sb = new StringBuilder();
+            sb.append("Test: ");
+            sb.append(testIdentifier.getLegacyReportingName());
+            final long timeElapsed = stats.endedAt - stats.startedAt;
+            sb.append(" took ");
+            if (timeElapsed < 1000) {
+                sb.append(timeElapsed).append(" milli sec(s)");
+            } else {
+                sb.append(TimeUnit.SECONDS.convert(timeElapsed, TimeUnit.MILLISECONDS)).append(" sec(s)");
+            }
+            sb.append(" SKIPPED");
+            if (reason != null && !reason.isEmpty()) {
+                sb.append(": ").append(reason);
+            }
+            try {
+                this.writer.write(sb.toString());
+                this.writer.newLine();
+            } catch (IOException ioe) {
+                handleException(ioe);
+                return;
+            }
+        }
+        // get the parent test class to which this skipped test belongs to
+        final Optional<TestIdentifier> parentTestClass = traverseAndFindTestClass(this.testPlan, testIdentifier);
+        if (!parentTestClass.isPresent()) {
+            return;
+        }
+        final Stats parentClassStats = this.testIds.get(parentTestClass.get());
+        parentClassStats.numTestsSkipped.incrementAndGet();
+    }
+
+    @Override
+    public void executionStarted(final TestIdentifier testIdentifier) {
+        final long currentTime = System.currentTimeMillis();
+        // record this testidentifier's start
+        this.testIds.putIfAbsent(testIdentifier, new Stats(testIdentifier, currentTime));
+        final Optional<ClassSource> testClass = isTestClass(testIdentifier);
+        if (testClass.isPresent()) {
+            // if this is a test class, then print it out
+            try {
+                this.writer.write("Testcase: " + testClass.get().getClassName());
+                this.writer.newLine();
+            } catch (IOException ioe) {
+                handleException(ioe);
+                return;
+            }
+        }
+        // if this is a test (method) then increment the tests run for the test class to which
+        // this test belongs to
+        if (testIdentifier.isTest()) {
+            final Optional<TestIdentifier> parentTestClass = traverseAndFindTestClass(this.testPlan, testIdentifier);
+            if (parentTestClass.isPresent()) {
+                final Stats parentClassStats = this.testIds.get(parentTestClass.get());
+                if (parentClassStats != null) {
+                    parentClassStats.numTestsRun.incrementAndGet();
+                }
+            }
+        }
+    }
+
+    @Override
+    public void executionFinished(final TestIdentifier testIdentifier, final TestExecutionResult testExecutionResult) {
+        final long currentTime = System.currentTimeMillis();
+        final Stats stats = this.testIds.get(testIdentifier);
+        if (stats != null) {
+            stats.setEndedAt(currentTime);
+        }
+        if (testIdentifier.isTest() && shouldReportExecutionFinished(testIdentifier, testExecutionResult)) {
+            final StringBuilder sb = new StringBuilder();
+            sb.append("Test: ");
+            sb.append(testIdentifier.getLegacyReportingName());
+            if (stats != null) {
+                final long timeElapsed = stats.endedAt - stats.startedAt;
+                sb.append(" took ");
+                if (timeElapsed < 1000) {
+                    sb.append(timeElapsed).append(" milli sec(s)");
+                } else {
+                    sb.append(TimeUnit.SECONDS.convert(timeElapsed, TimeUnit.MILLISECONDS)).append(" sec(s)");
+                }
+            }
+            switch (testExecutionResult.getStatus()) {
+                case ABORTED: {
+                    sb.append(" ABORTED");
+                    appendThrowable(sb, testExecutionResult);
+                    break;
+                }
+                case FAILED: {
+                    sb.append(" FAILED");
+                    appendThrowable(sb, testExecutionResult);
+                    break;
+                }
+            }
+            try {
+                this.writer.write(sb.toString());
+                this.writer.newLine();
+            } catch (IOException ioe) {
+                handleException(ioe);
+                return;
+            }
+        }
+        // get the parent test class in which this test completed
+        final Optional<TestIdentifier> parentTestClass = traverseAndFindTestClass(this.testPlan, testIdentifier);
+        if (!parentTestClass.isPresent()) {
+            return;
+        }
+        // update the stats of the parent test class
+        final Stats parentClassStats = this.testIds.get(parentTestClass.get());
+        switch (testExecutionResult.getStatus()) {
+            case ABORTED: {
+                parentClassStats.numTestsAborted.incrementAndGet();
+                break;
+            }
+            case FAILED: {
+                parentClassStats.numTestsFailed.incrementAndGet();
+                break;
+            }
+        }
+    }
+
+    @Override
+    public void reportingEntryPublished(final TestIdentifier testIdentifier, final ReportEntry entry) {
+        // nothing to do
+    }
+
+    @Override
+    public void setDestination(final OutputStream os) {
+        this.outputStream = os;
+        try {
+            this.writer = new BufferedWriter(new OutputStreamWriter(this.outputStream, "UTF-8"));
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException("Failed to create a writer", e);
+        }
+    }
+
+    protected boolean shouldReportExecutionFinished(final TestIdentifier testIdentifier, final TestExecutionResult testExecutionResult) {
+        return true;
+    }
+
+    private static void appendThrowable(final StringBuilder sb, TestExecutionResult result) {
+        if (!result.getThrowable().isPresent()) {
+            return;
+        }
+        final Throwable throwable = result.getThrowable().get();
+        sb.append(": ").append(throwable.getMessage());
+        sb.append(NEW_LINE);
+        final StringWriter stacktrace = new StringWriter();
+        throwable.printStackTrace(new PrintWriter(stacktrace));
+        sb.append(stacktrace.toString());
+    }
+
+    @Override
+    public void close() throws IOException {
+        if (this.writer != null) {
+            this.writer.close();
+        }
+        super.close();
+    }
+
+    private final class Stats {
+        private final TestIdentifier testIdentifier;
+        private final AtomicLong numTestsRun = new AtomicLong(0);
+        private final AtomicLong numTestsFailed = new AtomicLong(0);
+        private final AtomicLong numTestsSkipped = new AtomicLong(0);
+        private final AtomicLong numTestsAborted = new AtomicLong(0);
+        private final long startedAt;
+        private long endedAt;
+
+        private Stats(final TestIdentifier testIdentifier, final long startedAt) {
+            this.testIdentifier = testIdentifier;
+            this.startedAt = startedAt;
+        }
+
+        private void setEndedAt(final long endedAt) {
+            this.endedAt = endedAt;
+        }
+    }
+}