Skip to content

Commit

Permalink
Merge pull request quarkusio#45170 from mkouba/issue-45146
Browse files Browse the repository at this point in the history
ArC: introduce CurrentManagedContext, built-in session context, `@ActivateSessionContext`
  • Loading branch information
mkouba authored Dec 17, 2024
2 parents 849ec70 + 09706b9 commit 9943ebd
Show file tree
Hide file tree
Showing 22 changed files with 613 additions and 390 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 String 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,49 @@
package io.quarkus.arc.test.context.session;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
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.arc.ManagedContext;
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;

@Inject
SimpleBean simpleBean;

@Test
public void testContexts() {
assertFalse(Arc.container().sessionContext().isActive());
assertNotNull(client.ping());
assertTrue(SimpleBean.DESTROYED.get());
assertFalse(Arc.container().sessionContext().isActive());
SimpleBean.DESTROYED.set(false);

ManagedContext sessionContext = Arc.container().sessionContext();
try {
sessionContext.activate();
String id = simpleBean.ping();
assertEquals(id, client.ping());
assertFalse(SimpleBean.DESTROYED.get());
} finally {
sessionContext.terminate();
}
assertTrue(SimpleBean.DESTROYED.get());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.quarkus.arc.test.context.session;

import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;

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

@SessionScoped
class SimpleBean {

static final AtomicBoolean DESTROYED = new AtomicBoolean();

private String id;

@PostConstruct
void init() {
id = UUID.randomUUID().toString();
}

public String ping() {
return id;
}

@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
Expand Up @@ -45,9 +45,6 @@
import io.quarkus.arc.deployment.BeanArchiveIndexBuildItem;
import io.quarkus.arc.deployment.BeanDefiningAnnotationBuildItem;
import io.quarkus.arc.deployment.BeanDiscoveryFinishedBuildItem;
import io.quarkus.arc.deployment.ContextRegistrationPhaseBuildItem;
import io.quarkus.arc.deployment.ContextRegistrationPhaseBuildItem.ContextConfiguratorBuildItem;
import io.quarkus.arc.deployment.CustomScopeBuildItem;
import io.quarkus.arc.deployment.InvokerFactoryBuildItem;
import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
import io.quarkus.arc.deployment.SyntheticBeansRuntimeInitBuildItem;
Expand Down Expand Up @@ -126,7 +123,6 @@
import io.quarkus.websockets.next.runtime.WebSocketEndpointBase;
import io.quarkus.websockets.next.runtime.WebSocketHttpServerOptionsCustomizer;
import io.quarkus.websockets.next.runtime.WebSocketServerRecorder;
import io.quarkus.websockets.next.runtime.WebSocketSessionContext;
import io.quarkus.websockets.next.runtime.kotlin.ApplicationCoroutineScope;
import io.quarkus.websockets.next.runtime.kotlin.CoroutineInvoker;
import io.quarkus.websockets.next.runtime.telemetry.ErrorInterceptor;
Expand Down Expand Up @@ -229,19 +225,6 @@ void produceCoroutineScope(BuildProducer<AdditionalBeanBuildItem> additionalBean
.build());
}

@BuildStep
ContextConfiguratorBuildItem registerSessionContext(ContextRegistrationPhaseBuildItem phase) {
return new ContextConfiguratorBuildItem(phase.getContext()
.configure(SessionScoped.class)
.normal()
.contextClass(WebSocketSessionContext.class));
}

@BuildStep
CustomScopeBuildItem registerSessionScope() {
return new CustomScopeBuildItem(DotName.createSimple(SessionScoped.class));
}

@BuildStep
void builtinCallbackArguments(BuildProducer<CallbackArgumentBuildItem> providers) {
providers.produce(new CallbackArgumentBuildItem(new MessageCallbackArgument()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import io.quarkus.arc.InjectableContext.ContextState;
import io.quarkus.arc.ManagedContext;
import io.quarkus.vertx.core.runtime.context.VertxContextSafetyToggle;
import io.quarkus.websockets.next.runtime.WebSocketSessionContext.SessionContextState;
import io.smallrye.common.vertx.VertxContext;
import io.vertx.core.Context;

Expand All @@ -21,12 +20,12 @@ public class ContextSupport {
static final String WEB_SOCKET_CONN_KEY = WebSocketConnectionBase.class.getName();

private final WebSocketConnectionBase connection;
private final SessionContextState sessionContextState;
private final WebSocketSessionContext sessionContext;
private final ContextState sessionContextState;
private final ManagedContext sessionContext;
private final ManagedContext requestContext;

ContextSupport(WebSocketConnectionBase connection, SessionContextState sessionContextState,
WebSocketSessionContext sessionContext,
ContextSupport(WebSocketConnectionBase connection, ContextState sessionContextState,
ManagedContext sessionContext,
ManagedContext requestContext) {
this.connection = connection;
this.sessionContext = sessionContext;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,18 @@
import java.util.Optional;
import java.util.function.Consumer;

import jakarta.enterprise.context.SessionScoped;

import org.jboss.logging.Logger;

import io.netty.handler.codec.http.websocketx.WebSocketCloseStatus;
import io.quarkus.arc.ArcContainer;
import io.quarkus.arc.InjectableContext;
import io.quarkus.arc.ManagedContext;
import io.quarkus.runtime.LaunchMode;
import io.quarkus.security.AuthenticationFailedException;
import io.quarkus.security.ForbiddenException;
import io.quarkus.security.UnauthorizedException;
import io.quarkus.websockets.next.CloseReason;
import io.quarkus.websockets.next.WebSocketException;
import io.quarkus.websockets.next.runtime.WebSocketSessionContext.SessionContextState;
import io.quarkus.websockets.next.runtime.config.UnhandledFailureStrategy;
import io.quarkus.websockets.next.runtime.telemetry.ErrorInterceptor;
import io.quarkus.websockets.next.runtime.telemetry.TelemetrySupport;
Expand All @@ -43,11 +41,11 @@ static void initialize(Vertx vertx, ArcContainer container, Codecs codecs, WebSo

// Initialize and capture the session context state that will be activated
// during message processing
WebSocketSessionContext sessionContext = null;
SessionContextState sessionContextState = null;
ManagedContext sessionContext = null;
InjectableContext.ContextState sessionContextState = null;
if (activateSessionContext) {
sessionContext = sessionContext(container);
sessionContextState = sessionContext.initializeContextState();
sessionContext = container.sessionContext();
sessionContextState = sessionContext.initializeState();
}
ContextSupport contextSupport = new ContextSupport(connection, sessionContextState,
sessionContext, activateRequestContext ? container.requestContext() : null);
Expand Down Expand Up @@ -406,12 +404,4 @@ private static WebSocketEndpoint createEndpoint(String endpointClassName, Contex
}
}

private static WebSocketSessionContext sessionContext(ArcContainer container) {
for (InjectableContext injectableContext : container.getContexts(SessionScoped.class)) {
if (WebSocketSessionContext.class.equals(injectableContext.getClass())) {
return (WebSocketSessionContext) injectableContext;
}
}
throw new WebSocketException("CDI session context not registered");
}
}
Loading

0 comments on commit 9943ebd

Please sign in to comment.