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