Browse Source

JUnit 5 support - A new junitlauncher task

master
Jaikiran Pai 7 years ago
parent
commit
063e60813a
24 changed files with 2996 additions and 2 deletions
  1. +3
    -0
      WHATSNEW
  2. +28
    -1
      build.xml
  3. +20
    -1
      fetch.xml
  4. +5
    -0
      lib/libraries.properties
  5. +481
    -0
      manual/Tasks/junitlauncher.html
  6. +113
    -0
      src/etc/testcases/taskdefs/optional/junitlauncher.xml
  7. +1
    -0
      src/main/org/apache/tools/ant/taskdefs/defaults.properties
  8. +295
    -0
      src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/AbstractJUnitResultFormatter.java
  9. +537
    -0
      src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/JUnitLauncherTask.java
  10. +17
    -0
      src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyBriefResultFormatter.java
  11. +294
    -0
      src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyPlainResultFormatter.java
  12. +363
    -0
      src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyXmlResultFormatter.java
  13. +121
    -0
      src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/ListenerDefinition.java
  14. +14
    -0
      src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/NamedTest.java
  15. +101
    -0
      src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/SingleTestClass.java
  16. +112
    -0
      src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestClasses.java
  17. +113
    -0
      src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestDefinition.java
  18. +28
    -0
      src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestExecutionContext.java
  19. +74
    -0
      src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestRequest.java
  20. +58
    -0
      src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestResultFormatter.java
  21. +127
    -0
      src/tests/junit/org/apache/tools/ant/taskdefs/optional/junitlauncher/JUnitLauncherTaskTest.java
  22. +50
    -0
      src/tests/junit/org/apache/tools/ant/taskdefs/optional/junitlauncher/example/jupiter/JupiterSampleTest.java
  23. +16
    -0
      src/tests/junit/org/example/junitlauncher/vintage/AlwaysFailingJUnit4Test.java
  24. +25
    -0
      src/tests/junit/org/example/junitlauncher/vintage/JUnit4SampleTest.java

+ 3
- 0
WHATSNEW View File

@@ -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
=====================================



+ 28
- 1
build.xml View File

@@ -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"/>


+ 20
- 1
fetch.xml View File

@@ -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>

+ 5
- 0
lib/libraries.properties View File

@@ -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


+ 481
- 0
manual/Tasks/junitlauncher.html View File

@@ -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>

+ 113
- 0
src/etc/testcases/taskdefs/optional/junitlauncher.xml View File

@@ -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>


+ 1
- 0
src/main/org/apache/tools/ant/taskdefs/defaults.properties View File

@@ -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


+ 295
- 0
src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/AbstractJUnitResultFormatter.java View File

@@ -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());
}
}
}

+ 537
- 0
src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/JUnitLauncherTask.java View File

@@ -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());
}
}
}

+ 17
- 0
src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyBriefResultFormatter.java View File

@@ -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;
}
}

+ 294
- 0
src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyPlainResultFormatter.java View File

@@ -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;
}
}
}

+ 363
- 0
src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyXmlResultFormatter.java View File

@@ -0,0 +1,363 @@
package org.apache.tools.ant.taskdefs.optional.junitlauncher;

import org.apache.tools.ant.util.DOMElementWriter;
import org.apache.tools.ant.util.DateUtils;
import org.junit.platform.engine.TestExecutionResult;
import org.junit.platform.engine.TestSource;
import org.junit.platform.engine.reporting.ReportEntry;
import org.junit.platform.engine.support.descriptor.ClassSource;
import org.junit.platform.launcher.TestIdentifier;
import org.junit.platform.launcher.TestPlan;

import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Reader;
import java.util.Date;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

