Skip to content

Commit df883f0

Browse files
committed
[TestNG] Fix concurrent modification of events
When a TestNG suite with `parallel="methods"` is executed TestNG will invoke all `@DataProvider` annotated methods. Because these data providers also emit the `TestRunStarted` event this event is emitted twice concurrently. These events collide in the `CanonicalOrderEventPublisher` resulting in a concurrent modification that inserts a null element rather then either of the events. By moving the start of the test execution to the constructor we ensure that test execution is always started serially. Additionally this ensures that it is impossible to emit a `TestRunFinished` event without also emitting a `TestRunStarted` event. This comes at the cost of starting the execution as soon as the `@BeforeClass` method is invoked. This will result in some apparent overhead as `TestNG` is still preparing for execution. Fixes: #1917
1 parent ca140d4 commit df883f0

File tree

2 files changed

+13
-27
lines changed

2 files changed

+13
-27
lines changed

testng/src/main/java/io/cucumber/testng/TestNGCucumberRunner.java

+12-23
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,7 @@ public final class TestNGCucumberRunner {
6060
private final Predicate<Pickle> filters;
6161
private final ThreadLocalRunnerSupplier runnerSupplier;
6262
private final RuntimeOptions runtimeOptions;
63-
private final Plugins plugins;
64-
private final FeaturePathFeatureSupplier featureSupplier;
65-
private List<Feature> features = null;
63+
private final List<Feature> features;
6664

6765
/**
6866
* Bootstrap the cucumber runtime
@@ -101,21 +99,28 @@ public TestNGCucumberRunner(Class<?> clazz) {
10199

102100
Supplier<ClassLoader> classLoader = ClassLoaders::getDefaultClassLoader;
103101
FeatureParser parser = new FeatureParser(bus::generateId);
104-
this.featureSupplier = new FeaturePathFeatureSupplier(classLoader, runtimeOptions, parser);
102+
FeaturePathFeatureSupplier featureSupplier = new FeaturePathFeatureSupplier(classLoader, runtimeOptions, parser);
105103

106-
this.plugins = new Plugins(new PluginFactory(), runtimeOptions);
104+
Plugins plugins = new Plugins(new PluginFactory(), runtimeOptions);
107105
ObjectFactoryServiceLoader objectFactoryServiceLoader = new ObjectFactoryServiceLoader(runtimeOptions);
108106
ObjectFactorySupplier objectFactorySupplier = new ThreadLocalObjectFactorySupplier(objectFactoryServiceLoader);
109107
BackendServiceLoader backendSupplier = new BackendServiceLoader(clazz::getClassLoader, objectFactorySupplier);
110108
this.filters = new Filters(runtimeOptions);
111109
TypeRegistryConfigurerSupplier typeRegistryConfigurerSupplier = new ScanningTypeRegistryConfigurerSupplier(classLoader, runtimeOptions);
112110
this.runnerSupplier = new ThreadLocalRunnerSupplier(runtimeOptions, bus, backendSupplier, objectFactorySupplier, typeRegistryConfigurerSupplier);
111+
112+
// Start test execution now.
113+
// provideScenarios may be invoked concurrently
114+
plugins.setSerialEventBusOnEventListenerPlugins(bus);
115+
features = featureSupplier.get();
116+
bus.send(new TestRunStarted(bus.getInstant()));
117+
features.forEach(feature -> bus.send(new TestSourceRead(bus.getInstant(), feature.getUri(), feature.getSource())));
113118
}
114119

115120
public void runScenario(io.cucumber.testng.Pickle pickle) throws Throwable {
116121
//Possibly invoked in a multi-threaded context
117122
Runner runner = runnerSupplier.get();
118-
try(TestCaseResultObserver observer = TestCaseResultObserver.observe(runner.getBus(), runtimeOptions.isStrict())){
123+
try (TestCaseResultObserver observer = TestCaseResultObserver.observe(runner.getBus(), runtimeOptions.isStrict())) {
119124
Pickle cucumberPickle = pickle.getPickle();
120125
runner.runPickle(cucumberPickle);
121126
observer.assertTestCasePassed();
@@ -132,7 +137,7 @@ public void finish() {
132137
*/
133138
public Object[][] provideScenarios() {
134139
try {
135-
return getFeatures().stream()
140+
return features.stream()
136141
.flatMap(feature -> feature.getPickles().stream()
137142
.filter(filters)
138143
.map(cucumberPickle -> new Object[]{
@@ -144,20 +149,4 @@ public Object[][] provideScenarios() {
144149
return new Object[][]{new Object[]{new CucumberExceptionWrapper(e), null}};
145150
}
146151
}
147-
148-
/**
149-
* Gets features found on the feature path and sends {@link TestRunStarted} and {@link TestSourceRead} events. The method is
150-
* idempotent.
151-
*
152-
* @return a list of {@link Feature} features found on the feature path.
153-
*/
154-
private List<Feature> getFeatures() {
155-
if (features == null) {
156-
plugins.setSerialEventBusOnEventListenerPlugins(bus);
157-
features = featureSupplier.get();
158-
bus.send(new TestRunStarted(bus.getInstant()));
159-
features.forEach(feature -> bus.send(new TestSourceRead(bus.getInstant(), feature.getUri(), feature.getSource())));
160-
}
161-
return features;
162-
}
163152
}

testng/src/test/java/io/cucumber/testng/TestNGCucumberRunnerTest.java

+1-4
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,8 @@ public void runScenarioWithUndefinedStepsStrict() {
4848

4949
@Test
5050
public void parse_error_propagated_to_testng_test_execution() {
51-
testNGCucumberRunner = new TestNGCucumberRunner(ParseError.class);
5251
try {
53-
Object[][] scenarios = testNGCucumberRunner.provideScenarios(); // a CucumberException is caught
54-
PickleWrapper pickleWrapper = (PickleWrapper) scenarios[0][0];
55-
pickleWrapper.getPickle();
52+
testNGCucumberRunner = new TestNGCucumberRunner(ParseError.class);
5653
Assert.fail("CucumberException not thrown");
5754
} catch (FeatureParserException e) {
5855
assertEquals(e.getMessage(), "Failed to parse resource at: classpath:io/cucumber/error/parse-error.feature");

0 commit comments

Comments
 (0)