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