/**
* A {@link TestResultFormatter} which generates an XML report of the tests. The generated XML reports
* conforms to the schema of the XML that was generated by the {@code junit} task's XML
* report formatter and can be used by the {@code junitreport} task
*/
class LegacyXmlResultFormatter extends AbstractJUnitResultFormatter implements TestResultFormatter {

private static final double ONE_SECOND = 1000.0;

private OutputStream outputStream;
private final Map<TestIdentifier, Stats> testIds = new ConcurrentHashMap<>();
private final Map<TestIdentifier, Optional<String>> skipped = new ConcurrentHashMap<>();
private final Map<TestIdentifier, Optional<Throwable>> failed = new ConcurrentHashMap<>();
private final Map<TestIdentifier, Optional<Throwable>> aborted = new ConcurrentHashMap<>();

private TestPlan testPlan;
private long testPlanStartedAt = -1;
private long testPlanEndedAt = -1;
private final AtomicLong numTestsRun = new AtomicLong(0);
private final AtomicLong numTestsFailed = new AtomicLong(0);
private final AtomicLong numTestsSkipped = new AtomicLong(0);
private final AtomicLong numTestsAborted = new AtomicLong(0);


@Override
public void testPlanExecutionStarted(final TestPlan testPlan) {
this.testPlan = testPlan;
this.testPlanStartedAt = System.currentTimeMillis();
}

@Override
public void testPlanExecutionFinished(final TestPlan testPlan) {
this.testPlanEndedAt = System.currentTimeMillis();
// format and print out the result
try {
new XMLReportWriter().write();
} catch (IOException | XMLStreamException e) {
handleException(e);
return;
}
}

@Override
public void dynamicTestRegistered(final TestIdentifier testIdentifier) {
// nothing to do
}

@Override
public void executionSkipped(final TestIdentifier testIdentifier, final String reason) {
final long currentTime = System.currentTimeMillis();
this.numTestsSkipped.incrementAndGet();
this.skipped.put(testIdentifier, Optional.ofNullable(reason));
// a skipped test is considered started and ended now
final Stats stats = new Stats(testIdentifier, currentTime);
stats.endedAt = currentTime;
this.testIds.put(testIdentifier, stats);
}

@Override
public void executionStarted(final TestIdentifier testIdentifier) {
final long currentTime = System.currentTimeMillis();
this.testIds.putIfAbsent(testIdentifier, new Stats(testIdentifier, currentTime));
if (testIdentifier.isTest()) {
this.numTestsRun.incrementAndGet();
}
}

@Override
public void executionFinished(final TestIdentifier testIdentifier, final TestExecutionResult testExecutionResult) {
final long currentTime = System.currentTimeMillis();
final Stats stats = this.testIds.get(testIdentifier);
if (stats != null) {
stats.endedAt = currentTime;
}
switch (testExecutionResult.getStatus()) {
case SUCCESSFUL: {
break;
}
case ABORTED: {
this.numTestsAborted.incrementAndGet();
this.aborted.put(testIdentifier, testExecutionResult.getThrowable());
break;
}
case FAILED: {
this.numTestsFailed.incrementAndGet();
this.failed.put(testIdentifier, testExecutionResult.getThrowable());
break;
}
}
}

@Override
public void reportingEntryPublished(final TestIdentifier testIdentifier, final ReportEntry entry) {
// nothing to do
}

@Override
public void setDestination(final OutputStream os) {
this.outputStream = os;
}

private final class Stats {
private final TestIdentifier testIdentifier;
private final long startedAt;
private long endedAt;

private Stats(final TestIdentifier testIdentifier, final long startedAt) {
this.testIdentifier = testIdentifier;
this.startedAt = startedAt;
}
}

private final class XMLReportWriter {

private static final String ELEM_TESTSUITE = "testsuite";
private static final String ELEM_PROPERTIES = "properties";
private static final String ELEM_PROPERTY = "property";
private static final String ELEM_TESTCASE = "testcase";
private static final String ELEM_SKIPPED = "skipped";
private static final String ELEM_FAILURE = "failure";
private static final String ELEM_ABORTED = "aborted";
private static final String ELEM_SYSTEM_OUT = "system-out";
private static final String ELEM_SYSTEM_ERR = "system-err";


private static final String ATTR_CLASSNAME = "classname";
private static final String ATTR_NAME = "name";
private static final String ATTR_VALUE = "value";
private static final String ATTR_TIME = "time";
private static final String ATTR_TIMESTAMP = "timestamp";
private static final String ATTR_NUM_ABORTED = "aborted";
private static final String ATTR_NUM_FAILURES = "failures";
private static final String ATTR_NUM_TESTS = "tests";
private static final String ATTR_NUM_SKIPPED = "skipped";
private static final String ATTR_MESSAGE = "message";
private static final String ATTR_TYPE = "type";

void write() throws XMLStreamException, IOException {
final XMLStreamWriter writer = XMLOutputFactory.newFactory().createXMLStreamWriter(outputStream, "UTF-8");
try {
writer.writeStartDocument();
writeTestSuite(writer);
writer.writeEndDocument();
} finally {
writer.close();
}
}

void writeTestSuite(final XMLStreamWriter writer) throws XMLStreamException, IOException {
// write the testsuite element
writer.writeStartElement(ELEM_TESTSUITE);
final String testsuiteName = determineTestSuiteName();
writer.writeAttribute(ATTR_NAME, testsuiteName);
// time taken for the tests execution
writer.writeAttribute(ATTR_TIME, String.valueOf((testPlanEndedAt - testPlanStartedAt) / ONE_SECOND));
// add the timestamp of report generation
final String timestamp = DateUtils.format(new Date(), DateUtils.ISO8601_DATETIME_PATTERN);
writer.writeAttribute(ATTR_TIMESTAMP, timestamp);
writer.writeAttribute(ATTR_NUM_TESTS, String.valueOf(numTestsRun.longValue()));
writer.writeAttribute(ATTR_NUM_FAILURES, String.valueOf(numTestsFailed.longValue()));
writer.writeAttribute(ATTR_NUM_SKIPPED, String.valueOf(numTestsSkipped.longValue()));
writer.writeAttribute(ATTR_NUM_ABORTED, String.valueOf(numTestsAborted.longValue()));

// write the properties
writeProperties(writer);
// write the tests
writeTestCase(writer);
writeSysOut(writer);
writeSysErr(writer);
// end the testsuite
writer.writeEndElement();
}

void writeProperties(final XMLStreamWriter writer) throws XMLStreamException {
final Properties properties = LegacyXmlResultFormatter.this.context.getProperties();
if (properties == null || properties.isEmpty()) {
return;
}
writer.writeStartElement(ELEM_PROPERTIES);
for (final String prop : properties.stringPropertyNames()) {
writer.writeStartElement(ELEM_PROPERTY);
writer.writeAttribute(ATTR_NAME, prop);
writer.writeAttribute(ATTR_VALUE, properties.getProperty(prop));
writer.writeEndElement();
}
writer.writeEndElement();
}

void writeTestCase(final XMLStreamWriter writer) throws XMLStreamException {
for (final Map.Entry<TestIdentifier, Stats> entry : testIds.entrySet()) {
final TestIdentifier testId = entry.getKey();
if (!testId.isTest()) {
// only interested in test methods
continue;
}
// find the parent class of this test method
final Optional<TestIdentifier> parent = testPlan.getParent(testId);
if (!parent.isPresent() || !parent.get().getSource().isPresent()) {
// we need to know the parent test class, else we aren't interested
continue;
}
final TestSource parentSource = parent.get().getSource().get();
if (!(parentSource instanceof ClassSource)) {
continue;
}
final String classname = ((ClassSource) parentSource).getClassName();
writer.writeStartElement(ELEM_TESTCASE);
writer.writeAttribute(ATTR_CLASSNAME, classname);
writer.writeAttribute(ATTR_NAME, testId.getDisplayName());
final Stats stats = entry.getValue();
writer.writeAttribute(ATTR_TIME, String.valueOf((stats.endedAt - stats.startedAt) / ONE_SECOND));
// skipped element if the test was skipped
writeSkipped(writer, testId);
// failed element if the test failed
writeFailed(writer, testId);
// aborted element if the test was aborted
writeAborted(writer, testId);

writer.writeEndElement();
}
}

private void writeSkipped(final XMLStreamWriter writer, final TestIdentifier testIdentifier) throws XMLStreamException {
if (!skipped.containsKey(testIdentifier)) {
return;
}
writer.writeStartElement(ELEM_SKIPPED);
final Optional<String> reason = skipped.get(testIdentifier);
if (reason.isPresent()) {
writer.writeAttribute(ATTR_MESSAGE, reason.get());
}
writer.writeEndElement();
}

private void writeFailed(final XMLStreamWriter writer, final TestIdentifier testIdentifier) throws XMLStreamException {
if (!failed.containsKey(testIdentifier)) {
return;
}
writer.writeStartElement(ELEM_FAILURE);
final Optional<Throwable> cause = failed.get(testIdentifier);
if (cause.isPresent()) {
final Throwable t = cause.get();
final String message = t.getMessage();
if (message != null && !message.trim().isEmpty()) {
writer.writeAttribute(ATTR_MESSAGE, message);
}
writer.writeAttribute(ATTR_TYPE, t.getClass().getName());
}
writer.writeEndElement();
}

private void writeAborted(final XMLStreamWriter writer, final TestIdentifier testIdentifier) throws XMLStreamException {
if (!aborted.containsKey(testIdentifier)) {
return;
}
writer.writeStartElement(ELEM_ABORTED);
final Optional<Throwable> cause = aborted.get(testIdentifier);
if (cause.isPresent()) {
final Throwable t = cause.get();
final String message = t.getMessage();
if (message != null && !message.trim().isEmpty()) {
writer.writeAttribute(ATTR_MESSAGE, message);
}
writer.writeAttribute(ATTR_TYPE, t.getClass().getName());
}
writer.writeEndElement();
}

private void writeSysOut(final XMLStreamWriter writer) throws XMLStreamException, IOException {
if (!LegacyXmlResultFormatter.this.hasSysOut()) {
return;
}
writer.writeStartElement(ELEM_SYSTEM_OUT);
try (final Reader reader = LegacyXmlResultFormatter.this.getSysOutReader()) {
writeCharactersFrom(reader, writer);
}
writer.writeEndElement();
}

private void writeSysErr(final XMLStreamWriter writer) throws XMLStreamException, IOException {
if (!LegacyXmlResultFormatter.this.hasSysErr()) {
return;
}
writer.writeStartElement(ELEM_SYSTEM_ERR);
try (final Reader reader = LegacyXmlResultFormatter.this.getSysErrReader()) {
writeCharactersFrom(reader, writer);
}
writer.writeEndElement();
}

private void writeCharactersFrom(final Reader reader, final XMLStreamWriter writer) throws IOException, XMLStreamException {
final char[] chars = new char[1024];
int numRead = -1;
while ((numRead = reader.read(chars)) != -1) {
// although it's called a DOMElementWriter, the encode method is just a
// straight forward XML util method which doesn't concern about whether
// DOM, SAX, StAX semantics.
// TODO: Perhaps make it a static method
final String encoded = new DOMElementWriter().encode(new String(chars, 0, numRead));
writer.writeCharacters(encoded);
}
}

private String determineTestSuiteName() {
// this is really a hack to try and match the expectations of the XML report in JUnit4.x
// world. In JUnit5, the TestPlan doesn't have a name and a TestPlan (for which this is a
// listener) can have numerous tests within it
final Set<TestIdentifier> roots = testPlan.getRoots();
if (roots.isEmpty()) {
return "UNKNOWN";
}
for (final TestIdentifier root : roots) {
final Optional<ClassSource> classSource = findFirstClassSource(root);
if (classSource.isPresent()) {
return classSource.get().getClassName();
}
}
return "UNKNOWN";
}

private Optional<ClassSource> findFirstClassSource(final TestIdentifier root) {
if (root.getSource().isPresent()) {
final TestSource source = root.getSource().get();
if (source instanceof ClassSource) {
return Optional.of((ClassSource) source);
}
}
for (final TestIdentifier child : testPlan.getChildren(root)) {
final Optional<ClassSource> classSource = findFirstClassSource(child);
if (classSource.isPresent()) {
return classSource;
}
}
return Optional.empty();
}
}

}

