Skip to content

Commit

Permalink
ArC: introduce the ActivateSessionContext interceptor binding
Browse files Browse the repository at this point in the history
- it's only available in tests
- fixes quarkusio#45146
  • Loading branch information
mkouba committed Dec 17, 2024
1 parent a3f0d32 commit 39f821f
Show file tree
Hide file tree
Showing 8 changed files with 217 additions and 24 deletions.
2 changes: 2 additions & 0 deletions docs/src/main/asciidoc/getting-started-testing.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,8 @@ public class GreetingServiceTest {
----
<1> The `GreetingService` bean will be injected into the test

TIP: If you want to inject/test a `@SessionScoped` bean then it's very likely that the session context is not active and you would receive the `ContextNotActiveException` when a method of the injected bean is invoked. However, it's possible to use the `@io.quarkus.test.ActivateSessionContext` interceptor binding to activate the session context for a specific business method. Please read the javadoc for futher limitations.

== Applying Interceptors to Tests

As mentioned above Quarkus tests are actually full CDI beans, and as such you can apply CDI interceptors as you would
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,16 +68,13 @@
import io.quarkus.arc.runtime.LoggerProducer;
import io.quarkus.arc.runtime.appcds.AppCDSRecorder;
import io.quarkus.arc.runtime.context.ArcContextProvider;
import io.quarkus.arc.runtime.test.PreloadedTestApplicationClassPredicate;
import io.quarkus.bootstrap.BootstrapDebug;
import io.quarkus.deployment.Capabilities;
import io.quarkus.deployment.Capability;
import io.quarkus.deployment.Feature;
import io.quarkus.deployment.IsTest;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.Consume;
import io.quarkus.deployment.annotations.ExecutionTime;
import io.quarkus.deployment.annotations.Produce;
import io.quarkus.deployment.annotations.Record;
import io.quarkus.deployment.builditem.AdditionalApplicationArchiveMarkerBuildItem;
Expand Down Expand Up @@ -653,27 +650,6 @@ public void signalBeanContainerReady(AppCDSRecorder recorder, PreBeanContainerBu
beanContainerProducer.produce(new BeanContainerBuildItem(bi.getValue()));
}

@BuildStep(onlyIf = IsTest.class)
public AdditionalBeanBuildItem testApplicationClassPredicateBean() {
// We need to register the bean implementation for TestApplicationClassPredicate
// TestApplicationClassPredicate is used programmatically in the ArC recorder when StartupEvent is fired
return AdditionalBeanBuildItem.unremovableOf(PreloadedTestApplicationClassPredicate.class);
}

@BuildStep(onlyIf = IsTest.class)
@Record(ExecutionTime.STATIC_INIT)
void initTestApplicationClassPredicateBean(ArcRecorder recorder, BeanContainerBuildItem beanContainer,
BeanDiscoveryFinishedBuildItem beanDiscoveryFinished,
CompletedApplicationClassPredicateBuildItem predicate) {
Set<String> applicationBeanClasses = new HashSet<>();
for (BeanInfo bean : beanDiscoveryFinished.beanStream().classBeans()) {
if (predicate.test(bean.getBeanClass())) {
applicationBeanClasses.add(bean.getBeanClass().toString());
}
}
recorder.initTestApplicationClassPredicate(applicationBeanClasses);
}

@BuildStep
List<AdditionalApplicationArchiveMarkerBuildItem> marker() {
return Arrays.asList(new AdditionalApplicationArchiveMarkerBuildItem("META-INF/beans.xml"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package io.quarkus.arc.deployment;

import java.util.HashSet;
import java.util.Set;
import java.util.function.Predicate;

import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTransformation;
import org.jboss.jandex.DotName;

import io.quarkus.arc.processor.BeanInfo;
import io.quarkus.arc.runtime.ArcRecorder;
import io.quarkus.arc.runtime.test.ActivateSessionContextInterceptor;
import io.quarkus.arc.runtime.test.PreloadedTestApplicationClassPredicate;
import io.quarkus.deployment.IsTest;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.BuildSteps;
import io.quarkus.deployment.annotations.ExecutionTime;
import io.quarkus.deployment.annotations.Record;
import io.quarkus.deployment.builditem.ApplicationClassPredicateBuildItem;

@BuildSteps(onlyIf = IsTest.class)
public class ArcTestSteps {

@BuildStep
public void additionalBeans(BuildProducer<AdditionalBeanBuildItem> additionalBeans) {
// We need to register the bean implementation for TestApplicationClassPredicate
// TestApplicationClassPredicate is used programmatically in the ArC recorder when StartupEvent is fired
additionalBeans.produce(AdditionalBeanBuildItem.unremovableOf(PreloadedTestApplicationClassPredicate.class));
// In tests, register the ActivateSessionContextInterceptor and ActivateSessionContext interceptor binding
additionalBeans.produce(new AdditionalBeanBuildItem(ActivateSessionContextInterceptor.class));
additionalBeans.produce(new AdditionalBeanBuildItem("io.quarkus.test.ActivateSessionContext"));
}

@BuildStep
AnnotationsTransformerBuildItem addInterceptorBinding() {
return new AnnotationsTransformerBuildItem(
AnnotationTransformation.forClasses().whenClass(ActivateSessionContextInterceptor.class).transform(tc -> tc.add(
AnnotationInstance.builder(DotName.createSimple("io.quarkus.test.ActivateSessionContext")).build())));
}

// For some reason the annotation literal generated for io.quarkus.test.ActivateSessionContext lives in app class loader.
// This predicates ensures that the generated bean is considered an app class too.
// As a consequence, the type and all methods of ActivateSessionContextInterceptor must be public.
@BuildStep
ApplicationClassPredicateBuildItem appClassPredicate() {
return new ApplicationClassPredicateBuildItem(new Predicate<String>() {

@Override
public boolean test(String name) {
return name.startsWith(ActivateSessionContextInterceptor.class.getName());
}
});
}

@BuildStep
@Record(ExecutionTime.STATIC_INIT)
void initTestApplicationClassPredicateBean(ArcRecorder recorder, BeanContainerBuildItem beanContainer,
BeanDiscoveryFinishedBuildItem beanDiscoveryFinished,
CompletedApplicationClassPredicateBuildItem predicate) {
Set<String> applicationBeanClasses = new HashSet<>();
for (BeanInfo bean : beanDiscoveryFinished.beanStream().classBeans()) {
if (predicate.test(bean.getBeanClass())) {
applicationBeanClasses.add(bean.getBeanClass().toString());
}
}
recorder.initTestApplicationClassPredicate(applicationBeanClasses);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package io.quarkus.arc.test.context.session;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

import jakarta.enterprise.context.Dependent;
import jakarta.enterprise.context.SessionScoped;
import jakarta.inject.Inject;

import io.quarkus.arc.Arc;
import io.quarkus.arc.ClientProxy;
import io.quarkus.test.ActivateSessionContext;

@Dependent
class Client {

@Inject
SimpleBean bean;

@ActivateSessionContext
public boolean ping() {
assertTrue(Arc.container().sessionContext().isActive());
if (bean instanceof ClientProxy proxy) {
assertEquals(SessionScoped.class, proxy.arc_bean().getScope());
} else {
fail("Not a client proxy");
}
return bean.ping();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package io.quarkus.arc.test.context.session;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import jakarta.inject.Inject;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.arc.Arc;
import io.quarkus.test.QuarkusUnitTest;

public class SessionContextTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot(root -> root
.addClasses(SimpleBean.class, Client.class));

@Inject
Client client;

@Test
public void testContexts() {
assertFalse(Arc.container().sessionContext().isActive());
assertTrue(client.ping());
assertTrue(SimpleBean.DESTROYED.get());
assertFalse(Arc.container().sessionContext().isActive());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.quarkus.arc.test.context.session;

import java.util.concurrent.atomic.AtomicBoolean;

import jakarta.annotation.PreDestroy;
import jakarta.enterprise.context.SessionScoped;

@SessionScoped
class SimpleBean {

static final AtomicBoolean DESTROYED = new AtomicBoolean();

public boolean ping() {
return true;
}

@PreDestroy
void destroy() {
DESTROYED.set(true);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.quarkus.arc.runtime.test;

import jakarta.annotation.Priority;
import jakarta.interceptor.AroundInvoke;
import jakarta.interceptor.Interceptor;
import jakarta.interceptor.InvocationContext;

import io.quarkus.arc.Arc;
import io.quarkus.arc.ManagedContext;

// The @ActivateSessionContext interceptor binding is added by the extension
@Interceptor
@Priority(Interceptor.Priority.PLATFORM_BEFORE + 100)
public class ActivateSessionContextInterceptor {

@AroundInvoke
public Object aroundInvoke(InvocationContext ctx) throws Exception {
ManagedContext sessionContext = Arc.container().sessionContext();
if (sessionContext.isActive()) {
return ctx.proceed();
}
try {
sessionContext.activate();
return ctx.proceed();
} finally {
sessionContext.terminate();
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.quarkus.test;

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 java.util.concurrent.CompletionStage;

import jakarta.interceptor.InterceptorBinding;

/**
* Activates the session context before the intercepted method is called, and terminates the context when the method invocation
* completes (regardless of any exceptions being thrown).
* <p>
* If the context is already active, it's a noop - the context is neither activated nor deactivated.
* <p>
* Keep in mind that if the method returns an asynchronous type (such as {@link CompletionStage} then the session context is
* still terminate when the invocation completes and not at the time the asynchronous type is completed. Also note that session
* context is not propagated by MicroProfile Context Propagation.
* <p>
* This interceptor binding is only available in tests.
*/
@InterceptorBinding
@Target({ METHOD, TYPE })
@Retention(RUNTIME)
public @interface ActivateSessionContext {

}

0 comments on commit 39f821f

Please sign in to comment.