From notifications-return-31869-archive-asf-public=cust-asf.ponee.io@ant.apache.org Fri Aug 10 06:49:50 2018 Return-Path: X-Original-To: archive-asf-public@cust-asf.ponee.io Delivered-To: archive-asf-public@cust-asf.ponee.io Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by mx-eu-01.ponee.io (Postfix) with SMTP id A5F8F180676 for ; Fri, 10 Aug 2018 06:49:48 +0200 (CEST) Received: (qmail 64580 invoked by uid 500); 10 Aug 2018 04:49:47 -0000 Mailing-List: contact notifications-help@ant.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@ant.apache.org Delivered-To: mailing list notifications@ant.apache.org Received: (qmail 64556 invoked by uid 99); 10 Aug 2018 04:49:47 -0000 Received: from git1-us-west.apache.org (HELO git1-us-west.apache.org) (140.211.11.23) by apache.org (qpsmtpd/0.29) with ESMTP; Fri, 10 Aug 2018 04:49:47 +0000 Received: by git1-us-west.apache.org (ASF Mail Server at git1-us-west.apache.org, from userid 33) id 0B3B2E0181; Fri, 10 Aug 2018 04:49:47 +0000 (UTC) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit From: jaikiran@apache.org To: notifications@ant.apache.org Date: Fri, 10 Aug 2018 04:49:48 -0000 Message-Id: <7742c5de4fdb4988b2c484304d7568f7@git.apache.org> In-Reply-To: <630bcfbf9c5a4e18b97af344781d8874@git.apache.org> References: <630bcfbf9c5a4e18b97af344781d8874@git.apache.org> X-Mailer: ASF-Git Admin Mailer Subject: [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 Authored: Wed Jul 25 19:23:00 2018 +0530 Committer: Jaikiran Pai 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 @@ + + @@ -109,5 +111,15 @@ + + + + + + + + + + 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 requests = buildTestRequests(); - for (final TestRequest testRequest : requests) { - try { - final TestDefinition test = testRequest.getOwner(); - final LauncherDiscoveryRequest request = testRequest.getDiscoveryRequest().build(); - final List 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 buildTestRequests() { - if (this.tests.isEmpty()) { - return Collections.emptyList(); - } - final List requests = new ArrayList<>(); - for (final TestDefinition test : this.tests) { - final List 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 getListeners(final TestRequest testRequest, final ClassLoader classLoader) { - final TestDefinition test = testRequest.getOwner(); - final List applicableListenerElements = test.getListeners().isEmpty() ? this.listeners : test.getListeners(); - final List 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 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 + 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 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 + 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 resultFormatters; - private volatile SysOutErrContentDeliverer contentDeliverer; - - SysOutErrStreamReader(final JUnitLauncherTask task, final InputStream source, final StreamType streamType, final Collection 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 resultFormatters; - private final StreamType streamType; - private final BlockingQueue 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 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 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 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 inVMTests; + private final ClassLoader executionCL; - private final class Listener extends SummaryGeneratingListener { - private Optional switchedSysOutHandle; - private Optional switchedSysErrHandle; + private InVMLaunch(final List 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 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 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 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 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 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. + *

+ * This class relies on a {@link LaunchDefinition} for setting up the launch of the + * JUnit platform. + *

+ * 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. + *

+ * 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 requests = buildTestRequests(); + for (final TestRequest testRequest : requests) { + try { + final TestDefinition test = testRequest.getOwner(); + final LauncherDiscoveryRequest request = testRequest.getDiscoveryRequest().build(); + final List 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 buildTestRequests() { + final List tests = this.launchDefinition.getTests(); + if (tests.isEmpty()) { + return Collections.emptyList(); + } + final List requests = new ArrayList<>(); + for (final TestDefinition test : tests) { + final List testRequests = test.createTestRequests(); + if (testRequests == null || testRequests.isEmpty()) { + continue; + } + requests.addAll(testRequests); + } + return requests; + } + + private List getListeners(final TestRequest testRequest, final ClassLoader classLoader) { + final TestDefinition test = testRequest.getOwner(); + final List applicableListenerElements = test.getListeners().isEmpty() + ? this.launchDefinition.getListeners() : test.getListeners(); + final List listeners = new ArrayList<>(); + final Optional 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 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 resultFormatters; + private volatile SysOutErrContentDeliverer contentDeliverer; + + SysOutErrStreamReader(final LauncherSupport launchManager, final InputStream source, final StreamType streamType, final Collection 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 resultFormatters; + private final StreamType streamType; + private final BlockingQueue availableData = new LinkedBlockingQueue<>(); + private final CountDownLatch completionLatch = new CountDownLatch(1); + + SysOutErrContentDeliverer(final StreamType streamType, final Collection 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 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 switchedSysOutHandle; + private Optional 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 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 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()); + } }