+ 121
- 0
src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/ListenerDefinition.java View File

@@ -0,0 +1,121 @@
package org.apache.tools.ant.taskdefs.optional.junitlauncher;

import org.apache.tools.ant.Project;
import org.apache.tools.ant.PropertyHelper;
import org.apache.tools.ant.types.EnumeratedAttribute;

/**
* Represents the {@code &lt;listener&gt;} element within the {@code &lt;junitlauncher&gt;}
* task
*/
public class ListenerDefinition {

private static final String LEGACY_PLAIN = "legacy-plain";
private static final String LEGACY_BRIEF = "legacy-brief";
private static final String LEGACY_XML = "legacy-xml";

private String ifProperty;
private String unlessProperty;
private String className;
private String resultFile;
private boolean sendSysOut;
private boolean sendSysErr;

private String defaultResultFileSuffix = "txt";

public ListenerDefinition() {

}

public void setClassName(final String className) {
this.className = className;
}

String getClassName() {
return this.className;
}

String getIfProperty() {
return ifProperty;
}

public void setIf(final String ifProperty) {
this.ifProperty = ifProperty;
}

String getUnlessProperty() {
return unlessProperty;
}

public void setUnless(final String unlessProperty) {
this.unlessProperty = unlessProperty;
}

public void setType(final ListenerType type) {
switch (type.getValue()) {
case LEGACY_PLAIN: {
this.setClassName("org.apache.tools.ant.taskdefs.optional.junitlauncher.LegacyPlainResultFormatter");
this.defaultResultFileSuffix = "txt";
break;
}
case LEGACY_BRIEF: {
this.setClassName("org.apache.tools.ant.taskdefs.optional.junitlauncher.LegacyBriefResultFormatter");
this.defaultResultFileSuffix = "txt";
break;
}
case LEGACY_XML: {
this.setClassName("org.apache.tools.ant.taskdefs.optional.junitlauncher.LegacyXmlResultFormatter");
this.defaultResultFileSuffix = "xml";
break;
}
}
}

public void setResultFile(final String filename) {
this.resultFile = filename;
}

String requireResultFile(final TestDefinition test) {
if (this.resultFile != null) {
return this.resultFile;
}
final StringBuilder sb = new StringBuilder("TEST-");
if (test instanceof NamedTest) {
sb.append(((NamedTest) test).getName());
} else {
sb.append("unknown");
}
sb.append(".").append(this.defaultResultFileSuffix);
return sb.toString();
}

public void setSendSysOut(final boolean sendSysOut) {
this.sendSysOut = sendSysOut;
}

boolean shouldSendSysOut() {
return this.sendSysOut;
}

public void setSendSysErr(final boolean sendSysErr) {
this.sendSysErr = sendSysErr;
}

boolean shouldSendSysErr() {
return this.sendSysErr;
}

protected boolean shouldUse(final Project project) {
final PropertyHelper propertyHelper = PropertyHelper.getPropertyHelper(project);
return propertyHelper.testIfCondition(this.ifProperty) && propertyHelper.testUnlessCondition(this.unlessProperty);
}

public static class ListenerType extends EnumeratedAttribute {

@Override
public String[] getValues() {
return new String[]{LEGACY_PLAIN, LEGACY_BRIEF, LEGACY_XML};
}
}

}

