[Cucumber-jvm][jvm][jvm 1.2.5] Injecting different object to glue classes programatically so I can run them paralelly.

56 views
Skip to first unread message

Laszlo E.

unread,
Dec 28, 2019, 8:41:06 AM12/28/19
to Cukes
Wrote (hacked together) a parallel runner with JVM 1.2.5 (because that is what we have at the moment) and it runs fine without extra parameters. Kinda looks like this:

public class App {
   
public static void main(String[] args) throws InterruptedException {


       
ExecutorService executorService = Executors.newFixedThreadPool(2);
       
Callable<Boolean> worker1 = () -> run(new CustomConfiguration("P1"));
       
Callable<Boolean> worker2 = () -> run(new CustomConfiguration("P2"));
       
List<Callable<Boolean>> workers = Arrays.asList(worker1, worker2);


       
List<Future<Boolean>> futures = executorService.invokeAll(workers);


       
while (futures.stream().anyMatch(f -> !f.isDone())) {
           
System.out.println("Task is still not done...");
           
Thread.sleep(200);
       
}


       
System.out.println("All Done");
        executorService
.shutdown();
   
}


   
public static Boolean run(CustomConfiguration customConfiguration) {


       
Class clazz = App.class;
       
ClassLoader classLoader = clazz.getClassLoader();
       
ResourceLoader resourceLoader = new MultiLoader(classLoader);


       
CustomCucumberOptions customCustomCucumberOptions = new CustomCucumberOptions();
        customCustomCucumberOptions
.setFeatures(new String[]{"src/main/java/com/yvr"});
        customCustomCucumberOptions
.setGlue(new String[]{"com.yvr"});


        customCustomCucumberOptions
.setPlugin(new String[]{"pretty", "html:build/brazil-cucumber-tests/", "json:build/brazil-cucumber-tests/report.json"});
       
CustomRuntimeOptionsFactory runtimeOptionsFactory = new CustomRuntimeOptionsFactory(customCustomCucumberOptions, clazz);
        runtimeOptionsFactory
.getExtraArgs().add("--tags");
        runtimeOptionsFactory
.getExtraArgs().add("@Two");
       
RuntimeOptions runtimeOptions = runtimeOptionsFactory.create();


       
ClassFinder classFinder = new ResourceLoaderClassFinder(resourceLoader, classLoader);
       
FeatureResultListener resultListener = new FeatureResultListener(runtimeOptions.reporter(classLoader), runtimeOptions.isStrict());
       
Runtime runtime = new Runtime(resourceLoader, classFinder, classLoader, runtimeOptions);


       
// Todo: How should I pass the customConfiguration to this run so every scenario has it?


       
for (CucumberFeature cucumberFeature : runtimeOptions.cucumberFeatures(resourceLoader)) {
            cucumberFeature
.run(
                    runtimeOptions
.formatter(classLoader),
                    resultListener
,
                    runtime
);
       
}


       
Formatter formatter = runtimeOptions.formatter(classLoader);


        formatter
.done();
        formatter
.close();
        runtime
.printSummary();


       
if (!resultListener.isPassed()) {
           
throw new CucumberException(resultListener.getFirstError());
       
}


       
return true;
   
}
}


The problem that I have if my Glue class looks like this:

public class FirstFeatureGlue {


   
CustomConfiguration customConfiguration;


   
public FirstFeatureGlue(CustomConfiguration customConfiguration) {
       
this.customConfiguration = customConfiguration;
   
}


   
@Given("^A Customer (.*).$")
   
public void a_Customer(String customer) {
       
System.out.println("Customer: " + customer);
   
}


   
@When("^Do something.$")
   
public void do_something() {
       
System.out.println("Do Something, depending on: " + customConfiguration.getMyParameter());
   
}


   
@Then("^Something will happen.$")
   
public void something_will_happen() {
       
System.out.println("Something will happen");
   
}
}


Sometimes I need different parameters in the runs (Example I am runniing those tests in a parallel way against different country implementations, or different target server deployments).
Is there a way to pass, the or a "customConfiguration" where I wrote the question?
// Todo: How should I pass the customConfiguration to this run so every scenario has it?


Should I try to upgrade to Version 3.0 or Version 4.0 that has this ability to set such a paramter programatically?
Should I wait for this project: Implement Cucumber as JUnit Platform Engine and Version 5.0
Has any body done something similar?

MP Korstanje

