You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@ant.apache.org by ja...@apache.org on 2018/03/17 10:54:05 UTC
[1/2] ant git commit: JUnit 5 support - A new junitlauncher task
Repository: ant
Updated Branches:
refs/heads/master c5e87fec8 -> 063e60813
http://git-wip-us.apache.org/repos/asf/ant/blob/063e6081/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyXmlResultFormatter.java
----------------------------------------------------------------------
diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyXmlResultFormatter.java b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyXmlResultFormatter.java
new file mode 100644
index 0000000..3a94a80
--- /dev/null
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyXmlResultFormatter.java
@@ -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();
+ }
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/ant/blob/063e6081/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/ListenerDefinition.java
----------------------------------------------------------------------
diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/ListenerDefinition.java b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/ListenerDefinition.java
new file mode 100644
index 0000000..afbc161
--- /dev/null
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/ListenerDefinition.java
@@ -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};
+ }
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/ant/blob/063e6081/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/NamedTest.java
----------------------------------------------------------------------
diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/NamedTest.java b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/NamedTest.java
new file mode 100644
index 0000000..5061c31
--- /dev/null
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/NamedTest.java
@@ -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();
+}
http://git-wip-us.apache.org/repos/asf/ant/blob/063e6081/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/SingleTestClass.java
----------------------------------------------------------------------
diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/SingleTestClass.java b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/SingleTestClass.java
new file mode 100644
index 0000000..f044e7e
--- /dev/null
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/SingleTestClass.java
@@ -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));
+ }
+}
http://git-wip-us.apache.org/repos/asf/ant/blob/063e6081/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestClasses.java
----------------------------------------------------------------------
diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestClasses.java b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestClasses.java
new file mode 100644
index 0000000..451e79f
--- /dev/null
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestClasses.java
@@ -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();
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/ant/blob/063e6081/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestDefinition.java
----------------------------------------------------------------------
diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestDefinition.java b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestDefinition.java
new file mode 100644
index 0000000..d05ae3e
--- /dev/null
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestDefinition.java
@@ -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()]);
+ }
+}
http://git-wip-us.apache.org/repos/asf/ant/blob/063e6081/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestExecutionContext.java
----------------------------------------------------------------------
diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestExecutionContext.java b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestExecutionContext.java
new file mode 100644
index 0000000..fd08a1b
--- /dev/null
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestExecutionContext.java
@@ -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();
+}
http://git-wip-us.apache.org/repos/asf/ant/blob/063e6081/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestRequest.java
----------------------------------------------------------------------
diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestRequest.java b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestRequest.java
new file mode 100644
index 0000000..a56b3d2
--- /dev/null
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestRequest.java
@@ -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();
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/ant/blob/063e6081/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestResultFormatter.java
----------------------------------------------------------------------
diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestResultFormatter.java b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestResultFormatter.java
new file mode 100644
index 0000000..ad1e4f3
--- /dev/null
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestResultFormatter.java
@@ -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) {
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/ant/blob/063e6081/src/tests/junit/org/apache/tools/ant/taskdefs/optional/junitlauncher/JUnitLauncherTaskTest.java
----------------------------------------------------------------------
diff --git a/src/tests/junit/org/apache/tools/ant/taskdefs/optional/junitlauncher/JUnitLauncherTaskTest.java b/src/tests/junit/org/apache/tools/ant/taskdefs/optional/junitlauncher/JUnitLauncherTaskTest.java
new file mode 100644
index 0000000..84d4875
--- /dev/null
+++ b/src/tests/junit/org/apache/tools/ant/taskdefs/optional/junitlauncher/JUnitLauncherTaskTest.java
@@ -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");
+ }
+}
http://git-wip-us.apache.org/repos/asf/ant/blob/063e6081/src/tests/junit/org/apache/tools/ant/taskdefs/optional/junitlauncher/example/jupiter/JupiterSampleTest.java
----------------------------------------------------------------------
diff --git a/src/tests/junit/org/apache/tools/ant/taskdefs/optional/junitlauncher/example/jupiter/JupiterSampleTest.java b/src/tests/junit/org/apache/tools/ant/taskdefs/optional/junitlauncher/example/jupiter/JupiterSampleTest.java
new file mode 100644
index 0000000..38b4919
--- /dev/null
+++ b/src/tests/junit/org/apache/tools/ant/taskdefs/optional/junitlauncher/example/jupiter/JupiterSampleTest.java
@@ -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() {
+ }
+}
http://git-wip-us.apache.org/repos/asf/ant/blob/063e6081/src/tests/junit/org/example/junitlauncher/vintage/AlwaysFailingJUnit4Test.java
----------------------------------------------------------------------
diff --git a/src/tests/junit/org/example/junitlauncher/vintage/AlwaysFailingJUnit4Test.java b/src/tests/junit/org/example/junitlauncher/vintage/AlwaysFailingJUnit4Test.java
new file mode 100644
index 0000000..ce4f162
--- /dev/null
+++ b/src/tests/junit/org/example/junitlauncher/vintage/AlwaysFailingJUnit4Test.java
@@ -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);
+ }
+}
http://git-wip-us.apache.org/repos/asf/ant/blob/063e6081/src/tests/junit/org/example/junitlauncher/vintage/JUnit4SampleTest.java
----------------------------------------------------------------------
diff --git a/src/tests/junit/org/example/junitlauncher/vintage/JUnit4SampleTest.java b/src/tests/junit/org/example/junitlauncher/vintage/JUnit4SampleTest.java
new file mode 100644
index 0000000..bf606a2
--- /dev/null
+++ b/src/tests/junit/org/example/junitlauncher/vintage/JUnit4SampleTest.java
@@ -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);
+ }
+}
[2/2] ant git commit: JUnit 5 support - A new junitlauncher task
Posted by ja...@apache.org.
JUnit 5 support - A new junitlauncher task
Project: http://git-wip-us.apache.org/repos/asf/ant/repo
Commit: http://git-wip-us.apache.org/repos/asf/ant/commit/063e6081
Tree: http://git-wip-us.apache.org/repos/asf/ant/tree/063e6081
Diff: http://git-wip-us.apache.org/repos/asf/ant/diff/063e6081
Branch: refs/heads/master
Commit: 063e60813af955e6db6353ede79249a7bab9d63e
Parents: c5e87fe
Author: Jaikiran Pai <ja...@apache.org>
Authored: Wed Dec 13 19:07:41 2017 +0530
Committer: Jaikiran Pai <ja...@apache.org>
Committed: Sat Mar 17 16:23:44 2018 +0530
----------------------------------------------------------------------
WHATSNEW | 3 +
build.xml | 29 +-
fetch.xml | 21 +-
lib/libraries.properties | 5 +
manual/Tasks/junitlauncher.html | 481 +++++++++++++++++
.../taskdefs/optional/junitlauncher.xml | 113 ++++
.../tools/ant/taskdefs/defaults.properties | 1 +
.../AbstractJUnitResultFormatter.java | 295 ++++++++++
.../junitlauncher/JUnitLauncherTask.java | 537 +++++++++++++++++++
.../LegacyBriefResultFormatter.java | 17 +
.../LegacyPlainResultFormatter.java | 294 ++++++++++
.../junitlauncher/LegacyXmlResultFormatter.java | 363 +++++++++++++
.../junitlauncher/ListenerDefinition.java | 121 +++++
.../optional/junitlauncher/NamedTest.java | 14 +
.../optional/junitlauncher/SingleTestClass.java | 101 ++++
.../optional/junitlauncher/TestClasses.java | 112 ++++
.../optional/junitlauncher/TestDefinition.java | 113 ++++
.../junitlauncher/TestExecutionContext.java | 28 +
.../optional/junitlauncher/TestRequest.java | 74 +++
.../junitlauncher/TestResultFormatter.java | 58 ++
.../junitlauncher/JUnitLauncherTaskTest.java | 127 +++++
.../example/jupiter/JupiterSampleTest.java | 50 ++
.../vintage/AlwaysFailingJUnit4Test.java | 16 +
.../junitlauncher/vintage/JUnit4SampleTest.java | 25 +
24 files changed, 2996 insertions(+), 2 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/ant/blob/063e6081/WHATSNEW
----------------------------------------------------------------------
diff --git a/WHATSNEW b/WHATSNEW
index cd960d2..0415592 100644
--- a/WHATSNEW
+++ b/WHATSNEW
@@ -37,6 +37,9 @@ Other changes:
requested. Java11 removes support for CORBA and the switches have
been removed from the rmic tool.
+ * A new junitlauncher task which support JUnit 5 test framework.
+ Bugzilla Report 61796
+
Changes from Ant 1.10.1 TO Ant 1.10.2
=====================================
http://git-wip-us.apache.org/repos/asf/ant/blob/063e6081/build.xml
----------------------------------------------------------------------
diff --git a/build.xml b/build.xml
index e2fe0ae..108c0a2 100644
--- a/build.xml
+++ b/build.xml
@@ -208,6 +208,20 @@
</or>
</selector>
+ <selector id="needs.junitlauncher">
+ <filename name="${optional.package}/junitlauncher/"/>
+ </selector>
+
+ <selector id="needs.junit.engine.vintage">
+ <!-- we need JUnit vintage engine only in tests where we test the junitlauncher task -->
+ <filename name="${src.junit}/org/apache/tools/ant/taskdefs/optional/junitlauncher/**/*"/>
+ </selector>
+
+ <selector id="needs.junit.engine.jupiter">
+ <!-- we need JUnit jupiter engine only in tests where we test the junitlauncher task -->
+ <filename name="${src.junit}/org/apache/tools/ant/taskdefs/optional/junitlauncher/**/*"/>
+ </selector>
+
<selector id="needs.apache-regexp">
<filename name="${regexp.package}/JakartaRegexp*"/>
</selector>
@@ -322,6 +336,7 @@
<selector refid="needs.jsch"/>
<selector refid="needs.junit"/>
<selector refid="needs.junit4"/>
+ <selector refid="needs.junitlauncher"/>
<selector refid="needs.netrexx"/>
<selector refid="needs.swing"/>
<selector refid="needs.xz"/>
@@ -405,6 +420,15 @@
<available property="junit4.present"
classname="org.junit.Test"
classpathref="classpath" ignoresystemclasses="${ignoresystemclasses}"/>
+ <available property="junitlauncher.present"
+ classname="org.junit.platform.launcher.Launcher"
+ classpathref="classpath" ignoresystemclasses="${ignoresystemclasses}"/>
+ <available property="junit.engine.vintage.present"
+ classname="org.junit.vintage.engine.VintageTestEngine"
+ classpathref="classpath" ignoresystemclasses="${ignoresystemclasses}"/>
+ <available property="junit.engine.jupiter.present"
+ classname="org.junit.jupiter.engine.JupiterTestEngine"
+ classpathref="classpath" ignoresystemclasses="${ignoresystemclasses}"/>
<available property="antunit.present"
classname="org.apache.ant.antunit.AntUnit"
classpathref="classpath" ignoresystemclasses="${ignoresystemclasses}"/>
@@ -562,10 +586,12 @@
<not>
<or>
<selector refid="not.in.kaffe" if="kaffe"/>
-
<selector refid="needs.apache-resolver" unless="apache.resolver.present"/>
<selector refid="needs.junit" unless="junit.present"/> <!-- TODO should perhaps use -source 1.4? -->
<selector refid="needs.junit4" unless="junit4.present"/>
+ <selector refid="needs.junitlauncher" unless="junitlauncher.present"/>
+ <selector refid="needs.junit.engine.vintage" unless="junit.engine.vintage.present"/>
+ <selector refid="needs.junit.engine.jupiter" unless="junit.engine.jupiter.present"/>
<selector refid="needs.apache-regexp" unless="apache.regexp.present"/>
<selector refid="needs.apache-oro" unless="apache.oro.present"/>
<selector refid="needs.apache-bcel" unless="bcel.present"/>
@@ -733,6 +759,7 @@
<optional-jar dep="apache-resolver"/>
<optional-jar dep="junit"/>
<optional-jar dep="junit4"/>
+ <optional-jar dep="junitlauncher"/>
<optional-jar dep="apache-regexp"/>
<optional-jar dep="apache-oro"/>
<optional-jar dep="apache-bcel"/>
http://git-wip-us.apache.org/repos/asf/ant/blob/063e6081/fetch.xml
----------------------------------------------------------------------
diff --git a/fetch.xml b/fetch.xml
index 166a2bb..9a98699 100644
--- a/fetch.xml
+++ b/fetch.xml
@@ -232,6 +232,24 @@ Set -Ddest=LOCATION on the command line
<f2 project="org.hamcrest" archive="hamcrest-library"/>
</target>
+ <target name="junitlauncher"
+ description="load junitlauncher libraries"
+ depends="init">
+ <f2 project="org.junit.platform" archive="junit-platform-launcher" />
+ </target>
+
+ <target name="junit-engine-jupiter"
+ description="load junit jupiter engine libraries (necessary only for internal Ant project tests)"
+ depends="init">
+ <f2 project="org.junit.jupiter" archive="junit-jupiter-engine" />
+ </target>
+
+ <target name="junit-engine-vintage"
+ description="load junit vintage engine libraries (necessary only for internal Ant project tests)"
+ depends="init">
+ <f2 project="org.junit.vintage" archive="junit-vintage-engine" />
+ </target>
+
<target name="xml"
description="load full XML libraries (Xalan and xml-resolver)"
depends="init">
@@ -367,5 +385,6 @@ Set -Ddest=LOCATION on the command line
<target name="all"
description="load all the libraries (except jython)"
- depends="antunit,ivy,logging,junit,xml,networking,regexp,antlr,bcel,jdepend,bsf,debugging,script,javamail,jspc,jai,xz,netrexx"/>
+ depends="antunit,ivy,logging,junit,junitlauncher,xml,networking,regexp,antlr,bcel,jdepend,bsf,debugging,script,
+ javamail,jspc,jai,xz,netrexx,junit-engine-vintage,junit-engine-jupiter"/>
</project>
http://git-wip-us.apache.org/repos/asf/ant/blob/063e6081/lib/libraries.properties
----------------------------------------------------------------------
diff --git a/lib/libraries.properties b/lib/libraries.properties
index 3d4f4a1..cf8e930 100644
--- a/lib/libraries.properties
+++ b/lib/libraries.properties
@@ -53,6 +53,11 @@ jdepend.version=2.9.1
jruby.version=1.6.8
junit.version=4.12
rhino.version=1.7.8
+junit-platform-launcher.version=1.1.0
+# Only used for internal tests in Ant project
+junit-vintage-engine.version=5.1.0
+# Only used for internal tests in Ant project
+junit-jupiter-engine.version=5.1.0
jsch.version=0.1.54
jython.version=2.7.0
# log4j 1.2.15 requires JMS and a few other Sun jars that are not in the m2 repo
http://git-wip-us.apache.org/repos/asf/ant/blob/063e6081/manual/Tasks/junitlauncher.html
----------------------------------------------------------------------
diff --git a/manual/Tasks/junitlauncher.html b/manual/Tasks/junitlauncher.html
new file mode 100644
index 0000000..8603f59
--- /dev/null
+++ b/manual/Tasks/junitlauncher.html
@@ -0,0 +1,481 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<html>
+<head>
+ <link rel="stylesheet" type="text/css" href="../stylesheets/style.css">
+ <title>JUnitLauncher Task</title>
+</head>
+<body>
+
+<h2 id="junitlauncher">JUnitLauncher</h2>
+<h3>Description</h3>
+
+<p>
+ This task allows tests to be launched and run using the JUnit 5 framework.
+</p>
+<p>
+ JUnit 5 introduced a newer set of APIs to write and launch tests. It also introduced
+ the concept of test engines. Test engines decide which classes are considered as testcases
+ and how they are executed. JUnit 5 supports running tests that have been written using
+ JUnit 4 constructs as well as tests that have been written using JUnit 5 constructs.
+ For more details about JUnit 5 itself, please refer to the JUnit 5 project's documentation at
+ <a href="https://junit.org/junit5/">https://junit.org/junit5/</a>.
+</p>
+<p>
+ The goal of this <code>junitlauncher</code> task is to allow launching the JUnit 5
+ test launcher and building the test requests so that the selected tests can then be parsed
+ and executed by the test engine(s) supported by JUnit 5. This task in itself does <i>not</i>
+ understand what a test case is nor does it execute the tests itself.
+</p>
+<p>
+ <strong>Note</strong>: This task depends on external libraries not included
+ in the Apache Ant distribution. See <a href="../install.html#librarydependencies">
+ Library Dependencies</a> for more information.
+</p>
+<p>
+ <strong>Note</strong>:
+ You must have the necessary JUnit 5 libraries in the classpath of the tests. At the time of
+ writing this documentation, the list of JUnit 5 platform libraries that are necessary to run the tests
+ are:
+<ul>
+ <li>
+ junit-platform-commons.jar
+ </li>
+ <li>
+ junit-platform-engine.jar
+ </li>
+ <li>
+ junit-platform-launcher.jar
+ </li>
+</ul>
+</p>
+<p>
+ Depending on the test engine(s) that you want to use in your tests, you will further need the following
+ libraries in the classpath
+</p>
+
+<p>
+ For <code>junit-vintage</code> engine:
+<ul>
+ <li>
+ junit-vintage-engine.jar
+ </li>
+ <li>
+ junit.jar (JUnit 4.x version)
+ </li>
+</ul>
+</p>
+<p>
+ For <code>junit-jupiter</code> engine:
+<ul>
+ <li>
+ junit-jupiter-api.jar
+ </li>
+ <li>
+ junit-jupiter-engine.jar
+ </li>
+ <li>
+ opentest4j.jar
+ </li>
+</ul>
+
+</p>
+<p>
+ To have these in the test classpath, you can follow <i>either</i> of the following approaches:
+<ul>
+ <li>Put all these relevant jars along with the <code>ant-junitlauncher.jar</code> in <code>ANT_HOME/lib</code>
+ directory
+ </li>
+ <li>OR Leave <code>ant-junitlauncher.jar</code> in the <code>ANT_HOME/lib</code> directory and include all
+ other relevant jars in the classpath by passing them as a <code>-lib</code> option, while invoking Ant
+ </li>
+</ul>
+</p>
+
+<p>
+ Tests are defined by nested elements like <code>test</code>,
+ <code>testclasses</code> tags (see <a href="#nested">nested
+ elements</a>).</p>
+
+<h3>Parameters</h3>
+<table>
+ <tr>
+ <td valign="top"><b>Attribute</b></td>
+ <td valign="top"><b>Description</b></td>
+ <td valign="top"><b>Required</b></td>
+ </tr>
+ <tr>
+ <td valign="top">haltOnFailure</td>
+ <td valign="top">A value of <code>true</code> implies that build has to stop
+ if any failure occurs in any of the tests. JUnit 5 classifies failures
+ as both assertion failures as well as exceptions that get thrown during
+ test execution. As such, this task too considers both these cases as
+ failures and doesn't distinguish one from another.
+ </td>
+ <td align="center" valign="top">No; default is <code>false</code>.</td>
+ </tr>
+ <tr>
+ <td valign="top">failureProperty</td>
+ <td valign="top">The name of a property to set in the event of a failure
+ (exceptions in tests are considered failures as well).
+ </td>
+ <td align="center" valign="top">No.</td>
+ </tr>
+</table>
+
+<h3 id="nested">Nested Elements</h3>
+
+<h4>classpath</h4>
+<p>
+ The nested <code><classpath></code> element that represents a
+ <a href="../using.html#path">PATH like structure</a> can be used to configure
+ the task to use this classpath for finding and running the tests. This classpath
+ will be used for:
+<ul>
+ <li>Finding the test classes to execute</li>
+ <li>Finding the JUnit 5 framework libraries (which include the API jars and test engine jars). The complete
+ set of jars that are relevant in JUnit 5 framework are listed in the <a href="#junit5deps">dependecies</a>
+ section
+ </li>
+</ul>
+If the <code>classpath</code> element isn't configured for the task, then the classpath of
+Ant itself will be used for finding the test classes and JUnit 5 libraries.
+
+</p>
+
+<h4>listener</h4>
+
+<p>
+ The <code>junitlauncher</code> task can be configured with <code>listener</code>(s) to listen
+ to test execution events (such as a test execution starting, completing etc...). The listener
+ is expected to be a class which implements the <code>org.junit.platform.launcher.TestExecutionListener</code>.
+ This <code>TestExecutionListener</code> interface is an API exposed by the JUnit 5 platform APIs and isn't
+ specific to Ant. As such, you can use any existing implementation of <code>TestExecutionListener</code> in
+ this task.
+</p>
+
+<h5>Test result formatter</h5>
+<p>
+ <code>junitlauncher</code> provides a way where the test execution results can be formatted and presented
+ in a way that's customizable. The task allows for configuring test result formatters, through the use of
+ <code>listener</code> element. As noted previously, the <code>listener</code> element expects the listener
+ to implement the <code>org.junit.platform.launcher.TestExecutionListener</code> interface. Typically, result
+ formatters need a bit more configuration details to be fed to them, during the test execution - details
+ like where to write out the formatted result. Any such listener can optionally implement
+ the <code>org.apache.tools.ant.taskdefs.optional.junitlauncher.TestResultFormatter</code> interface. This interface
+ is specific to Ant <code>junitlauncher</code> task and it extends the <code>org.junit.platform.launcher.TestExecutionListener</code>
+ interface
+</p>
+<p>
+ The <code>junitlauncher</code> task comes with the following pre-defined test result formatter types:
+<ul>
+ <li>
+ <code>legacy-plain</code> : This formatter prints a short statistics line for all test cases.
+ </li>
+ <li>
+ <code>legacy-brief</code> : This formatter prints information for tests that failed or were skipped.
+ </li>
+ <li>
+ <code>legacy-xml</code> : This formatter prints statistics for the tests in xml format.
+ </li>
+</ul>
+<em>NOTE:</em> Each of these formatters, that are named "legacy" try, and format the results to be almost similar to
+what the <code>junit</code> task's formatters used to do. Furthermore, the <code>legacy-xml</code> formatters
+generates the XML to comply with the same schema that the <code>junit</code> task's XML formatter used to follow.
+As a result, the XML generated by this formatter, can be used as-is by the <code>junitreport</code> task.
+
+</p>
+
+The <code>listener</code> element supports the following attributes:
+
+<table>
+ <tr>
+ <td valign="top"><b>Attribute</b></td>
+ <td valign="top"><b>Description</b></td>
+ <td valign="top"><b>Required</b></td>
+ </tr>
+ <tr>
+ <td valign="top">type</td>
+ <td valign="top">Use a predefined formatter (either
+ <code>legacy-xml</code>, <code>legacy-plain</code> or <code>legacy-brief</code>).
+ </td>
+ <td align="center" rowspan="2">Exactly one of these</td>
+ </tr>
+ <tr>
+ <td valign="top">classname</td>
+ <td valign="top">Name of a listener class which implements <code>org.junit.platform.launcher.TestExecutionListener</code>
+ or the <code>org.apache.tools.ant.taskdefs.optional.junitlauncher.TestResultFormatter</code> interface
+ </td>
+ </tr>
+ <tr>
+ <td valign="top">resultFile</td>
+ <td valign="top">The file name to which the formatted result needs to be written to. This attribute is only
+ relevant
+ when the listener class implements the <code>org.apache.tools.ant.taskdefs.optional.junitlauncher.TestResultFormatter</code>
+ interface.
+ <p> If no value is specified for this attribute and the listener implements the
+ <code>org.apache.tools.ant.taskdefs.optional.junitlauncher.TestResultFormatter</code> then the file name
+ will be defaulted
+ to and will be of the form <code>TEST-<testname>.<formatter-specific-extension></code>
+ (ex: TEST-org.myapp.SomeTest.xml for the <code>legacy-xml</code> type formatter)
+ </p>
+ </td>
+ <td align="center">No</td>
+ </tr>
+ <tr>
+ <td valign="top">sendSysOut</td>
+ <td valign="top">If set to <code>true</code> then the listener will be passed the <code>stdout</code> content
+ generated by the test(s). This attribute is relevant only if the listener
+ class implements the <code>org.apache.tools.ant.taskdefs.optional.junitlauncher.TestResultFormatter</code>
+ interface.
+ </td>
+ <td align="center">No; defaults to <code>false</code></td>
+ </tr>
+ <tr>
+ <td valign="top">sendSysErr</td>
+ <td valign="top">If set to <code>true</code> then the listener will be passed the <code>stderr</code> content
+ generated by the test(s). This attribute is relevant only if the listener
+ class implements the <code>org.apache.tools.ant.taskdefs.optional.junitlauncher.TestResultFormatter</code>
+ interface.
+ </td>
+ <td align="center">No; defaults to <code>false</code></td>
+ </tr>
+ <tr>
+ <td valign="top">if</td>
+ <td valign="top">Only use this listener <a href="../properties.html#if+unless">if the named property is set</a>.
+ </td>
+ <td align="center">No</td>
+ </tr>
+ <tr>
+ <td valign="top">unless</td>
+ <td valign="top">Only use this listener <a href="../properties.html#if+unless">if the named property is
+ <b>not</b>
+ set</a>.
+ </td>
+ <td align="center">No</td>
+ </tr>
+</table>
+
+<h4>test</h4>
+
+<p>Defines a single test class.</p>
+
+<table>
+ <tr>
+ <td valign="top"><b>Attribute</b></td>
+ <td valign="top"><b>Description</b></td>
+ <td valign="top"><b>Required</b></td>
+ </tr>
+ <tr>
+ <td valign="top">name</td>
+ <td valign="top">Fully qualified name of the test class.</td>
+ <td align="center">Yes</td>
+ </tr>
+ <tr>
+ <td valign="top">methods</td>
+ <td valign="top">Comma-separated list of names of test case methods to execute.
+ If this is specified, then only these test methods from the test class will be
+ executed.
+ </td>
+ <td align="center">No</td>
+ </tr>
+ <tr>
+ <td valign="top">haltOnFailure</td>
+ <td valign="top">Stop the build process if a failure occurs during the test
+ run (exceptions are considered as failures too).
+ Overrides value set on <code>junitlauncher</code> element.
+ </td>
+ <td align="center" valign="top">No</td>
+ </tr>
+ <tr>
+ <td valign="top">failureProperty</td>
+ <td valign="top">The name of a property to set in the event of a failure
+ (exceptions are considered failures as well). Overrides value set on
+ <code>junitlauncher</code> element.
+ </td>
+ <td align="center" valign="top">No</td>
+ </tr>
+ <tr>
+ <td valign="top">outputDir</td>
+ <td valign="top">Directory to write the reports to.</td>
+ <td align="center" valign="top">No; default is the base directory of the project.</td>
+ </tr>
+ <tr>
+ <td valign="top">if</td>
+ <td valign="top">Only run this test <a href="../properties.html#if+unless">if the named property is set</a>.
+ </td>
+ <td align="center" valign="top">No</td>
+ </tr>
+ <tr>
+ <td valign="top">unless</td>
+ <td valign="top">Only run this test <a href="../properties.html#if+unless">if the named property is <b>not</b>
+ set</a>.
+ </td>
+ <td align="center" valign="top">No</td>
+ </tr>
+</table>
+
+<p>
+ Tests can define their own listeners via nested <code>listener</code> elements.
+</p>
+
+<h4>testclasses</h4>
+
+<p>Define a number of tests based on pattern matching.</p>
+
+<p>
+ <code>testclasses</code> collects the included <a href="../Types/resources.html">resources</a> from any number
+ of nested <a
+ href="../Types/resources.html#collection">Resource Collection</a>s. It then
+ selects each resource whose name ends in <code>.class</code>. These classes are then passed on to the
+ JUnit 5 platform for it to decide and run them as tests.
+</p>
+
+<table>
+ <tr>
+ <td valign="top"><b>Attribute</b></td>
+ <td valign="top"><b>Description</b></td>
+ <td valign="top"><b>Required</b></td>
+ </tr>
+ <tr>
+ <td valign="top">haltOnFailure</td>
+ <td valign="top">Stop the build process if a failure occurs during the test
+ run (exceptions are considered as failures too).
+ Overrides value set on <code>junitlauncher</code> element.
+ </td>
+ <td align="center" valign="top">No</td>
+ </tr>
+ <tr>
+ <td valign="top">failureProperty</td>
+ <td valign="top">The name of a property to set in the event of a failure
+ (exceptions are considered failures as well). Overrides value set on
+ <code>junitlauncher</code> element.
+ </td>
+ <td align="center" valign="top">No</td>
+ </tr>
+ <tr>
+ <td valign="top">outputDir</td>
+ <td valign="top">Directory to write the reports to.</td>
+ <td align="center" valign="top">No; default is the base directory of the project.</td>
+ </tr>
+ <tr>
+ <td valign="top">if</td>
+ <td valign="top">Only run the tests <a href="../properties.html#if+unless">if the named property is set</a>.
+ </td>
+ <td align="center" valign="top">No</td>
+ </tr>
+ <tr>
+ <td valign="top">unless</td>
+ <td valign="top">Only run the tests <a href="../properties.html#if+unless">if the named property is <b>not</b>
+ set</a>.
+ </td>
+ <td align="center" valign="top">No</td>
+ </tr>
+</table>
+
+<p>
+ <code>testclasses</code> can define their own listeners via nested <code>listener</code> elements.
+</p>
+
+<h3>Examples</h3>
+
+<pre>
+<path id="test.classpath">
+ ...
+</path>
+
+<junitlauncher>
+ <classpath refid="test.classpath"/>
+ <test name="org.myapp.SimpleTest"/>
+</junitlauncher>
+
+</pre>
+
+<p>
+ Launches the JUnit 5 platform to run the <code>org.myapp.SimpleTest</code> test
+</p>
+
+<pre>
+<junitlauncher>
+ <classpath refid="test.classpath"/>
+ <test name="org.myapp.SimpleTest" haltOnFailure="true"/>
+ <test name="org.myapp.AnotherTest"/>
+</junitlauncher>
+</pre>
+
+<p>
+ Launches the JUnit 5 platform to run the <code>org.myapp.SimpleTest</code> and the
+ <code>org.myapp.AnotherTest</code> tests. The build process will be stopped if any
+ test, in the <code>org.myapp.SimpleTest</code>, fails.
+</p>
+
+<pre>
+<junitlauncher>
+ <classpath refid="test.classpath"/>
+ <test name="org.myapp.SimpleTest" methods="testFoo, testBar"/>
+</junitlauncher>
+</pre>
+<p>
+ Launches the JUnit 5 platform to run only the <code>testFoo</code> and <code>testBar</code> methods of the
+ <code>org.myapp.SimpleTest</code> test class.
+</p>
+
+<pre>
+<junitlauncher>
+ <classpath refid="test.classpath"/>
+
+ <testclasses outputdir="${output.dir}">
+ <fileset dir="${build.classes.dir}">
+ <include name="org/example/**/tests/**/"/>
+ </fileset>
+ </testclasses>
+</junitlauncher>
+</pre>
+
+<p>
+ Selects any <code>.class</code> files that match the <code>org/example/**/tests/**/</code> <code>fileset</code>
+ filter, under the <code>${build.classes.dir}</code> and passes those classes to the JUnit 5 platform for
+ execution as tests.
+</p>
+
+<pre>
+<junitlauncher>
+ <classpath refid="test.classpath"/>
+
+ <testclasses outputdir="${output.dir}">
+ <fileset dir="${build.classes.dir}">
+ <include name="org/example/**/tests/**/"/>
+ </fileset>
+ <listener type="legacy-xml" sendSysOut="true" sendSysErr="true"/>
+ <listener type="legacy-plain" sendSysOut="true" />
+ </testclasses>
+</junitlauncher>
+</pre>
+<p>
+ Selects any <code>.class</code> files that match the <code>org/example/**/tests/**/</code> <code>fileset</code>
+ filter, under the <code>${build.classes.dir}</code> and passes those classes to the JUnit 5 platform for
+ execution as tests. Test results will be written out to the <code>${output.dir}</code> by the
+ <code>legacy-xml</code> and <code>legacy-plain</code> formatters, in separate files.
+ Furthermore, both the <code>legacy-xml</code> and the <code>legacy-plain</code>
+ listeners, above, are configured to receive the standard output content generated by the tests.
+ The <code>legacy-xml</code> listener is configured to receive standard error content as well.
+
+</p>
+
+
+</body>
+</html>
http://git-wip-us.apache.org/repos/asf/ant/blob/063e6081/src/etc/testcases/taskdefs/optional/junitlauncher.xml
----------------------------------------------------------------------
diff --git a/src/etc/testcases/taskdefs/optional/junitlauncher.xml b/src/etc/testcases/taskdefs/optional/junitlauncher.xml
new file mode 100644
index 0000000..ccae7ae
--- /dev/null
+++ b/src/etc/testcases/taskdefs/optional/junitlauncher.xml
@@ -0,0 +1,113 @@
+<?xml version="1.0"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<project name="junitlauncher-test" basedir=".">
+
+ <property name="output.dir" location="${java.io.tmpdir}"/>
+ <property name="build.classes.dir" value="../../../../../build/testcases"/>
+ <target name="init">
+ <mkdir dir="${output.dir}"/>
+ </target>
+
+ <path id="junit.platform.classpath">
+ <fileset dir="../../../../../lib/optional" includes="junit-platform*.jar"/>
+ </path>
+
+ <path id="junit.engine.vintage.classpath">
+ <fileset dir="../../../../../lib/optional" includes="junit-vintage-engine*.jar"/>
+ </path>
+
+ <path id="junit.engine.jupiter.classpath">
+ <fileset dir="../../../../../lib/optional">
+ <include name="junit-jupiter*.jar"/>
+ <include name="opentest4j*.jar"/>
+ </fileset>
+ </path>
+
+ <path id="test.classpath">
+ <pathelement location="${build.classes.dir}"/>
+ <path refid="junit.platform.classpath"/>
+ <path refid="junit.engine.vintage.classpath"/>
+ <path refid="junit.engine.jupiter.classpath"/>
+ </path>
+
+ <target name="test-failure-stops-build" depends="init">
+ <junitlauncher>
+ <!-- A specific test meant to fail -->
+ <test name="org.example.junitlauncher.vintage.AlwaysFailingJUnit4Test" haltOnFailure="true"/>
+ <!-- classpath to be used for the tests -->
+ <classpath refid="test.classpath"/>
+ </junitlauncher>
+ </target>
+
+ <target name="test-failure-continues-build" depends="init">
+ <junitlauncher>
+ <!-- A specific test meant to fail -->
+ <test name="org.example.junitlauncher.vintage.AlwaysFailingJUnit4Test"/>
+ <classpath refid="test.classpath"/>
+ </junitlauncher>
+ </target>
+
+ <target name="test-success" depends="init">
+ <junitlauncher>
+ <!-- A specific test meant to pass -->
+ <test name="org.example.junitlauncher.vintage.JUnit4SampleTest"/>
+ <classpath refid="test.classpath"/>
+ </junitlauncher>
+ </target>
+
+ <target name="test-one-specific-method" depends="init">
+ <junitlauncher>
+ <test name="org.example.junitlauncher.vintage.JUnit4SampleTest" methods="testBar" haltonfailure="true"/>
+ <classpath refid="test.classpath"/>
+ </junitlauncher>
+ </target>
+
+ <target name="test-multiple-specific-methods" depends="init">
+ <junitlauncher>
+ <test name="org.example.junitlauncher.vintage.JUnit4SampleTest" methods=" testFoo, testFooBar "
+ haltonfailure="true"/>
+ <classpath refid="test.classpath"/>
+ </junitlauncher>
+ </target>
+
+ <target name="test-multiple-individual" depends="init">
+ <junitlauncher>
+ <test name="org.example.junitlauncher.vintage.AlwaysFailingJUnit4Test"/>
+ <test name="org.example.junitlauncher.vintage.JUnit4SampleTest"/>
+ <classpath refid="test.classpath"/>
+ </junitlauncher>
+ </target>
+
+ <target name="test-batch" depends="init">
+ <junitlauncher>
+ <classpath refid="test.classpath"/>
+ <testclasses outputdir="${output.dir}">
+ <fileset dir="${build.classes.dir}">
+ <include name="org/example/**/junitlauncher/**/"/>
+ </fileset>
+ <fileset dir="${build.classes.dir}">
+ <include name="org/apache/tools/ant/taskdefs/optional/junitlauncher/example/**/"/>
+ </fileset>
+ <listener type="legacy-brief" sendSysOut="true"/>
+ <listener type="legacy-xml" sendSysErr="true" sendSysOut="true"/>
+ </testclasses>
+ </junitlauncher>
+ </target>
+</project>
+
http://git-wip-us.apache.org/repos/asf/ant/blob/063e6081/src/main/org/apache/tools/ant/taskdefs/defaults.properties
----------------------------------------------------------------------
diff --git a/src/main/org/apache/tools/ant/taskdefs/defaults.properties b/src/main/org/apache/tools/ant/taskdefs/defaults.properties
index 8db1ebc..7b4781c 100644
--- a/src/main/org/apache/tools/ant/taskdefs/defaults.properties
+++ b/src/main/org/apache/tools/ant/taskdefs/defaults.properties
@@ -160,6 +160,7 @@ jjdoc=org.apache.tools.ant.taskdefs.optional.javacc.JJDoc
jjtree=org.apache.tools.ant.taskdefs.optional.javacc.JJTree
junit=org.apache.tools.ant.taskdefs.optional.junit.JUnitTask
junitreport=org.apache.tools.ant.taskdefs.optional.junit.XMLResultAggregator
+junitlauncher=org.apache.tools.ant.taskdefs.optional.junitlauncher.JUnitLauncherTask
native2ascii=org.apache.tools.ant.taskdefs.optional.Native2Ascii
netrexxc=org.apache.tools.ant.taskdefs.optional.NetRexxC
propertyfile=org.apache.tools.ant.taskdefs.optional.PropertyFile
http://git-wip-us.apache.org/repos/asf/ant/blob/063e6081/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/AbstractJUnitResultFormatter.java
----------------------------------------------------------------------
diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/AbstractJUnitResultFormatter.java b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/AbstractJUnitResultFormatter.java
new file mode 100644
index 0000000..4d1aa117
--- /dev/null
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/AbstractJUnitResultFormatter.java
@@ -0,0 +1,295 @@
+package org.apache.tools.ant.taskdefs.optional.junitlauncher;
+
+import org.apache.tools.ant.Project;
+import org.apache.tools.ant.util.FileUtils;
+import org.junit.platform.engine.TestSource;
+import org.junit.platform.engine.support.descriptor.ClassSource;
+import org.junit.platform.launcher.TestIdentifier;
+import org.junit.platform.launcher.TestPlan;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.Closeable;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.Writer;
+import java.nio.BufferOverflowException;
+import java.nio.ByteBuffer;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * Contains some common behaviour that's used by our internal {@link TestResultFormatter}s
+ */
+abstract class AbstractJUnitResultFormatter implements TestResultFormatter {
+
+
+ protected static String NEW_LINE = System.getProperty("line.separator");
+ protected TestExecutionContext context;
+
+ private SysOutErrContentStore sysOutStore;
+ private SysOutErrContentStore sysErrStore;
+
+ @Override
+ public void sysOutAvailable(final byte[] data) {
+ if (this.sysOutStore == null) {
+ this.sysOutStore = new SysOutErrContentStore(true);
+ }
+ try {
+ this.sysOutStore.store(data);
+ } catch (IOException e) {
+ handleException(e);
+ return;
+ }
+ }
+
+ @Override
+ public void sysErrAvailable(final byte[] data) {
+ if (this.sysErrStore == null) {
+ this.sysErrStore = new SysOutErrContentStore(false);
+ }
+ try {
+ this.sysErrStore.store(data);
+ } catch (IOException e) {
+ handleException(e);
+ return;
+ }
+ }
+
+ @Override
+ public void setContext(final TestExecutionContext context) {
+ this.context = context;
+ }
+
+ /**
+ * @return Returns true if there's any stdout data, that was generated during the
+ * tests, is available for use. Else returns false.
+ */
+ boolean hasSysOut() {
+ return this.sysOutStore != null && this.sysOutStore.hasData();
+ }
+
+ /**
+ * @return Returns true if there's any stderr data, that was generated during the
+ * tests, is available for use. Else returns false.
+ */
+ boolean hasSysErr() {
+ return this.sysErrStore != null && this.sysErrStore.hasData();
+ }
+
+ /**
+ * @return Returns a {@link Reader} for reading any stdout data that was generated
+ * during the test execution. It is expected that the {@link #hasSysOut()} be first
+ * called to see if any such data is available and only if there is, then this method
+ * be called
+ * @throws IOException If there's any I/O problem while creating the {@link Reader}
+ */
+ Reader getSysOutReader() throws IOException {
+ return this.sysOutStore.getReader();
+ }
+
+ /**
+ * @return Returns a {@link Reader} for reading any stderr data that was generated
+ * during the test execution. It is expected that the {@link #hasSysErr()} be first
+ * called to see if any such data is available and only if there is, then this method
+ * be called
+ * @throws IOException If there's any I/O problem while creating the {@link Reader}
+ */
+ Reader getSysErrReader() throws IOException {
+ return this.sysErrStore.getReader();
+ }
+
+ /**
+ * Writes out any stdout data that was generated during the
+ * test execution. If there was no such data then this method just returns.
+ *
+ * @param writer The {@link Writer} to use. Cannot be null.
+ * @throws IOException If any I/O problem occurs during writing the data
+ */
+ void writeSysOut(final Writer writer) throws IOException {
+ Objects.requireNonNull(writer, "Writer cannot be null");
+ this.writeFrom(this.sysOutStore, writer);
+ }
+
+ /**
+ * Writes out any stderr data that was generated during the
+ * test execution. If there was no such data then this method just returns.
+ *
+ * @param writer The {@link Writer} to use. Cannot be null.
+ * @throws IOException If any I/O problem occurs during writing the data
+ */
+ void writeSysErr(final Writer writer) throws IOException {
+ Objects.requireNonNull(writer, "Writer cannot be null");
+ this.writeFrom(this.sysErrStore, writer);
+ }
+
+ static Optional<TestIdentifier> traverseAndFindTestClass(final TestPlan testPlan, final TestIdentifier testIdentifier) {
+ if (isTestClass(testIdentifier).isPresent()) {
+ return Optional.of(testIdentifier);
+ }
+ final Optional<TestIdentifier> parent = testPlan.getParent(testIdentifier);
+ return parent.isPresent() ? traverseAndFindTestClass(testPlan, parent.get()) : Optional.empty();
+ }
+
+ static Optional<ClassSource> isTestClass(final TestIdentifier testIdentifier) {
+ if (testIdentifier == null) {
+ return Optional.empty();
+ }
+ final Optional<TestSource> source = testIdentifier.getSource();
+ if (!source.isPresent()) {
+ return Optional.empty();
+ }
+ final TestSource testSource = source.get();
+ if (testSource instanceof ClassSource) {
+ return Optional.of((ClassSource) testSource);
+ }
+ return Optional.empty();
+ }
+
+ private void writeFrom(final SysOutErrContentStore store, final Writer writer) throws IOException {
+ final char[] chars = new char[1024];
+ int numRead = -1;
+ try (final Reader reader = store.getReader()) {
+ while ((numRead = reader.read(chars)) != -1) {
+ writer.write(chars, 0, numRead);
+ }
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ FileUtils.close(this.sysOutStore);
+ FileUtils.close(this.sysErrStore);
+ }
+
+ protected void handleException(final Throwable t) {
+ // we currently just log it and move on.
+ this.context.getProject().ifPresent((p) -> p.log("Exception in listener "
+ + AbstractJUnitResultFormatter.this.getClass().getName(), t, Project.MSG_DEBUG));
+ }
+
+
+ /*
+ A "store" for sysout/syserr content that gets sent to the AbstractJUnitResultFormatter.
+ This store first uses a relatively decent sized in-memory buffer for storing the sysout/syserr
+ content. This in-memory buffer will be used as long as it can fit in the new content that
+ keeps coming in. When the size limit is reached, this store switches to a file based store
+ by creating a temporarily file and writing out the already in-memory held buffer content
+ and any new content that keeps arriving to this store. Once the file has been created,
+ the in-memory buffer will never be used any more and in fact is destroyed as soon as the
+ file is created.
+ Instances of this class are not thread-safe and users of this class are expected to use necessary thread
+ safety guarantees, if they want to use an instance of this class by multiple threads.
+ */
+ private static final class SysOutErrContentStore implements Closeable {
+ private static final int DEFAULT_CAPACITY_IN_BYTES = 50 * 1024; // 50 KB
+ private static final Reader EMPTY_READER = new Reader() {
+ @Override
+ public int read(final char[] cbuf, final int off, final int len) throws IOException {
+ return -1;
+ }
+
+ @Override
+ public void close() throws IOException {
+ }
+ };
+
+ private final String tmpFileSuffix;
+ private ByteBuffer inMemoryStore = ByteBuffer.allocate(DEFAULT_CAPACITY_IN_BYTES);
+ private boolean usingFileStore = false;
+ private Path filePath;
+ private FileOutputStream fileOutputStream;
+
+ private SysOutErrContentStore(final boolean isSysOut) {
+ this.tmpFileSuffix = isSysOut ? ".sysout" : ".syserr";
+ }
+
+ private void store(final byte[] data) throws IOException {
+ if (this.usingFileStore) {
+ this.storeToFile(data, 0, data.length);
+ return;
+ }
+ // we haven't yet created a file store and the data can fit in memory,
+ // so we write it in our buffer
+ try {
+ this.inMemoryStore.put(data);
+ return;
+ } catch (BufferOverflowException boe) {
+ // the buffer capacity can't hold this incoming data, so this
+ // incoming data hasn't been transferred to the buffer. let's
+ // now fall back to a file store
+ this.usingFileStore = true;
+ }
+ // since the content couldn't be transferred into in-memory buffer,
+ // we now create a file and transfer already (previously) stored in-memory
+ // content into that file, before finally transferring this new content
+ // into the file too. We then finally discard this in-memory buffer and
+ // just keep using the file store instead
+ this.fileOutputStream = createFileStore();
+ // first the existing in-memory content
+ storeToFile(this.inMemoryStore.array(), 0, this.inMemoryStore.position());
+ storeToFile(data, 0, data.length);
+ // discard the in-memory store
+ this.inMemoryStore = null;
+ }
+
+ private void storeToFile(final byte[] data, final int offset, final int length) throws IOException {
+ if (this.fileOutputStream == null) {
+ // no backing file was created so we can't do anything
+ return;
+ }
+ this.fileOutputStream.write(data, offset, length);
+ }
+
+ private FileOutputStream createFileStore() throws IOException {
+ this.filePath = Files.createTempFile(null, this.tmpFileSuffix);
+ this.filePath.toFile().deleteOnExit();
+ return new FileOutputStream(this.filePath.toFile());
+ }
+
+ /*
+ * Returns a Reader for reading the sysout/syserr content. If there's no data
+ * available in this store, then this returns a Reader which when used for read operations,
+ * will immediately indicate an EOF.
+ */
+ private Reader getReader() throws IOException {
+ if (this.usingFileStore && this.filePath != null) {
+ // we use a FileReader here so that we can use the system default character
+ // encoding for reading the contents on sysout/syserr stream, since that's the
+ // encoding that System.out/System.err uses to write out the messages
+ return new BufferedReader(new FileReader(this.filePath.toFile()));
+ }
+ if (this.inMemoryStore != null) {
+ return new InputStreamReader(new ByteArrayInputStream(this.inMemoryStore.array(), 0, this.inMemoryStore.position()));
+ }
+ // no data to read, so we return an "empty" reader
+ return EMPTY_READER;
+ }
+
+ /*
+ * Returns true if this store has any data (either in-memory or in a file). Else
+ * returns false.
+ */
+ private boolean hasData() {
+ if (this.inMemoryStore != null && this.inMemoryStore.position() > 0) {
+ return true;
+ }
+ if (this.usingFileStore && this.filePath != null) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void close() throws IOException {
+ this.inMemoryStore = null;
+ FileUtils.close(this.fileOutputStream);
+ FileUtils.delete(this.filePath.toFile());
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/ant/blob/063e6081/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/JUnitLauncherTask.java
----------------------------------------------------------------------
diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/JUnitLauncherTask.java b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/JUnitLauncherTask.java
new file mode 100644
index 0000000..ac4ef44
--- /dev/null
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/JUnitLauncherTask.java
@@ -0,0 +1,537 @@
+package org.apache.tools.ant.taskdefs.optional.junitlauncher;
+
+import org.apache.tools.ant.AntClassLoader;
+import org.apache.tools.ant.BuildException;
+import org.apache.tools.ant.Project;
+import org.apache.tools.ant.Task;
+import org.apache.tools.ant.types.Path;
+import org.apache.tools.ant.util.FileUtils;
+import org.apache.tools.ant.util.KeepAliveOutputStream;
+import org.junit.platform.launcher.Launcher;
+import org.junit.platform.launcher.LauncherDiscoveryRequest;
+import org.junit.platform.launcher.TestExecutionListener;
+import org.junit.platform.launcher.TestPlan;
+import org.junit.platform.launcher.core.LauncherFactory;
+import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
+import org.junit.platform.launcher.listeners.TestExecutionSummary;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.io.PrintStream;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.Properties;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * An Ant {@link Task} responsible for launching the JUnit platform for running tests.
+ * This requires a minimum of JUnit 5, since that's the version in which the JUnit platform launcher
+ * APIs were introduced.
+ * <p>
+ * This task in itself doesn't run the JUnit tests, instead the sole responsibility of
+ * this task is to setup the JUnit platform launcher, build requests, launch those requests and then parse the
+ * result of the execution to present in a way that's been configured on this Ant task.
+ * </p>
+ * <p>
+ * Furthermore, this task allows users control over which classes to select for passing on to the JUnit 5
+ * platform for test execution. It however, is solely the JUnit 5 platform, backed by test engines that
+ * decide and execute the tests.
+ *
+ * @see <a href="https://junit.org/junit5/">JUnit 5 documentation</a> for more details
+ * on how JUnit manages the platform and the test engines.
+ */
+public class JUnitLauncherTask extends Task {
+
+ private Path classPath;
+ private boolean haltOnFailure;
+ private String failureProperty;
+ private final List<TestDefinition> tests = new ArrayList<>();
+ private final List<ListenerDefinition> listeners = new ArrayList<>();
+
+ public JUnitLauncherTask() {
+ }
+
+ @Override
+ public void execute() throws BuildException {
+ final ClassLoader previousClassLoader = Thread.currentThread().getContextClassLoader();
+ try {
+ final ClassLoader executionCL = createClassLoaderForTestExecution();
+ Thread.currentThread().setContextClassLoader(executionCL);
+ final Launcher launcher = LauncherFactory.create();
+ final List<TestRequest> requests = buildTestRequests();
+ for (final TestRequest testRequest : requests) {
+ try {
+ final TestDefinition test = testRequest.getOwner();
+ final LauncherDiscoveryRequest request = testRequest.getDiscoveryRequest().build();
+ final List<TestExecutionListener> testExecutionListeners = new ArrayList<>();
+ // a listener that we always put at the front of list of listeners
+ // for this request.
+ final Listener firstListener = new Listener();
+ // we always enroll the summary generating listener, to the request, so that we
+ // get to use some of the details of the summary for our further decision making
+ testExecutionListeners.add(firstListener);
+ testExecutionListeners.addAll(getListeners(testRequest, executionCL));
+ final PrintStream originalSysOut = System.out;
+ final PrintStream originalSysErr = System.err;
+ try {
+ firstListener.switchedSysOutHandle = trySwitchSysOutErr(testRequest, StreamType.SYS_OUT);
+ firstListener.switchedSysErrHandle = trySwitchSysOutErr(testRequest, StreamType.SYS_ERR);
+ launcher.execute(request, testExecutionListeners.toArray(new TestExecutionListener[testExecutionListeners.size()]));
+ } finally {
+ // switch back sysout/syserr to the original
+ try {
+ System.setOut(originalSysOut);
+ } catch (Exception e) {
+ // ignore
+ }
+ try {
+ System.setErr(originalSysErr);
+ } catch (Exception e) {
+ // ignore
+ }
+ }
+ handleTestExecutionCompletion(test, firstListener.getSummary());
+ } finally {
+ try {
+ testRequest.close();
+ } catch (Exception e) {
+ // log and move on
+ log("Failed to cleanly close test request", e, Project.MSG_DEBUG);
+ }
+ }
+ }
+ } finally {
+ Thread.currentThread().setContextClassLoader(previousClassLoader);
+ }
+ }
+
+ /**
+ * Adds the {@link Path} to the classpath which will be used for execution of the tests
+ *
+ * @param path The classpath
+ */
+ public void addConfiguredClassPath(final Path path) {
+ if (this.classPath == null) {
+ // create a "wrapper" path which can hold on to multiple
+ // paths that get passed to this method (if at all the task in the build is
+ // configured with multiple classpaht elements)
+ this.classPath = new Path(getProject());
+ }
+ this.classPath.add(path);
+ }
+
+ /**
+ * Adds a {@link SingleTestClass} that will be passed on to the underlying JUnit platform
+ * for possible execution of the test
+ *
+ * @param test The test
+ */
+ public void addConfiguredTest(final SingleTestClass test) {
+ this.preConfigure(test);
+ this.tests.add(test);
+ }
+
+ /**
+ * Adds {@link TestClasses} that will be passed on to the underlying JUnit platform for
+ * possible execution of the tests
+ *
+ * @param testClasses The test classes
+ */
+ public void addConfiguredTestClasses(final TestClasses testClasses) {
+ this.preConfigure(testClasses);
+ this.tests.add(testClasses);
+ }
+
+ /**
+ * Adds a {@link ListenerDefinition listener} which will be enrolled for listening to test
+ * execution events
+ *
+ * @param listener The listener
+ */
+ public void addConfiguredListener(final ListenerDefinition listener) {
+ this.listeners.add(listener);
+ }
+
+ public void setHaltonfailure(final boolean haltonfailure) {
+ this.haltOnFailure = haltonfailure;
+ }
+
+ public void setFailureProperty(final String failureProperty) {
+ this.failureProperty = failureProperty;
+ }
+
+ private void preConfigure(final TestDefinition test) {
+ if (test.getHaltOnFailure() == null) {
+ test.setHaltOnFailure(this.haltOnFailure);
+ }
+ if (test.getFailureProperty() == null) {
+ test.setFailureProperty(this.failureProperty);
+ }
+ }
+
+ private List<TestRequest> buildTestRequests() {
+ if (this.tests.isEmpty()) {
+ return Collections.emptyList();
+ }
+ final List<TestRequest> requests = new ArrayList<>();
+ for (final TestDefinition test : this.tests) {
+ final List<TestRequest> testRequests = test.createTestRequests(this);
+ if (testRequests == null || testRequests.isEmpty()) {
+ continue;
+ }
+ requests.addAll(testRequests);
+ }
+ return requests;
+ }
+
+ private List<TestExecutionListener> getListeners(final TestRequest testRequest, final ClassLoader classLoader) {
+ final TestDefinition test = testRequest.getOwner();
+ final List<ListenerDefinition> applicableListenerElements = test.getListeners().isEmpty() ? this.listeners : test.getListeners();
+ final List<TestExecutionListener> listeners = new ArrayList<>();
+ final Project project = getProject();
+ for (final ListenerDefinition applicableListener : applicableListenerElements) {
+ if (!applicableListener.shouldUse(project)) {
+ log("Excluding listener " + applicableListener.getClassName() + " since it's not applicable" +
+ " in the context of project " + project, Project.MSG_DEBUG);
+ continue;
+ }
+ final TestExecutionListener listener = requireTestExecutionListener(applicableListener, classLoader);
+ if (listener instanceof TestResultFormatter) {
+ // setup/configure the result formatter
+ setupResultFormatter(testRequest, applicableListener, (TestResultFormatter) listener);
+ }
+ listeners.add(listener);
+ }
+ return listeners;
+ }
+
+ private void setupResultFormatter(final TestRequest testRequest, final ListenerDefinition formatterDefinition,
+ final TestResultFormatter resultFormatter) {
+
+ testRequest.closeUponCompletion(resultFormatter);
+ // set the execution context
+ resultFormatter.setContext(new InVMExecution());
+ // set the destination output stream for writing out the formatted result
+ final TestDefinition test = testRequest.getOwner();
+ final java.nio.file.Path outputDir = test.getOutputDir() != null ? Paths.get(test.getOutputDir()) : getProject().getBaseDir().toPath();
+ final String filename = formatterDefinition.requireResultFile(test);
+ final java.nio.file.Path resultOutputFile = Paths.get(outputDir.toString(), filename);
+ try {
+ final OutputStream resultOutputStream = Files.newOutputStream(resultOutputFile);
+ // enroll the output stream to be closed when the execution of the TestRequest completes
+ testRequest.closeUponCompletion(resultOutputStream);
+ resultFormatter.setDestination(new KeepAliveOutputStream(resultOutputStream));
+ } catch (IOException e) {
+ throw new BuildException(e);
+ }
+ // check if system.out/system.err content needs to be passed on to the listener
+ if (formatterDefinition.shouldSendSysOut()) {
+ testRequest.addSysOutInterest(resultFormatter);
+ }
+ if (formatterDefinition.shouldSendSysErr()) {
+ testRequest.addSysErrInterest(resultFormatter);
+ }
+ }
+
+ private TestExecutionListener requireTestExecutionListener(final ListenerDefinition listener, final ClassLoader classLoader) {
+ final String className = listener.getClassName();
+ if (className == null || className.trim().isEmpty()) {
+ throw new BuildException("classname attribute value is missing on listener element");
+ }
+ final Class<?> klass;
+ try {
+ klass = Class.forName(className, false, classLoader);
+ } catch (ClassNotFoundException e) {
+ throw new BuildException("Failed to load listener class " + className, e);
+ }
+ if (!TestExecutionListener.class.isAssignableFrom(klass)) {
+ throw new BuildException("Listener class " + className + " is not of type " + TestExecutionListener.class.getName());
+ }
+ try {
+ return TestExecutionListener.class.cast(klass.newInstance());
+ } catch (Exception e) {
+ throw new BuildException("Failed to create an instance of listener " + className, e);
+ }
+ }
+
+ private void handleTestExecutionCompletion(final TestDefinition test, final TestExecutionSummary summary) {
+ final boolean hasTestFailures = summary.getTestsFailedCount() != 0;
+ try {
+ if (hasTestFailures && test.getFailureProperty() != null) {
+ // if there are test failures and the test is configured to set a property in case
+ // of failure, then set the property to true
+ getProject().setNewProperty(test.getFailureProperty(), "true");
+ }
+ } finally {
+ if (hasTestFailures && test.isHaltOnFailure()) {
+ // if the test is configured to halt on test failures, throw a build error
+ final String errorMessage;
+ if (test instanceof NamedTest) {
+ errorMessage = "Test " + ((NamedTest) test).getName() + " has " + summary.getTestsFailedCount() + " failure(s)";
+ } else {
+ errorMessage = "Some test(s) have failure(s)";
+ }
+ throw new BuildException(errorMessage);
+ }
+ }
+ }
+
+ private ClassLoader createClassLoaderForTestExecution() {
+ if (this.classPath == null) {
+ return this.getClass().getClassLoader();
+ }
+ return new AntClassLoader(this.getClass().getClassLoader(), getProject(), this.classPath, true);
+ }
+
+ private Optional<SwitchedStreamHandle> trySwitchSysOutErr(final TestRequest testRequest, final StreamType streamType) {
+ switch (streamType) {
+ case SYS_OUT: {
+ if (!testRequest.interestedInSysOut()) {
+ return Optional.empty();
+ }
+ break;
+ }
+ case SYS_ERR: {
+ if (!testRequest.interestedInSysErr()) {
+ return Optional.empty();
+ }
+ break;
+ }
+ default: {
+ // unknown, but no need to error out, just be lenient
+ // and return back
+ return Optional.empty();
+ }
+ }
+ final PipedOutputStream pipedOutputStream = new PipedOutputStream();
+ final PipedInputStream pipedInputStream;
+ try {
+ pipedInputStream = new PipedInputStream(pipedOutputStream);
+ } catch (IOException ioe) {
+ // log and return
+ return Optional.empty();
+ }
+ final PrintStream printStream = new PrintStream(pipedOutputStream, true);
+ final SysOutErrStreamReader streamer;
+ switch (streamType) {
+ case SYS_OUT: {
+ System.setOut(new PrintStream(printStream));
+ streamer = new SysOutErrStreamReader(this, pipedInputStream,
+ StreamType.SYS_OUT, testRequest.getSysOutInterests());
+ final Thread sysOutStreamer = new Thread(streamer);
+ sysOutStreamer.setDaemon(true);
+ sysOutStreamer.setName("junitlauncher-sysout-stream-reader");
+ sysOutStreamer.setUncaughtExceptionHandler((t, e) -> this.log("Failed in sysout streaming", e, Project.MSG_INFO));
+ sysOutStreamer.start();
+ break;
+ }
+ case SYS_ERR: {
+ System.setErr(new PrintStream(printStream));
+ streamer = new SysOutErrStreamReader(this, pipedInputStream,
+ StreamType.SYS_ERR, testRequest.getSysErrInterests());
+ final Thread sysErrStreamer = new Thread(streamer);
+ sysErrStreamer.setDaemon(true);
+ sysErrStreamer.setName("junitlauncher-syserr-stream-reader");
+ sysErrStreamer.setUncaughtExceptionHandler((t, e) -> this.log("Failed in syserr streaming", e, Project.MSG_INFO));
+ sysErrStreamer.start();
+ break;
+ }
+ default: {
+ return Optional.empty();
+ }
+ }
+ return Optional.of(new SwitchedStreamHandle(pipedOutputStream, streamer));
+ }
+
+ private enum StreamType {
+ SYS_OUT,
+ SYS_ERR
+ }
+
+ private static final class SysOutErrStreamReader implements Runnable {
+ private static final byte[] EMPTY = new byte[0];
+
+ private final JUnitLauncherTask task;
+ private final InputStream sourceStream;
+ private final StreamType streamType;
+ private final Collection<TestResultFormatter> resultFormatters;
+ private volatile SysOutErrContentDeliverer contentDeliverer;
+
+ SysOutErrStreamReader(final JUnitLauncherTask task, final InputStream source, final StreamType streamType, final Collection<TestResultFormatter> resultFormatters) {
+ this.task = task;
+ this.sourceStream = source;
+ this.streamType = streamType;
+ this.resultFormatters = resultFormatters;
+ }
+
+ @Override
+ public void run() {
+ final SysOutErrContentDeliverer streamContentDeliver = new SysOutErrContentDeliverer(this.streamType, this.resultFormatters);
+ final Thread deliveryThread = new Thread(streamContentDeliver);
+ deliveryThread.setName("junitlauncher-" + (this.streamType == StreamType.SYS_OUT ? "sysout" : "syserr") + "-stream-deliverer");
+ deliveryThread.setDaemon(true);
+ deliveryThread.start();
+ this.contentDeliverer = streamContentDeliver;
+ int numRead = -1;
+ final byte[] data = new byte[1024];
+ try {
+ while ((numRead = this.sourceStream.read(data)) != -1) {
+ final byte[] copy = Arrays.copyOf(data, numRead);
+ streamContentDeliver.availableData.offer(copy);
+ }
+ } catch (IOException e) {
+ task.log("Failed while streaming " + (this.streamType == StreamType.SYS_OUT ? "sysout" : "syserr") + " data",
+ e, Project.MSG_INFO);
+ return;
+ } finally {
+ streamContentDeliver.stop = true;
+ // just "wakeup" the delivery thread, to take into account
+ // those race conditions, where that other thread didn't yet
+ // notice that it was asked to stop and has now gone into a
+ // X amount of wait, waiting for any new data
+ streamContentDeliver.availableData.offer(EMPTY);
+ }
+ }
+ }
+
+ private static final class SysOutErrContentDeliverer implements Runnable {
+ private volatile boolean stop;
+ private final Collection<TestResultFormatter> resultFormatters;
+ private final StreamType streamType;
+ private final BlockingQueue<byte[]> availableData = new LinkedBlockingQueue<>();
+ private final CountDownLatch completionLatch = new CountDownLatch(1);
+
+ SysOutErrContentDeliverer(final StreamType streamType, final Collection<TestResultFormatter> resultFormatters) {
+ this.streamType = streamType;
+ this.resultFormatters = resultFormatters;
+ }
+
+ @Override
+ public void run() {
+ try {
+ while (!this.stop) {
+ final byte[] streamData;
+ try {
+ streamData = this.availableData.poll(2, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return;
+ }
+ if (streamData != null) {
+ deliver(streamData);
+ }
+ }
+ // drain it
+ final List<byte[]> remaining = new ArrayList<>();
+ this.availableData.drainTo(remaining);
+ if (!remaining.isEmpty()) {
+ for (final byte[] data : remaining) {
+ deliver(data);
+ }
+ }
+ } finally {
+ this.completionLatch.countDown();
+ }
+ }
+
+ private void deliver(final byte[] data) {
+ if (data == null || data.length == 0) {
+ return;
+ }
+ for (final TestResultFormatter resultFormatter : this.resultFormatters) {
+ // send it to the formatter
+ switch (streamType) {
+ case SYS_OUT: {
+ resultFormatter.sysOutAvailable(data);
+ break;
+ }
+ case SYS_ERR: {
+ resultFormatter.sysErrAvailable(data);
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ private final class SwitchedStreamHandle {
+ private final PipedOutputStream outputStream;
+ private final SysOutErrStreamReader streamReader;
+
+ SwitchedStreamHandle(final PipedOutputStream outputStream, final SysOutErrStreamReader streamReader) {
+ this.streamReader = streamReader;
+ this.outputStream = outputStream;
+ }
+ }
+
+ private final class Listener extends SummaryGeneratingListener {
+ private Optional<SwitchedStreamHandle> switchedSysOutHandle;
+ private Optional<SwitchedStreamHandle> switchedSysErrHandle;
+
+ @Override
+ public void testPlanExecutionFinished(final TestPlan testPlan) {
+ super.testPlanExecutionFinished(testPlan);
+ // now that the test plan execution is finished, close the switched sysout/syserr output streams
+ // and wait for the sysout and syserr content delivery, to result formatters, to finish
+ if (this.switchedSysOutHandle.isPresent()) {
+ final SwitchedStreamHandle sysOut = this.switchedSysOutHandle.get();
+ try {
+ closeAndWait(sysOut);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return;
+ }
+ }
+ if (this.switchedSysErrHandle.isPresent()) {
+ final SwitchedStreamHandle sysErr = this.switchedSysErrHandle.get();
+ try {
+ closeAndWait(sysErr);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return;
+ }
+ }
+ }
+
+ private void closeAndWait(final SwitchedStreamHandle handle) throws InterruptedException {
+ FileUtils.close(handle.outputStream);
+ if (handle.streamReader.contentDeliverer == null) {
+ return;
+ }
+ // wait for a few seconds
+ handle.streamReader.contentDeliverer.completionLatch.await(2, TimeUnit.SECONDS);
+ }
+ }
+
+ private final class InVMExecution implements TestExecutionContext {
+
+ private final Properties props;
+
+ InVMExecution() {
+ this.props = new Properties();
+ this.props.putAll(JUnitLauncherTask.this.getProject().getProperties());
+ }
+
+ @Override
+ public Properties getProperties() {
+ return this.props;
+ }
+
+ @Override
+ public Optional<Project> getProject() {
+ return Optional.of(JUnitLauncherTask.this.getProject());
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/ant/blob/063e6081/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyBriefResultFormatter.java
----------------------------------------------------------------------
diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyBriefResultFormatter.java b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyBriefResultFormatter.java
new file mode 100644
index 0000000..d5d4670
--- /dev/null
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyBriefResultFormatter.java
@@ -0,0 +1,17 @@
+package org.apache.tools.ant.taskdefs.optional.junitlauncher;
+
+import org.junit.platform.engine.TestExecutionResult;
+import org.junit.platform.launcher.TestIdentifier;
+
+/**
+ * A {@link TestResultFormatter} which prints a brief statistic for tests that have
+ * failed, aborted or skipped
+ */
+class LegacyBriefResultFormatter extends LegacyPlainResultFormatter implements TestResultFormatter {
+
+ @Override
+ protected boolean shouldReportExecutionFinished(final TestIdentifier testIdentifier, final TestExecutionResult testExecutionResult) {
+ final TestExecutionResult.Status resultStatus = testExecutionResult.getStatus();
+ return resultStatus == TestExecutionResult.Status.ABORTED || resultStatus == TestExecutionResult.Status.FAILED;
+ }
+}
http://git-wip-us.apache.org/repos/asf/ant/blob/063e6081/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyPlainResultFormatter.java
----------------------------------------------------------------------
diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyPlainResultFormatter.java b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyPlainResultFormatter.java
new file mode 100644
index 0000000..49ce7e3
--- /dev/null
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyPlainResultFormatter.java
@@ -0,0 +1,294 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.tools.ant.taskdefs.optional.junitlauncher;
+
+import org.junit.platform.engine.TestExecutionResult;
+import org.junit.platform.engine.reporting.ReportEntry;
+import org.junit.platform.engine.support.descriptor.ClassSource;
+import org.junit.platform.launcher.TestIdentifier;
+import org.junit.platform.launcher.TestPlan;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.io.UnsupportedEncodingException;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+
+/**
+ * A {@link TestResultFormatter} which prints a short statistic for each of the tests
+ */
+class LegacyPlainResultFormatter extends AbstractJUnitResultFormatter implements TestResultFormatter {
+
+ private OutputStream outputStream;
+ private final Map<TestIdentifier, Stats> testIds = new ConcurrentHashMap<>();
+ private TestPlan testPlan;
+ private BufferedWriter writer;
+
+ @Override
+ public void testPlanExecutionStarted(final TestPlan testPlan) {
+ this.testPlan = testPlan;
+ }
+
+ @Override
+ public void testPlanExecutionFinished(final TestPlan testPlan) {
+ for (final Map.Entry<TestIdentifier, Stats> entry : this.testIds.entrySet()) {
+ final TestIdentifier testIdentifier = entry.getKey();
+ if (!isTestClass(testIdentifier).isPresent()) {
+ // we are not interested in anything other than a test "class" in this section
+ continue;
+ }
+ final Stats stats = entry.getValue();
+ final StringBuilder sb = new StringBuilder("Tests run: ").append(stats.numTestsRun.get());
+ sb.append(", Failures: ").append(stats.numTestsFailed.get());
+ sb.append(", Skipped: ").append(stats.numTestsSkipped.get());
+ sb.append(", Aborted: ").append(stats.numTestsAborted.get());
+ final long timeElapsed = stats.endedAt - stats.startedAt;
+ sb.append(", Time elapsed: ");
+ if (timeElapsed < 1000) {
+ sb.append(timeElapsed).append(" milli sec(s)");
+ } else {
+ sb.append(TimeUnit.SECONDS.convert(timeElapsed, TimeUnit.MILLISECONDS)).append(" sec(s)");
+ }
+ try {
+ this.writer.write(sb.toString());
+ this.writer.newLine();
+ } catch (IOException ioe) {
+ handleException(ioe);
+ return;
+ }
+ }
+ // write out sysout and syserr content if any
+ try {
+ if (this.hasSysOut()) {
+ this.writer.write("------------- Standard Output ---------------");
+ this.writer.newLine();
+ writeSysOut(writer);
+ this.writer.write("------------- ---------------- ---------------");
+ this.writer.newLine();
+ }
+ if (this.hasSysErr()) {
+ this.writer.write("------------- Standard Error ---------------");
+ this.writer.newLine();
+ writeSysErr(writer);
+ this.writer.write("------------- ---------------- ---------------");
+ this.writer.newLine();
+ }
+ } catch (IOException ioe) {
+ handleException(ioe);
+ return;
+ }
+ }
+
+ @Override
+ public void dynamicTestRegistered(final TestIdentifier testIdentifier) {
+ // nothing to do
+ }
+
+ @Override
+ public void executionSkipped(final TestIdentifier testIdentifier, final String reason) {
+ final long currentTime = System.currentTimeMillis();
+ this.testIds.putIfAbsent(testIdentifier, new Stats(testIdentifier, currentTime));
+ final Stats stats = this.testIds.get(testIdentifier);
+ stats.setEndedAt(currentTime);
+ if (testIdentifier.isTest()) {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("Test: ");
+ sb.append(testIdentifier.getLegacyReportingName());
+ final long timeElapsed = stats.endedAt - stats.startedAt;
+ sb.append(" took ");
+ if (timeElapsed < 1000) {
+ sb.append(timeElapsed).append(" milli sec(s)");
+ } else {
+ sb.append(TimeUnit.SECONDS.convert(timeElapsed, TimeUnit.MILLISECONDS)).append(" sec(s)");
+ }
+ sb.append(" SKIPPED");
+ if (reason != null && !reason.isEmpty()) {
+ sb.append(": ").append(reason);
+ }
+ try {
+ this.writer.write(sb.toString());
+ this.writer.newLine();
+ } catch (IOException ioe) {
+ handleException(ioe);
+ return;
+ }
+ }
+ // get the parent test class to which this skipped test belongs to
+ final Optional<TestIdentifier> parentTestClass = traverseAndFindTestClass(this.testPlan, testIdentifier);
+ if (!parentTestClass.isPresent()) {
+ return;
+ }
+ final Stats parentClassStats = this.testIds.get(parentTestClass.get());
+ parentClassStats.numTestsSkipped.incrementAndGet();
+ }
+
+ @Override
+ public void executionStarted(final TestIdentifier testIdentifier) {
+ final long currentTime = System.currentTimeMillis();
+ // record this testidentifier's start
+ this.testIds.putIfAbsent(testIdentifier, new Stats(testIdentifier, currentTime));
+ final Optional<ClassSource> testClass = isTestClass(testIdentifier);
+ if (testClass.isPresent()) {
+ // if this is a test class, then print it out
+ try {
+ this.writer.write("Testcase: " + testClass.get().getClassName());
+ this.writer.newLine();
+ } catch (IOException ioe) {
+ handleException(ioe);
+ return;
+ }
+ }
+ // if this is a test (method) then increment the tests run for the test class to which
+ // this test belongs to
+ if (testIdentifier.isTest()) {
+ final Optional<TestIdentifier> parentTestClass = traverseAndFindTestClass(this.testPlan, testIdentifier);
+ if (parentTestClass.isPresent()) {
+ final Stats parentClassStats = this.testIds.get(parentTestClass.get());
+ if (parentClassStats != null) {
+ parentClassStats.numTestsRun.incrementAndGet();
+ }
+ }
+ }
+ }
+
+ @Override
+ public void executionFinished(final TestIdentifier testIdentifier, final TestExecutionResult testExecutionResult) {
+ final long currentTime = System.currentTimeMillis();
+ final Stats stats = this.testIds.get(testIdentifier);
+ if (stats != null) {
+ stats.setEndedAt(currentTime);
+ }
+ if (testIdentifier.isTest() && shouldReportExecutionFinished(testIdentifier, testExecutionResult)) {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("Test: ");
+ sb.append(testIdentifier.getLegacyReportingName());
+ if (stats != null) {
+ final long timeElapsed = stats.endedAt - stats.startedAt;
+ sb.append(" took ");
+ if (timeElapsed < 1000) {
+ sb.append(timeElapsed).append(" milli sec(s)");
+ } else {
+ sb.append(TimeUnit.SECONDS.convert(timeElapsed, TimeUnit.MILLISECONDS)).append(" sec(s)");
+ }
+ }
+ switch (testExecutionResult.getStatus()) {
+ case ABORTED: {
+ sb.append(" ABORTED");
+ appendThrowable(sb, testExecutionResult);
+ break;
+ }
+ case FAILED: {
+ sb.append(" FAILED");
+ appendThrowable(sb, testExecutionResult);
+ break;
+ }
+ }
+ try {
+ this.writer.write(sb.toString());
+ this.writer.newLine();
+ } catch (IOException ioe) {
+ handleException(ioe);
+ return;
+ }
+ }
+ // get the parent test class in which this test completed
+ final Optional<TestIdentifier> parentTestClass = traverseAndFindTestClass(this.testPlan, testIdentifier);
+ if (!parentTestClass.isPresent()) {
+ return;
+ }
+ // update the stats of the parent test class
+ final Stats parentClassStats = this.testIds.get(parentTestClass.get());
+ switch (testExecutionResult.getStatus()) {
+ case ABORTED: {
+ parentClassStats.numTestsAborted.incrementAndGet();
+ break;
+ }
+ case FAILED: {
+ parentClassStats.numTestsFailed.incrementAndGet();
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void reportingEntryPublished(final TestIdentifier testIdentifier, final ReportEntry entry) {
+ // nothing to do
+ }
+
+ @Override
+ public void setDestination(final OutputStream os) {
+ this.outputStream = os;
+ try {
+ this.writer = new BufferedWriter(new OutputStreamWriter(this.outputStream, "UTF-8"));
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException("Failed to create a writer", e);
+ }
+ }
+
+ protected boolean shouldReportExecutionFinished(final TestIdentifier testIdentifier, final TestExecutionResult testExecutionResult) {
+ return true;
+ }
+
+ private static void appendThrowable(final StringBuilder sb, TestExecutionResult result) {
+ if (!result.getThrowable().isPresent()) {
+ return;
+ }
+ final Throwable throwable = result.getThrowable().get();
+ sb.append(": ").append(throwable.getMessage());
+ sb.append(NEW_LINE);
+ final StringWriter stacktrace = new StringWriter();
+ throwable.printStackTrace(new PrintWriter(stacktrace));
+ sb.append(stacktrace.toString());
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (this.writer != null) {
+ this.writer.close();
+ }
+ super.close();
+ }
+
+ private final class Stats {
+ private final TestIdentifier testIdentifier;
+ private final AtomicLong numTestsRun = new AtomicLong(0);
+ private final AtomicLong numTestsFailed = new AtomicLong(0);
+ private final AtomicLong numTestsSkipped = new AtomicLong(0);
+ private final AtomicLong numTestsAborted = new AtomicLong(0);
+ private final long startedAt;
+ private long endedAt;
+
+ private Stats(final TestIdentifier testIdentifier, final long startedAt) {
+ this.testIdentifier = testIdentifier;
+ this.startedAt = startedAt;
+ }
+
+ private void setEndedAt(final long endedAt) {
+ this.endedAt = endedAt;
+ }
+ }
+}