+ 14
- 0
src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/NamedTest.java View File

@@ -0,0 +1,14 @@
package org.apache.tools.ant.taskdefs.optional.junitlauncher;

/**
* A test that has a name associated with it
*/
public interface NamedTest {

/**
* Returns the name of the test
*
* @return
*/
String getName();
}

+ 101
- 0
src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/SingleTestClass.java View File

@@ -0,0 +1,101 @@
package org.apache.tools.ant.taskdefs.optional.junitlauncher;

import org.apache.tools.ant.Project;
import org.junit.Test;
import org.junit.platform.engine.discovery.DiscoverySelectors;
import org.junit.platform.launcher.EngineFilter;
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;

import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;

/**
* Represents the single {@code test} (class) that's configured to be launched by the {@link JUnitLauncherTask}
*/
public class SingleTestClass extends TestDefinition implements NamedTest {

private String testClass;
private Set<String> testMethods;

public SingleTestClass() {

}

public void setName(final String test) {
if (test == null || test.trim().isEmpty()) {
throw new IllegalArgumentException("Test name cannot be null or empty string");
}
this.testClass = test;
}

@Test
public String getName() {
return this.testClass;
}

public void setMethods(final String methods) {
// parse the comma separated set of methods
if (methods == null || methods.trim().isEmpty()) {
this.testMethods = Collections.emptySet();
return;
}
final StringTokenizer tokenizer = new StringTokenizer(methods, ",");
if (!tokenizer.hasMoreTokens()) {
this.testMethods = Collections.emptySet();
return;
}
// maintain specified ordering
this.testMethods = new LinkedHashSet<>();
while (tokenizer.hasMoreTokens()) {
final String method = tokenizer.nextToken().trim();
if (method.isEmpty()) {
continue;
}
this.testMethods.add(method);
}
}

boolean hasMethodsSpecified() {
return this.testMethods != null && !this.testMethods.isEmpty();
}

String[] getMethods() {
if (!hasMethodsSpecified()) {
return null;
}
return this.testMethods.toArray(new String[this.testMethods.size()]);
}

@Override
List<TestRequest> createTestRequests(final JUnitLauncherTask launcherTask) {
final Project project = launcherTask.getProject();
if (!shouldRun(project)) {
launcherTask.log("Excluding test " + this.testClass + " since it's considered not to run " +
"in context of project " + project, Project.MSG_DEBUG);
return Collections.emptyList();
}
final LauncherDiscoveryRequestBuilder requestBuilder = LauncherDiscoveryRequestBuilder.request();
if (!this.hasMethodsSpecified()) {
requestBuilder.selectors(DiscoverySelectors.selectClass(this.testClass));
} else {
// add specific methods
final String[] methods = this.getMethods();
for (final String method : methods) {
requestBuilder.selectors(DiscoverySelectors.selectMethod(this.testClass, method));
}
}
// add any engine filters
final String[] enginesToInclude = this.getIncludeEngines();
if (enginesToInclude != null && enginesToInclude.length > 0) {
requestBuilder.filters(EngineFilter.includeEngines(enginesToInclude));
}
final String[] enginesToExclude = this.getExcludeEngines();
if (enginesToExclude != null && enginesToExclude.length > 0) {
requestBuilder.filters(EngineFilter.excludeEngines(enginesToExclude));
}
return Collections.singletonList(new TestRequest(this, requestBuilder));
}
}

