Skip to content

Commit 372619e

Browse files
Ismail.Bhanampkorstanje
Ismail.Bhana
authored andcommitted
[Spring] Support multithreaded execution of scenarios
When running in parallel the dependency injection context was shared between scenarios. This made it impossible to run cucumber test with spring without forking the JVM. To reduce the resources required to execute a test each thread now will have its own dependency injection context allowing tests to be executed in parallel threads. Related issues: - #1106 This fixes #1106
1 parent aa4c9a0 commit 372619e

File tree

7 files changed

+113
-5
lines changed

7 files changed

+113
-5
lines changed

History.md

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
## [2.0.0-SNAPSHOT](https://github.com/cucumber/cucumber-jvm/compare/v1.2.5...master) (In Git)
22

3+
* [Spring] Support multithreaded execution of scenarios ([#1106](https://github.com/cucumber/cucumber-jvm/issues/1106), [#1107](https://github.com/cucumber/cucumber-jvm/issues/1107), [#1148](https://github.com/cucumber/cucumber-jvm/issues/1148) Ismail Bhana, M.P. Korstanje)
34
* [Java8, Kotlin Java8] Support java 8 method references ([#1140](https://github.com/cucumber/cucumber-jvm/pull/1140) M.P. Korstanje)
45
* [Core] Show explicit error message when field name missed in table header ([#1014](https://github.com/cucumber/cucumber-jvm/pull/1014) Mykola Gurov)
56
* [Examples] Properly quit selenium in webbit examples ([#1146](https://github.com/cucumber/cucumber-jvm/pull/1146) Alberto Scotto)

spring/src/main/java/cucumber/runtime/java/spring/GlueCodeContext.java

+11-1
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,24 @@
44
import java.util.Map;
55

66
class GlueCodeContext {
7-
public static final GlueCodeContext INSTANCE = new GlueCodeContext();
7+
8+
private static final ThreadLocal<GlueCodeContext> localContext = new ThreadLocal<GlueCodeContext>() {
9+
protected GlueCodeContext initialValue() {
10+
return new GlueCodeContext();
11+
}
12+
};
13+
814
private final Map<String, Object> objects = new HashMap<String, Object>();
915
private final Map<String, Runnable> callbacks = new HashMap<String, Runnable>();
1016
private int counter;
1117

1218
private GlueCodeContext() {
1319
}
1420

21+
public static GlueCodeContext getInstance() {
22+
return localContext.get();
23+
}
24+
1525
public void start() {
1626
cleanUp();
1727
counter++;

spring/src/main/java/cucumber/runtime/java/spring/GlueCodeScope.java

+4-2
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,9 @@
66
class GlueCodeScope implements Scope {
77
public static final String NAME = "cucumber-glue";
88

9-
private final GlueCodeContext context = GlueCodeContext.INSTANCE;
10-
119
@Override
1210
public Object get(String name, ObjectFactory<?> objectFactory) {
11+
GlueCodeContext context = GlueCodeContext.getInstance();
1312
Object obj = context.get(name);
1413
if (obj == null) {
1514
obj = objectFactory.getObject();
@@ -21,11 +20,13 @@ public Object get(String name, ObjectFactory<?> objectFactory) {
2120

2221
@Override
2322
public Object remove(String name) {
23+
GlueCodeContext context = GlueCodeContext.getInstance();
2424
return context.remove(name);
2525
}
2626

2727
@Override
2828
public void registerDestructionCallback(String name, Runnable callback) {
29+
GlueCodeContext context = GlueCodeContext.getInstance();
2930
context.registerDestructionCallback(name, callback);
3031
}
3132

@@ -36,6 +37,7 @@ public Object resolveContextualObject(String key) {
3637

3738
@Override
3839
public String getConversationId() {
40+
GlueCodeContext context = GlueCodeContext.getInstance();
3941
return context.getId();
4042
}
4143
}

spring/src/main/java/cucumber/runtime/java/spring/SpringFactory.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ public void start() {
112112
registerStepClassBeanDefinition(beanFactory, stepClass);
113113
}
114114
}
115-
GlueCodeContext.INSTANCE.start();
115+
GlueCodeContext.getInstance().start();
116116
}
117117

118118
@SuppressWarnings("resource")
@@ -161,7 +161,7 @@ private void registerStepClassBeanDefinition(ConfigurableListableBeanFactory bea
161161
@Override
162162
public void stop() {
163163
notifyContextManagerAboutTestClassFinished();
164-
GlueCodeContext.INSTANCE.stop();
164+
GlueCodeContext.getInstance().stop();
165165
}
166166

167167
private void notifyContextManagerAboutTestClassFinished() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package cucumber.runtime.java.spring.threading;
2+
3+
import static java.util.concurrent.Executors.newFixedThreadPool;
4+
import static org.junit.Assert.assertEquals;
5+
6+
import cucumber.api.cli.Main;
7+
import org.junit.Test;
8+
9+
import java.util.concurrent.Callable;
10+
import java.util.concurrent.ExecutionException;
11+
import java.util.concurrent.ExecutorService;
12+
import java.util.concurrent.Future;
13+
14+
15+
public class RunParallelCukesTest {
16+
17+
private final Callable<Byte> runCuke = new Callable<Byte>() {
18+
@Override
19+
public Byte call() throws Exception {
20+
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
21+
String[] args = {
22+
"--glue", "cucumber.runtime.java.spring.threading",
23+
"classpath:cucumber/runtime/java/spring/threadingCukes.feature",
24+
"--strict"
25+
};
26+
return Main.run(args, classLoader);
27+
}
28+
};
29+
30+
@Test
31+
public void test() throws InterruptedException, ExecutionException {
32+
ExecutorService executorService = newFixedThreadPool(2);
33+
Future<Byte> result1 = executorService.submit(runCuke);
34+
Future<Byte> result2 = executorService.submit(runCuke);
35+
assertEquals(result1.get().byteValue(), 0x0);
36+
assertEquals(result2.get().byteValue(), 0x0);
37+
}
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package cucumber.runtime.java.spring.threading;
2+
3+
import static java.lang.Thread.currentThread;
4+
import static org.junit.Assert.assertEquals;
5+
import static org.junit.Assert.assertNotSame;
6+
import static org.junit.Assert.assertSame;
7+
8+
import cucumber.api.java.en.Given;
9+
import cucumber.api.java.en.Then;
10+
import cucumber.api.java.en.When;
11+
import org.springframework.test.context.ContextConfiguration;
12+
import org.springframework.test.context.web.WebAppConfiguration;
13+
14+
import java.util.Map;
15+
import java.util.concurrent.ConcurrentHashMap;
16+
import java.util.concurrent.CountDownLatch;
17+
import java.util.concurrent.TimeUnit;
18+
19+
@WebAppConfiguration
20+
@ContextConfiguration("classpath:cucumber.xml")
21+
public class ThreadingStepDefs {
22+
23+
private static final ConcurrentHashMap<Thread, ThreadingStepDefs> map = new ConcurrentHashMap<Thread, ThreadingStepDefs>();
24+
25+
private static final CountDownLatch latch = new CountDownLatch(2);
26+
27+
@Given("^I am a step definition$")
28+
public void iAmAStepDefinition() throws Throwable {
29+
map.put(currentThread(), this);
30+
}
31+
32+
@When("^when executed in parallel$")
33+
public void whenExecutedInParallel() throws Throwable {
34+
latch.await(10, TimeUnit.SECONDS);
35+
}
36+
37+
@Then("^I should not be shared between threads$")
38+
public void iShouldNotBeSharedBetweenThreads() throws Throwable {
39+
for (Map.Entry<Thread, ThreadingStepDefs> entries : map.entrySet()) {
40+
if (entries.getKey().equals(currentThread())) {
41+
assertSame(entries.getValue(), this);
42+
} else {
43+
assertNotSame(entries.getValue(), this);
44+
}
45+
}
46+
assertEquals(2, map.size());
47+
}
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Feature: Spring Threading Cukes
2+
In order to have a completely clean system for each scenario
3+
As a purity activist
4+
I want that beans have both scenario and thread scope.
5+
6+
Scenario: A parallel execution
7+
Given I am a step definition
8+
When when executed in parallel
9+
Then I should not be shared between threads

0 commit comments

Comments
 (0)