Skip to content

Commit

Permalink
Merge pull request #182 from senerh/feature/14_ScenarioScope
Browse files Browse the repository at this point in the history
feat(#14): provide a @ScenarioScope annotation
  • Loading branch information
bmscomp authored Oct 5, 2024
2 parents d49be00 + f5a4d1c commit f80bc3e
Show file tree
Hide file tree
Showing 6 changed files with 182 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@
import io.cucumber.java.StepDefinitionAnnotation;
import io.cucumber.java.StepDefinitionAnnotations;
import io.quarkiverse.cucumber.CucumberQuarkusTest;
import io.quarkiverse.cucumber.ScenarioContext;
import io.quarkiverse.cucumber.ScenarioScope;
import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.arc.processor.DotNames;
import io.quarkus.arc.deployment.ContextRegistrationPhaseBuildItem;
import io.quarkus.arc.deployment.ContextRegistrationPhaseBuildItem.ContextConfiguratorBuildItem;
import io.quarkus.arc.deployment.CustomScopeBuildItem;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
import io.quarkus.deployment.builditem.FeatureBuildItem;
Expand Down Expand Up @@ -53,7 +57,24 @@ AdditionalBeanBuildItem beanDefiningAnnotation(CombinedIndexBuildItem indexBuild
.getAllKnownSubclasses(DotName.createSimple(CucumberQuarkusTest.class.getName()))) {
stepClasses.add(i.name().toString());
}
return AdditionalBeanBuildItem.builder().addBeanClasses(stepClasses).setDefaultScope(DotNames.SINGLETON)
.setUnremovable().build();
return AdditionalBeanBuildItem.builder()
.addBeanClasses(stepClasses)
.setDefaultScope(DotName.createSimple(ScenarioScope.class.getName()))
.setUnremovable()
.build();
}

@BuildStep
ContextConfiguratorBuildItem scenarioContext(ContextRegistrationPhaseBuildItem contextRegistrationPhase) {
return new ContextConfiguratorBuildItem(
contextRegistrationPhase.getContext()
.configure(ScenarioScope.class)
.normal()
.contextClass(ScenarioContext.class));
}