+ 112
- 0
src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestClasses.java View File

@@ -0,0 +1,112 @@
package org.apache.tools.ant.taskdefs.optional.junitlauncher;

import org.apache.tools.ant.types.Resource;
import org.apache.tools.ant.types.ResourceCollection;
import org.apache.tools.ant.types.resources.Resources;

import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
* Represents a {@code testclasses} that's configured to be launched by the {@link JUnitLauncherTask}
*/
public class TestClasses extends TestDefinition {

private final Resources resources = new Resources();

public TestClasses() {

}

public void add(final ResourceCollection resourceCollection) {
this.resources.add(resourceCollection);
}

@Override
List<TestRequest> createTestRequests(final JUnitLauncherTask launcherTask) {
final List<SingleTestClass> tests = this.getTests();
if (tests.isEmpty()) {
return Collections.emptyList();
}
final List<TestRequest> requests = new ArrayList<>();
for (final SingleTestClass test : tests) {
requests.addAll(test.createTestRequests(launcherTask));
}
return requests;
}

private List<SingleTestClass> getTests() {
if (this.resources.isEmpty()) {
return Collections.emptyList();
}
final List<SingleTestClass> tests = new ArrayList<>();
for (final Resource resource : resources) {
if (!resource.isExists()) {
continue;
}
final String name = resource.getName();
// we only consider .class files
if (!name.endsWith(".class")) {
continue;
}
final String className = name.substring(0, name.lastIndexOf('.'));
final BatchSourcedSingleTest test = new BatchSourcedSingleTest(className.replace(File.separatorChar, '.').replace('/', '.').replace('\\', '.'));
tests.add(test);
}
return tests;
}

/**
* A {@link BatchSourcedSingleTest} is similar to a {@link SingleTestClass} except that
* some of the characteristics of the test (like whether to halt on failure) are borrowed
* from the {@link TestClasses batch} to which this test belongs to
*/
private final class BatchSourcedSingleTest extends SingleTestClass {

private BatchSourcedSingleTest(final String testClassName) {
this.setName(testClassName);
}

@Override
String getIfProperty() {
return TestClasses.this.getIfProperty();
}

@Override
String getUnlessProperty() {
return TestClasses.this.getUnlessProperty();
}

@Override
boolean isHaltOnFailure() {
return TestClasses.this.isHaltOnFailure();
}

@Override
String getFailureProperty() {
return TestClasses.this.getFailureProperty();
}

@Override
List<ListenerDefinition> getListeners() {
return TestClasses.this.getListeners();
}

@Override
String getOutputDir() {
return TestClasses.this.getOutputDir();
}

@Override
String[] getIncludeEngines() {
return TestClasses.this.getIncludeEngines();
}

@Override
String[] getExcludeEngines() {
return TestClasses.this.getExcludeEngines();
}
}
}

