Skip to content

Commit

Permalink
Parallel Tests using the CLI #664
Browse files Browse the repository at this point in the history
* modifies FeatureBuilder to use absolute paths when resolving features using a direct path (as before) as well as a directory containing features (new)
* adds a ParallelCucumberMain which runs multiple .feature-files in parallel and contains a  mechanism to configure formatters as before (e.g. -f pretty:out -f fully.qual.name:out) - each executer caches the calls to formatter/reporter and replays them after a feature is executed.
  • Loading branch information
klausbayrhammer committed Oct 6, 2014
1 parent 73fd27d commit 0854dd4
Show file tree
Hide file tree
Showing 11 changed files with 551 additions and 15 deletions.
129 changes: 129 additions & 0 deletions core/src/main/java/cucumber/api/cli/ParallelCucumberMain.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package cucumber.api.cli;

import cucumber.runtime.ClassFinder;
import cucumber.runtime.Runtime;
import cucumber.runtime.RuntimeOptions;
import cucumber.runtime.io.MultiLoader;
import cucumber.runtime.io.ResourceLoaderClassFinder;
import cucumber.runtime.model.CucumberFeature;
import gherkin.formatter.Formatter;

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class ParallelCucumberMain {
private static final int ERRORS = 0x1;
private static final int NO_ERRORS = 0x00;
private static byte exitStatus = NO_ERRORS;

public static void main(String[] args) throws Throwable {
final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
RuntimeOptions runtimeOptions = new RuntimeOptions(Arrays.asList(args));
List<CucumberFeature> cucumberFeatures = resolveFeaturesToRun(classLoader, runtimeOptions);
List<Object> realPlugins = runtimeOptions.getPlugins();

ExecutorService executorService = createExecutorService();

for (final CucumberFeature cucumberFeature : cucumberFeatures) {
asyncRunFeature(args, classLoader, executorService, cucumberFeature, realPlugins);
}
awaitTermination(executorService);

finishFormatting(runtimeOptions.pluginProxy(classLoader, Formatter.class));
System.exit(exitStatus);
}

private static void finishFormatting(final Formatter formatter) {
formatter.done();
formatter.close();
}

private static List<CucumberFeature> resolveFeaturesToRun(final ClassLoader classLoader,
final RuntimeOptions runtimeOptions) {
return runtimeOptions.cucumberFeatures(new MultiLoader(classLoader));
}

private static ExecutorService createExecutorService() {
int numberOfThreads = getNumberOfThreads();
return Executors.newFixedThreadPool(numberOfThreads, new ThreadFactory() {
private AtomicInteger threadNumber = new AtomicInteger();

@Override
public Thread newThread(final Runnable r) {
Thread thread = new Thread(r);
thread.setDaemon(true);
thread.setName("cucumber_" + threadNumber.incrementAndGet());

return thread;
}
});
}

private static void asyncRunFeature(final String[] argv, final ClassLoader classLoader,
final ExecutorService executorService, final CucumberFeature cucumberFeature,
final List<Object> realPlugins) {
Runnable runnable = new Runnable() {
public void run() {
try {
RuntimeOptions options = new RuntimeOptions(Arrays.asList(argv));
options.getPlugins().clear();
options.addPlugin(new RecordingPluginImpl());
options.getFeaturePaths().clear();
options.getFeaturePaths().add(cucumberFeature.getPath());

runFeature(classLoader, options);

RecordingPlugin recordingPlugin = options.pluginProxy(classLoader, RecordingPlugin.class);
replayOnRealFormatters(recordingPlugin, realPlugins);
} catch (Exception e) {
e.printStackTrace();
}
}
};
executorService.submit(runnable);
}

private synchronized static void replayOnRealFormatters(final RecordingPlugin recordingPlugin,
final List<Object> realPlugins) {
for (Object plugin : realPlugins) {
recordingPlugin.replay(plugin);
}
}

private static void runFeature(final ClassLoader classLoader, final RuntimeOptions fixedRuntimeOptions) {
try {
final MultiLoader resourceLoader = new MultiLoader(classLoader);
final ClassFinder classFinder = new ResourceLoaderClassFinder(resourceLoader, classLoader);
Runtime runtime = new Runtime(resourceLoader, classFinder, classLoader, fixedRuntimeOptions);
runtime.run();
exitStatus |= runtime.exitStatus();
} catch (Exception e) {
exitStatus = ERRORS;
e.printStackTrace();
}
}

private static void awaitTermination(final ExecutorService executorService) {
try {
executorService.shutdown();
executorService.awaitTermination(getTimeout(), TimeUnit.SECONDS);
} catch (InterruptedException e) {
exitStatus = ERRORS;
e.printStackTrace();
}
}

// 15 minutes timeout
private static int getTimeout() {
return 60 * 15;
}

private static int getNumberOfThreads() {
return 2;
}
}
10 changes: 10 additions & 0 deletions core/src/main/java/cucumber/api/cli/RecordingPlugin.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package cucumber.api.cli;

import cucumber.api.StepDefinitionReporter;
import gherkin.formatter.Formatter;
import gherkin.formatter.Reporter;

public interface RecordingPlugin extends Reporter, Formatter, StepDefinitionReporter {

void replay(Object plugin);
}
Loading

0 comments on commit 0854dd4

Please sign in to comment.