Skip to content

Commit 7b1e01b

Browse files
authored
[TestNG] Fix concurrent modification of events (#1919)
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 7b1e01b

File tree

2 files changed

+19
-29
lines changed

2 files changed

+19
-29
lines changed

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

+18-25
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,38 +99,49 @@ 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+
plugins.setSerialEventBusOnEventListenerPlugins(bus);
114+
features = featureSupplier.get();
115+
bus.send(new TestRunStarted(bus.getInstant()));
116+
features.forEach(feature -> bus.send(new TestSourceRead(bus.getInstant(), feature.getUri(), feature.getSource())));
113117
}
114118

115119
public void runScenario(io.cucumber.testng.Pickle pickle) throws Throwable {
116120
//Possibly invoked in a multi-threaded context
117121
Runner runner = runnerSupplier.get();
118-
try(TestCaseResultObserver observer = TestCaseResultObserver.observe(runner.getBus(), runtimeOptions.isStrict())){
122+
try (TestCaseResultObserver observer = TestCaseResultObserver.observe(runner.getBus(), runtimeOptions.isStrict())) {
119123
Pickle cucumberPickle = pickle.getPickle();
120124
runner.runPickle(cucumberPickle);
121125
observer.assertTestCasePassed();
122126
}
123127
}
124128

129+
/**
130+
* Finishes test execution by Cucumber.
131+
*/
125132
public void finish() {
126133
bus.send(new TestRunFinished(bus.getInstant()));
127134
}
128135

129136
/**
130-
* @return returns the cucumber scenarios as a two dimensional array of {@link PickleWrapper}
131-
* scenarios combined with their {@link FeatureWrapper} feature.
137+
* @return returns the cucumber scenarios as a two dimensional array of
138+
* {@link PickleWrapper} scenarios combined with their
139+
* {@link FeatureWrapper} feature.
132140
*/
133141
public Object[][] provideScenarios() {
142+
//Possibly invoked in a multi-threaded context
134143
try {
135-
return getFeatures().stream()
144+
return features.stream()
136145
.flatMap(feature -> feature.getPickles().stream()
137146
.filter(filters)
138147
.map(cucumberPickle -> new Object[]{
@@ -144,20 +153,4 @@ public Object[][] provideScenarios() {
144153
return new Object[][]{new Object[]{new CucumberExceptionWrapper(e), null}};
145154
}
146155
}
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-
}
163156
}

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)