+ 113
- 0
src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestDefinition.java View File

@@ -0,0 +1,113 @@
package org.apache.tools.ant.taskdefs.optional.junitlauncher;

import org.apache.tools.ant.Project;
import org.apache.tools.ant.PropertyHelper;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
* Represents the configuration details of a test that needs to be launched by the {@link JUnitLauncherTask}
*/
abstract class TestDefinition {
protected String ifProperty;
protected String unlessProperty;
protected Boolean haltOnFailure;
protected String failureProperty;
protected String outputDir;
protected String includeEngines;
protected String excludeEngines;

protected List<ListenerDefinition> listeners = new ArrayList<>();

String getIfProperty() {
return ifProperty;
}

public void setIf(final String ifProperty) {
this.ifProperty = ifProperty;
}

String getUnlessProperty() {
return unlessProperty;
}

public void setUnless(final String unlessProperty) {
this.unlessProperty = unlessProperty;
}

boolean isHaltOnFailure() {
return this.haltOnFailure == null ? false : this.haltOnFailure;
}

Boolean getHaltOnFailure() {
return this.haltOnFailure;
}

public void setHaltOnFailure(final boolean haltonfailure) {
this.haltOnFailure = haltonfailure;
}

String getFailureProperty() {
return failureProperty;
}

public void setFailureProperty(final String failureProperty) {
this.failureProperty = failureProperty;
}

public void addConfiguredListener(final ListenerDefinition listener) {
this.listeners.add(listener);
}

List<ListenerDefinition> getListeners() {
return Collections.unmodifiableList(this.listeners);
}

public void setOutputDir(final String dir) {
this.outputDir = dir;
}

String getOutputDir() {
return this.outputDir;
}

abstract List<TestRequest> createTestRequests(final JUnitLauncherTask launcherTask);

protected boolean shouldRun(final Project project) {
final PropertyHelper propertyHelper = PropertyHelper.getPropertyHelper(project);
return propertyHelper.testIfCondition(this.ifProperty) && propertyHelper.testUnlessCondition(this.unlessProperty);
}

String[] getIncludeEngines() {
return includeEngines == null ? new String[0] : split(this.includeEngines, ",");
}

public void setIncludeEngines(final String includeEngines) {
this.includeEngines = includeEngines;
}

String[] getExcludeEngines() {
return excludeEngines == null ? new String[0] : split(this.excludeEngines, ",");
}

public void setExcludeEngines(final String excludeEngines) {
this.excludeEngines = excludeEngines;
}

private static String[] split(final String value, final String delimiter) {
if (value == null) {
return new String[0];
}
final List<String> parts = new ArrayList<>();
for (final String part : value.split(delimiter)) {
if (part.trim().isEmpty()) {
// skip it
continue;
}
parts.add(part);
}
return parts.toArray(new String[parts.size()]);
}
}