unread,
Dec 29, 2019, 9:49:58 AM12/29/19
to Cukes
Cucumber v4 supports parallel execution over scenarios. You are executing different configurations of Cucumber in parallel. There are benefits to upgrading to v4 but not specifically for what you are trying to achieve.

Now I am guess you're trying to pass some sort of environment configuration to your step definitions and execute against different environments in parallel. There are a few ways to do this but the least complicated is to configure multiple executions in your CI system and execute these in parallel. You configure each execution with a different parameter. Depending on this environment parameter your CustomConfiguration will return different values. For example if you are using Maven with the JUnit Runner you'd configure two jobs in your CI to execute `mvn verify -DENVIRONMENT=acceptance` and `mvn verify -DENVIRONMENT=integration`




Laszlo E.

unread,
Dec 30, 2019, 3:37:38 PM12/30/19
to Cukes

Thank you for the help Rien, but Maven does not play for me, because our company does not uses Maven, we have our own old build system. Importing Cucumber jvm 4 into our build system from Maven would be very hard too...
However I did come up with a hack for, for running this. :-)

Cucumber v4 supports parallel execution over scenarios. You are executing different configurations of Cucumber in parallel. There are benefits to upgrading to v4 but not specifically for what you are trying to achieve.

Now I am guess you're trying to pass some sort of environment configuration to your step definitions and execute against different environments in parallel. There are a few ways to do this but the least complicated is to configure multiple executions in your CI system and execute these in parallel. You configure each execution with a different parameter. Depending on this environment parameter your CustomConfiguration will return different values. For example if you are using Maven with the JUnit Runner you'd configure two jobs in your CI to execute `mvn verify -DENVIRONMENT=acceptance` and `mvn verify -DENVIRONMENT=integration`

I will post my "solution". :-)
The main idea is to have a global (singleton) container where to store the parameters based on thread id.
 
class CustomConfigurationsContainerSingleton {

    private volatile Map<Long, CustomConfiguration> parameters = new ConcurrentHashMap<>();

    private static class InstanceHolder {
        public static CustomConfigurationsContainerSingleton instance = new CustomConfigurationsContainerSingleton();
    }

    private CustomConfigurationsContainerSingleton() {
    }

    static CustomConfigurationsContainerSingleton getInstance() {
        return InstanceHolder.instance;
    }

    public void set(CustomConfiguration customConfiguration) {
        parameters.put(Thread.currentThread().getId(), customConfiguration);
    }

    public CustomConfiguration get() {
        return parameters.get(Thread.currentThread().getId());
    }

    public void remove() {
        parameters.remove(Thread.currentThread().getId());
    }
}

And then set and remove them in the runner like this:

public class App {

    private static final SimpleDateFormat TIMESTAMP = new SimpleDateFormat("yyyy-MM-dd_HH.mm.ss");

    public static void main(String[] args) throws InterruptedException {

        ExecutorService executorService = Executors.newFixedThreadPool(2);
        Date datetime = new Date();
        Callable<Boolean> worker1 = () -> run(datetime, "@First", new CustomConfiguration("P1"));
        Callable<Boolean> worker2 = () -> run(datetime, "@Second", new CustomConfiguration("P2"));
        List<Callable<Boolean>> workers = Arrays.asList(worker1, worker2);

        List<Future<Boolean>> futures = executorService.invokeAll(workers);

        while (futures.stream().anyMatch(f -> !f.isDone())) {
            System.out.println("Task is still not done...");
            Thread.sleep(200);
        }

        System.out.println("All Done");
        executorService.shutdown();
    }

