From 0854dd46d481087be08940772d996030dc48bedc Mon Sep 17 00:00:00 2001 From: bayrhammerklaus Date: Thu, 4 Sep 2014 17:25:07 +0200 Subject: [PATCH] Parallel Tests using the CLI #664 * 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. --- .../api/cli/ParallelCucumberMain.java | 129 ++++++ .../cucumber/api/cli/RecordingPlugin.java | 10 + .../cucumber/api/cli/RecordingPluginImpl.java | 402 ++++++++++++++++++ .../java/cucumber/runtime/FeatureBuilder.java | 2 +- .../java/cucumber/runtime/RuntimeOptions.java | 2 +- .../cucumber/runtime/FeatureBuilderTest.java | 1 + .../cucumber/runtime/RuntimeOptionsTest.java | 13 +- .../java/cucumber/runtime/TestHelper.java | 2 +- .../autocomplete/StepdefGeneratorTest.java | 2 +- .../runtime/model/CucumberFeatureTest.java | 1 + .../runtime/junit/TestFeatureBuilder.java | 2 +- 11 files changed, 551 insertions(+), 15 deletions(-) create mode 100644 core/src/main/java/cucumber/api/cli/ParallelCucumberMain.java create mode 100644 core/src/main/java/cucumber/api/cli/RecordingPlugin.java create mode 100644 core/src/main/java/cucumber/api/cli/RecordingPluginImpl.java diff --git a/core/src/main/java/cucumber/api/cli/ParallelCucumberMain.java b/core/src/main/java/cucumber/api/cli/ParallelCucumberMain.java new file mode 100644 index 0000000000..836344c1e6 --- /dev/null +++ b/core/src/main/java/cucumber/api/cli/ParallelCucumberMain.java @@ -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 cucumberFeatures = resolveFeaturesToRun(classLoader, runtimeOptions); + List 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 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 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 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; + } +} diff --git a/core/src/main/java/cucumber/api/cli/RecordingPlugin.java b/core/src/main/java/cucumber/api/cli/RecordingPlugin.java new file mode 100644 index 0000000000..1a08a6d64d --- /dev/null +++ b/core/src/main/java/cucumber/api/cli/RecordingPlugin.java @@ -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); +} diff --git a/core/src/main/java/cucumber/api/cli/RecordingPluginImpl.java b/core/src/main/java/cucumber/api/cli/RecordingPluginImpl.java new file mode 100644 index 0000000000..f4342a804a --- /dev/null +++ b/core/src/main/java/cucumber/api/cli/RecordingPluginImpl.java @@ -0,0 +1,402 @@ +package cucumber.api.cli; + +import cucumber.api.StepDefinitionReporter; +import cucumber.runtime.StepDefinition; +import gherkin.formatter.Formatter; +import gherkin.formatter.Reporter; +import gherkin.formatter.model.Background; +import gherkin.formatter.model.Examples; +import gherkin.formatter.model.Feature; +import gherkin.formatter.model.Match; +import gherkin.formatter.model.Result; +import gherkin.formatter.model.Scenario; +import gherkin.formatter.model.ScenarioOutline; +import gherkin.formatter.model.Step; + +import java.util.ArrayList; +import java.util.List; + +public class RecordingPluginImpl implements RecordingPlugin { + List commands = new ArrayList(); + + public void replay(Object plugin) { + for (Command command : commands) { + command.apply(plugin); + } + } + + @Override + public void uri(final String uri) { + commands.add(new UriCommand(uri)); + } + + @Override + public void feature(final Feature feature) { + commands.add(new FeatureCommand(feature)); + } + + @Override + public void background(final Background background) { + commands.add(new BackgroundCommand(background)); + } + + @Override + public void scenario(Scenario scenario) { + commands.add(new ScenarioCommand(scenario)); + } + + @Override + public void scenarioOutline(ScenarioOutline scenarioOutline) { + commands.add(new ScenarioOutlineCommand(scenarioOutline)); + } + + @Override + public void examples(Examples examples) { + commands.add(new ExamplesCommand(examples)); + } + + @Override + public void startOfScenarioLifeCycle(final Scenario scenario) { + commands.add(new StartOfScenarioLifeCycleCommand(scenario)); + } + + @Override + public void step(Step step) { + commands.add(new StepCommand(step)); + } + + @Override + public void endOfScenarioLifeCycle(final Scenario scenario) { + commands.add(new EndOfScenarioLifeCycleCommand(scenario)); + } + + @Override + public void eof() { + commands.add(new EofCommand()); + } + + @Override + public void syntaxError(final String state, final String event, final List legalEvents, final String uri, + final Integer line) { + commands.add(new SyntaxErrorCommand(state, event, legalEvents, uri, line)); + } + + @Override + public void done() { + // do not keep the done command + } + + @Override + public void close() { + // do not keep the close command + } + + @Override + public void before(Match match, Result result) { + commands.add(new BeforeCommand(match, result)); + } + + @Override + public void result(Result result) { + commands.add(new ResultCommand(result)); + } + + @Override + public void after(Match match, Result result) { + commands.add(new AfterCommand(match, result)); + } + + @Override + public void match(final Match match) { + commands.add(new MatchCommand(match)); + } + + @Override + public void embedding(String mimeType, byte[] data) { + commands.add(new EmbeddingCommand(mimeType, data)); + } + + @Override + public void write(String text) { + commands.add(new WriteCommand(text)); + } + + @Override + public void stepDefinition(StepDefinition stepDefinition) { + commands.add(new StepDefinitionCommand(stepDefinition)); + } + + private interface Command { + + void apply(Object plugin); + } + + private static class SyntaxErrorCommand implements Command { + private final String state; + private final String event; + private final List legalEvents; + private final String uri; + private final Integer line; + + public SyntaxErrorCommand(final String state, final String event, final List legalEvents, + final String uri, final Integer line) { + this.state = state; + this.event = event; + this.legalEvents = legalEvents; + this.uri = uri; + this.line = line; + } + + @Override + public void apply(Object plugin) { + if (plugin instanceof Formatter) { + ((Formatter) plugin).syntaxError(state, event, legalEvents, uri, line); + } + } + } + + private static class ScenarioOutlineCommand implements Command { + private ScenarioOutline scenarioOutline; + + public ScenarioOutlineCommand(ScenarioOutline scenarioOutline) { + this.scenarioOutline = scenarioOutline; + } + + public void apply(Object plugin) { + if (plugin instanceof Formatter) { + ((Formatter) plugin).scenarioOutline(scenarioOutline); + } + } + } + + private static class ExamplesCommand implements Command { + private Examples examples; + + public ExamplesCommand(Examples examples) { + this.examples = examples; + } + + public void apply(Object plugin) { + if (plugin instanceof Formatter) { + ((Formatter) plugin).examples(examples); + } + } + } + + private static class StepCommand implements Command { + private Step step; + + public StepCommand(Step step) { + this.step = step; + } + + public void apply(Object plugin) { + if (plugin instanceof Formatter) { + ((Formatter) plugin).step(step); + } + } + } + + private static class StartOfScenarioLifeCycleCommand implements Command { + private Scenario scenario; + + private StartOfScenarioLifeCycleCommand(Scenario scenario) { + this.scenario = scenario; + } + + public void apply(Object plugin) { + if (plugin instanceof Formatter) { + ((Formatter) plugin).startOfScenarioLifeCycle(scenario); + } + } + } + + private static class EndOfScenarioLifeCycleCommand implements Command { + private Scenario scenario; + + private EndOfScenarioLifeCycleCommand(Scenario scenario) { + this.scenario = scenario; + } + + public void apply(Object plugin) { + if (plugin instanceof Formatter) { + ((Formatter) plugin).endOfScenarioLifeCycle(scenario); + } + } + } + + + private static class ResultCommand implements Command { + private Result result; + + public ResultCommand(Result result) { + this.result = result; + } + + public void apply(Object plugin) { + if (plugin instanceof Reporter) { + ((Reporter) plugin).result(result); + } + } + } + + private static class WriteCommand implements Command { + private String text; + + public WriteCommand(String text) { + this.text = text; + } + + public void apply(Object plugin) { + if (plugin instanceof Reporter) { + ((Reporter) plugin).write(text); + } + } + } + + private static class StepDefinitionCommand implements Command { + private StepDefinition stepDefinition; + + public StepDefinitionCommand(StepDefinition stepDefinition) { + this.stepDefinition = stepDefinition; + } + + public void apply(Object plugin) { + if (plugin instanceof StepDefinitionReporter) { + ((StepDefinitionReporter) plugin).stepDefinition(stepDefinition); + } + } + } + + private static class ScenarioCommand implements Command { + private Scenario scenario; + + public ScenarioCommand(Scenario scenario) { + this.scenario = scenario; + } + + public void apply(Object plugin) { + if (plugin instanceof Formatter) { + ((Formatter) plugin).scenario(scenario); + } + } + } + + private static class UriCommand implements Command { + private String uri; + + UriCommand(final String uri) { + this.uri = uri; + } + + + public void apply(Object plugin) { + if (plugin instanceof Formatter) { + ((Formatter) plugin).uri(uri); + } + } + } + + private static class FeatureCommand implements Command { + private Feature feature; + + FeatureCommand(final Feature feature) { + this.feature = feature; + } + + + public void apply(Object plugin) { + if (plugin instanceof Formatter) { + ((Formatter) plugin).feature(feature); + } + } + } + + private static class MatchCommand implements Command { + private Match match; + + MatchCommand(final Match match) { + this.match = match; + } + + @Override + public void apply(Object plugin) { + if (plugin instanceof Reporter) { + ((Reporter) plugin).match(match); + } + } + } + + private static class BackgroundCommand implements Command { + private Background background; + + BackgroundCommand(final Background background) { + this.background = background; + } + + + public void apply(Object plugin) { + if (plugin instanceof Formatter) { + ((Formatter) plugin).background(background); + } + } + } + + private static class BeforeCommand implements Command { + private Match match; + private Result result; + + public BeforeCommand(Match match, Result result) { + this.match = match; + this.result = result; + } + + public void apply(Object plugin) { + if (plugin instanceof Reporter) { + ((Reporter) plugin).before(match, result); + } + } + } + + private static class EmbeddingCommand implements Command { + private String mimeType; + private byte[] data; + + public EmbeddingCommand(String mimeType, byte[] data) { + this.mimeType = mimeType; + this.data = data; + } + + public void apply(Object plugin) { + if (plugin instanceof Reporter) { + ((Reporter) plugin).embedding(mimeType, data); + } + } + } + + private static class AfterCommand implements Command { + private Match match; + private Result result; + + public AfterCommand(Match match, Result result) { + this.match = match; + this.result = result; + } + + public void apply(Object plugin) { + if (plugin instanceof Reporter) { + ((Reporter) plugin).after(match, result); + } + } + } + + private static class EofCommand implements Command { + + public void apply(Object plugin) { + if (plugin instanceof Formatter) { + ((Formatter) plugin).eof(); + } + } + } + +} diff --git a/core/src/main/java/cucumber/runtime/FeatureBuilder.java b/core/src/main/java/cucumber/runtime/FeatureBuilder.java index 4e500aeaef..7fe72a7e05 100644 --- a/core/src/main/java/cucumber/runtime/FeatureBuilder.java +++ b/core/src/main/java/cucumber/runtime/FeatureBuilder.java @@ -128,7 +128,7 @@ public void parse(Resource resource, List filters) { Parser parser = new Parser(formatter); try { - parser.parse(gherkin, convertFileSeparatorToForwardSlash(resource.getPath()), 0); + parser.parse(gherkin, convertFileSeparatorToForwardSlash(resource.getAbsolutePath()), 0); } catch (Exception e) { throw new CucumberException(String.format("Error parsing feature file %s", convertFileSeparatorToForwardSlash(resource.getPath())), e); } diff --git a/core/src/main/java/cucumber/runtime/RuntimeOptions.java b/core/src/main/java/cucumber/runtime/RuntimeOptions.java index 032c840599..f249f82567 100644 --- a/core/src/main/java/cucumber/runtime/RuntimeOptions.java +++ b/core/src/main/java/cucumber/runtime/RuntimeOptions.java @@ -150,7 +150,7 @@ public List cucumberFeatures(ResourceLoader resourceLoader) { return load(resourceLoader, featurePaths, filters, System.out); } - List getPlugins() { + public List getPlugins() { if (!pluginNamesInstantiated) { for (String pluginName : pluginNames) { Object plugin = pluginFactory.create(pluginName); diff --git a/core/src/test/java/cucumber/runtime/FeatureBuilderTest.java b/core/src/test/java/cucumber/runtime/FeatureBuilderTest.java index 48351877c2..f304d0e6a8 100644 --- a/core/src/test/java/cucumber/runtime/FeatureBuilderTest.java +++ b/core/src/test/java/cucumber/runtime/FeatureBuilderTest.java @@ -63,6 +63,7 @@ public void converts_windows_path_to_forward_slash() throws IOException { private Resource createResourceMock(String featurePath) throws IOException { Resource resource = mock(Resource.class); when(resource.getPath()).thenReturn(featurePath); + when(resource.getAbsolutePath()).thenReturn(featurePath); ByteArrayInputStream feature = new ByteArrayInputStream("Feature: foo".getBytes("UTF-8")); when(resource.getInputStream()).thenReturn(feature); return resource; diff --git a/core/src/test/java/cucumber/runtime/RuntimeOptionsTest.java b/core/src/test/java/cucumber/runtime/RuntimeOptionsTest.java index 358c10d79d..94585130ac 100644 --- a/core/src/test/java/cucumber/runtime/RuntimeOptionsTest.java +++ b/core/src/test/java/cucumber/runtime/RuntimeOptionsTest.java @@ -13,8 +13,6 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.UnsupportedEncodingException; -import java.net.MalformedURLException; -import java.net.URL; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -22,14 +20,8 @@ import java.util.regex.Pattern; import static java.util.Arrays.asList; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.mockito.Mockito.withSettings; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; public class RuntimeOptionsTest { @Test @@ -281,6 +273,7 @@ private void mockResource(ResourceLoader resourceLoader, String featurePath, Str throws IOException, UnsupportedEncodingException { Resource resource1 = mock(Resource.class); when(resource1.getPath()).thenReturn(featurePath); + when(resource1.getAbsolutePath()).thenReturn(featurePath); when(resource1.getInputStream()).thenReturn(new ByteArrayInputStream(feature.getBytes("UTF-8"))); when(resourceLoader.resources(featurePath, ".feature")).thenReturn(asList(resource1)); } diff --git a/core/src/test/java/cucumber/runtime/TestHelper.java b/core/src/test/java/cucumber/runtime/TestHelper.java index ada047b5ee..a2183b9ce2 100644 --- a/core/src/test/java/cucumber/runtime/TestHelper.java +++ b/core/src/test/java/cucumber/runtime/TestHelper.java @@ -40,7 +40,7 @@ public String getPath() { @Override public String getAbsolutePath() { - throw new UnsupportedOperationException(); + return path; } @Override diff --git a/core/src/test/java/cucumber/runtime/autocomplete/StepdefGeneratorTest.java b/core/src/test/java/cucumber/runtime/autocomplete/StepdefGeneratorTest.java index 30deb3fa72..e8cc67a877 100644 --- a/core/src/test/java/cucumber/runtime/autocomplete/StepdefGeneratorTest.java +++ b/core/src/test/java/cucumber/runtime/autocomplete/StepdefGeneratorTest.java @@ -84,7 +84,7 @@ public String getPath() { @Override public String getAbsolutePath() { - throw new UnsupportedOperationException(); + return getPath(); } @Override diff --git a/core/src/test/java/cucumber/runtime/model/CucumberFeatureTest.java b/core/src/test/java/cucumber/runtime/model/CucumberFeatureTest.java index 8bcdde1f52..34a0088c7e 100644 --- a/core/src/test/java/cucumber/runtime/model/CucumberFeatureTest.java +++ b/core/src/test/java/cucumber/runtime/model/CucumberFeatureTest.java @@ -223,6 +223,7 @@ private void mockFileResource(ResourceLoader resourceLoader, String featurePath, throws IOException, UnsupportedEncodingException { Resource resource = mock(Resource.class); when(resource.getPath()).thenReturn(featurePath); + when(resource.getAbsolutePath()).thenReturn(featurePath); when(resource.getInputStream()).thenReturn(new ByteArrayInputStream(feature.getBytes("UTF-8"))); when(resourceLoader.resources(featurePath, extension)).thenReturn(asList(resource)); } diff --git a/junit/src/test/java/cucumber/runtime/junit/TestFeatureBuilder.java b/junit/src/test/java/cucumber/runtime/junit/TestFeatureBuilder.java index 570529e9ae..47c553dc9d 100644 --- a/junit/src/test/java/cucumber/runtime/junit/TestFeatureBuilder.java +++ b/junit/src/test/java/cucumber/runtime/junit/TestFeatureBuilder.java @@ -23,7 +23,7 @@ public String getPath() { @Override public String getAbsolutePath() { - throw new UnsupportedOperationException(); + return getPath(); } @Override