+ 28
- 0
src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestExecutionContext.java View File

@@ -0,0 +1,28 @@
package org.apache.tools.ant.taskdefs.optional.junitlauncher;

import org.apache.tools.ant.Project;

import java.util.Optional;
import java.util.Properties;

/**
* A {@link TestExecutionContext} represents the execution context for a test
* that has been launched by the {@link JUnitLauncherTask} and provides any necessary
* contextual information about such tests.
*/
public interface TestExecutionContext {

/**
* @return Returns the properties that were used for the execution of the test
*/
Properties getProperties();


/**
* @return Returns the {@link Project} in whose context the test is being executed.
* The {@code Project} is sometimes not available, like in the case where
* the test is being run in a forked mode, in such cases this method returns
* {@link Optional#empty() an empty value}
*/
Optional<Project> getProject();
}

+ 74
- 0
src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestRequest.java View File

@@ -0,0 +1,74 @@
package org.apache.tools.ant.taskdefs.optional.junitlauncher;

import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;

import java.io.Closeable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

/**
* Holds together the necessary details about a request that will be launched by the {@link JUnitLauncherTask}
*/
final class TestRequest implements AutoCloseable {
private final TestDefinition ownerTest;
private final LauncherDiscoveryRequestBuilder discoveryRequest;
private final List<Closeable> closables = new ArrayList<>();
private final List<TestResultFormatter> interestedInSysOut = new ArrayList<>();
private final List<TestResultFormatter> interestedInSysErr = new ArrayList<>();


TestRequest(final TestDefinition ownerTest, final LauncherDiscoveryRequestBuilder discoveryRequest) {
this.ownerTest = ownerTest;
this.discoveryRequest = discoveryRequest;
}

TestDefinition getOwner() {
return ownerTest;
}

LauncherDiscoveryRequestBuilder getDiscoveryRequest() {
return discoveryRequest;
}

void closeUponCompletion(final Closeable closeable) {
if (closeable == null) {
return;
}
this.closables.add(closeable);
}

void addSysOutInterest(final TestResultFormatter out) {
this.interestedInSysOut.add(out);
}

boolean interestedInSysOut() {
return !this.interestedInSysOut.isEmpty();
}

Collection<TestResultFormatter> getSysOutInterests() {
return Collections.unmodifiableList(this.interestedInSysOut);
}

void addSysErrInterest(final TestResultFormatter err) {
this.interestedInSysErr.add(err);
}

boolean interestedInSysErr() {
return !this.interestedInSysErr.isEmpty();
}

Collection<TestResultFormatter> getSysErrInterests() {
return Collections.unmodifiableList(this.interestedInSysErr);
}

public void close() throws Exception {
if (this.closables.isEmpty()) {
return;
}
for (final Closeable closeable : closables) {
closeable.close();
}
}
}

+ 58
- 0
src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestResultFormatter.java View File

@@ -0,0 +1,58 @@
package org.apache.tools.ant.taskdefs.optional.junitlauncher;

import org.junit.platform.launcher.TestExecutionListener;

import java.io.Closeable;
import java.io.OutputStream;

/**
* A {@link TestExecutionListener} which lets implementing classes format and write out
* the test execution results.
*/
public interface TestResultFormatter extends TestExecutionListener, Closeable {

/**
* This method will be invoked by the <code>junitlauncher</code> and will be passed the
* {@link OutputStream} to a file, to which the formatted result is expected to be written
* to.
* <p>
* This method will be called once, early on, during the initialization of this
* {@link TestResultFormatter}, typically before the test execution itself has started.
* </p>
*
* @param os The output stream to which to write out the result
*/
void setDestination(OutputStream os);

/**
* This method will be invoked by the <code>junitlauncher</code> and will be passed a
* {@link TestExecutionContext}. This allows the {@link TestResultFormatter} to have access
* to any additional contextual information to use in the test reports.
*
* @param context The context of the execution of the test
*/
void setContext(TestExecutionContext context);

/**
* This method will be invoked by the <code>junitlauncher</code>, <strong>regularly/multiple times</strong>,
* as and when any content is generated on the standard output stream during the test execution.
* This method will be only be called if the <code>sendSysOut</code> attribute of the <code>listener</code>,
* to which this {@link TestResultFormatter} is configured for, is enabled
*
* @param data The content generated on standard output stream
*/
default void sysOutAvailable(byte[] data) {
}

/**
* This method will be invoked by the <code>junitlauncher</code>, <strong>regularly/multiple times</strong>,
* as and when any content is generated on the standard error stream during the test execution.
* This method will be only be called if the <code>sendSysErr</code> attribute of the <code>listener</code>,
* to which this {@link TestResultFormatter} is configured for, is enabled
*
* @param data The content generated on standard error stream
*/
default void sysErrAvailable(byte[] data) {
}

}

