| @@ -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> | |||
| @@ -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() { | |||
| } | |||
| } | |||
| @@ -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"; | |||
| } | |||
| } | |||
| @@ -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; | |||
| } | |||
| } | |||
| } | |||
| @@ -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(); | |||
| } | |||
| @@ -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); | |||
| } | |||
| } | |||
| } | |||
| @@ -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()); | |||
| } | |||
| } | |||
| @@ -23,7 +23,6 @@ package org.apache.tools.ant.taskdefs.optional.junitlauncher; | |||
| public interface NamedTest { | |||
| /** | |||
| * | |||
| * @return Returns the name of the test | |||
| */ | |||
| String getName(); | |||
| @@ -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()); | |||
| } | |||
| } | |||
| @@ -0,0 +1,259 @@ | |||
| /* | |||
| * 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 javax.xml.stream.XMLInputFactory; | |||
| import javax.xml.stream.XMLStreamReader; | |||
| import java.io.InputStream; | |||
| import java.nio.file.Files; | |||
| import java.nio.file.Path; | |||
| import java.nio.file.Paths; | |||
| import java.util.ArrayList; | |||
| import java.util.Collections; | |||
| import java.util.List; | |||
| import java.util.Optional; | |||
| import java.util.Properties; | |||
| import static javax.xml.stream.XMLStreamConstants.END_DOCUMENT; | |||
| import static javax.xml.stream.XMLStreamConstants.END_ELEMENT; | |||
| import static javax.xml.stream.XMLStreamConstants.START_DOCUMENT; | |||
| import static javax.xml.stream.XMLStreamConstants.START_ELEMENT; | |||
| 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; | |||
| import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ELM_LISTENER; | |||
| import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ELM_TEST; | |||
| import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ELM_TEST_CLASSES; | |||
| /** | |||
| * Used for launching forked tests from the {@link JUnitLauncherTask}. | |||
| * <p> | |||
| * Although this class is public, this isn't meant for external use. The contract of what | |||
| * program arguments {@link #main(String[]) the main method} accepts and how it interprets it, | |||
| * is also an internal detail and can change across Ant releases. | |||
| * | |||
| * @since Ant 1.10.6 | |||
| */ | |||
| public class StandaloneLauncher { | |||
| /** | |||
| * Entry point to launching the forked test. | |||
| * | |||
| * @param args The arguments passed to this program for launching the tests | |||
| * @throws Exception | |||
| */ | |||
| public static void main(final String[] args) throws Exception { | |||
| // The main responsibility of this entry point is to create a LaunchDefinition, | |||
| // by parsing the passed arguments and then use the LauncherSupport to | |||
| // LauncherSupport#launch the tests | |||
| try { | |||
| ForkedLaunch launchDefinition = null; | |||
| final ForkedExecution forkedExecution = new ForkedExecution(); | |||
| for (int i = 0; i < args.length; ) { | |||
| final String arg = args[i]; | |||
| int numArgsConsumed = 1; | |||
| switch (arg) { | |||
| case Constants.ARG_PROPERTIES: { | |||
| final Path propsPath = Paths.get(args[i + 1]); | |||
| if (!Files.isRegularFile(propsPath)) { | |||
| throw new IllegalArgumentException(propsPath + " does not point to a properties file"); | |||
| } | |||
| final Properties properties = new Properties(); | |||
| try (final InputStream is = Files.newInputStream(propsPath)) { | |||
| properties.load(is); | |||
| } | |||
| forkedExecution.setProperties(properties); | |||
| numArgsConsumed = 2; | |||
| break; | |||
| } | |||
| case Constants.ARG_LAUNCH_DEFINITION: { | |||
| final Path launchDefXmlPath = Paths.get(args[i + 1]); | |||
| if (!Files.isRegularFile(launchDefXmlPath)) { | |||
| throw new IllegalArgumentException(launchDefXmlPath + " does not point to a launch definition file"); | |||
| } | |||
| launchDefinition = parseLaunchDefinition(launchDefXmlPath); | |||
| numArgsConsumed = 2; | |||
| break; | |||
| } | |||
| } | |||
| i = i + numArgsConsumed; | |||
| } | |||
| launchDefinition.setTestExecutionContext(forkedExecution); | |||
| final LauncherSupport launcherSupport = new LauncherSupport(launchDefinition); | |||
| try { | |||
| launcherSupport.launch(); | |||
| } catch (Throwable t) { | |||
| if (launcherSupport.hasTestFailures()) { | |||
| System.exit(Constants.FORK_EXIT_CODE_TESTS_FAILED); | |||
| throw t; | |||
| } | |||
| } | |||
| if (launcherSupport.hasTestFailures()) { | |||
| System.exit(Constants.FORK_EXIT_CODE_TESTS_FAILED); | |||
| return; | |||
| } | |||
| System.exit(Constants.FORK_EXIT_CODE_SUCCESS); | |||
| return; | |||
| } catch (Throwable t) { | |||
| t.printStackTrace(); | |||
| throw t; | |||
| } | |||
| } | |||
| private static ForkedLaunch parseLaunchDefinition(final Path pathToLaunchDefXml) { | |||
| if (pathToLaunchDefXml == null || !Files.isRegularFile(pathToLaunchDefXml)) { | |||
| throw new IllegalArgumentException(pathToLaunchDefXml + " is not a file"); | |||
| } | |||
| final ForkedLaunch forkedLaunch = new ForkedLaunch(); | |||
| try (final InputStream is = Files.newInputStream(pathToLaunchDefXml)) { | |||
| final XMLStreamReader reader = XMLInputFactory.newFactory().createXMLStreamReader(is); | |||
| reader.require(START_DOCUMENT, null, null); | |||
| reader.nextTag(); | |||
| reader.require(START_ELEMENT, null, LD_XML_ELM_LAUNCH_DEF); | |||
| final String haltOnfFailure = reader.getAttributeValue(null, LD_XML_ATTR_HALT_ON_FAILURE); | |||
| if (haltOnfFailure != null) { | |||
| forkedLaunch.setHaltOnFailure(Boolean.parseBoolean(haltOnfFailure)); | |||
| } | |||
| final String printSummary = reader.getAttributeValue(null, LD_XML_ATTR_PRINT_SUMMARY); | |||
| if (printSummary != null) { | |||
| forkedLaunch.setPrintSummary(Boolean.parseBoolean(printSummary)); | |||
| } | |||
| if (haltOnfFailure != null) { | |||
| forkedLaunch.setHaltOnFailure(Boolean.parseBoolean(haltOnfFailure)); | |||
| } | |||
| reader.nextTag(); | |||
| reader.require(START_ELEMENT, null, null); | |||
| final String elementName = reader.getLocalName(); | |||
| switch (elementName) { | |||
| case LD_XML_ELM_TEST: { | |||
| forkedLaunch.addTests(Collections.singletonList(SingleTestClass.fromForkedRepresentation(reader))); | |||
| break; | |||
| } | |||
| case LD_XML_ELM_TEST_CLASSES: { | |||
| forkedLaunch.addTests(TestClasses.fromForkedRepresentation(reader)); | |||
| break; | |||
| } | |||
| case LD_XML_ELM_LISTENER: { | |||
| forkedLaunch.addListener(ListenerDefinition.fromForkedRepresentation(reader)); | |||
| break; | |||
| } | |||
| } | |||
| reader.nextTag(); | |||
| reader.require(END_ELEMENT, null, LD_XML_ELM_LAUNCH_DEF); | |||
| reader.next(); | |||
| reader.require(END_DOCUMENT, null, null); | |||
| return forkedLaunch; | |||
| } catch (Exception e) { | |||
| throw new BuildException("Failed to construct definition from forked representation", e); | |||
| } | |||
| } | |||
| private static final class ForkedExecution implements TestExecutionContext { | |||
| private Properties properties = new Properties(); | |||
| private ForkedExecution() { | |||
| } | |||
| private ForkedExecution setProperties(final Properties properties) { | |||
| this.properties = properties; | |||
| return this; | |||
| } | |||
| @Override | |||
| public Properties getProperties() { | |||
| return this.properties; | |||
| } | |||
| @Override | |||
| public Optional<Project> getProject() { | |||
| // forked execution won't have access to the Ant Project | |||
| return Optional.empty(); | |||
| } | |||
| } | |||
| private static final class ForkedLaunch implements LaunchDefinition { | |||
| private boolean printSummary; | |||
| private boolean haltOnFailure; | |||
| private TestExecutionContext testExecutionContext; | |||
| private List<TestDefinition> tests = new ArrayList<>(); | |||
| private List<ListenerDefinition> listeners = new ArrayList<>(); | |||
| @Override | |||
| public List<TestDefinition> getTests() { | |||
| return this.tests; | |||
| } | |||
| ForkedLaunch addTests(final List<TestDefinition> tests) { | |||
| this.tests.addAll(tests); | |||
| return this; | |||
| } | |||
| @Override | |||
| public List<ListenerDefinition> getListeners() { | |||
| return this.listeners; | |||
| } | |||
| ForkedLaunch addListener(final ListenerDefinition listener) { | |||
| this.listeners.add(listener); | |||
| return this; | |||
| } | |||
| @Override | |||
| public boolean isPrintSummary() { | |||
| return this.printSummary; | |||
| } | |||
| private ForkedLaunch setPrintSummary(final boolean printSummary) { | |||
| this.printSummary = printSummary; | |||
| return this; | |||
| } | |||
| @Override | |||
| public boolean isHaltOnFailure() { | |||
| return this.haltOnFailure; | |||
| } | |||
| public ForkedLaunch setHaltOnFailure(final boolean haltOnFailure) { | |||
| this.haltOnFailure = haltOnFailure; | |||
| return this; | |||
| } | |||
| public ForkedLaunch setTestExecutionContext(final TestExecutionContext testExecutionContext) { | |||
| this.testExecutionContext = testExecutionContext; | |||
| return this; | |||
| } | |||
| @Override | |||
| public ClassLoader getClassLoader() { | |||
| return this.getClass().getClassLoader(); | |||
| } | |||
| @Override | |||
| public TestExecutionContext getTestExecutionContext() { | |||
| return this.testExecutionContext; | |||
| } | |||
| } | |||
| } | |||
| @@ -21,11 +21,17 @@ import org.apache.tools.ant.types.Resource; | |||
| import org.apache.tools.ant.types.ResourceCollection; | |||
| import org.apache.tools.ant.types.resources.Resources; | |||
| import javax.xml.stream.XMLStreamConstants; | |||
| import javax.xml.stream.XMLStreamException; | |||
| import javax.xml.stream.XMLStreamReader; | |||
| import javax.xml.stream.XMLStreamWriter; | |||
| import java.io.File; | |||
| import java.util.ArrayList; | |||
| import java.util.Collections; | |||
| import java.util.List; | |||
| import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ELM_TEST_CLASSES; | |||
| /** | |||
| * Represents a {@code testclasses} that's configured to be launched by the {@link JUnitLauncherTask} | |||
| */ | |||
| @@ -42,14 +48,14 @@ public class TestClasses extends TestDefinition { | |||
| } | |||
| @Override | |||
| List<TestRequest> createTestRequests(final JUnitLauncherTask launcherTask) { | |||
| List<TestRequest> createTestRequests() { | |||
| 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)); | |||
| requests.addAll(test.createTestRequests()); | |||
| } | |||
| return requests; | |||
| } | |||
| @@ -126,4 +132,26 @@ public class TestClasses extends TestDefinition { | |||
| return TestClasses.this.getExcludeEngines(); | |||
| } | |||
| } | |||
| @Override | |||
| protected void toForkedRepresentation(final JUnitLauncherTask task, final XMLStreamWriter writer) throws XMLStreamException { | |||
| writer.writeStartElement(LD_XML_ELM_TEST_CLASSES); | |||
| // write out as multiple SingleTestClass representations | |||
| for (final SingleTestClass singleTestClass : getTests()) { | |||
| singleTestClass.toForkedRepresentation(task, writer); | |||
| } | |||
| writer.writeEndElement(); | |||
| } | |||
| static List<TestDefinition> fromForkedRepresentation(final XMLStreamReader reader) throws XMLStreamException { | |||
| reader.require(XMLStreamConstants.START_ELEMENT, null, LD_XML_ELM_TEST_CLASSES); | |||
| final List<TestDefinition> testDefinitions = new ArrayList<>(); | |||
| // read out as multiple SingleTestClass representations | |||
| while (reader.nextTag() != XMLStreamConstants.END_ELEMENT) { | |||
| reader.require(XMLStreamConstants.START_ELEMENT, null, Constants.LD_XML_ELM_TEST); | |||
| testDefinitions.add(SingleTestClass.fromForkedRepresentation(reader)); | |||
| } | |||
| reader.require(XMLStreamConstants.END_ELEMENT, null, LD_XML_ELM_TEST_CLASSES); | |||
| return testDefinitions; | |||
| } | |||
| } | |||
| @@ -17,9 +17,12 @@ | |||
| */ | |||
| 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.PropertyHelper; | |||
| import javax.xml.stream.XMLStreamException; | |||
| import javax.xml.stream.XMLStreamWriter; | |||
| import java.util.ArrayList; | |||
| import java.util.Collections; | |||
| import java.util.List; | |||
| @@ -28,6 +31,7 @@ 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; | |||
| @@ -35,6 +39,7 @@ abstract class TestDefinition { | |||
| protected String outputDir; | |||
| protected String includeEngines; | |||
| protected String excludeEngines; | |||
| protected ForkDefinition forkDefinition; | |||
| protected List<ListenerDefinition> listeners = new ArrayList<>(); | |||
| @@ -90,7 +95,19 @@ abstract class TestDefinition { | |||
| return this.outputDir; | |||
| } | |||
| abstract List<TestRequest> createTestRequests(final JUnitLauncherTask launcherTask); | |||
| public ForkDefinition createFork() { | |||
| if (this.forkDefinition != null) { | |||
| throw new BuildException("Test definition cannot have more than one fork elements"); | |||
| } | |||
| this.forkDefinition = new ForkDefinition(); | |||
| return this.forkDefinition; | |||
| } | |||
| ForkDefinition getForkDefinition() { | |||
| return this.forkDefinition; | |||
| } | |||
| abstract List<TestRequest> createTestRequests(); | |||
| protected boolean shouldRun(final Project project) { | |||
| final PropertyHelper propertyHelper = PropertyHelper.getPropertyHelper(project); | |||
| @@ -127,4 +144,7 @@ abstract class TestDefinition { | |||
| } | |||
| return parts.toArray(new String[parts.size()]); | |||
| } | |||
| protected abstract void toForkedRepresentation(JUnitLauncherTask task, XMLStreamWriter writer) throws XMLStreamException; | |||
| } | |||
| @@ -125,4 +125,13 @@ public class JUnitLauncherTaskTest { | |||
| public void testTestClasses() { | |||
| buildRule.executeTarget("test-batch"); | |||
| } | |||
| /** | |||
| * Tests the execution of a forked test | |||
| */ | |||
| @Test | |||
| public void testBasicFork() { | |||
| buildRule.executeTarget("test-basic-fork"); | |||
| } | |||
| } | |||