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><classpath></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-<testname>.<formatter-specific-extension></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>
+<path id="test.classpath">
+ ...
+</path>
+
+<junitlauncher>
+ <classpath refid="test.classpath"/>
+ <test name="org.myapp.SimpleTest"/>
+</junitlauncher>
+
+</pre>
+
+<p>
+ Launches the JUnit 5 platform to run the <code>org.myapp.SimpleTest</code> test
+</p>
+
+<pre>
+<junitlauncher>
+ <classpath refid="test.classpath"/>
+ <test name="org.myapp.SimpleTest" haltOnFailure="true"/>
+ <test name="org.myapp.AnotherTest"/>
+</junitlauncher>
+</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>
+<junitlauncher>
+ <classpath refid="test.classpath"/>
+ <test name="org.myapp.SimpleTest" methods="testFoo, testBar"/>
+</junitlauncher>
+</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>
+<junitlauncher>
+ <classpath refid="test.classpath"/>
+
+ <testclasses outputdir="${output.dir}">
+ <fileset dir="${build.classes.dir}">
+ <include name="org/example/**/tests/**/"/>
+ </fileset>
+ </testclasses>
+</junitlauncher>
+</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>
+<junitlauncher>
+ <classpath refid="test.classpath"/>
+
+ <testclasses outputdir="${output.dir}">
+ <fileset dir="${build.classes.dir}">
+ <include name="org/example/**/tests/**/"/>
+ </fileset>
+ <listener type="legacy-xml" sendSysOut="true" sendSysErr="true"/>
+ <listener type="legacy-plain" sendSysOut="true" />
+ </testclasses>
+</junitlauncher>
+</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;
+ }
+ }
+}