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/08/10 04:49:48 UTC
[2/2] ant git commit: Support for fork mode in junitlauncher
Support for fork mode in junitlauncher
Project: http://git-wip-us.apache.org/repos/asf/ant/repo
Commit: http://git-wip-us.apache.org/repos/asf/ant/commit/c9ca84fd
Tree: http://git-wip-us.apache.org/repos/asf/ant/tree/c9ca84fd
Diff: http://git-wip-us.apache.org/repos/asf/ant/diff/c9ca84fd
Branch: refs/heads/master
Commit: c9ca84fd5301aee6d0f58ef0a0907c94ea0cf38b
Parents: 3f36f0b
Author: Jaikiran Pai <ja...@apache.org>
Authored: Wed Jul 25 19:23:00 2018 +0530
Committer: Jaikiran Pai <ja...@apache.org>
Committed: Fri Aug 10 10:18:36 2018 +0530
----------------------------------------------------------------------
.../taskdefs/optional/junitlauncher.xml | 12 +
.../optional/junitlauncher/Constants.java | 54 ++
.../optional/junitlauncher/ForkDefinition.java | 156 ++++++
.../junitlauncher/JUnitLauncherTask.java | 552 ++++++-------------
.../junitlauncher/LaunchDefinition.java | 75 +++
.../optional/junitlauncher/LauncherSupport.java | 513 +++++++++++++++++
.../junitlauncher/ListenerDefinition.java | 53 ++
.../optional/junitlauncher/NamedTest.java | 1 -
.../optional/junitlauncher/SingleTestClass.java | 100 +++-
.../junitlauncher/StandaloneLauncher.java | 259 +++++++++
.../optional/junitlauncher/TestClasses.java | 32 +-
.../optional/junitlauncher/TestDefinition.java | 22 +-
.../junitlauncher/JUnitLauncherTaskTest.java | 9 +
13 files changed, 1457 insertions(+), 381 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/ant/blob/c9ca84fd/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
index ccae7ae..81861a7 100644
--- a/src/etc/testcases/taskdefs/optional/junitlauncher.xml
+++ b/src/etc/testcases/taskdefs/optional/junitlauncher.xml
@@ -30,6 +30,8 @@
<path id="junit.engine.vintage.classpath">
<fileset dir="../../../../../lib/optional" includes="junit-vintage-engine*.jar"/>
+ <fileset dir="../../../../../lib/optional" includes="junit-*.jar"/>
+ <fileset dir="../../../../../lib/optional" includes="hamcrest*.jar"/>
</path>
<path id="junit.engine.jupiter.classpath">
@@ -109,5 +111,15 @@
</testclasses>
</junitlauncher>
</target>
+
+ <target name="test-basic-fork" depends="init">
+ <junitlauncher>
+ <classpath refid="test.classpath"/>
+ <test name="org.example.junitlauncher.vintage.JUnit4SampleTest" outputdir="${output.dir}">
+ <fork dir="${basedir}"/>
+ <listener type="legacy-xml" sendSysErr="true" sendSysOut="true"/>
+ </test>
+ </junitlauncher>
+ </target>
</project>
http://git-wip-us.apache.org/repos/asf/ant/blob/c9ca84fd/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/Constants.java
----------------------------------------------------------------------
diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/Constants.java b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/Constants.java
new file mode 100644
index 0000000..a8b501c
--- /dev/null
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/Constants.java
@@ -0,0 +1,54 @@
+/*
+ * 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;
+
+/**
+ * Constants used within the junitlauncher task
+ */
+final class Constants {
+
+ static final int FORK_EXIT_CODE_SUCCESS = 0;
+ static final int FORK_EXIT_CODE_EXCEPTION = 1;
+ static final int FORK_EXIT_CODE_TESTS_FAILED = 2;
+ static final int FORK_EXIT_CODE_TIMED_OUT = 3;
+
+ static final String ARG_PROPERTIES = "--properties";
+ static final String ARG_LAUNCH_DEFINITION = "--launch-definition";
+
+
+ static final String LD_XML_ELM_LAUNCH_DEF = "launch-definition";
+ static final String LD_XML_ELM_TEST = "test";
+ static final String LD_XML_ELM_TEST_CLASSES = "test-classes";
+ static final String LD_XML_ATTR_HALT_ON_FAILURE = "haltOnFailure";
+ static final String LD_XML_ATTR_OUTPUT_DIRECTORY = "outDir";
+ static final String LD_XML_ATTR_INCLUDE_ENGINES = "includeEngines";
+ static final String LD_XML_ATTR_EXCLUDE_ENGINES = "excludeEngines";
+ static final String LD_XML_ATTR_CLASS_NAME = "classname";
+ static final String LD_XML_ATTR_METHODS = "methods";
+ static final String LD_XML_ATTR_PRINT_SUMMARY = "printSummary";
+ static final String LD_XML_ELM_LISTENER = "listener";
+ static final String LD_XML_ATTR_SEND_SYS_ERR = "sendSysErr";
+ static final String LD_XML_ATTR_SEND_SYS_OUT = "sendSysOut";
+ static final String LD_XML_ATTR_LISTENER_RESULT_FILE = "resultFile";
+
+
+ private Constants() {
+
+ }
+}
http://git-wip-us.apache.org/repos/asf/ant/blob/c9ca84fd/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/ForkDefinition.java
----------------------------------------------------------------------
diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/ForkDefinition.java b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/ForkDefinition.java
new file mode 100644
index 0000000..bda3381
--- /dev/null
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/ForkDefinition.java
@@ -0,0 +1,156 @@
+/*
+ * 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.apache.tools.ant.BuildException;
+import org.apache.tools.ant.Project;
+import org.apache.tools.ant.Task;
+import org.apache.tools.ant.launch.AntMain;
+import org.apache.tools.ant.types.Commandline;
+import org.apache.tools.ant.types.CommandlineJava;
+import org.apache.tools.ant.types.Environment;
+import org.apache.tools.ant.types.Path;
+import org.apache.tools.ant.types.PropertySet;
+import org.apache.tools.ant.util.LoaderUtils;
+import org.junit.platform.commons.annotation.Testable;
+import org.junit.platform.engine.TestEngine;
+import org.junit.platform.launcher.core.LauncherFactory;
+
+import java.io.File;
+
+/**
+ * Represents the {@code fork} element within test definitions of the
+ * {@code junitlauncher} task
+ */
+public class ForkDefinition {
+
+ private boolean includeAntRuntimeLibraries = true;
+ private boolean includeJunitPlatformLibraries = true;
+
+ private final CommandlineJava commandLineJava;
+ private final Environment env = new Environment();
+
+ private String dir;
+ private long timeout = -1;
+
+ ForkDefinition() {
+ this.commandLineJava = new CommandlineJava();
+ }
+
+ public void setDir(final String dir) {
+ this.dir = dir;
+ }
+
+ String getDir() {
+ return this.dir;
+ }
+
+ public void setTimeout(final long timeout) {
+ this.timeout = timeout;
+ }
+
+ long getTimeout() {
+ return this.timeout;
+ }
+
+ public Commandline.Argument createJvmArg() {
+ return this.commandLineJava.createVmArgument();
+ }
+
+ public void addConfiguredSysProperty(final Environment.Variable sysProp) {
+ // validate that key/value are present
+ sysProp.validate();
+ this.commandLineJava.addSysproperty(sysProp);
+ }
+
+ public void addConfiguredSysPropertySet(final PropertySet propertySet) {
+ this.commandLineJava.addSyspropertyset(propertySet);
+ }
+
+ public void addConfiguredEnv(final Environment.Variable var) {
+ this.env.addVariable(var);
+ }
+
+ public void addConfiguredModulePath(final Path modulePath) {
+ this.commandLineJava.createModulepath(modulePath.getProject()).add(modulePath);
+ }
+
+ public void addConfiguredUpgradeModulePath(final Path upgradeModulePath) {
+ this.commandLineJava.createUpgrademodulepath(upgradeModulePath.getProject()).add(upgradeModulePath);
+ }
+
+ Environment getEnv() {
+ return this.env;
+ }
+
+ /**
+ * Generates a new {@link CommandlineJava} constructed out of the configurations set on this
+ * {@link ForkDefinition}
+ *
+ * @param task The junitlaunchertask for which this is a fork definition
+ * @return
+ */
+ CommandlineJava generateCommandLine(final JUnitLauncherTask task) {
+ final CommandlineJava cmdLine;
+ try {
+ cmdLine = (CommandlineJava) this.commandLineJava.clone();
+ } catch (CloneNotSupportedException e) {
+ throw new BuildException(e);
+ }
+ cmdLine.setClassname(StandaloneLauncher.class.getName());
+ // VM arguments
+ final Project project = task.getProject();
+ final Path antRuntimeResourceSources = new Path(project);
+ if (this.includeAntRuntimeLibraries) {
+ addAntRuntimeResourceSource(antRuntimeResourceSources, task, toResourceName(AntMain.class));
+ addAntRuntimeResourceSource(antRuntimeResourceSources, task, toResourceName(Task.class));
+ addAntRuntimeResourceSource(antRuntimeResourceSources, task, toResourceName(JUnitLauncherTask.class));
+ }
+
+ if (this.includeJunitPlatformLibraries) {
+ // platform-engine
+ addAntRuntimeResourceSource(antRuntimeResourceSources, task, toResourceName(TestEngine.class));
+ // platform-launcher
+ addAntRuntimeResourceSource(antRuntimeResourceSources, task, toResourceName(LauncherFactory.class));
+ // platform-commons
+ addAntRuntimeResourceSource(antRuntimeResourceSources, task, toResourceName(Testable.class));
+ }
+ final Path classPath = cmdLine.createClasspath(project);
+ classPath.createPath().append(antRuntimeResourceSources);
+
+ return cmdLine;
+ }
+
+ private static boolean addAntRuntimeResourceSource(final Path path, final JUnitLauncherTask task, final String resource) {
+ final File f = LoaderUtils.getResourceSource(task.getClass().getClassLoader(), resource);
+ if (f == null) {
+ task.log("Could not locate source of resource " + resource);
+ return false;
+ }
+ task.log("Found source " + f + " of resource " + resource);
+ path.createPath().setLocation(f);
+ return true;
+ }
+
+ private static String toResourceName(final Class klass) {
+ final String name = klass.getName();
+ return name.replaceAll("\\.", "/") + ".class";
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/ant/blob/c9ca84fd/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
index a6423ca..a328e4b 100644
--- a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/JUnitLauncherTask.java
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/JUnitLauncherTask.java
@@ -21,37 +21,32 @@ 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.taskdefs.Execute;
+import org.apache.tools.ant.taskdefs.ExecuteWatchdog;
+import org.apache.tools.ant.taskdefs.LogOutputStream;
+import org.apache.tools.ant.taskdefs.PumpStreamHandler;
+import org.apache.tools.ant.types.CommandlineJava;
+import org.apache.tools.ant.types.Environment;
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 javax.xml.stream.XMLOutputFactory;
+import javax.xml.stream.XMLStreamWriter;
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.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
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.Hashtable;
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;
+import java.util.concurrent.TimeoutException;
+
+import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ATTR_HALT_ON_FAILURE;
+import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ATTR_PRINT_SUMMARY;
+import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ELM_LAUNCH_DEF;
/**
* An Ant {@link Task} responsible for launching the JUnit platform for running tests.
@@ -84,55 +79,22 @@ public class JUnitLauncherTask extends Task {
@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);
- }
- }
+ if (this.tests.isEmpty()) {
+ return;
+ }
+ final Project project = getProject();
+ for (final TestDefinition test : this.tests) {
+ if (!test.shouldRun(project)) {
+ log("Excluding test " + test + " since it's considered not to run " +
+ "in context of project " + project, Project.MSG_DEBUG);
+ continue;
+ }
+ if (test.getForkDefinition() != null) {
+ forkTest(test);
+ } else {
+ final LauncherSupport launcherSupport = new LauncherSupport(new InVMLaunch(Collections.singletonList(test)));
+ launcherSupport.launch();
}
- } finally {
- Thread.currentThread().setContextClassLoader(previousClassLoader);
}
}
@@ -204,360 +166,212 @@ public class JUnitLauncherTask extends Task {
}
}
- 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);
+ private ClassLoader createClassLoaderForTestExecution() {
+ if (this.classPath == null) {
+ return this.getClass().getClassLoader();
}
- return requests;
+ return new AntClassLoader(this.getClass().getClassLoader(), getProject(), this.classPath, true);
}
- 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 java.nio.file.Path dumpProjectProperties() throws IOException {
+ final java.nio.file.Path propsPath = Files.createTempFile(null, "properties");
+ propsPath.toFile().deleteOnExit();
+ final Hashtable<String, Object> props = this.getProject().getProperties();
+ final Properties projProperties = new Properties();
+ projProperties.putAll(props);
+ try (final OutputStream os = Files.newOutputStream(propsPath)) {
+ // TODO: Is it always UTF-8?
+ projProperties.store(os, StandardCharsets.UTF_8.name());
}
+ return propsPath;
}
- 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");
+ private void forkTest(final TestDefinition test) {
+ // create launch command
+ final ForkDefinition forkDefinition = test.getForkDefinition();
+ final CommandlineJava commandlineJava = forkDefinition.generateCommandLine(this);
+ if (this.classPath != null) {
+ commandlineJava.createClasspath(getProject()).createPath().append(this.classPath);
}
- final Class<?> klass;
+ final java.nio.file.Path projectPropsPath;
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);
+ projectPropsPath = dumpProjectProperties();
+ } catch (IOException e) {
+ throw new BuildException("Could not create the necessary properties file while forking a process" +
+ " for a test", e);
}
- }
+ // --properties <path-to-properties-file>
+ commandlineJava.createArgument().setValue(Constants.ARG_PROPERTIES);
+ commandlineJava.createArgument().setValue(projectPropsPath.toAbsolutePath().toString());
- private void handleTestExecutionCompletion(final TestDefinition test, final TestExecutionSummary summary) {
- if (printSummary) {
- // print the summary to System.out
- summary.printTo(new PrintWriter(System.out, true));
- }
- 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)";
+ final java.nio.file.Path launchDefXmlPath = newLaunchDefinitionXml();
+ try (final OutputStream os = Files.newOutputStream(launchDefXmlPath)) {
+ final XMLStreamWriter writer = XMLOutputFactory.newFactory().createXMLStreamWriter(os, "UTF-8");
+ try {
+ writer.writeStartDocument();
+ writer.writeStartElement(LD_XML_ELM_LAUNCH_DEF);
+ if (this.printSummary) {
+ writer.writeAttribute(LD_XML_ATTR_PRINT_SUMMARY, "true");
}
- 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);
- }
-
- @SuppressWarnings("resource")
- private Optional<SwitchedStreamHandle> trySwitchSysOutErr(final TestRequest testRequest, final StreamType streamType) {
- switch (streamType) {
- case SYS_OUT: {
- if (!testRequest.interestedInSysOut()) {
- return Optional.empty();
+ if (this.haltOnFailure) {
+ writer.writeAttribute(LD_XML_ATTR_HALT_ON_FAILURE, "true");
}
- break;
- }
- case SYS_ERR: {
- if (!testRequest.interestedInSysErr()) {
- return Optional.empty();
+ // task level listeners
+ for (final ListenerDefinition listenerDef : this.listeners) {
+ if (!listenerDef.shouldUse(getProject())) {
+ continue;
+ }
+ // construct the listener definition argument
+ listenerDef.toForkedRepresentation(writer);
}
- break;
- }
- default: {
- // unknown, but no need to error out, just be lenient
- // and return back
- return Optional.empty();
+ // test definition as XML
+ test.toForkedRepresentation(this, writer);
+ writer.writeEndElement();
+ writer.writeEndDocument();
+ } finally {
+ writer.close();
}
+ } catch (Exception e) {
+ throw new BuildException("Failed to construct command line for test", e);
}
- 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();
+ // --launch-definition <xml-file-path>
+ commandlineJava.createArgument().setValue(Constants.ARG_LAUNCH_DEFINITION);
+ commandlineJava.createArgument().setValue(launchDefXmlPath.toAbsolutePath().toString());
+
+ // launch the process and wait for process to complete
+ final int exitCode = executeForkedTest(forkDefinition, commandlineJava);
+ switch (exitCode) {
+ case Constants.FORK_EXIT_CODE_SUCCESS: {
+ // success
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();
+ case Constants.FORK_EXIT_CODE_EXCEPTION: {
+ // process failed with some exception
+ throw new BuildException("Forked test(s) failed with an exception");
+ }
+ case Constants.FORK_EXIT_CODE_TESTS_FAILED: {
+ // test has failure(s)
+ try {
+ if (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
+ this.getProject().setNewProperty(test.getFailureProperty(), "true");
+ }
+ } finally {
+ if (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 failure(s)";
+ } else {
+ errorMessage = "Some test(s) have failure(s)";
+ }
+ throw new BuildException(errorMessage);
+ }
+ }
break;
}
- default: {
- return Optional.empty();
+ case Constants.FORK_EXIT_CODE_TIMED_OUT: {
+ throw new BuildException(new TimeoutException("Forked test(s) timed out"));
}
}
- 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;
+ private int executeForkedTest(final ForkDefinition forkDefinition, final CommandlineJava commandlineJava) {
+ final LogOutputStream outStream = new LogOutputStream(this, Project.MSG_INFO);
+ final LogOutputStream errStream = new LogOutputStream(this, Project.MSG_WARN);
+ final ExecuteWatchdog watchdog = forkDefinition.getTimeout() > 0 ? new ExecuteWatchdog(forkDefinition.getTimeout()) : null;
+ final Execute execute = new Execute(new PumpStreamHandler(outStream, errStream), watchdog);
+ execute.setCommandline(commandlineJava.getCommandline());
+ execute.setAntRun(getProject());
+ if (forkDefinition.getDir() != null) {
+ execute.setWorkingDirectory(Paths.get(forkDefinition.getDir()).toFile());
}
+ final Environment env = forkDefinition.getEnv();
+ if (env != null && env.getVariables() != null) {
+ execute.setEnvironment(env.getVariables());
+ }
+ log(commandlineJava.describeCommand(), Project.MSG_VERBOSE);
+ int exitCode;
+ try {
+ exitCode = execute.execute();
+ } catch (IOException e) {
+ throw new BuildException("Process fork failed", e, getLocation());
+ }
+ return (watchdog != null && watchdog.killedProcess()) ? Constants.FORK_EXIT_CODE_TIMED_OUT : exitCode;
+ }
- @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);
- } 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 java.nio.file.Path newLaunchDefinitionXml() {
+ final java.nio.file.Path xmlFilePath;
+ try {
+ xmlFilePath = Files.createTempFile(null, ".xml");
+ } catch (IOException e) {
+ throw new BuildException("Failed to construct command line for test", e);
}
+ xmlFilePath.toFile().deleteOnExit();
+ return xmlFilePath;
}
- 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);
+ private final class InVMExecution implements TestExecutionContext {
+
+ private final Properties props;
- SysOutErrContentDeliverer(final StreamType streamType, final Collection<TestResultFormatter> resultFormatters) {
- this.streamType = streamType;
- this.resultFormatters = resultFormatters;
+ InVMExecution() {
+ this.props = new Properties();
+ this.props.putAll(JUnitLauncherTask.this.getProject().getProperties());
}
@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();
- }
+ public Properties getProperties() {
+ return this.props;
}
- 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;
- }
- }
- }
+ @Override
+ public Optional<Project> getProject() {
+ return Optional.of(JUnitLauncherTask.this.getProject());
}
}
- private final class SwitchedStreamHandle {
- private final PipedOutputStream outputStream;
- private final SysOutErrStreamReader streamReader;
+ private final class InVMLaunch implements LaunchDefinition {
- SwitchedStreamHandle(final PipedOutputStream outputStream, final SysOutErrStreamReader streamReader) {
- this.streamReader = streamReader;
- this.outputStream = outputStream;
- }
- }
+ private final TestExecutionContext testExecutionContext = new InVMExecution();
+ private final List<TestDefinition> inVMTests;
+ private final ClassLoader executionCL;
- private final class Listener extends SummaryGeneratingListener {
- private Optional<SwitchedStreamHandle> switchedSysOutHandle;
- private Optional<SwitchedStreamHandle> switchedSysErrHandle;
+ private InVMLaunch(final List<TestDefinition> inVMTests) {
+ this.inVMTests = inVMTests;
+ this.executionCL = createClassLoaderForTestExecution();
+ }
@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();
- }
- }
+ public List<TestDefinition> getTests() {
+ return this.inVMTests;
}
- 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);
+ @Override
+ public List<ListenerDefinition> getListeners() {
+ return listeners;
}
- }
- private final class InVMExecution implements TestExecutionContext {
-
- private final Properties props;
+ @Override
+ public boolean isPrintSummary() {
+ return printSummary;
+ }
- InVMExecution() {
- this.props = new Properties();
- this.props.putAll(JUnitLauncherTask.this.getProject().getProperties());
+ @Override
+ public boolean isHaltOnFailure() {
+ return haltOnFailure;
}
@Override
- public Properties getProperties() {
- return this.props;
+ public ClassLoader getClassLoader() {
+ return this.executionCL;
}
@Override
- public Optional<Project> getProject() {
- return Optional.of(JUnitLauncherTask.this.getProject());
+ public TestExecutionContext getTestExecutionContext() {
+ return this.testExecutionContext;
}
}
}
http://git-wip-us.apache.org/repos/asf/ant/blob/c9ca84fd/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LaunchDefinition.java
----------------------------------------------------------------------
diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LaunchDefinition.java b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LaunchDefinition.java
new file mode 100644
index 0000000..d3e5ae3
--- /dev/null
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LaunchDefinition.java
@@ -0,0 +1,75 @@
+/*
+ * 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 java.util.List;
+
+/**
+ * Defines the necessary context for launching the JUnit platform for running
+ * tests.
+ */
+public interface LaunchDefinition {
+
+ /**
+ * Returns the {@link TestDefinition tests} that have to be launched
+ *
+ * @return
+ */
+ List<TestDefinition> getTests();
+
+ /**
+ * Returns the default {@link ListenerDefinition listeners} that will be used
+ * for the tests, if the {@link #getTests() tests} themselves don't specify any
+ *
+ * @return
+ */
+ List<ListenerDefinition> getListeners();
+
+ /**
+ * Returns true if a summary needs to be printed out after the execution of the
+ * tests. False otherwise.
+ *
+ * @return
+ */
+ boolean isPrintSummary();
+
+ /**
+ * Returns true if any remaining tests launch need to be stopped if any test execution
+ * failed. False otherwise.
+ *
+ * @return
+ */
+ boolean isHaltOnFailure();
+
+ /**
+ * Returns the {@link ClassLoader} that has to be used for launching and execution of the
+ * tests
+ *
+ * @return
+ */
+ ClassLoader getClassLoader();
+
+ /**
+ * Returns the {@link TestExecutionContext} that will be passed to {@link TestResultFormatter#setContext(TestExecutionContext)
+ * result formatters} which are applicable during the execution of the tests.
+ *
+ * @return
+ */
+ TestExecutionContext getTestExecutionContext();
+}
http://git-wip-us.apache.org/repos/asf/ant/blob/c9ca84fd/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LauncherSupport.java
----------------------------------------------------------------------
diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LauncherSupport.java b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LauncherSupport.java
new file mode 100644
index 0000000..6a8027b
--- /dev/null
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LauncherSupport.java
@@ -0,0 +1,513 @@
+/*
+ * 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.apache.tools.ant.BuildException;
+import org.apache.tools.ant.Project;
+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.io.PrintWriter;
+import java.nio.file.Files;
+import java.nio.file.Path;
+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.concurrent.BlockingQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Responsible for doing the real work involved in launching the JUnit platform
+ * and passing it the relevant tests that need to be executed by the JUnit platform.
+ * <p>
+ * This class relies on a {@link LaunchDefinition} for setting up the launch of the
+ * JUnit platform.
+ * <p>
+ * The {@code LauncherSupport} isn't concerned with whether or not
+ * it's being executed in the same JVM as the build in which the {@code junitlauncher}
+ * was triggered or if it's running as part of a forked JVM. Instead it just relies
+ * on the {@code LaunchDefinition} to do whatever decisions need to be done before and
+ * after launching the tests.
+ * <p>
+ * This class is not thread-safe and isn't expected to be used for launching from
+ * multiple different threads simultaneously.
+ */
+class LauncherSupport {
+
+ private final LaunchDefinition launchDefinition;
+
+ private boolean testsFailed;
+
+ /**
+ * Create a {@link LauncherSupport} for the passed {@link LaunchDefinition}
+ *
+ * @param definition The launch definition which will be used for launching the tests
+ */
+ LauncherSupport(final LaunchDefinition definition) {
+ if (definition == null) {
+ throw new IllegalArgumentException("Launch definition cannot be null");
+ }
+ this.launchDefinition = definition;
+ }
+
+ /**
+ * Launches the tests defined in the {@link LaunchDefinition}
+ *
+ * @throws BuildException If any tests failed and the launch definition was configured to throw
+ * an exception, or if any other exception occurred before or after launching
+ * the tests
+ */
+ void launch() throws BuildException {
+ final ClassLoader previousClassLoader = Thread.currentThread().getContextClassLoader();
+ try {
+ Thread.currentThread().setContextClassLoader(this.launchDefinition.getClassLoader());
+ 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, this.launchDefinition.getClassLoader()));
+ 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);
+ }
+ }
+
+ /**
+ * Returns true if there were any test failures, when this {@link LauncherSupport} was used
+ * to {@link #launch()} tests. False otherwise.
+ *
+ * @return
+ */
+ boolean hasTestFailures() {
+ return this.testsFailed;
+ }
+
+ private List<TestRequest> buildTestRequests() {
+ final List<TestDefinition> tests = this.launchDefinition.getTests();
+ if (tests.isEmpty()) {
+ return Collections.emptyList();
+ }
+ final List<TestRequest> requests = new ArrayList<>();
+ for (final TestDefinition test : tests) {
+ final List<TestRequest> testRequests = test.createTestRequests();
+ 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.launchDefinition.getListeners() : test.getListeners();
+ final List<TestExecutionListener> listeners = new ArrayList<>();
+ final Optional<Project> project = this.launchDefinition.getTestExecutionContext().getProject();
+ for (final ListenerDefinition applicableListener : applicableListenerElements) {
+ if (project.isPresent() && !applicableListener.shouldUse(project.get())) {
+ log("Excluding listener " + applicableListener.getClassName() + " since it's not applicable" +
+ " in the context of project", null, 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(this.launchDefinition.getTestExecutionContext());
+ // set the destination output stream for writing out the formatted result
+ final TestDefinition test = testRequest.getOwner();
+ final TestExecutionContext testExecutionContext = this.launchDefinition.getTestExecutionContext();
+ final Path baseDir = testExecutionContext.getProject().isPresent()
+ ? testExecutionContext.getProject().get().getBaseDir().toPath() : Paths.get(System.getProperty("user.dir"));
+ final java.nio.file.Path outputDir = test.getOutputDir() != null ? Paths.get(test.getOutputDir()) : baseDir;
+ 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) {
+ if (this.launchDefinition.isPrintSummary()) {
+ // print the summary to System.out
+ summary.printTo(new PrintWriter(System.out, true));
+ }
+ final boolean hasTestFailures = summary.getTestsFailedCount() != 0;
+ if (hasTestFailures) {
+ // keep track of the test failure(s) for the entire launched instance
+ this.testsFailed = true;
+ }
+ 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
+ final TestExecutionContext testExecutionContext = this.launchDefinition.getTestExecutionContext();
+ if (testExecutionContext.getProject().isPresent()) {
+ final Project project = testExecutionContext.getProject().get();
+ project.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 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 void log(final String message, final Throwable t, final int level) {
+ final TestExecutionContext testExecutionContext = this.launchDefinition.getTestExecutionContext();
+ if (testExecutionContext.getProject().isPresent()) {
+ testExecutionContext.getProject().get().log(message, t, level);
+ return;
+ }
+ if (t == null) {
+ System.out.println(message);
+ } else {
+ System.err.println(message);
+ t.printStackTrace();
+ }
+ }
+
+ private enum StreamType {
+ SYS_OUT,
+ SYS_ERR
+ }
+
+ private static final class SysOutErrStreamReader implements Runnable {
+ private static final byte[] EMPTY = new byte[0];
+
+ private final LauncherSupport launchManager;
+ private final InputStream sourceStream;
+ private final StreamType streamType;
+ private final Collection<TestResultFormatter> resultFormatters;
+ private volatile SysOutErrContentDeliverer contentDeliverer;
+
+ SysOutErrStreamReader(final LauncherSupport launchManager, final InputStream source, final StreamType streamType, final Collection<TestResultFormatter> resultFormatters) {
+ this.launchManager = launchManager;
+ 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) {
+ this.launchManager.log("Failed while streaming " + (this.streamType == StreamType.SYS_OUT ? "sysout" : "syserr") + " data",
+ e, Project.MSG_INFO);
+ } 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();
+ }
+ }
+ }
+
+ 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);
+ }
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/ant/blob/c9ca84fd/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
index 6b50ce2..c24e872 100644
--- a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/ListenerDefinition.java
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/ListenerDefinition.java
@@ -21,12 +21,24 @@ import org.apache.tools.ant.Project;
import org.apache.tools.ant.PropertyHelper;
import org.apache.tools.ant.types.EnumeratedAttribute;
+import javax.xml.stream.XMLStreamConstants;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+import javax.xml.stream.XMLStreamWriter;
+
+import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ATTR_CLASS_NAME;
+import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ATTR_LISTENER_RESULT_FILE;
+import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ATTR_SEND_SYS_ERR;
+import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ATTR_SEND_SYS_OUT;
+import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ELM_LISTENER;
+
/**
* 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";
@@ -135,4 +147,45 @@ public class ListenerDefinition {
}
}
+ void toForkedRepresentation(final XMLStreamWriter writer) throws XMLStreamException {
+ writer.writeStartElement(LD_XML_ELM_LISTENER);
+ writer.writeAttribute(LD_XML_ATTR_CLASS_NAME, this.className);
+ writer.writeAttribute(LD_XML_ATTR_SEND_SYS_ERR, Boolean.toString(this.sendSysErr));
+ writer.writeAttribute(LD_XML_ATTR_SEND_SYS_OUT, Boolean.toString(this.sendSysOut));
+ if (this.resultFile != null) {
+ writer.writeAttribute(LD_XML_ATTR_LISTENER_RESULT_FILE, this.resultFile);
+ }
+ writer.writeEndElement();
+ }
+
+ static ListenerDefinition fromForkedRepresentation(final XMLStreamReader reader) throws XMLStreamException {
+ reader.require(XMLStreamConstants.START_ELEMENT, null, LD_XML_ELM_LISTENER);
+ final ListenerDefinition listenerDef = new ListenerDefinition();
+ final String className = requireAttributeValue(reader, LD_XML_ATTR_CLASS_NAME);
+ listenerDef.setClassName(className);
+ final String sendSysErr = reader.getAttributeValue(null, LD_XML_ATTR_SEND_SYS_ERR);
+ if (sendSysErr != null) {
+ listenerDef.setSendSysErr(Boolean.parseBoolean(sendSysErr));
+ }
+ final String sendSysOut = reader.getAttributeValue(null, LD_XML_ATTR_SEND_SYS_OUT);
+ if (sendSysOut != null) {
+ listenerDef.setSendSysOut(Boolean.parseBoolean(sendSysOut));
+ }
+ final String resultFile = reader.getAttributeValue(null, LD_XML_ATTR_LISTENER_RESULT_FILE);
+ if (resultFile != null) {
+ listenerDef.setResultFile(resultFile);
+ }
+ reader.nextTag();
+ reader.require(XMLStreamConstants.END_ELEMENT, null, LD_XML_ELM_LISTENER);
+ return listenerDef;
+ }
+
+ private static String requireAttributeValue(final XMLStreamReader reader, final String attrName) throws XMLStreamException {
+ final String val = reader.getAttributeValue(null, attrName);
+ if (val != null) {
+ return val;
+ }
+ throw new XMLStreamException("Attribute " + attrName + " is missing at " + reader.getLocation());
+ }
+
}
http://git-wip-us.apache.org/repos/asf/ant/blob/c9ca84fd/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
index 01c23cb..07039a6 100644
--- a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/NamedTest.java
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/NamedTest.java
@@ -23,7 +23,6 @@ package org.apache.tools.ant.taskdefs.optional.junitlauncher;
public interface NamedTest {
/**
- *
* @return Returns the name of the test
*/
String getName();
http://git-wip-us.apache.org/repos/asf/ant/blob/c9ca84fd/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
index a950f85..3744a81 100644
--- a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/SingleTestClass.java
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/SingleTestClass.java
@@ -17,17 +17,28 @@
*/
package org.apache.tools.ant.taskdefs.optional.junitlauncher;
-import org.apache.tools.ant.Project;
import org.junit.platform.engine.discovery.DiscoverySelectors;
import org.junit.platform.launcher.EngineFilter;
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
+import javax.xml.stream.XMLStreamConstants;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+import javax.xml.stream.XMLStreamWriter;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;
+import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ATTR_CLASS_NAME;
+import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ATTR_EXCLUDE_ENGINES;
+import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ATTR_HALT_ON_FAILURE;
+import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ATTR_INCLUDE_ENGINES;
+import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ATTR_METHODS;
+import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ATTR_OUTPUT_DIRECTORY;
+import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ELM_TEST;
+
/**
* Represents the single {@code test} (class) that's configured to be launched by the {@link JUnitLauncherTask}
*/
@@ -85,13 +96,7 @@ public class SingleTestClass extends TestDefinition implements NamedTest {
}
@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();
- }
+ List<TestRequest> createTestRequests() {
final LauncherDiscoveryRequestBuilder requestBuilder = LauncherDiscoveryRequestBuilder.request();
if (!this.hasMethodsSpecified()) {
requestBuilder.selectors(DiscoverySelectors.selectClass(this.testClass));
@@ -112,4 +117,83 @@ public class SingleTestClass extends TestDefinition implements NamedTest {
}
return Collections.singletonList(new TestRequest(this, requestBuilder));
}
+
+ @Override
+ protected void toForkedRepresentation(final JUnitLauncherTask task, final XMLStreamWriter writer) throws XMLStreamException {
+ writer.writeStartElement(LD_XML_ELM_TEST);
+ writer.writeAttribute(LD_XML_ATTR_CLASS_NAME, testClass);
+ if (testMethods != null) {
+ final StringBuilder sb = new StringBuilder();
+ for (final String method : testMethods) {
+ if (sb.length() != 0) {
+ sb.append(",");
+ }
+ sb.append(method);
+ }
+ writer.writeAttribute(LD_XML_ATTR_METHODS, sb.toString());
+ }
+ if (haltOnFailure != null) {
+ writer.writeAttribute(LD_XML_ATTR_HALT_ON_FAILURE, haltOnFailure.toString());
+ }
+ if (outputDir != null) {
+ writer.writeAttribute(LD_XML_ATTR_OUTPUT_DIRECTORY, outputDir);
+ }
+ if (includeEngines != null) {
+ writer.writeAttribute(LD_XML_ATTR_INCLUDE_ENGINES, includeEngines);
+ }
+ if (excludeEngines != null) {
+ writer.writeAttribute(LD_XML_ATTR_EXCLUDE_ENGINES, excludeEngines);
+ }
+ // listeners for this test
+ if (listeners != null) {
+ for (final ListenerDefinition listenerDef : getListeners()) {
+ if (!listenerDef.shouldUse(task.getProject())) {
+ // not applicable
+ continue;
+ }
+ listenerDef.toForkedRepresentation(writer);
+ }
+ }
+ writer.writeEndElement();
+ }
+
+ static TestDefinition fromForkedRepresentation(final XMLStreamReader reader) throws XMLStreamException {
+ reader.require(XMLStreamConstants.START_ELEMENT, null, LD_XML_ELM_TEST);
+ final SingleTestClass testDefinition = new SingleTestClass();
+ final String testClassName = requireAttributeValue(reader, LD_XML_ATTR_CLASS_NAME);
+ testDefinition.setName(testClassName);
+ final String methodNames = reader.getAttributeValue(null, LD_XML_ATTR_METHODS);
+ if (methodNames != null) {
+ testDefinition.setMethods(methodNames);
+ }
+ final String halt = reader.getAttributeValue(null, LD_XML_ATTR_HALT_ON_FAILURE);
+ if (halt != null) {
+ testDefinition.setHaltOnFailure(Boolean.parseBoolean(halt));
+ }
+ final String outDir = reader.getAttributeValue(null, LD_XML_ATTR_OUTPUT_DIRECTORY);
+ if (outDir != null) {
+ testDefinition.setOutputDir(outDir);
+ }
+ final String includeEngs = reader.getAttributeValue(null, LD_XML_ATTR_INCLUDE_ENGINES);
+ if (includeEngs != null) {
+ testDefinition.setIncludeEngines(includeEngs);
+ }
+ final String excludeEngs = reader.getAttributeValue(null, LD_XML_ATTR_EXCLUDE_ENGINES);
+ if (excludeEngs != null) {
+ testDefinition.setExcludeEngines(excludeEngs);
+ }
+ while (reader.nextTag() != XMLStreamConstants.END_ELEMENT) {
+ reader.require(XMLStreamConstants.START_ELEMENT, null, Constants.LD_XML_ELM_LISTENER);
+ testDefinition.addConfiguredListener(ListenerDefinition.fromForkedRepresentation(reader));
+ }
+ return testDefinition;
+ }
+
+ private static String requireAttributeValue(final XMLStreamReader reader, final String attrName) throws XMLStreamException {
+ final String val = reader.getAttributeValue(null, attrName);
+ if (val != null) {
+ return val;
+ }
+ throw new XMLStreamException("Attribute " + attrName + " is missing at " + reader.getLocation());
+ }
}