Skip to content

Commit

Permalink
Merge pull request #15144 from mkouba/issue-15083
Browse files Browse the repository at this point in the history
QuarkusTestProfile - make it possible to disable app lifecycle observers
  • Loading branch information
mkouba authored Feb 25, 2021
2 parents 4362eb3 + 62f6719 commit 3aeb74e
Show file tree
Hide file tree
Showing 14 changed files with 206 additions and 50 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.quarkus.runtime.test;

import java.util.function.Predicate;

/**
* This predicate can be used to distinguish application classes in the test mode.
*/
public interface TestApplicationClassPredicate extends Predicate<String> {

}
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,12 @@ public class ArcConfig {
@ConfigItem
public ArcDevModeConfig devMode;

/**
* Test mode configuration.
*/
@ConfigItem
public ArcTestConfig test;

public final boolean isRemoveUnusedBeansFieldValid() {
return ALLOWED_REMOVE_UNUSED_BEANS_VALUES.contains(removeUnusedBeans.toLowerCase());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import io.quarkus.arc.deployment.ObserverRegistrationPhaseBuildItem.ObserverConfiguratorBuildItem;
import io.quarkus.arc.deployment.UnremovableBeanBuildItem.BeanClassAnnotationExclusion;
import io.quarkus.arc.deployment.UnremovableBeanBuildItem.BeanClassNameExclusion;
import io.quarkus.arc.deployment.UnremovableBeanBuildItem.BeanTypeExclusion;
import io.quarkus.arc.deployment.ValidationPhaseBuildItem.ValidationErrorBuildItem;
import io.quarkus.arc.processor.AlternativePriorities;
import io.quarkus.arc.processor.AnnotationsTransformer;
Expand All @@ -56,14 +57,16 @@
import io.quarkus.arc.runtime.ArcRecorder;
import io.quarkus.arc.runtime.BeanContainer;
import io.quarkus.arc.runtime.LaunchModeProducer;
import io.quarkus.arc.runtime.LifecycleEventRunner;
import io.quarkus.arc.runtime.LoggerProducer;
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.ExecutionTime;
import io.quarkus.deployment.annotations.Record;
import io.quarkus.deployment.builditem.AdditionalApplicationArchiveMarkerBuildItem;
import io.quarkus.deployment.builditem.ApplicationClassPredicateBuildItem;
Expand All @@ -85,6 +88,7 @@
import io.quarkus.runtime.LaunchMode;
import io.quarkus.runtime.QuarkusApplication;
import io.quarkus.runtime.annotations.QuarkusMain;
import io.quarkus.runtime.test.TestApplicationClassPredicate;

/**
* This class contains build steps that trigger various phases of the bean processing.
Expand Down Expand Up @@ -147,7 +151,8 @@ public ContextRegistrationPhaseBuildItem initialize(
Optional<TestClassPredicateBuildItem> testClassPredicate,
Capabilities capabilities,
CustomScopeAnnotationsBuildItem customScopes,
LaunchModeBuildItem launchModeBuildItem) {
LaunchModeBuildItem launchModeBuildItem,
BuildProducer<CompletedApplicationClassPredicateBuildItem> applicationClassPredicateProducer) {

if (!arcConfig.isRemoveUnusedBeansFieldValid()) {
throw new IllegalArgumentException("Invalid configuration value set for 'quarkus.arc.remove-unused-beans'." +
Expand All @@ -173,13 +178,15 @@ public ContextRegistrationPhaseBuildItem initialize(
IndexView index = beanArchiveIndex.getIndex();
BeanProcessor.Builder builder = BeanProcessor.builder();
IndexView applicationClassesIndex = applicationIndex.getIndex();
builder.setApplicationClassPredicate(new AbstractCompositeApplicationClassesPredicate<DotName>(
Predicate<DotName> applicationClassPredicate = new AbstractCompositeApplicationClassesPredicate<DotName>(
applicationClassesIndex, generatedClassNames, applicationClassPredicates, testClassPredicate) {
@Override
protected DotName getDotName(DotName dotName) {
return dotName;
}
});
};
applicationClassPredicateProducer.produce(new CompletedApplicationClassPredicateBuildItem(applicationClassPredicate));
builder.setApplicationClassPredicate(applicationClassPredicate);

builder.addAnnotationTransformer(new AnnotationsTransformer() {

Expand Down Expand Up @@ -257,7 +264,7 @@ protected DotName getDotName(BeanInfo bean) {
}
});
}
builder.addRemovalExclusion(new BeanClassNameExclusion(LifecycleEventRunner.class.getName()));
builder.addRemovalExclusion(new BeanTypeExclusion(DotName.createSimple(TestApplicationClassPredicate.class.getName())));
for (AdditionalBeanBuildItem additionalBean : additionalBeans) {
if (!additionalBean.isRemovable()) {
for (String beanClass : additionalBean.getBeanClasses()) {
Expand Down Expand Up @@ -494,6 +501,27 @@ public void registerField(FieldInfo fieldInfo) {
return new BeanContainerBuildItem(beanContainer);
}

@BuildStep(onlyIf = IsTest.class)
public AdditionalBeanBuildItem testApplicationClassPredicateBean() {
// We need to register the bean implementation for TestApplicationClassPredicate
// TestApplicationClassPredicate is used programatically 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,16 @@
package io.quarkus.arc.deployment;

import io.quarkus.runtime.annotations.ConfigGroup;
import io.quarkus.runtime.annotations.ConfigItem;

@ConfigGroup
public class ArcTestConfig {

/**
* If set to true then disable {@code StartupEvent} and {@code ShutdownEvent} observers declared on application bean classes
* during the tests.
*/
@ConfigItem(defaultValue = "false")
public boolean disableApplicationLifecycleObservers;

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import io.quarkus.arc.processor.BeanDefiningAnnotation;
import io.quarkus.arc.processor.BeanDeployment;
import io.quarkus.arc.processor.DotNames;
import io.quarkus.arc.runtime.LifecycleEventRunner;
import io.quarkus.bootstrap.model.AppArtifactKey;
import io.quarkus.deployment.ApplicationArchive;
import io.quarkus.deployment.annotations.BuildProducer;
Expand Down Expand Up @@ -52,8 +51,6 @@ public BeanArchiveIndexBuildItem build(ArcConfig config, ApplicationArchivesBuil
for (AdditionalBeanBuildItem i : additionalBeans) {
additionalBeanClasses.addAll(i.getBeanClasses());
}
// NOTE: the types added directly must always declare a scope annotation otherwise they will be ignored during bean discovery
additionalBeanClasses.add(LifecycleEventRunner.class.getName());

// Build the index for additional beans and generated bean classes
Set<DotName> additionalIndex = new HashSet<>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.quarkus.arc.deployment;

import java.util.function.Predicate;

import org.jboss.jandex.DotName;

import io.quarkus.builder.item.SimpleBuildItem;

/**
* This build item hold the "final" predicate that is used to distinguish application classes from framework/library classes.
*/
public final class CompletedApplicationClassPredicateBuildItem extends SimpleBuildItem implements Predicate<DotName> {

private final Predicate<DotName> applicationClassPredicate;

CompletedApplicationClassPredicateBuildItem(Predicate<DotName> applicationClassPredicate) {
this.applicationClassPredicate = applicationClassPredicate;
}

public Predicate<DotName> getApplicationClassPredicate() {
return applicationClassPredicate;
}

@Override
public boolean test(DotName name) {
return applicationClassPredicate.test(name);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import io.quarkus.deployment.annotations.Consume;
import io.quarkus.deployment.annotations.Record;
import io.quarkus.deployment.builditem.ApplicationStartBuildItem;
import io.quarkus.deployment.builditem.LaunchModeBuildItem;
import io.quarkus.deployment.builditem.RuntimeConfigSetupCompleteBuildItem;
import io.quarkus.deployment.builditem.ServiceStartBuildItem;
import io.quarkus.deployment.builditem.ShutdownContextBuildItem;
Expand All @@ -21,8 +22,9 @@ public class LifecycleEventsBuildStep {
ApplicationStartBuildItem startupEvent(ArcRecorder recorder,
List<ServiceStartBuildItem> startList,
BeanContainerBuildItem beanContainer,
ShutdownContextBuildItem shutdown) {
recorder.handleLifecycleEvents(shutdown, beanContainer.getValue());
ShutdownContextBuildItem shutdown,
LaunchModeBuildItem launchMode, ArcConfig config) {
recorder.handleLifecycleEvents(shutdown, launchMode.getLaunchMode(), config.test.disableApplicationLifecycleObservers);
return new ApplicationStartBuildItem();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
package io.quarkus.arc.runtime;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.function.Predicate;
import java.util.function.Supplier;

import io.quarkus.arc.Arc;
import io.quarkus.arc.ArcContainer;
import io.quarkus.arc.InjectableBean;
import io.quarkus.arc.InjectableBean.Kind;
import io.quarkus.arc.impl.ArcContainerImpl;
import io.quarkus.arc.runtime.test.PreloadedTestApplicationClassPredicate;
import io.quarkus.runtime.LaunchMode;
import io.quarkus.runtime.RuntimeValue;
import io.quarkus.runtime.ShutdownContext;
import io.quarkus.runtime.ShutdownEvent;
import io.quarkus.runtime.StartupEvent;
import io.quarkus.runtime.annotations.Recorder;
import io.quarkus.runtime.test.TestApplicationClassPredicate;

@Recorder
public class ArcRecorder {
Expand Down Expand Up @@ -55,13 +67,32 @@ public BeanContainer initBeanContainer(ArcContainer container, List<BeanContaine
return beanContainer;
}

public void handleLifecycleEvents(ShutdownContext context, BeanContainer beanContainer) {
LifecycleEventRunner instance = beanContainer.instance(LifecycleEventRunner.class);
instance.fireStartupEvent();
public void handleLifecycleEvents(ShutdownContext context, LaunchMode launchMode,
boolean disableApplicationLifecycleObservers) {
ArcContainerImpl container = ArcContainerImpl.instance();
List<Class<?>> mockBeanClasses;

// If needed then mock all app observers in the test mode
if (launchMode == LaunchMode.TEST && disableApplicationLifecycleObservers) {
Predicate<String> predicate = container
.select(TestApplicationClassPredicate.class).get();
mockBeanClasses = new ArrayList<>();
for (InjectableBean<?> bean : container.getBeans()) {
// Mock observers for all application class beans
if (bean.getKind() == Kind.CLASS && predicate.test(bean.getBeanClass().getName())) {
mockBeanClasses.add(bean.getBeanClass());
}
}
} else {
mockBeanClasses = Collections.emptyList();
}

fireLifecycleEvent(container, new StartupEvent(), mockBeanClasses);

context.addShutdownTask(new Runnable() {
@Override
public void run() {
instance.fireShutdownEvent();
fireLifecycleEvent(container, new ShutdownEvent(), mockBeanClasses);
}
});
}
Expand All @@ -75,4 +106,24 @@ public Object get() {
};
}

public void initTestApplicationClassPredicate(Set<String> applicationBeanClasses) {
PreloadedTestApplicationClassPredicate predicate = Arc.container()
.instance(PreloadedTestApplicationClassPredicate.class).get();
predicate.setApplicationBeanClasses(applicationBeanClasses);
}

private void fireLifecycleEvent(ArcContainerImpl container, Object event, List<Class<?>> mockBeanClasses) {
if (!mockBeanClasses.isEmpty()) {
for (Class<?> beanClass : mockBeanClasses) {
container.mockObserversFor(beanClass, true);
}
}
container.beanManager().getEvent().fire(event);
if (!mockBeanClasses.isEmpty()) {
for (Class<?> beanClass : mockBeanClasses) {
container.mockObserversFor(beanClass, false);
}
}
}

}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.quarkus.arc.runtime.test;

import java.util.Set;

import javax.inject.Singleton;

import io.quarkus.runtime.test.TestApplicationClassPredicate;

@Singleton
public class PreloadedTestApplicationClassPredicate implements TestApplicationClassPredicate {

private volatile Set<String> applicationBeanClasses;

@Override
public boolean test(String name) {
return applicationBeanClasses.contains(name);
}

public void setApplicationBeanClasses(Set<String> applicationBeanClasses) {
this.applicationBeanClasses = applicationBeanClasses;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -287,12 +287,6 @@ protected void implementNotify(ObserverInfo observer, ClassCreator observerCreat
MethodCreator notify = observerCreator.getMethodCreator("notify", void.class, EventContext.class)
.setModifiers(ACC_PUBLIC);

if (observer.isSynthetic()) {
// Synthetic observers generate the notify method themselves
observer.getNotify().accept(notify);
return;
}

if (mockable) {
// If mockable and mocked then just return from the method
ResultHandle mock = notify.readInstanceField(
Expand All @@ -301,6 +295,12 @@ protected void implementNotify(ObserverInfo observer, ClassCreator observerCreat
notify.ifTrue(mock).trueBranch().returnValue(null);
}

if (observer.isSynthetic()) {
// Synthetic observers generate the notify method themselves
observer.getNotify().accept(notify);
return;
}

boolean isStatic = Modifier.isStatic(observer.getObserverMethod().flags());
// It is safe to skip CreationalContext.release() for observers with noor normal scoped declaring provider, and
boolean skipRelease = observer.getInjection().injectionPoints.isEmpty();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,19 @@ private void mockObserversFor(String beanIdentifier, boolean mock) {
}
}

public void mockObserversFor(Class<?> beanClass, boolean mock) {
for (InjectableObserverMethod<?> observer : observers) {
if (observer instanceof Mockable && beanClass.equals(observer.getBeanClass())) {
Mockable mockable = (Mockable) observer;
if (mock) {
mockable.arc$setMock(null);
} else {
mockable.arc$clearMock();
}
}
}
}

public static ArcContainerImpl instance() {
return unwrap(Arc.container());
}
Expand Down
Loading

0 comments on commit 3aeb74e

Please sign in to comment.