cukes
cukes copied to clipboard
feature request: parallel execution
(thanks for the recent upgrade to cucumber 6 :partying_face:)
Are there any ongoing efforts for supporting parallel execution?
After some failed attempts, it seems that it fails on the 2 following points (depending on the timing of the threads):
-
io.cucumber.guice.SequentialScenarioScope
enter/exit methods throw IllegalStateException https://stackoverflow.com/questions/44166354/cucumber-guice-injector-seems-not-to-be-thread-safe-parallel-execution-exec -
RestAssured -> Apache HttpClient -> BasicClientConnManager. Maybe replace with
PoolingHttpClientConnectionManager
? I think that one is thread-safe.
java.lang.IllegalStateException: Invalid use of BasicClientConnManager: connection still allocated.
Make sure to release the connection before allocating another one.
... at lv.ctco.cukes.rest.api.WhenSteps.perform_Http_Request(WhenSteps.java:19)
Not sure if there are other issues related to cukes-rest specifically, like how some variables are used (e.g. world
).
Probably there needs to be some more thread-isolation changes :/
If anyone else is interested (or the maintainers of this library), I managed to achieve parallel execution for this library, with the following ObjectFactory.
The basic idea, is that instead of a single Guice context, it starts a new context for every thread that is started by surefire.
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Module;
import com.google.inject.Stage;
import io.cucumber.core.backend.ObjectFactory;
import io.cucumber.guice.CucumberModules;
import io.cucumber.guice.ScenarioScope;
import java.lang.annotation.Annotation;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import lv.ctco.cukes.core.extension.CukesInjectableModule;
import lv.ctco.cukes.core.internal.di.CukesGuiceModule;
import org.reflections.Reflections;
/**
* Will create a new Guice context for each <code>threadCount</code> that is configured in surefire.
*
* <p>
* This class can be enabled by <code>classpath:cucumber.properties#cucumber.object-factory</code,
*
* or by {@link io.cucumber.junit.CucumberOptions#objectFactory()}
* </p>
*/
public class ThreadedObjectFactory implements ObjectFactory {
private final ThreadLocal<Injector> localInjector = new ThreadLocal<>();
private Injector getInjector() {
lazyInitInjector();
return localInjector.get();
}
public void start() {
getInjector().getInstance(ScenarioScope.class).enterScope();
}
public void stop() {
getInjector().getInstance(ScenarioScope.class).exitScope();
}
public boolean addClass(Class<?> aClass) {
return true;
}
public <T> T getInstance(Class<T> aClass) {
return getInjector().getInstance(aClass);
}
private void lazyInitInjector() {
if (localInjector.get() == null) {
Set<Module> modules = new HashSet<>();
modules.add(CucumberModules.createScenarioModule());
modules.add(new CukesGuiceModule());
modules.addAll(createInstancesOf(CukesInjectableModule.class, "lv.ctco.cukes"));
localInjector.set(Guice.createInjector(Stage.PRODUCTION, modules));
}
}
private <T> Set<T> createInstancesOf(Class<? extends Annotation> annotation, String scanPackage) {
return new Reflections(scanPackage)
.getTypesAnnotatedWith(annotation)
.stream()
.map(aClass -> {
try {
return (T) aClass.getConstructor().newInstance();
} catch (Exception e) {
return null;
}
})
.filter(Objects::nonNull)
.collect(Collectors.toSet());
}
}
I understand that this solution might be a bit somewhat memory consuming, but for my use case it's not a problem. Thankfully, Guice is very light.
The alternative of having a single Guice context for all threads, and making all singletons inside the cukes-rest library thread-isolated is a huge amount of work and I wouldn't recommend it.