/**
* Copyright 2012-2013 LMAX Ltd.
*
* Licensed 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 com.lmax.ant.paralleljunit;
import com.lmax.ant.paralleljunit.remote.TestSpecificationFactory;
import com.lmax.ant.paralleljunit.remote.controller.RemoteTestRunnerControllerFactory;
import com.lmax.ant.paralleljunit.remote.controller.RemoteTestRunnerProcessFactory;
import com.lmax.ant.paralleljunit.remote.process.RemoteTestRunner;
import com.lmax.ant.paralleljunit.util.DaemonThreadFactory;
import com.lmax.ant.paralleljunit.util.io.EOFAwareInputStreamFactory;
import com.lmax.ant.paralleljunit.util.io.ExecuteStreamHandlerFactory;
import com.lmax.ant.paralleljunit.util.io.PumpStreamHandlerFactory;
import com.lmax.ant.paralleljunit.util.io.SynchronisedOutputStream;
import com.lmax.ant.paralleljunit.util.net.ConnectionEstablisherFactory;
import com.lmax.ant.paralleljunit.util.process.ExecuteWatchdogFactory;
import com.lmax.ant.paralleljunit.util.process.ManagedProcessFactory;
import com.lmax.ant.paralleljunit.util.process.ProcessBuilderFactory;
import com.lmax.ant.paralleljunit.util.process.ProcessDestroyer;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.ProjectComponent;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.launch.AntMain;
import org.apache.tools.ant.taskdefs.LogOutputStream;
import org.apache.tools.ant.taskdefs.optional.junit.FormatterElement;
import org.apache.tools.ant.taskdefs.optional.junit.JUnitTask.SummaryAttribute;
import org.apache.tools.ant.taskdefs.optional.junit.JUnitTest;
import org.apache.tools.ant.types.Assertions;
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 javax.net.ServerSocketFactory;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import static java.lang.Math.max;
import static java.util.Arrays.asList;
import static org.apache.tools.ant.util.LoaderUtils.getClassSource;
public class ParallelJUnitTask extends Task implements ParallelJUnitTaskConfig
{
private boolean haltOnError = false;
private String errorProperty;
private boolean haltOnFailure = false;
private String failureProperty;
private boolean filterTrace = true;
private long timeout = NO_TIMEOUT;
private File dir;
private boolean newEnvironment = false;
private boolean logFailedTests = true;
private boolean enableTestListenerEvents = false;
private boolean shuffle = false;
private final int availableProcessors = Runtime.getRuntime().availableProcessors();
private int threads = availableProcessors;
private boolean dirPerWorker = false;
private String workerDirPrefix = "parallel-junit-";
private CommandlineJava commandLine = new CommandlineJava();
private Environment environment = new Environment();
private final TestResultCoordinator testResultCoordinator = new TestResultCoordinator();
private WorkerCoordinator workerCoordinator =
new WorkerCoordinator(
new WorkerFactory(
new RemoteTestRunnerControllerFactory(
new ManagedProcessFactory(
new RemoteTestRunnerProcessFactory(
new ProcessBuilderFactory()),
new ProcessDestroyer(),
new ExecuteStreamHandlerFactory(new PumpStreamHandlerFactory(
new SynchronisedOutputStream(new LogOutputStream(this, Project.MSG_INFO)), //TODO novakd JUnitLogOutputStream
new SynchronisedOutputStream(new LogOutputStream(this, Project.MSG_WARN)))),
new ExecuteWatchdogFactory(),
new EOFAwareInputStreamFactory(),
Executors.newCachedThreadPool()),
Executors.newCachedThreadPool(new DaemonThreadFactory()), //TODO novakd does this have to be a deamon threadpool???
ServerSocketFactory.getDefault(),
new ConnectionEstablisherFactory(),
new TestSpecificationFactory()),
testResultCoordinator),
Executors.newCachedThreadPool(),
testResultCoordinator);
private BatchTestFactory batchTestFactory = new BatchTestFactory();
private final NumberParser numberParser = new NumberParser();
private ThreadsParser threadsParser = new ThreadsParser(new PercentileParser(numberParser, availableProcessors),
new AdditiveParser(numberParser, availableProcessors),
numberParser);
private Collection<DelegatingBatchTest> batchTests = new LinkedList<DelegatingBatchTest>();
private final List<FormatterElement> formatters = new LinkedList<FormatterElement>();
private Queue<JUnitTest> testQueue;
public ParallelJUnitTask()
{
}
ParallelJUnitTask(final CommandlineJava commandLine, final Environment environment, final WorkerCoordinator workerCoordinator, final BatchTestFactory batchTestFactory,
final ThreadsParser threadsParser, final Collection<DelegatingBatchTest> batchTests)
{
this.commandLine = commandLine;
this.environment = environment;
this.workerCoordinator = workerCoordinator;
this.batchTestFactory = batchTestFactory;
this.threadsParser = threadsParser;
this.batchTests = batchTests;
}
@Override
public void init() throws BuildException
{
super.init();
final Path remoteTestRunnerClasses = commandLine.createClasspath(getProject()).createPath();
remoteTestRunnerClasses.setLocation(getClassSource(RemoteTestRunner.class));
remoteTestRunnerClasses.setLocation(getClassSource(JUnitTest.class));
remoteTestRunnerClasses.setLocation(getClassSource(AntMain.class));
remoteTestRunnerClasses.setLocation(getClassSource(Task.class));
final File antJunit4Lib = LoaderUtils.getResourceSource(getClass().getClassLoader(), "org/apache/tools/ant/taskdefs/optional/junit/JUnit4TestMethodAdapter.class");
if (antJunit4Lib != null)
{
remoteTestRunnerClasses.setLocation(antJunit4Lib);
}
}
public void setPrintSummary(final SummaryAttribute printSummary)
{
if (printSummary.asBoolean())
{
final String prefix = printSummary.getValue().equalsIgnoreCase("withoutanderr") ? "OutErr" : "";
commandLine.createArgument().setValue("formatter=org.apache.tools.ant.taskdefs.optional.junit." + prefix + "SummaryJUnitResultFormatter");
}
}
public void setHaltOnError(final boolean haltOnError)
{
this.haltOnError = haltOnError;
}
public void setErrorProperty(final String errorProperty)
{
this.errorProperty = errorProperty;
}
public void setHaltOnFailure(final boolean haltOnFailure)
{
this.haltOnFailure = haltOnFailure;
}
public void setFailureProperty(final String failureProperty)
{
this.failureProperty = failureProperty;
}
public void setFilterTrace(final boolean filterTrace)
{
this.filterTrace = filterTrace;
}
public void setTimeout(final int timeout)
{
this.timeout = timeout;
}
public void setMaxMemory(final String maxMemory)
{
commandLine.setMaxmemory(maxMemory);
}
public void setJvm(final String jvm)
{
commandLine.setVm(jvm);
}
public void setDir(final File dir)
{
this.dir = dir;
}
public void setNewEnvironment(final boolean newEnvironment)
{
this.newEnvironment = newEnvironment;
}
public void setDirPerWorker(boolean dirPerWorker)
{
this.dirPerWorker = dirPerWorker;
}
public void setWorkerDirPrefix(String workerDirPrefix)
{
this.workerDirPrefix = workerDirPrefix;
}
public void setShowOutput(final boolean showOutput)
{
commandLine.createArgument().setValue("showoutput=" + showOutput);
}
public void setCloneVm(final boolean cloneVm)
{
commandLine.setCloneVm(cloneVm);
}
public void setLogFailedTests(final boolean logFailedTests)
{
if (logFailedTests)
{
commandLine.createArgument().setValue("logfailedtests=true");
}
this.logFailedTests = logFailedTests;
}
public void setEnableTestListenerEvents(final boolean enableTestListenerEvents)
{
this.enableTestListenerEvents = enableTestListenerEvents;
}
public void setShuffle(final boolean shuffle)
{
this.shuffle = shuffle;
}
public void setThreads(final String threads)
{
this.threads = max(1, threadsParser.parse(threads));
}
public DelegatingBatchTest createBatchTest()
{
final DelegatingBatchTest batchTest = batchTestFactory.createBatchTest(getProject());
batchTest.setFiltertrace(filterTrace);
batchTest.setHaltonerror(haltOnError);
batchTest.setErrorProperty(errorProperty);
batchTest.setHaltonfailure(haltOnFailure);
batchTest.setFailureProperty(failureProperty);
for (final FormatterElement formatter : formatters)
{
batchTest.addFormatter(formatter);
}
batchTests.add(batchTest);
return batchTest;
}
public void addFormatter(final FormatterElement formatter)
{
formatters.add(formatter);
}
public Commandline.Argument createJvmArg()
{
return commandLine.createVmArgument();
}
public void addConfiguredSysProperty(final Environment.Variable sysProp)
{
commandLine.addSysproperty(sysProp);
}
public void addSysPropertySet(final PropertySet propertySet)
{
commandLine.addSyspropertyset(propertySet);
}
public void addEnv(final Environment.Variable variable)
{
environment.addVariable(variable);
}
public Path createBootClassPath()
{
return commandLine.createBootclasspath(getProject()).createPath();
}
public Path createClasspath()
{
return commandLine.createClasspath(getProject()).createPath();
}
public void addAssertions(final Assertions assertions)
{
if (commandLine.getAssertions() != null)
{
throw new BuildException("Only one assertion declaration is allowed");
}
commandLine.setAssertions(assertions);
}
@Override
public void execute() throws BuildException
{
populateTestQueue();
workerCoordinator.execute(this);
}
public Queue<JUnitTest> getTestQueue()
{
return testQueue;
}
public List<String> getCommand(final Class<?> mainClass, final int workerId, final int serverPort)
{
try
{
final CommandlineJava clonedCommandLine = (CommandlineJava)commandLine.clone();
clonedCommandLine.setClassname(mainClass.getCanonicalName());
clonedCommandLine.createArgument().setValue("serverPort=" + serverPort);
clonedCommandLine.createArgument().setValue("workerId=" + workerId);
final String enableListenerEvents = getProject().getProperty("ant.junit.enabletestlistenerevents");
if (enableListenerEvents != null)
{
if (Project.toBoolean(enableListenerEvents))
{
clonedCommandLine.createArgument().setValue("logtestlistenerevents=true");
}
}
else if (enableTestListenerEvents)
{
clonedCommandLine.createArgument().setValue("logtestlistenerevents=true");
}
return new ArrayList<String>(asList(clonedCommandLine.getCommandline()));
}
catch (final CloneNotSupportedException e)
{
// Will not happen - honestly. CommandlineJava is cloneable.
throw new BuildException("Error cloning commandLine [" + commandLine + "]", e);
}
}
public File getDirectory(int workerId)
{
if (dirPerWorker)
{
File baseDirectory = dir != null ? dir : new File(System.getProperty("user.dir"));
return new File(baseDirectory, workerDirPrefix + workerId);
}
return dir;
}
public boolean isNewEnvironment()
{
return newEnvironment;
}
public Map<String, String> getEnvironment()
{
if (environment.getVariables() == null)
{
return Collections.emptyMap();
}
final Map<String, String> environmentMap = new HashMap<String, String>();
for (final String envVariable : environment.getVariables())
{
final int equalsSignIndex = envVariable.indexOf('=');
// Silently ignore envVariable lacking the required `='.
if (equalsSignIndex != -1)
{
environmentMap.put(envVariable.substring(0, equalsSignIndex), envVariable.substring(equalsSignIndex + 1));
}
}
return environmentMap;
}
public long getTimeout()
{
return timeout;
}
public boolean isLogFailedTests()
{
return logFailedTests;
}
public ProjectComponent getProjectComponent()
{
return this;
}
public int getThreads()
{
return threads;
}
private void populateTestQueue()
{
final List<JUnitTest> testList = new LinkedList<JUnitTest>();
for (final DelegatingBatchTest batchTest : batchTests)
{
final Enumeration<JUnitTest> enumerationOfTests = batchTest.elements();
while (enumerationOfTests.hasMoreElements())
{
testList.add(enumerationOfTests.nextElement());
}
}
if (shuffle)
{
Collections.shuffle(testList);
}
testQueue = new ArrayBlockingQueue<JUnitTest>(testList.size() + 1, false, testList);
}
}