+ 127
- 0
src/tests/junit/org/apache/tools/ant/taskdefs/optional/junitlauncher/JUnitLauncherTaskTest.java View File

@@ -0,0 +1,127 @@
package org.apache.tools.ant.taskdefs.optional.junitlauncher;

import org.apache.tools.ant.BuildEvent;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.BuildListener;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.ProjectHelper;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

import java.io.File;

/**
* Tests the {@link JUnitLauncherTask}
*/
public class JUnitLauncherTaskTest {

private Project project;

/**
* The JUnit setup method.
*/
@Before
public void setUp() {
File antFile = new File(System.getProperty("root"), "src/etc/testcases/taskdefs/optional/junitlauncher.xml");
this.project = new Project();
this.project.init();
ProjectHelper.configureProject(project, antFile);
project.addBuildListener(new BuildListener() {
@Override
public void buildStarted(final BuildEvent event) {

}

@Override
public void buildFinished(final BuildEvent event) {

}

@Override
public void targetStarted(final BuildEvent event) {

}

@Override
public void targetFinished(final BuildEvent event) {

}

@Override
public void taskStarted(final BuildEvent event) {

}

@Override
public void taskFinished(final BuildEvent event) {

}

@Override
public void messageLogged(final BuildEvent event) {
if (event.getPriority() <= Project.MSG_INFO) {
System.out.println(event.getMessage());
}
}
});
}

/**
* Tests that when a test, that's configured with {@code haltOnFailure=true}, stops the build, when the
* test fails
*/
@Test
public void testFailureStopsBuild() {
try {
project.executeTarget("test-failure-stops-build");
Assert.fail("Test execution failure was expected to stop the build but didn't");
} catch (BuildException be) {
// expected
}
}

/**
* Tests that when a test, that's isn't configured with {@code haltOnFailure=true}, continues the
* build even when there are test failures
*/
@Test
public void testFailureContinuesBuild() {
project.executeTarget("test-failure-continues-build");
}


/**
* Tests the execution of test that's expected to succeed
*/
@Test
public void testSuccessfulTests() {
project.executeTarget("test-success");
}

/**
* Tests execution of a test which is configured to execute only a particular set of test methods
*/
@Test
public void testSpecificMethodTest() {
project.executeTarget("test-one-specific-method");
project.executeTarget("test-multiple-specific-methods");
}

/**
* Tests the execution of more than one {@code &lt;test&gt;} elements in the {@code &lt;junitlauncher&gt;} task
*/
@Test
public void testMultipleIndividualTests() {
project.executeTarget("test-multiple-individual");
}

/**
* Tests execution of tests, that have been configured using the {@code &lt;testclasses&gt;} nested element
* of the {@code &lt;junitlauncher&gt;} task
*/
@Test
public void testTestClasses() {
project.executeTarget("test-batch");
}
}

+ 50
- 0
src/tests/junit/org/apache/tools/ant/taskdefs/optional/junitlauncher/example/jupiter/JupiterSampleTest.java View File

@@ -0,0 +1,50 @@
package org.apache.tools.ant.taskdefs.optional.junitlauncher.example.jupiter;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.fail;

/**
*
*/
public class JupiterSampleTest {

private static final String message = "The quick brown fox jumps over the lazy dog";

@BeforeAll
static void beforeAll() {
}

@BeforeEach
void beforeEach() {
}

@Test
void testSucceeds() {
System.out.println(message);
System.out.print("<some-other-message>Hello world! <!-- some comment --></some-other-message>");
}

@Test
void testFails() {
fail("intentionally failing");
}

@Test
@Disabled("intentionally skipped")
void testSkipped() {
}

@AfterEach
void afterEach() {
}

@AfterAll
static void afterAll() {
}
}

+ 16
- 0
src/tests/junit/org/example/junitlauncher/vintage/AlwaysFailingJUnit4Test.java View File

@@ -0,0 +1,16 @@
package org.example.junitlauncher.vintage;

import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;

/**
*
*/
public class AlwaysFailingJUnit4Test {

@Test
public void testWillFail() throws Exception {
Assert.assertEquals("Values weren't equal", 3, 4);
}
}

+ 25
- 0
src/tests/junit/org/example/junitlauncher/vintage/JUnit4SampleTest.java View File

@@ -0,0 +1,25 @@
package org.example.junitlauncher.vintage;

import org.junit.Assert;
import org.junit.Test;

/**
*
*/
public class JUnit4SampleTest {

@Test
public void testFoo() {
Assert.assertEquals(1, 1);
}

@Test
public void testBar() throws Exception {
Assert.assertTrue(true);
}

@Test
public void testFooBar() {
Assert.assertFalse(false);
}
}

Loading…
Cancel
Save