    public static Boolean run(Date datetime, String tag, CustomConfiguration customConfiguration) {

        Class clazz = App.class;
        ClassLoader classLoader = clazz.getClassLoader();
        ResourceLoader resourceLoader = new MultiLoader(classLoader);

        CustomCucumberOptions customCustomCucumberOptions = new CustomCucumberOptions();
        customCustomCucumberOptions.setFeatures(new String[]{"src/main/java/com/yvr"});
        customCustomCucumberOptions.setGlue(new String[]{"com.yvr"});

        String outputFolder = "Out_" + TIMESTAMP.format(datetime) + "_" + tag + "/";

        customCustomCucumberOptions.setPlugin(new String[]{"pretty", "html:build/" + outputFolder, "json:build/" + outputFolder + "report.json"});
        CustomRuntimeOptionsFactory runtimeOptionsFactory = new CustomRuntimeOptionsFactory(customCustomCucumberOptions, clazz);
        runtimeOptionsFactory.getExtraArgs().add("--tags");
        runtimeOptionsFactory.getExtraArgs().add(tag);
        RuntimeOptions runtimeOptions = runtimeOptionsFactory.create();

        ClassFinder classFinder = new ResourceLoaderClassFinder(resourceLoader, classLoader);
        FeatureResultListener resultListener = new FeatureResultListener(runtimeOptions.reporter(classLoader), runtimeOptions.isStrict());
        Runtime runtime = new Runtime(resourceLoader, classFinder, classLoader, runtimeOptions);

        try {
            System.out.println("Thread Id: " + Thread.currentThread().getId() + ", parameter: " + customConfiguration.getMyParameter());

            CustomConfigurationsContainerSingleton.getInstance().set(customConfiguration);

            for (CucumberFeature cucumberFeature : runtimeOptions.cucumberFeatures(resourceLoader)) {
                cucumberFeature.run(
                        runtimeOptions.formatter(classLoader),
                        resultListener,
                        runtime);
            }

            Formatter formatter = runtimeOptions.formatter(classLoader);

            formatter.done();
            formatter.close();
            runtime.printSummary();

            if (!resultListener.isPassed()) {
                throw new CucumberException(resultListener.getFirstError());
            }
        } finally {
            CustomConfigurationsContainerSingleton.getInstance().remove();
        }

        return true;
    }
}

And get the parameter in the glue code:

public class MainFeatureGlue {

    @Given("^A Customer (.*).$")
    public void a_Customer(String customer) {
        System.out.println("Customer: " + customer);
    }

    @When("^Do something.$")
    public void do_something() {
        System.out.println("Thread Id: " + Thread.currentThread().getId() + ", do Something, depending on: " +
                CustomConfigurationsContainerSingleton.getInstance().get().getMyParameter());
    }

    @Then("^Something will happen.$")
    public void something_will_happen() {
        System.out.println("Something will happen");
    }
}

This hack works.

This line might be over engineering, but I did get some sporadic null Pointer exception is the When step without it.

private volatile Map<Long, CustomConfiguration> parameters = new ConcurrentHashMap<>();

So that it, hopefully this will run in our production system, not just simple skeleton.

Bye
Laszlo




MP Korstanje

unread,
Dec 31, 2019, 2:40:31 AM12/31/19
to Cukes
Wow. Okay. Good luck with that. :D


> private volatile Map<Long, CustomConfiguration> parameters = new ConcurrentHashMap<>();

This looks like something that would be best solved with a ThreadLocal.

Laszlo E.

unread,
Dec 31, 2019, 1:20:07 PM12/31/19
to Cukes

Wow. Okay. Good luck with that. :D
Yes I need all the luck in the world.
 

> private volatile Map<Long, CustomConfiguration> parameters = new ConcurrentHashMap<>();

This looks like something that would be best solved with a ThreadLocal.
Yes ThreadLocal works nicely.

class CustomConfigurationsContainerSingleton {


   
private final ThreadLocal threadLocal = new ThreadLocal();



   
private static class InstanceHolder {
       
public static CustomConfigurationsContainerSingleton instance = new CustomConfigurationsContainerSingleton();
   
}


   
private CustomConfigurationsContainerSingleton() {
   
}


   
static CustomConfigurationsContainerSingleton getInstance() {
       
return InstanceHolder.instance;
   
}


   
public void set(CustomConfiguration customConfiguration) {

        threadLocal
.set(customConfiguration);
   
}


   
public CustomConfiguration get() {
       
return (CustomConfiguration) threadLocal.get();
   
}


   
public void remove() {
        threadLocal
.remove();
   
}
}




 

Laszlo E.

unread,
Dec 31, 2019, 1:25:11 PM12/31/19
to Cukes


Hopefully final version:

class CustomConfigurationsContainerSingleton {


   
private final ThreadLocal<CustomConfiguration> threadLocal = new ThreadLocal<>();



   
private static class InstanceHolder {
       
public static CustomConfigurationsContainerSingleton instance = new CustomConfigurationsContainerSingleton();
   
}


   
private CustomConfigurationsContainerSingleton() {
   
}


   
static CustomConfigurationsContainerSingleton getInstance() {
       
return InstanceHolder.instance;
   
}


   
public void set(CustomConfiguration customConfiguration) {
        threadLocal
.set(customConfiguration);
   
}


   
public CustomConfiguration get() {

       
return threadLocal.get();
   
}


   
public void remove() {
        threadLocal
.remove();
   
}
}



Reply all
Reply to author
Forward
0 new messages