@BuildStep
CustomScopeBuildItem scenarioScope() {
return new CustomScopeBuildItem(DotName.createSimple(ScenarioScope.class.getName()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.quarkiverse.cucumber.it;

import java.util.Objects;

import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;

public class StatefulSteps {

String state;

@Given("the state {string}")
public void theState(String state) {
this.state = state;
}

@Then("the state is not initialized")
public void theStateIsNotInitialized() {
assert state == null;
}

@Then("the state is {string}")
public void theStateIs(String state) {
assert Objects.equals(this.state, state);
}
}
9 changes: 8 additions & 1 deletion integration-tests/src/test/resources/simple.feature
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,11 @@ Feature: Test feature

Scenario: Test scenario
Given I call the endpoint
Then the response is ok
Then the response is ok

Scenario: Test initialized stateful scenario
Given the state "ok"
Then the state is "ok"

Scenario: Test non initialized stateful scenario
Then the state is not initialized
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,10 @@
import io.cucumber.plugin.event.EventHandler;
import io.cucumber.plugin.event.PickleStepTestStep;
import io.cucumber.plugin.event.Status;
import io.cucumber.plugin.event.TestCaseFinished;
import io.cucumber.plugin.event.TestStep;
import io.cucumber.plugin.event.TestStepFinished;
import io.quarkus.arc.Arc;
import io.quarkus.test.junit.QuarkusTest;

@QuarkusTest
Expand Down Expand Up @@ -112,6 +114,13 @@ List<DynamicNode> getTests() {

Predicate<Pickle> filters = new Filters(runtimeOptions);

EventHandler<TestCaseFinished> scenarioFinishedHandler = __ -> {
var scenarioContext = Arc.container().getActiveContext(ScenarioScope.class);
if (scenarioContext != null) {
scenarioContext.destroy();
}
};

featureSupplier.get().forEach(f -> {
List<DynamicTest> tests = new LinkedList<>();
tests.add(DynamicTest.dynamicTest("Start Feature", () -> context.beforeFeature(f)));
Expand All @@ -126,9 +135,12 @@ List<DynamicNode> getTests() {
resultAtomicReference.compareAndSet(null, event);
}
};

eventBus.registerHandlerFor(TestCaseFinished.class, scenarioFinishedHandler);
eventBus.registerHandlerFor(TestStepFinished.class, handler);
context.runTestCase(r -> r.runPickle(p));
eventBus.removeHandlerFor(TestStepFinished.class, handler);
eventBus.removeHandlerFor(TestCaseFinished.class, scenarioFinishedHandler);

// if we have no main arguments, we are running as part of a junit test suite, we need to fail the junit test explicitly
if (resultAtomicReference.get() != null) {
Expand All @@ -155,7 +167,6 @@ List<DynamicNode> getTests() {
features.add(DynamicContainer.dynamicContainer(f.getName().orElse(f.getSource()), tests.stream()));
}
});

features.add(DynamicTest.dynamicTest("After All Features", context::runAfterAllHooks));
features.add(DynamicTest.dynamicTest("Finish Cucumber", context::finishTestRun));

Expand Down
95 changes: 95 additions & 0 deletions runtime/src/main/java/io/quarkiverse/cucumber/ScenarioContext.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package io.quarkiverse.cucumber;

import java.lang.annotation.Annotation;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;

import jakarta.enterprise.context.spi.Contextual;
import jakarta.enterprise.context.spi.CreationalContext;

import io.quarkus.arc.ContextInstanceHandle;
import io.quarkus.arc.InjectableBean;
import io.quarkus.arc.InjectableContext;
import io.quarkus.arc.impl.ContextInstanceHandleImpl;

public class ScenarioContext implements InjectableContext {

private final ConcurrentMap<Contextual<?>, ContextInstanceHandle<?>> instances = new ConcurrentHashMap<>();
private final Lock beanLock = new ReentrantLock();

@Override
public void destroy() {
for (var contextInstanceHandle : instances.values()) {
contextInstanceHandle.destroy();
}
instances.clear();
}

@Override
public void destroy(Contextual<?> contextual) {
try (var contextInstanceHandle = instances.remove(contextual)) {
if (contextInstanceHandle != null) {
contextInstanceHandle.destroy();
}
}
}

@Override
public Class<? extends Annotation> getScope() {
return ScenarioScope.class;
}

@Override
@SuppressWarnings("unchecked")
public <T> T get(Contextual<T> contextual, CreationalContext<T> creationalContext) {
var contextInstanceHandle = (ContextInstanceHandle<T>) instances.get(contextual);
if (contextInstanceHandle != null) {
return contextInstanceHandle.get();
} else if (creationalContext != null) {
beanLock.lock();
try {
T createdInstance = contextual.create(creationalContext);
instances.put(
contextual,
new ContextInstanceHandleImpl<>(
(InjectableBean<T>) contextual,
createdInstance,
creationalContext));
return createdInstance;
} finally {
beanLock.unlock();
}
} else {
return null;
}
}

@Override
public <T> T get(Contextual<T> contextual) {
return get(contextual, null);
}

@Override
public boolean isActive() {
return true;
}

@Override
public ContextState getState() {
return new ScenarioContextState(instances);
}

private record ScenarioContextState(
Map<Contextual<?>, ContextInstanceHandle<?>> instances) implements ContextState {

@Override
public Map<InjectableBean<?>, Object> getContextualInstances() {
return instances.values().stream()
.collect(Collectors.toMap(ContextInstanceHandle::getBean, ContextInstanceHandle::get));
}
}
}
17 changes: 17 additions & 0 deletions runtime/src/main/java/io/quarkiverse/cucumber/ScenarioScope.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.quarkiverse.cucumber;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import jakarta.enterprise.context.NormalScope;

@NormalScope
@Retention(RUNTIME)
@Target({ TYPE, METHOD, FIELD })
public @interface ScenarioScope {
}

0 comments on commit f80bc3e

Please sign in to comment.