@@ -37,6 +37,9 @@ Other changes: | |||||
requested. Java11 removes support for CORBA and the switches have | requested. Java11 removes support for CORBA and the switches have | ||||
been removed from the rmic tool. | 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 | Changes from Ant 1.10.1 TO Ant 1.10.2 | ||||
===================================== | ===================================== | ||||
@@ -208,6 +208,20 @@ | |||||
</or> | </or> | ||||
</selector> | </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"> | <selector id="needs.apache-regexp"> | ||||
<filename name="${regexp.package}/JakartaRegexp*"/> | <filename name="${regexp.package}/JakartaRegexp*"/> | ||||
</selector> | </selector> | ||||
@@ -322,6 +336,7 @@ | |||||
<selector refid="needs.jsch"/> | <selector refid="needs.jsch"/> | ||||
<selector refid="needs.junit"/> | <selector refid="needs.junit"/> | ||||
<selector refid="needs.junit4"/> | <selector refid="needs.junit4"/> | ||||
<selector refid="needs.junitlauncher"/> | |||||
<selector refid="needs.netrexx"/> | <selector refid="needs.netrexx"/> | ||||
<selector refid="needs.swing"/> | <selector refid="needs.swing"/> | ||||
<selector refid="needs.xz"/> | <selector refid="needs.xz"/> | ||||
@@ -405,6 +420,15 @@ | |||||
<available property="junit4.present" | <available property="junit4.present" | ||||
classname="org.junit.Test" | classname="org.junit.Test" | ||||
classpathref="classpath" ignoresystemclasses="${ignoresystemclasses}"/> | 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" | <available property="antunit.present" | ||||
classname="org.apache.ant.antunit.AntUnit" | classname="org.apache.ant.antunit.AntUnit" | ||||
classpathref="classpath" ignoresystemclasses="${ignoresystemclasses}"/> | classpathref="classpath" ignoresystemclasses="${ignoresystemclasses}"/> | ||||
@@ -562,10 +586,12 @@ | |||||
<not> | <not> | ||||
<or> | <or> | ||||
<selector refid="not.in.kaffe" if="kaffe"/> | <selector refid="not.in.kaffe" if="kaffe"/> | ||||
<selector refid="needs.apache-resolver" unless="apache.resolver.present"/> | <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.junit" unless="junit.present"/> <!-- TODO should perhaps use -source 1.4? --> | ||||
<selector refid="needs.junit4" unless="junit4.present"/> | <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-regexp" unless="apache.regexp.present"/> | ||||
<selector refid="needs.apache-oro" unless="apache.oro.present"/> | <selector refid="needs.apache-oro" unless="apache.oro.present"/> | ||||
<selector refid="needs.apache-bcel" unless="bcel.present"/> | <selector refid="needs.apache-bcel" unless="bcel.present"/> | ||||
@@ -733,6 +759,7 @@ | |||||
<optional-jar dep="apache-resolver"/> | <optional-jar dep="apache-resolver"/> | ||||
<optional-jar dep="junit"/> | <optional-jar dep="junit"/> | ||||
<optional-jar dep="junit4"/> | <optional-jar dep="junit4"/> | ||||
<optional-jar dep="junitlauncher"/> | |||||
<optional-jar dep="apache-regexp"/> | <optional-jar dep="apache-regexp"/> | ||||
<optional-jar dep="apache-oro"/> | <optional-jar dep="apache-oro"/> | ||||
<optional-jar dep="apache-bcel"/> | <optional-jar dep="apache-bcel"/> | ||||
@@ -232,6 +232,24 @@ Set -Ddest=LOCATION on the command line | |||||
<f2 project="org.hamcrest" archive="hamcrest-library"/> | <f2 project="org.hamcrest" archive="hamcrest-library"/> | ||||
</target> | </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" | <target name="xml" | ||||
description="load full XML libraries (Xalan and xml-resolver)" | description="load full XML libraries (Xalan and xml-resolver)" | ||||
depends="init"> | depends="init"> | ||||
@@ -367,5 +385,6 @@ Set -Ddest=LOCATION on the command line | |||||
<target name="all" | <target name="all" | ||||
description="load all the libraries (except jython)" | 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> | </project> |
@@ -53,6 +53,11 @@ jdepend.version=2.9.1 | |||||
jruby.version=1.6.8 | jruby.version=1.6.8 | ||||
junit.version=4.12 | junit.version=4.12 | ||||
rhino.version=1.7.8 | 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 | jsch.version=0.1.54 | ||||
jython.version=2.7.0 | jython.version=2.7.0 | ||||
# log4j 1.2.15 requires JMS and a few other Sun jars that are not in the m2 repo | # 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 | jjtree=org.apache.tools.ant.taskdefs.optional.javacc.JJTree | ||||
junit=org.apache.tools.ant.taskdefs.optional.junit.JUnitTask | junit=org.apache.tools.ant.taskdefs.optional.junit.JUnitTask | ||||
junitreport=org.apache.tools.ant.taskdefs.optional.junit.XMLResultAggregator | 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 | native2ascii=org.apache.tools.ant.taskdefs.optional.Native2Ascii | ||||
netrexxc=org.apache.tools.ant.taskdefs.optional.NetRexxC | netrexxc=org.apache.tools.ant.taskdefs.optional.NetRexxC | ||||
propertyfile=org.apache.tools.ant.taskdefs.optional.PropertyFile | 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); | |||||
} | |||||
} |