diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 302a93d47ff7f..b9b7c2d596cd7 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -2953,6 +2953,11 @@ quarkus-test-security-webauthn ${project.version} + + io.quarkus + quarkus-junit5-component + ${project.version} + io.quarkus quarkus-junit5-internal diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/JunitTestRunner.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/JunitTestRunner.java index ee468ee0a929e..d39b31566b3bd 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/JunitTestRunner.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/JunitTestRunner.java @@ -30,6 +30,7 @@ import org.jboss.jandex.AnnotationTarget; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; +import org.jboss.jandex.FieldInfo; import org.jboss.jandex.Index; import org.jboss.jandex.Indexer; import org.jboss.logging.Logger; @@ -81,6 +82,7 @@ public class JunitTestRunner { public static final DotName QUARKUS_TEST = DotName.createSimple("io.quarkus.test.junit.QuarkusTest"); public static final DotName QUARKUS_MAIN_TEST = DotName.createSimple("io.quarkus.test.junit.main.QuarkusMainTest"); public static final DotName QUARKUS_INTEGRATION_TEST = DotName.createSimple("io.quarkus.test.junit.QuarkusIntegrationTest"); + public static final DotName QUARKUS_COMPONENT_TEST = DotName.createSimple("io.quarkus.test.component.QuarkusComponentTest"); public static final DotName TEST_PROFILE = DotName.createSimple("io.quarkus.test.junit.TestProfile"); public static final DotName TEST = DotName.createSimple(Test.class.getName()); public static final DotName REPEATED_TEST = DotName.createSimple(RepeatedTest.class.getName()); @@ -570,6 +572,15 @@ private DiscoveryResult discoverTestClasses() { } } } + Set componentTestClasses = new HashSet<>(); + for (ClassInfo testClass : index.getKnownUsers(QUARKUS_COMPONENT_TEST)) { + for (FieldInfo field : testClass.fields()) { + if (field.type().name().equals(QUARKUS_COMPONENT_TEST)) { + componentTestClasses.add(testClass.name().toString()); + } + } + } + Set allTestAnnotations = collectTestAnnotations(index); Set allTestClasses = new HashSet<>(); Map enclosingClasses = new HashMap<>(); @@ -601,7 +612,8 @@ private DiscoveryResult discoverTestClasses() { Set unitTestClasses = new HashSet<>(); for (DotName testClass : allTestClasses) { String name = testClass.toString(); - if (integrationTestClasses.contains(name) || quarkusTestClasses.contains(name)) { + if (integrationTestClasses.contains(name) || componentTestClasses.contains(name) + || quarkusTestClasses.contains(name)) { continue; } var enclosing = enclosingClasses.get(testClass); diff --git a/docs/src/main/asciidoc/getting-started-testing.adoc b/docs/src/main/asciidoc/getting-started-testing.adoc index 50673af86f6d6..5446268e071c2 100644 --- a/docs/src/main/asciidoc/getting-started-testing.adoc +++ b/docs/src/main/asciidoc/getting-started-testing.adoc @@ -1506,3 +1506,93 @@ For `@QuarkusIntegrationTest` tests that result in launcher the application as a This can be used by `QuarkusTestResourceLifecycleManager` that need to launch additional containers that the application will communicate with. ==== +== Testing Components + +In Quarkus, the component model is built on top CDI. +Therefore, Quarkus provides the `QuarkusComponentTest`, a JUnit extension to ease the testing of components and mocking of their dependencies. + +Let's have a component `Foo`: + +[source, java] +---- +package org.acme; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +@ApplicationScoped <1> +public class Foo { + + @Inject + Charlie charlie; <2> + + public String ping() { + return charlie.ping(); + } +} +---- +<1> `Foo` is an `@ApplicationScoped` CDI bean. +<2> `Foo` depends on `Charlie` which declares a method `ping()`. + +Then a component test could look like: + +[source, java] +---- +import static org.junit.jupiter.api.Assertions.assertEquals; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.Mockito; +import org.acme.Charlie; +import org.acme.Foo; + +public class FooTest { + + @RegisterExtension <1> + static final QuarkusComponentTest test = new QuarkusComponentTest(Foo.class); + + @Inject + Foo foo; <2> + + // Inject a mock that is created automatically... + @Inject + Charlie charlie; <3> + + @Test + public void testPing() { + Mockito.when(charlie.ping()).thenReturn("OK"); <4> + assertEquals("OK", foo()); + } +} +---- +<1> The `QuarkusComponentTest` extension is configured in a static field. +<2> The test injects the component under the test. +<3> The test also injects `Charlie`, a dependency for which a synthetic `@Singleton` bean is registered automatically. The injected reference is an "unconfigured" Mockito mock. +<4> We can leverage the Mockito API in a test method to configure the behavior. + +=== Lifecycle + +So what exactly does the `QuarkusComponentTest` do? +It starts the CDI container and registers a dedicated `SmallRyeConfig` during the `before all` test phase. +The container is stopped and the config is released during the `after all` test phase. +The fields annotated with `@Inject` are injected after a test instance is created and unset before a test instance is destroyed. +Finally, the CDI request context is activated and terminated per each test method. + +=== Auto Mocking Unsatisfied Dependencies + +Unlike in regular CDI environments the test does not fail if a component injects an unsatisfied dependency. +Instead, a synthetic bean is registered automatically for each combination of required type and qualifiers of an injection point that resolves to an unsatisfied dependency. +The bean has the `@Singleton` scope so it's shared across all injection points with the same required type and qualifiers. +The injected reference is an _unconfigured_ Mockito mock. +You can inject the mock in your test and leverage the Mockito API to configure the behavior. + +=== Custom Mocks For Unsatisfied Dependencies + +Sometimes you need the full control over the bean attributes and maybe even configure the default mock behavior. +You can use the mock configurator API via the `QuarkusComponentTest#mock()` method. + +=== Configuration + +A dedicated `SmallRyeConfig` is registered during the `before all` test phase. +Moreover, it's possible to set the configuration properties via the `QuarkusComponentTest#configProperty(String, String)` method. +If you only need to use the default values for missing config properties, then the `QuarkusComponentTest#useDefaultConfigProperties()` might come in useful. \ No newline at end of file diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Annotations.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Annotations.java index 163df5c5d3a46..204d889209515 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Annotations.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Annotations.java @@ -1,10 +1,15 @@ package io.quarkus.arc.processor; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.ArrayDeque; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Queue; import java.util.Set; import java.util.function.Function; @@ -196,4 +201,162 @@ public static Collection onlyRuntimeVisible(Collection annotationType = annotationType(annotation); + + DotName name = DotName.createSimple(annotationType.getName()); + @SuppressWarnings("unchecked") + org.jboss.jandex.AnnotationValue[] jandexAnnotationValues = jandexAnnotationValues( + (Class) annotationType, annotation); + + return org.jboss.jandex.AnnotationInstance.create(name, null, jandexAnnotationValues); + } + + @SuppressWarnings("unchecked") + private static Class annotationType(Annotation annotation) { + Class annotationType = null; + + Queue> candidates = new ArrayDeque<>(); + candidates.add(annotation.getClass()); + while (!candidates.isEmpty()) { + Class candidate = candidates.remove(); + + if (candidate.isAnnotation()) { + annotationType = (Class) candidate; + break; + } + + Collections.addAll(candidates, candidate.getInterfaces()); + } + + if (annotationType == null) { + throw new IllegalArgumentException("Not an annotation: " + annotation); + } + + return annotationType; + } + + private static org.jboss.jandex.AnnotationValue[] jandexAnnotationValues( + Class annotationType, A annotationInstance) { + List result = new ArrayList<>(); + for (Method member : annotationType.getDeclaredMethods()) { + try { + // annotation types do not necessarily have to be public (if the annotation type + // and the build compatible extension class reside in the same package) + if (!member.canAccess(annotationInstance)) { + member.setAccessible(true); + } + result.add(jandexAnnotationValue(member.getName(), member.invoke(annotationInstance))); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + return result.toArray(new org.jboss.jandex.AnnotationValue[0]); + } + + private static org.jboss.jandex.AnnotationValue jandexAnnotationValue(String name, Object value) { + if (value instanceof Boolean) { + return org.jboss.jandex.AnnotationValue.createBooleanValue(name, (Boolean) value); + } else if (value instanceof Byte) { + return org.jboss.jandex.AnnotationValue.createByteValue(name, (Byte) value); + } else if (value instanceof Short) { + return org.jboss.jandex.AnnotationValue.createShortValue(name, (Short) value); + } else if (value instanceof Integer) { + return org.jboss.jandex.AnnotationValue.createIntegerValue(name, (Integer) value); + } else if (value instanceof Long) { + return org.jboss.jandex.AnnotationValue.createLongValue(name, (Long) value); + } else if (value instanceof Float) { + return org.jboss.jandex.AnnotationValue.createFloatValue(name, (Float) value); + } else if (value instanceof Double) { + return org.jboss.jandex.AnnotationValue.createDoubleValue(name, (Double) value); + } else if (value instanceof Character) { + return org.jboss.jandex.AnnotationValue.createCharacterValue(name, (Character) value); + } else if (value instanceof String) { + return org.jboss.jandex.AnnotationValue.createStringValue(name, (String) value); + } else if (value instanceof Enum) { + return org.jboss.jandex.AnnotationValue.createEnumValue(name, + DotName.createSimple(((Enum) value).getDeclaringClass().getName()), ((Enum) value).name()); + } else if (value instanceof Class) { + return org.jboss.jandex.AnnotationValue.createClassValue(name, Types.jandexType((Class) value)); + } else if (value.getClass().isAnnotation()) { + Class annotationType = annotationType((Annotation) value); + @SuppressWarnings("unchecked") + org.jboss.jandex.AnnotationValue[] jandexAnnotationValues = jandexAnnotationValues( + (Class) annotationType, (Annotation) value); + org.jboss.jandex.AnnotationInstance jandexAnnotation = org.jboss.jandex.AnnotationInstance.create( + DotName.createSimple(annotationType.getName()), null, jandexAnnotationValues); + return org.jboss.jandex.AnnotationValue.createNestedAnnotationValue(name, jandexAnnotation); + } else if (value.getClass().isArray()) { + org.jboss.jandex.AnnotationValue[] jandexAnnotationValues = Arrays.stream(boxArray(value)) + .map(it -> jandexAnnotationValue(name, it)) + .toArray(org.jboss.jandex.AnnotationValue[]::new); + return org.jboss.jandex.AnnotationValue.createArrayValue(name, jandexAnnotationValues); + } else { + throw new IllegalArgumentException("Unknown annotation attribute value: " + value); + } + } + + private static Object[] boxArray(Object value) { + if (value instanceof boolean[]) { + boolean[] primitiveArray = (boolean[]) value; + Object[] boxedArray = new Boolean[primitiveArray.length]; + for (int i = 0; i < primitiveArray.length; i++) { + boxedArray[i] = primitiveArray[i]; + } + return boxedArray; + } else if (value instanceof byte[]) { + byte[] primitiveArray = (byte[]) value; + Object[] boxedArray = new Byte[primitiveArray.length]; + for (int i = 0; i < primitiveArray.length; i++) { + boxedArray[i] = primitiveArray[i]; + } + return boxedArray; + } else if (value instanceof short[]) { + short[] primitiveArray = (short[]) value; + Object[] boxedArray = new Short[primitiveArray.length]; + for (int i = 0; i < primitiveArray.length; i++) { + boxedArray[i] = primitiveArray[i]; + } + return boxedArray; + } else if (value instanceof int[]) { + int[] primitiveArray = (int[]) value; + Object[] boxedArray = new Integer[primitiveArray.length]; + for (int i = 0; i < primitiveArray.length; i++) { + boxedArray[i] = primitiveArray[i]; + } + return boxedArray; + } else if (value instanceof long[]) { + long[] primitiveArray = (long[]) value; + Object[] boxedArray = new Long[primitiveArray.length]; + for (int i = 0; i < primitiveArray.length; i++) { + boxedArray[i] = primitiveArray[i]; + } + return boxedArray; + } else if (value instanceof float[]) { + float[] primitiveArray = (float[]) value; + Object[] boxedArray = new Float[primitiveArray.length]; + for (int i = 0; i < primitiveArray.length; i++) { + boxedArray[i] = primitiveArray[i]; + } + return boxedArray; + } else if (value instanceof double[]) { + double[] primitiveArray = (double[]) value; + Object[] boxedArray = new Double[primitiveArray.length]; + for (int i = 0; i < primitiveArray.length; i++) { + boxedArray[i] = primitiveArray[i]; + } + return boxedArray; + } else if (value instanceof char[]) { + char[] primitiveArray = (char[]) value; + Object[] boxedArray = new Character[primitiveArray.length]; + for (int i = 0; i < primitiveArray.length; i++) { + boxedArray[i] = primitiveArray[i]; + } + return boxedArray; + } else if (value instanceof Object[]) { + return (Object[]) value; + } else { + throw new IllegalArgumentException("Not an array: " + value); + } + } } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanConfigurator.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanConfigurator.java index cd3a3793da0d1..21f3afb3c7ac6 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanConfigurator.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanConfigurator.java @@ -71,6 +71,7 @@ public void done() { BeanInfo.Builder builder = new BeanInfo.Builder() .implClazz(implClass) + .identifier(identifier) .providerType(providerType) .beanDeployment(beanDeployment) .scope(scope) diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanConfiguratorBase.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanConfiguratorBase.java index 05526bf52f0bc..ea4fa6864f584 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanConfiguratorBase.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanConfiguratorBase.java @@ -22,6 +22,7 @@ import io.quarkus.arc.BeanCreator; import io.quarkus.arc.BeanDestroyer; +import io.quarkus.arc.InjectableBean; import io.quarkus.arc.InjectableReferenceProvider; import io.quarkus.arc.SyntheticCreationalContext; import io.quarkus.arc.processor.InjectionPointInfo.TypeAndQualifiers; @@ -36,6 +37,7 @@ public abstract class BeanConfiguratorBase, T> extends ConfiguratorBase implements Consumer { + protected String identifier; protected final DotName implClazz; protected final Set types; protected final Set qualifiers; @@ -311,6 +313,21 @@ public THIS destroyer(Consumer methodCreatorConsumer) { return cast(this); } + /** + * The identifier becomes part of the {@link BeanInfo#getIdentifier()} and {@link InjectableBean#getIdentifier()}. + *

+ * An identifier can be used to register multiple synthetic beans with the same set of types and qualifiers. + * + * @param identifier + * @return self + * @see #defaultBean() + * @see #alternative(boolean) + */ + public THIS identifier(String identifier) { + this.identifier = identifier; + return cast(this); + } + @SuppressWarnings("unchecked") protected static T cast(Object obj) { return (T) obj; diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java index f5fb1364f1109..f2e10b46c3e86 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java @@ -127,7 +127,7 @@ public class BeanDeployment { BeanDeployment(String name, BuildContextImpl buildContext, BeanProcessor.Builder builder) { this.name = name; this.buildCompatibleExtensions = builder.buildCompatibleExtensions; - this.buildContext = buildContext; + this.buildContext = Objects.requireNonNull(buildContext); Map beanDefiningAnnotations = new HashMap<>(); if (builder.additionalBeanDefiningAnnotations != null) { for (BeanDefiningAnnotation bda : builder.additionalBeanDefiningAnnotations) { @@ -140,9 +140,8 @@ public class BeanDeployment { this.beanArchiveImmutableIndex = Objects.requireNonNull(builder.beanArchiveImmutableIndex); this.applicationIndex = builder.applicationIndex; this.annotationStore = new AnnotationStore(initAndSort(builder.annotationTransformers, buildContext), buildContext); - if (buildContext != null) { - buildContext.putInternal(Key.ANNOTATION_STORE.asString(), annotationStore); - } + buildContext.putInternal(Key.ANNOTATION_STORE, annotationStore); + this.injectionPointTransformer = new InjectionPointModifier( initAndSort(builder.injectionPointTransformers, buildContext), buildContext); this.observerTransformers = initAndSort(builder.observerTransformers, buildContext); @@ -173,7 +172,7 @@ public class BeanDeployment { } } repeatingQualifierAnnotations = findContainerAnnotations(qualifiers); - buildContextPut(Key.QUALIFIERS.asString(), Collections.unmodifiableMap(qualifiers)); + buildContext.putInternal(Key.QUALIFIERS, Collections.unmodifiableMap(qualifiers)); interceptorNonbindingMembers = new HashMap<>(); interceptorBindings = findInterceptorBindings(); @@ -194,7 +193,7 @@ public class BeanDeployment { } } repeatingInterceptorBindingAnnotations = findContainerAnnotations(interceptorBindings); - buildContextPut(Key.INTERCEPTOR_BINDINGS.asString(), Collections.unmodifiableMap(interceptorBindings)); + buildContext.putInternal(Key.INTERCEPTOR_BINDINGS, Collections.unmodifiableMap(interceptorBindings)); Set additionalStereotypes = new HashSet<>(); for (StereotypeRegistrar stereotypeRegistrar : builder.stereotypeRegistrars) { @@ -203,7 +202,7 @@ public class BeanDeployment { this.stereotypes = findStereotypes(interceptorBindings, customContexts, additionalStereotypes, annotationStore); - buildContextPut(Key.STEREOTYPES.asString(), Collections.unmodifiableMap(stereotypes)); + buildContext.putInternal(Key.STEREOTYPES, Collections.unmodifiableMap(stereotypes)); this.transitiveInterceptorBindings = findTransitiveInterceptorBindings(interceptorBindings.keySet(), new HashMap<>(), interceptorBindings, annotationStore); @@ -270,14 +269,14 @@ BeanRegistrar.RegistrationContext registerBeans(List beanRegistra injectionPoints, jtaCapabilities)); // Note that we use unmodifiable views because the underlying collections may change in the next phase // E.g. synthetic beans are added and unused interceptors removed - buildContextPut(Key.BEANS.asString(), Collections.unmodifiableList(beans)); - buildContextPut(Key.OBSERVERS.asString(), Collections.unmodifiableList(observers)); + buildContext.putInternal(Key.BEANS, Collections.unmodifiableList(beans)); + buildContext.putInternal(Key.OBSERVERS, Collections.unmodifiableList(observers)); this.interceptors.addAll(findInterceptors(injectionPoints)); - buildContextPut(Key.INTERCEPTORS.asString(), Collections.unmodifiableList(interceptors)); + buildContext.putInternal(Key.INTERCEPTORS, Collections.unmodifiableList(interceptors)); this.decorators.addAll(findDecorators(injectionPoints)); - buildContextPut(Key.DECORATORS.asString(), Collections.unmodifiableList(decorators)); + buildContext.putInternal(Key.DECORATORS, Collections.unmodifiableList(decorators)); this.injectionPoints.addAll(injectionPoints); - buildContextPut(Key.INJECTION_POINTS.asString(), Collections.unmodifiableList(this.injectionPoints)); + buildContext.putInternal(Key.INJECTION_POINTS, Collections.unmodifiableList(this.injectionPoints)); if (buildCompatibleExtensions != null) { buildCompatibleExtensions.runRegistration(beanArchiveComputingIndex, beans, observers); @@ -323,12 +322,10 @@ void init(Consumer bytecodeTransformerConsumer, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - removalStart)); //we need to re-initialize it, so it does not contain removed beans initBeanByTypeMap(); - buildContext.putInternal(BuildExtension.Key.REMOVED_INTERCEPTORS.asString(), - Collections.unmodifiableSet(removedInterceptors)); - buildContext.putInternal(BuildExtension.Key.REMOVED_DECORATORS.asString(), - Collections.unmodifiableSet(removedDecorators)); + buildContext.putInternal(BuildExtension.Key.REMOVED_INTERCEPTORS, Collections.unmodifiableSet(removedInterceptors)); + buildContext.putInternal(BuildExtension.Key.REMOVED_DECORATORS, Collections.unmodifiableSet(removedDecorators)); } - buildContext.putInternal(BuildExtension.Key.REMOVED_BEANS.asString(), Collections.unmodifiableSet(removedBeans)); + buildContext.putInternal(BuildExtension.Key.REMOVED_BEANS, Collections.unmodifiableSet(removedBeans)); LOGGER.debugf("Bean deployment initialized in %s ms", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)); } @@ -717,12 +714,6 @@ Set getObserverAndProducerMethods() { return ret; } - private void buildContextPut(String key, Object value) { - if (buildContext != null) { - buildContext.putInternal(key, value); - } - } - private boolean isRuntimeAnnotationType(ClassInfo annotationType) { if (!annotationType.isAnnotation()) { return false; diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanInfo.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanInfo.java index 160e9cb46cba8..298fac83245b4 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanInfo.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanInfo.java @@ -99,7 +99,7 @@ public class BeanInfo implements InjectionTargetInfo { Integer priority) { this(null, null, target, beanDeployment, scope, types, qualifiers, injections, declaringBean, disposer, alternative, stereotypes, name, isDefaultBean, null, null, Collections.emptyMap(), true, false, - targetPackageName, priority); + targetPackageName, priority, null); } BeanInfo(ClassInfo implClazz, Type providerType, AnnotationTarget target, BeanDeployment beanDeployment, ScopeInfo scope, @@ -107,7 +107,7 @@ public class BeanInfo implements InjectionTargetInfo { DisposerInfo disposer, boolean alternative, List stereotypes, String name, boolean isDefaultBean, Consumer creatorConsumer, Consumer destroyerConsumer, Map params, boolean isRemovable, - boolean forceApplicationClass, String targetPackageName, Integer priority) { + boolean forceApplicationClass, String targetPackageName, Integer priority, String identifier) { this.target = Optional.ofNullable(target); if (implClazz == null && target != null) { @@ -140,7 +140,7 @@ public class BeanInfo implements InjectionTargetInfo { this.removable = isRemovable; this.params = params; // Identifier must be unique for a specific deployment - this.identifier = Hashes.sha1(toString() + beanDeployment.toString()); + this.identifier = Hashes.sha1((identifier != null ? identifier : "") + toString() + beanDeployment.toString()); this.interceptedMethods = Collections.emptyMap(); this.decoratedMethods = Collections.emptyMap(); this.lifecycleInterceptors = Collections.emptyMap(); @@ -951,6 +951,8 @@ boolean isEmpty() { static class Builder { + private String identifier; + private ClassInfo implClazz; private Type providerType; @@ -998,6 +1000,11 @@ static class Builder { stereotypes = Collections.emptyList(); } + Builder identifier(String identifier) { + this.identifier = identifier; + return this; + } + Builder implClazz(ClassInfo implClazz) { this.implClazz = implClazz; return this; @@ -1110,7 +1117,7 @@ Builder targetPackageName(String name) { BeanInfo build() { return new BeanInfo(implClazz, providerType, target, beanDeployment, scope, types, qualifiers, injections, declaringBean, disposer, alternative, stereotypes, name, isDefaultBean, creatorConsumer, - destroyerConsumer, params, removable, forceApplicationClass, targetPackageName, priority); + destroyerConsumer, params, removable, forceApplicationClass, targetPackageName, priority, identifier); } public Builder forceApplicationClass(boolean forceApplicationClass) { diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java index ba749360e3c54..25305e681c2b2 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java @@ -110,15 +110,15 @@ private BeanProcessor(Builder builder) { // Initialize all build processors buildContext = new BuildContextImpl(); - buildContext.putInternal(Key.INDEX.asString(), - builder.beanArchiveComputingIndex != null ? builder.beanArchiveComputingIndex - : builder.beanArchiveImmutableIndex); + buildContext.putInternal(Key.INDEX, builder.beanArchiveComputingIndex != null ? builder.beanArchiveComputingIndex + : builder.beanArchiveImmutableIndex); this.beanRegistrars = initAndSort(builder.beanRegistrars, buildContext); this.observerRegistrars = initAndSort(builder.observerRegistrars, buildContext); this.contextRegistrars = initAndSort(builder.contextRegistrars, buildContext); this.beanDeploymentValidators = initAndSort(builder.beanDeploymentValidators, buildContext); this.beanDeployment = new BeanDeployment(name, buildContext, builder); + buildContext.putInternal(Key.DEPLOYMENT, this.beanDeployment); // Make it configurable if we find that the set of annotations needs to grow this.injectionPointAnnotationsPredicate = Predicate.not(DotNames.DEPRECATED::equals); @@ -862,6 +862,11 @@ public V put(Key key, V value) { return putInternal(keyStr, value); } + @SuppressWarnings("unchecked") + V putInternal(Key key, V value) { + return (V) data.put(key.asString(), value); + } + @SuppressWarnings("unchecked") V putInternal(String key, V value) { return (V) data.put(key, value); diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanResolver.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanResolver.java index c4be961818557..9e30614c94f04 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanResolver.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanResolver.java @@ -1,5 +1,6 @@ package io.quarkus.arc.processor; +import java.util.Collection; import java.util.Collections; import java.util.Set; @@ -76,4 +77,12 @@ default Set resolveBeans(Type requiredType, AnnotationInstance... requ */ boolean matchesType(BeanInfo bean, Type requiredType); + /** + * + * @param qualifiers + * @param requiredQualifier + * @return {@code true} if any qualifier from the collection matches the required qualifiers, {@code false} otherwise + */ + boolean hasQualifier(Collection qualifiers, AnnotationInstance requiredQualifier); + } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanResolverImpl.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanResolverImpl.java index 8d5cf0dcac47b..860a43c3ce460 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanResolverImpl.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanResolverImpl.java @@ -94,6 +94,11 @@ public boolean matchesType(BeanInfo bean, Type requiredType) { return false; } + @Override + public boolean hasQualifier(Collection qualifiers, AnnotationInstance requiredQualifier) { + return Beans.hasQualifier(beanDeployment, requiredQualifier, qualifiers); + } + protected BeanResolver getBeanResolver(BeanInfo bean) { return bean.getDeployment().beanResolver; } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Beans.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Beans.java index f6200b874084f..5c1183a4fe11d 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Beans.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Beans.java @@ -448,7 +448,7 @@ public static boolean matches(BeanInfo bean, TypeAndQualifiers typeAndQualifiers * Checks if given {@link BeanInfo} has all the required qualifiers and a bean type that matches required type. * Uses standard bean assignability rules; see {@link BeanResolverImpl}. */ - static boolean matches(BeanInfo bean, Type requiredType, Set requiredQualifiers) { + public static boolean matches(BeanInfo bean, Type requiredType, Set requiredQualifiers) { return bean.getDeployment().getBeanResolver().matches(bean, requiredType, requiredQualifiers); } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BuildExtension.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BuildExtension.java index 03370d9c17812..eb983d5284628 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BuildExtension.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BuildExtension.java @@ -67,6 +67,7 @@ interface Key { static Key> QUALIFIERS = simpleBuiltIn("qualifiers"); static Key> INTERCEPTOR_BINDINGS = simpleBuiltIn("interceptorBindings"); static Key> STEREOTYPES = simpleBuiltIn("stereotypes"); + static Key DEPLOYMENT = simpleBuiltIn("deployment"); String asString(); } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BuiltinBean.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BuiltinBean.java index d7e99844f2036..8ebee1d654d9c 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BuiltinBean.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BuiltinBean.java @@ -41,7 +41,7 @@ * * @author Martin Kouba */ -enum BuiltinBean { +public enum BuiltinBean { INSTANCE(BuiltinBean::generateInstanceBytecode, BuiltinBean::cdiAndRawTypeMatches, BuiltinBean::validateInstance, DotNames.INSTANCE, DotNames.PROVIDER, DotNames.INJECTABLE_INSTANCE), @@ -88,7 +88,7 @@ private BuiltinBean(Generator generator, BiPredicate clazz) { + if (clazz.isArray()) { + int dimensions = 1; + Class componentType = clazz.getComponentType(); + while (componentType.isArray()) { + dimensions++; + componentType = componentType.getComponentType(); + } + return org.jboss.jandex.ArrayType.create(jandexType(componentType), dimensions); + } + + if (clazz.isPrimitive()) { + if (clazz == Void.TYPE) { + return Type.create(DotName.createSimple("void"), org.jboss.jandex.Type.Kind.VOID); + } else if (clazz == Boolean.TYPE) { + return PrimitiveType.BOOLEAN; + } else if (clazz == Byte.TYPE) { + return PrimitiveType.BYTE; + } else if (clazz == Short.TYPE) { + return PrimitiveType.SHORT; + } else if (clazz == Integer.TYPE) { + return PrimitiveType.INT; + } else if (clazz == Long.TYPE) { + return PrimitiveType.LONG; + } else if (clazz == Float.TYPE) { + return PrimitiveType.FLOAT; + } else if (clazz == Double.TYPE) { + return PrimitiveType.DOUBLE; + } else if (clazz == Character.TYPE) { + return PrimitiveType.CHAR; + } else { + throw new IllegalArgumentException("Unknown primitive type " + clazz); + } + } + + return org.jboss.jandex.Type.create(DotName.createSimple(clazz.getName()), org.jboss.jandex.Type.Kind.CLASS); + } + + public static Type jandexType(java.lang.reflect.Type type) { + if (type instanceof java.lang.Class) { + return jandexType((Class) type); + } else if (type instanceof java.lang.reflect.ParameterizedType) { + java.lang.reflect.ParameterizedType p = (java.lang.reflect.ParameterizedType) type; + org.jboss.jandex.ParameterizedType.Builder builder = org.jboss.jandex.ParameterizedType + .builder((Class) p.getRawType()); + for (java.lang.reflect.Type typeArgument : p.getActualTypeArguments()) { + builder.addArgument(jandexType(typeArgument)); + } + return builder.build(); + } else { + throw new IllegalArgumentException("Unsupported type: " + type); + } + } + private static class TypeVariables { private final Map typeVariable = new HashMap<>(); private final Map typeVariableReference = new HashMap<>(); diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/AnnotationBuilderImpl.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/AnnotationBuilderImpl.java index 727d34b948ead..e45cf95bfeebc 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/AnnotationBuilderImpl.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/AnnotationBuilderImpl.java @@ -14,6 +14,9 @@ import org.jboss.jandex.DotName; +import io.quarkus.arc.processor.Annotations; +import io.quarkus.arc.processor.Types; + class AnnotationBuilderImpl implements AnnotationBuilder { private final org.jboss.jandex.IndexView jandexIndex; private final AllAnnotationOverlays annotationOverlays; @@ -327,7 +330,7 @@ public AnnotationBuilder member(String name, ClassInfo enumType, String[] enumVa @Override public AnnotationBuilder member(String name, Class value) { - jandexAnnotationMembers.add(org.jboss.jandex.AnnotationValue.createClassValue(name, TypesReflection.jandexType(value))); + jandexAnnotationMembers.add(org.jboss.jandex.AnnotationValue.createClassValue(name, Types.jandexType(value))); return this; } @@ -335,7 +338,7 @@ public AnnotationBuilder member(String name, Class value) { public AnnotationBuilder member(String name, Class[] values) { org.jboss.jandex.AnnotationValue[] array = new org.jboss.jandex.AnnotationValue[values.length]; for (int i = 0; i < values.length; i++) { - array[i] = org.jboss.jandex.AnnotationValue.createClassValue(name, TypesReflection.jandexType(values[i])); + array[i] = org.jboss.jandex.AnnotationValue.createClassValue(name, Types.jandexType(values[i])); } jandexAnnotationMembers.add(org.jboss.jandex.AnnotationValue.createArrayValue(name, array)); return this; @@ -424,7 +427,7 @@ public AnnotationBuilder member(String name, AnnotationInfo[] values) { @Override public AnnotationBuilder member(String name, Annotation value) { - org.jboss.jandex.AnnotationInstance jandexAnnotation = AnnotationsReflection.jandexAnnotation(value); + org.jboss.jandex.AnnotationInstance jandexAnnotation = Annotations.jandexAnnotation(value); jandexAnnotationMembers.add(org.jboss.jandex.AnnotationValue.createNestedAnnotationValue(name, jandexAnnotation)); return this; } @@ -433,7 +436,7 @@ public AnnotationBuilder member(String name, Annotation value) { public AnnotationBuilder member(String name, Annotation[] values) { org.jboss.jandex.AnnotationValue[] array = new org.jboss.jandex.AnnotationValue[values.length]; for (int i = 0; i < values.length; i++) { - org.jboss.jandex.AnnotationInstance jandexAnnotation = AnnotationsReflection.jandexAnnotation(values[i]); + org.jboss.jandex.AnnotationInstance jandexAnnotation = Annotations.jandexAnnotation(values[i]); array[i] = org.jboss.jandex.AnnotationValue.createNestedAnnotationValue(name, jandexAnnotation); } jandexAnnotationMembers.add(org.jboss.jandex.AnnotationValue.createArrayValue(name, array)); diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/AnnotationsReflection.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/AnnotationsReflection.java deleted file mode 100644 index e03ff5f3ca69c..0000000000000 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/AnnotationsReflection.java +++ /dev/null @@ -1,170 +0,0 @@ -package io.quarkus.arc.processor.bcextensions; - -import java.lang.annotation.Annotation; -import java.lang.reflect.Method; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Queue; - -import org.jboss.jandex.DotName; - -class AnnotationsReflection { - static org.jboss.jandex.AnnotationInstance jandexAnnotation(Annotation annotation) { - Class annotationType = annotationType(annotation); - - DotName name = DotName.createSimple(annotationType.getName()); - org.jboss.jandex.AnnotationValue[] jandexAnnotationValues = jandexAnnotationValues( - (Class) annotationType, annotation); - - return org.jboss.jandex.AnnotationInstance.create(name, null, jandexAnnotationValues); - } - - private static Class annotationType(Annotation annotation) { - Class annotationType = null; - - Queue> candidates = new ArrayDeque<>(); - candidates.add(annotation.getClass()); - while (!candidates.isEmpty()) { - Class candidate = candidates.remove(); - - if (candidate.isAnnotation()) { - annotationType = (Class) candidate; - break; - } - - Collections.addAll(candidates, candidate.getInterfaces()); - } - - if (annotationType == null) { - throw new IllegalArgumentException("Not an annotation: " + annotation); - } - - return annotationType; - } - - private static org.jboss.jandex.AnnotationValue[] jandexAnnotationValues( - Class annotationType, A annotationInstance) { - List result = new ArrayList<>(); - for (Method member : annotationType.getDeclaredMethods()) { - try { - // annotation types do not necessarily have to be public (if the annotation type - // and the build compatible extension class reside in the same package) - if (!member.canAccess(annotationInstance)) { - member.setAccessible(true); - } - result.add(jandexAnnotationValue(member.getName(), member.invoke(annotationInstance))); - } catch (ReflectiveOperationException e) { - throw new RuntimeException(e); - } - } - return result.toArray(new org.jboss.jandex.AnnotationValue[0]); - } - - private static org.jboss.jandex.AnnotationValue jandexAnnotationValue(String name, Object value) { - if (value instanceof Boolean) { - return org.jboss.jandex.AnnotationValue.createBooleanValue(name, (Boolean) value); - } else if (value instanceof Byte) { - return org.jboss.jandex.AnnotationValue.createByteValue(name, (Byte) value); - } else if (value instanceof Short) { - return org.jboss.jandex.AnnotationValue.createShortValue(name, (Short) value); - } else if (value instanceof Integer) { - return org.jboss.jandex.AnnotationValue.createIntegerValue(name, (Integer) value); - } else if (value instanceof Long) { - return org.jboss.jandex.AnnotationValue.createLongValue(name, (Long) value); - } else if (value instanceof Float) { - return org.jboss.jandex.AnnotationValue.createFloatValue(name, (Float) value); - } else if (value instanceof Double) { - return org.jboss.jandex.AnnotationValue.createDoubleValue(name, (Double) value); - } else if (value instanceof Character) { - return org.jboss.jandex.AnnotationValue.createCharacterValue(name, (Character) value); - } else if (value instanceof String) { - return org.jboss.jandex.AnnotationValue.createStringValue(name, (String) value); - } else if (value instanceof Enum) { - return org.jboss.jandex.AnnotationValue.createEnumValue(name, - DotName.createSimple(((Enum) value).getDeclaringClass().getName()), ((Enum) value).name()); - } else if (value instanceof Class) { - return org.jboss.jandex.AnnotationValue.createClassValue(name, TypesReflection.jandexType((Class) value)); - } else if (value.getClass().isAnnotation()) { - Class annotationType = annotationType((Annotation) value); - org.jboss.jandex.AnnotationValue[] jandexAnnotationValues = jandexAnnotationValues( - (Class) annotationType, (Annotation) value); - org.jboss.jandex.AnnotationInstance jandexAnnotation = org.jboss.jandex.AnnotationInstance.create( - DotName.createSimple(annotationType.getName()), null, jandexAnnotationValues); - return org.jboss.jandex.AnnotationValue.createNestedAnnotationValue(name, jandexAnnotation); - } else if (value.getClass().isArray()) { - org.jboss.jandex.AnnotationValue[] jandexAnnotationValues = Arrays.stream(boxArray(value)) - .map(it -> jandexAnnotationValue(name, it)) - .toArray(org.jboss.jandex.AnnotationValue[]::new); - return org.jboss.jandex.AnnotationValue.createArrayValue(name, jandexAnnotationValues); - } else { - throw new IllegalArgumentException("Unknown annotation attribute value: " + value); - } - } - - private static Object[] boxArray(Object value) { - if (value instanceof boolean[]) { - boolean[] primitiveArray = (boolean[]) value; - Object[] boxedArray = new Boolean[primitiveArray.length]; - for (int i = 0; i < primitiveArray.length; i++) { - boxedArray[i] = primitiveArray[i]; - } - return boxedArray; - } else if (value instanceof byte[]) { - byte[] primitiveArray = (byte[]) value; - Object[] boxedArray = new Byte[primitiveArray.length]; - for (int i = 0; i < primitiveArray.length; i++) { - boxedArray[i] = primitiveArray[i]; - } - return boxedArray; - } else if (value instanceof short[]) { - short[] primitiveArray = (short[]) value; - Object[] boxedArray = new Short[primitiveArray.length]; - for (int i = 0; i < primitiveArray.length; i++) { - boxedArray[i] = primitiveArray[i]; - } - return boxedArray; - } else if (value instanceof int[]) { - int[] primitiveArray = (int[]) value; - Object[] boxedArray = new Integer[primitiveArray.length]; - for (int i = 0; i < primitiveArray.length; i++) { - boxedArray[i] = primitiveArray[i]; - } - return boxedArray; - } else if (value instanceof long[]) { - long[] primitiveArray = (long[]) value; - Object[] boxedArray = new Long[primitiveArray.length]; - for (int i = 0; i < primitiveArray.length; i++) { - boxedArray[i] = primitiveArray[i]; - } - return boxedArray; - } else if (value instanceof float[]) { - float[] primitiveArray = (float[]) value; - Object[] boxedArray = new Float[primitiveArray.length]; - for (int i = 0; i < primitiveArray.length; i++) { - boxedArray[i] = primitiveArray[i]; - } - return boxedArray; - } else if (value instanceof double[]) { - double[] primitiveArray = (double[]) value; - Object[] boxedArray = new Double[primitiveArray.length]; - for (int i = 0; i < primitiveArray.length; i++) { - boxedArray[i] = primitiveArray[i]; - } - return boxedArray; - } else if (value instanceof char[]) { - char[] primitiveArray = (char[]) value; - Object[] boxedArray = new Character[primitiveArray.length]; - for (int i = 0; i < primitiveArray.length; i++) { - boxedArray[i] = primitiveArray[i]; - } - return boxedArray; - } else if (value instanceof Object[]) { - return (Object[]) value; - } else { - throw new IllegalArgumentException("Not an array: " + value); - } - } -} diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/AnnotationsTransformation.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/AnnotationsTransformation.java index 7db70f8d21bdd..955204c767a99 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/AnnotationsTransformation.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/AnnotationsTransformation.java @@ -13,6 +13,8 @@ import org.jboss.jandex.DotName; +import io.quarkus.arc.processor.Annotations; + // this must be symmetric with AnnotationsOverlay abstract class AnnotationsTransformation implements io.quarkus.arc.processor.AnnotationsTransformer { @@ -62,7 +64,7 @@ void addAnnotation(JandexDeclaration jandexDeclaration, AnnotationInfo annotatio } void addAnnotation(JandexDeclaration jandexDeclaration, Annotation annotation) { - addAnnotation(jandexDeclaration, AnnotationsReflection.jandexAnnotation(annotation)); + addAnnotation(jandexDeclaration, Annotations.jandexAnnotation(annotation)); } private void removeMatchingAnnotations(JandexDeclaration declaration, diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/SyntheticBeanBuilderImpl.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/SyntheticBeanBuilderImpl.java index 9479173c9f31b..455bb973b653d 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/SyntheticBeanBuilderImpl.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/SyntheticBeanBuilderImpl.java @@ -13,6 +13,9 @@ import org.jboss.jandex.DotName; +import io.quarkus.arc.processor.Annotations; +import io.quarkus.arc.processor.Types; + class SyntheticBeanBuilderImpl extends SyntheticComponentBuilderBase> implements SyntheticBeanBuilder { DotName implementationClass; @@ -37,7 +40,7 @@ SyntheticBeanBuilderImpl self() { @Override public SyntheticBeanBuilder type(Class type) { - this.types.add(TypesReflection.jandexType(type)); + this.types.add(Types.jandexType(type)); return this; } @@ -69,7 +72,7 @@ public SyntheticBeanBuilder qualifier(AnnotationInfo qualifierAnnotation) { @Override public SyntheticBeanBuilder qualifier(Annotation qualifierAnnotation) { - this.qualifiers.add(AnnotationsReflection.jandexAnnotation(qualifierAnnotation)); + this.qualifiers.add(Annotations.jandexAnnotation(qualifierAnnotation)); return this; } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/SyntheticComponentBuilderBase.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/SyntheticComponentBuilderBase.java index 2623b7f47032f..ebefbea6a40f1 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/SyntheticComponentBuilderBase.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/SyntheticComponentBuilderBase.java @@ -7,6 +7,8 @@ import jakarta.enterprise.lang.model.AnnotationInfo; import jakarta.enterprise.lang.model.declarations.ClassInfo; +import io.quarkus.arc.processor.Annotations; + abstract class SyntheticComponentBuilderBase> { Map params = new HashMap<>(); @@ -102,7 +104,7 @@ public THIS withParam(String key, AnnotationInfo value) { } public THIS withParam(String key, Annotation value) { - params.put(key, AnnotationsReflection.jandexAnnotation(value)); + params.put(key, Annotations.jandexAnnotation(value)); return self(); } @@ -118,7 +120,7 @@ public THIS withParam(String key, AnnotationInfo[] value) { public THIS withParam(String key, Annotation[] value) { org.jboss.jandex.AnnotationInstance[] jandexValues = new org.jboss.jandex.AnnotationInstance[value.length]; for (int i = 0; i < value.length; i++) { - jandexValues[i] = AnnotationsReflection.jandexAnnotation(value[i]); + jandexValues[i] = Annotations.jandexAnnotation(value[i]); } params.put(key, jandexValues); return self(); diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/SyntheticComponentsImpl.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/SyntheticComponentsImpl.java index 9b763d01d7e65..8c23a45e016ad 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/SyntheticComponentsImpl.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/SyntheticComponentsImpl.java @@ -9,6 +9,8 @@ import org.jboss.jandex.DotName; +import io.quarkus.arc.processor.Types; + class SyntheticComponentsImpl implements SyntheticComponents { final List> syntheticBeans; final List> syntheticObservers; @@ -30,7 +32,7 @@ public SyntheticBeanBuilder addBean(Class implementationClass) { @Override public SyntheticObserverBuilder addObserver(Class eventType) { - org.jboss.jandex.Type jandexType = TypesReflection.jandexType(eventType); + org.jboss.jandex.Type jandexType = Types.jandexType(eventType); SyntheticObserverBuilderImpl builder = new SyntheticObserverBuilderImpl<>(extensionClass, jandexType); syntheticObservers.add(builder); return builder; diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/SyntheticObserverBuilderImpl.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/SyntheticObserverBuilderImpl.java index b6fe87a56b30a..5e245ee6ef37f 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/SyntheticObserverBuilderImpl.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/SyntheticObserverBuilderImpl.java @@ -13,6 +13,8 @@ import org.jboss.jandex.DotName; +import io.quarkus.arc.processor.Annotations; + class SyntheticObserverBuilderImpl extends SyntheticComponentBuilderBase> implements SyntheticObserverBuilder { DotName declaringClass; @@ -60,7 +62,7 @@ public SyntheticObserverBuilder qualifier(AnnotationInfo qualifierAnnotation) @Override public SyntheticObserverBuilder qualifier(Annotation qualifierAnnotation) { - this.qualifiers.add(AnnotationsReflection.jandexAnnotation(qualifierAnnotation)); + this.qualifiers.add(Annotations.jandexAnnotation(qualifierAnnotation)); return this; } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/TypesReflection.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/TypesReflection.java deleted file mode 100644 index afc9cf89dc30b..0000000000000 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/TypesReflection.java +++ /dev/null @@ -1,43 +0,0 @@ -package io.quarkus.arc.processor.bcextensions; - -import org.jboss.jandex.DotName; - -class TypesReflection { - static org.jboss.jandex.Type jandexType(Class clazz) { - if (clazz.isArray()) { - int dimensions = 1; - Class componentType = clazz.getComponentType(); - while (componentType.isArray()) { - dimensions++; - componentType = componentType.getComponentType(); - } - return org.jboss.jandex.ArrayType.create(jandexType(componentType), dimensions); - } - - if (clazz.isPrimitive()) { - if (clazz == Void.TYPE) { - return org.jboss.jandex.Type.create(DotName.createSimple("void"), org.jboss.jandex.Type.Kind.VOID); - } else if (clazz == Boolean.TYPE) { - return org.jboss.jandex.PrimitiveType.BOOLEAN; - } else if (clazz == Byte.TYPE) { - return org.jboss.jandex.PrimitiveType.BYTE; - } else if (clazz == Short.TYPE) { - return org.jboss.jandex.PrimitiveType.SHORT; - } else if (clazz == Integer.TYPE) { - return org.jboss.jandex.PrimitiveType.INT; - } else if (clazz == Long.TYPE) { - return org.jboss.jandex.PrimitiveType.LONG; - } else if (clazz == Float.TYPE) { - return org.jboss.jandex.PrimitiveType.FLOAT; - } else if (clazz == Double.TYPE) { - return org.jboss.jandex.PrimitiveType.DOUBLE; - } else if (clazz == Character.TYPE) { - return org.jboss.jandex.PrimitiveType.CHAR; - } else { - throw new IllegalArgumentException("Unknown primitive type " + clazz); - } - } - - return org.jboss.jandex.Type.create(DotName.createSimple(clazz.getName()), org.jboss.jandex.Type.Kind.CLASS); - } -} diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/HierarchyDiscovery.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/HierarchyDiscovery.java index be9e382e6a1c3..44307ceb5a805 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/HierarchyDiscovery.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/HierarchyDiscovery.java @@ -23,7 +23,7 @@ * @author Marko Luksa * @author Jozef Hartinger */ -class HierarchyDiscovery { +public final class HierarchyDiscovery { private final Map, Type> types; private final Map, Type> resolvedTypeVariables; @@ -35,11 +35,11 @@ class HierarchyDiscovery { * * @param type the type whose hierarchy will be discovered */ - HierarchyDiscovery(Type type) { + public HierarchyDiscovery(Type type) { this(type, new TypeResolver(new HashMap, Type>())); } - HierarchyDiscovery(Type type, TypeResolver resolver) { + public HierarchyDiscovery(Type type, TypeResolver resolver) { this.types = new HashMap, Type>(); this.resolver = resolver; this.resolvedTypeVariables = resolver.getResolvedTypeVariables(); diff --git a/test-framework/junit5-component/pom.xml b/test-framework/junit5-component/pom.xml new file mode 100644 index 0000000000000..e6229efc7d448 --- /dev/null +++ b/test-framework/junit5-component/pom.xml @@ -0,0 +1,51 @@ + + + 4.0.0 + + + io.quarkus + quarkus-test-framework + 999-SNAPSHOT + + + quarkus-junit5-component + Quarkus - Test Framework - JUnit 5 Component Test Framework + + + + org.junit.jupiter + junit-jupiter-api + compile + + + org.junit.jupiter + junit-jupiter-params + compile + + + org.junit.jupiter + junit-jupiter-engine + compile + + + io.quarkus.arc + arc-processor + + + io.smallrye.config + smallrye-config + + + org.mockito + mockito-core + + + org.jboss.logmanager + jboss-logmanager-embedded + test + + + + diff --git a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/ConfigBeanCreator.java b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/ConfigBeanCreator.java new file mode 100644 index 0000000000000..53ecc0deefee2 --- /dev/null +++ b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/ConfigBeanCreator.java @@ -0,0 +1,21 @@ +package io.quarkus.test.component; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; + +import io.quarkus.arc.BeanCreator; +import io.quarkus.arc.SyntheticCreationalContext; +import io.smallrye.config.SmallRyeConfig; + +public class ConfigBeanCreator implements BeanCreator { + + @Override + public Config create(SyntheticCreationalContext context) { + return getConfig(); + } + + static Config getConfig() { + return ConfigProvider.getConfig(Thread.currentThread().getContextClassLoader()).unwrap(SmallRyeConfig.class); + } + +} diff --git a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/ConfigPropertyBeanCreator.java b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/ConfigPropertyBeanCreator.java new file mode 100644 index 0000000000000..3d9a1598a9e7b --- /dev/null +++ b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/ConfigPropertyBeanCreator.java @@ -0,0 +1,65 @@ +package io.quarkus.test.component; + +import java.lang.reflect.Array; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.NoSuchElementException; + +import jakarta.enterprise.inject.spi.InjectionPoint; + +import io.quarkus.arc.BeanCreator; +import io.quarkus.arc.SyntheticCreationalContext; +import io.smallrye.config.inject.ConfigProducerUtil; + +public class ConfigPropertyBeanCreator implements BeanCreator { + + @Override + public Object create(SyntheticCreationalContext context) { + InjectionPoint injectionPoint = context.getInjectedReference(InjectionPoint.class); + if (Boolean.TRUE.equals(context.getParams().get("useDefaultConfigProperties"))) { + try { + return ConfigProducerUtil.getValue(injectionPoint, ConfigBeanCreator.getConfig()); + } catch (NoSuchElementException e) { + Class rawType = getRawType(injectionPoint.getType()); + if (rawType == null) { + throw new IllegalStateException("Unable to get the raw type for: " + injectionPoint.getType()); + } + if (rawType.isPrimitive()) { + if (rawType == boolean.class) { + return false; + } else if (rawType == char.class) { + return Character.MIN_VALUE; + } else { + return 0; + } + } + return null; + } + } else { + return ConfigProducerUtil.getValue(injectionPoint, ConfigBeanCreator.getConfig()); + } + + } + + @SuppressWarnings("unchecked") + private static Class getRawType(Type type) { + if (type instanceof Class) { + return (Class) type; + } + if (type instanceof ParameterizedType) { + if (((ParameterizedType) type).getRawType() instanceof Class) { + return (Class) ((ParameterizedType) type).getRawType(); + } + } + if (type instanceof GenericArrayType) { + GenericArrayType genericArrayType = (GenericArrayType) type; + Class rawType = getRawType(genericArrayType.getGenericComponentType()); + if (rawType != null) { + return (Class) Array.newInstance(rawType, 0).getClass(); + } + } + return null; + } + +} diff --git a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/MockBeanConfiguratorImpl.java b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/MockBeanConfiguratorImpl.java new file mode 100644 index 0000000000000..be99dcba547cc --- /dev/null +++ b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/MockBeanConfiguratorImpl.java @@ -0,0 +1,189 @@ +package io.quarkus.test.component; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.context.RequestScoped; +import jakarta.enterprise.inject.Default; +import jakarta.inject.Qualifier; +import jakarta.inject.Singleton; + +import org.jboss.jandex.AnnotationInstance; +import org.mockito.Mockito; + +import io.quarkus.arc.SyntheticCreationalContext; +import io.quarkus.arc.impl.HierarchyDiscovery; +import io.quarkus.arc.processor.Annotations; +import io.quarkus.arc.processor.BeanResolver; +import io.quarkus.arc.processor.InjectionPointInfo; +import io.quarkus.arc.processor.Types; +import io.quarkus.test.component.QuarkusComponentTest.MockBeanConfigurator; + +class MockBeanConfiguratorImpl implements MockBeanConfigurator { + + final QuarkusComponentTest test; + final Class beanClass; + Set types; + Set qualifiers; + Class scope; + boolean alternative = false; + Integer priority; + String name; + boolean defaultBean = false; + + Function, T> create; + + Set jandexTypes; + Set jandexQualifiers; + + public MockBeanConfiguratorImpl(QuarkusComponentTest test, Class beanClass) { + this.test = test; + this.beanClass = beanClass; + this.types = new HierarchyDiscovery(beanClass).getTypeClosure(); + + // TODO autodetect all bean attributes + if (beanClass.isAnnotationPresent(Singleton.class)) { + this.scope = Singleton.class; + } else if (beanClass.isAnnotationPresent(ApplicationScoped.class)) { + this.scope = ApplicationScoped.class; + } else if (beanClass.isAnnotationPresent(RequestScoped.class)) { + this.scope = RequestScoped.class; + } else { + this.scope = Dependent.class; + } + this.qualifiers = new HashSet<>(); + for (Annotation annotation : beanClass.getAnnotations()) { + if (annotation.annotationType().isAnnotationPresent(Qualifier.class)) { + this.qualifiers.add(annotation); + } + } + if (this.qualifiers.isEmpty()) { + this.qualifiers.add(Default.Literal.INSTANCE); + } + } + + @Override + public MockBeanConfigurator types(Class... types) { + this.types = Set.of(types); + return this; + } + + @Override + public MockBeanConfigurator types(Type types) { + this.types = Set.of(types); + return this; + } + + @Override + public MockBeanConfigurator qualifiers(Annotation... qualifiers) { + this.qualifiers = Set.of(qualifiers); + return this; + } + + @Override + public MockBeanConfigurator scope(Class scope) { + this.scope = scope; + return this; + } + + @Override + public MockBeanConfigurator name(String name) { + this.name = name; + return this; + } + + @Override + public MockBeanConfigurator alternative(boolean alternative) { + this.alternative = alternative; + return this; + } + + @Override + public MockBeanConfigurator priority(int priority) { + this.priority = priority; + return this; + } + + @Override + public MockBeanConfigurator defaultBean(boolean defaultBean) { + this.defaultBean = defaultBean; + return this; + } + + @Override + public QuarkusComponentTest create(Function, T> create) { + this.create = create; + return register(); + } + + @Override + public QuarkusComponentTest createMockitoMock() { + this.create = c -> QuarkusComponentTest.cast(Mockito.mock(beanClass)); + return register(); + } + + @Override + public QuarkusComponentTest createMockitoMock(Consumer mockInitializer) { + this.create = c -> { + T mock = QuarkusComponentTest.cast(Mockito.mock(beanClass)); + mockInitializer.accept(mock); + return mock; + }; + return register(); + } + + public QuarkusComponentTest register() { + test.registerMockBean(this); + return test; + } + + boolean matches(BeanResolver beanResolver, InjectionPointInfo injectionPoint) { + return matchesType(injectionPoint.getRequiredType(), beanResolver) + && hasQualifiers(injectionPoint.getRequiredQualifiers(), beanResolver); + } + + boolean matchesType(org.jboss.jandex.Type requiredType, BeanResolver beanResolver) { + for (org.jboss.jandex.Type beanType : jandexTypes()) { + if (beanResolver.matches(requiredType, beanType)) { + return true; + } + } + return false; + } + + boolean hasQualifiers(Set requiredQualifiers, BeanResolver beanResolver) { + for (AnnotationInstance qualifier : requiredQualifiers) { + if (!beanResolver.hasQualifier(jandexQualifiers(), qualifier)) { + return false; + } + } + return true; + } + + Set jandexTypes() { + if (jandexTypes == null) { + jandexTypes = new HashSet<>(); + for (Type type : types) { + jandexTypes.add(Types.jandexType(type)); + } + } + return jandexTypes; + } + + Set jandexQualifiers() { + if (jandexQualifiers == null) { + jandexQualifiers = new HashSet<>(); + for (Annotation qualifier : qualifiers) { + jandexQualifiers.add(Annotations.jandexAnnotation(qualifier)); + } + } + return jandexQualifiers; + } + +} diff --git a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/MockBeanCreator.java b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/MockBeanCreator.java new file mode 100644 index 0000000000000..cd40fdd5c48f2 --- /dev/null +++ b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/MockBeanCreator.java @@ -0,0 +1,39 @@ +package io.quarkus.test.component; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +import org.jboss.logging.Logger; +import org.mockito.Mockito; + +import io.quarkus.arc.BeanCreator; +import io.quarkus.arc.SyntheticCreationalContext; + +public class MockBeanCreator implements BeanCreator { + + private static final Logger LOG = Logger.getLogger(MockBeanCreator.class); + + private static final Map, ?>> createFunctions = new HashMap<>(); + + @Override + public Object create(SyntheticCreationalContext context) { + Object createKey = context.getParams().get("createKey"); + if (createKey != null) { + Function, ?> createFun = createFunctions.get(createKey.toString()); + if (createFun != null) { + return createFun.apply(context); + } else { + throw new IllegalStateException("Create function not found: " + createKey); + } + } + Class implementationClass = (Class) context.getParams().get("implementationClass"); + LOG.debugf("Mock created for: %s", implementationClass); + return Mockito.mock(implementationClass); + } + + static void registerCreate(String key, Function, ?> create) { + createFunctions.put(key, create); + } + +} diff --git a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTest.java b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTest.java new file mode 100644 index 0000000000000..a6166cc248089 --- /dev/null +++ b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTest.java @@ -0,0 +1,737 @@ +package io.quarkus.test.component; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; + +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.enterprise.inject.spi.InjectionPoint; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.config.spi.ConfigProviderResolver; +import org.eclipse.microprofile.config.spi.ConfigSource; +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.ClassType; +import org.jboss.jandex.DotName; +import org.jboss.jandex.IndexView; +import org.jboss.jandex.Indexer; +import org.jboss.jandex.Type; +import org.jboss.jandex.Type.Kind; +import org.jboss.logging.Logger; +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.TestInstancePostProcessor; +import org.junit.jupiter.api.extension.TestInstancePreDestroyCallback; + +import io.quarkus.arc.Arc; +import io.quarkus.arc.ArcContainer; +import io.quarkus.arc.ComponentsProvider; +import io.quarkus.arc.InstanceHandle; +import io.quarkus.arc.SyntheticCreationalContext; +import io.quarkus.arc.processor.Annotations; +import io.quarkus.arc.processor.AnnotationsTransformer; +import io.quarkus.arc.processor.BeanArchives; +import io.quarkus.arc.processor.BeanConfigurator; +import io.quarkus.arc.processor.BeanDeployment; +import io.quarkus.arc.processor.BeanDeploymentValidator.ValidationContext; +import io.quarkus.arc.processor.BeanInfo; +import io.quarkus.arc.processor.BeanProcessor; +import io.quarkus.arc.processor.BeanRegistrar; +import io.quarkus.arc.processor.BeanResolver; +import io.quarkus.arc.processor.Beans; +import io.quarkus.arc.processor.BuildExtension.Key; +import io.quarkus.arc.processor.BuiltinBean; +import io.quarkus.arc.processor.BytecodeTransformer; +import io.quarkus.arc.processor.ContextRegistrar; +import io.quarkus.arc.processor.InjectionPointInfo; +import io.quarkus.arc.processor.InjectionPointInfo.TypeAndQualifiers; +import io.quarkus.arc.processor.ResourceOutput; +import io.quarkus.arc.processor.Types; +import io.smallrye.config.SmallRyeConfig; +import io.smallrye.config.SmallRyeConfigBuilder; +import io.smallrye.config.SmallRyeConfigProviderResolver; + +/** + * JUnit extension that can be used to test Quarkus components, aka the CDI beans. + * + *

Lifecycle

+ * + * The CDI container is started and a dedicated SmallRyeConfig is registered during the {@code before all} test phase. The + * container is stopped and the config is released during the {@code after all} test phase. The fields annotated with + * {@code jakarta.inject.Inject} are injected after a test instance is created and unset before a test instance is destroyed. + * Moreover, the dependent beans injected into fields annotated with {@code jakarta.inject.Inject} are correctly destroyed + * before a test instance is destroyed. Finally, the CDI request context is activated and terminated per + * each test method. + * + *

Auto Mocking Unsatisfied Dependencies

+ * + * Unlike in regular CDI environments the test does not fail if a component injects an unsatisfied dependency. Instead, a + * synthetic bean is registered automatically for each combination of required type and qualifiers of an injection point that + * resolves to an unsatisfied dependency. The bean has the {@link Singleton} scope so it's shared across all injection points + * with the same required type and qualifiers. The injected reference is an unconfigured Mockito mock. You can inject the mock + * in your test and leverage the Mockito API to configure the behavior. + * + *

Custom Mocks For Unsatisfied Dependencies

+ * + * Sometimes you need the full control over the bean attributes and maybe even configure the default mock behavior. You can use + * the mock configurator API via the {@link #mock(Class)} method. + * + *

Configuration

+ * + * A dedicated {@link SmallRyeConfig} is registered during the {@code before all} test phase. Moreover, it's possible to set the + * configuration properties via the {@link #configProperty(String, String)} method. If you only need to use the default values + * for missing config properties, then the {@link #useDefaultConfigProperties()} + * might come in useful. + * + */ +public class QuarkusComponentTest + implements BeforeAllCallback, AfterAllCallback, BeforeEachCallback, AfterEachCallback, TestInstancePostProcessor, + TestInstancePreDestroyCallback, ConfigSource { + + private static final Logger LOG = Logger.getLogger(QuarkusComponentTest.class); + + private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace + .create(QuarkusComponentTest.class); + + // Strings used as keys in ExtensionContext.Store + private static final String KEY_OLD_TCCL = "oldTccl"; + private static final String KEY_OLD_CONFIG_PROVIDER_RESOLVER = "oldConfigProviderResolver"; + private static final String KEY_GENERATED_RESOURCES = "generatedResources"; + private static final String KEY_DEPENDENT_INJECTED_FIELDS = "dependentInjectedFields"; + private static final String KEY_CONFIG = "config"; + + private static final String TARGET_TEST_CLASSES = "target/test-classes"; + + private final Map configProperties; + private final List> componentClasses; + private final List> mockConfigurators; + private final AtomicBoolean useDefaultConfigProperties = new AtomicBoolean(); + + /** + * + * @param componentClasses The set of components under test + */ + public QuarkusComponentTest(Class... componentClasses) { + this.componentClasses = List.of(componentClasses); + this.configProperties = new HashMap<>(); + this.mockConfigurators = new ArrayList<>(); + } + + /** + * Configure a new mock of a bean. + *

+ * Note that a mock is created automatically for all unsatisfied dependencies in the test. This API provides full control + * over the bean attributes. The default values are derived from the bean class. + * + * @param beanClass + * @return a new mock bean configurator + * @see MockBeanConfigurator#create(Function) + */ + public MockBeanConfigurator mock(Class beanClass) { + return new MockBeanConfiguratorImpl<>(this, beanClass); + } + + /** + * Set a configuration property for the test. + * + * @param key + * @param value + * @return the extension + */ + public QuarkusComponentTest configProperty(String key, String value) { + this.configProperties.put(key, value); + return this; + } + + /** + * Use the default values for missing config properties. By default, if missing config property results in a test failure. + *

+ * For primitives the default values as defined in the JLS are used. For any other type {@code null} is injected. + * + * @return the extension + */ + public QuarkusComponentTest useDefaultConfigProperties() { + this.useDefaultConfigProperties.set(true); + return this; + } + + @Override + public void postProcessTestInstance(Object testInstance, ExtensionContext context) throws Exception { + long start = System.nanoTime(); + + // Inject test class fields + ArcContainer container = Arc.container(); + BeanManager beanManager = container.beanManager(); + + List> dependentInjectedFields = new ArrayList<>(); + Class testClass = context.getRequiredTestClass(); + for (Field field : testClass.getDeclaredFields()) { + if (field.isAnnotationPresent(Inject.class)) { + InstanceHandle handle = container.instance(field.getGenericType(), getQualifiers(field, beanManager)); + field.setAccessible(true); + field.set(testInstance, handle.get()); + if (handle.getBean() != null && handle.getBean().getScope().equals(Dependent.class)) { + dependentInjectedFields.add(handle); + } + } + } + context.getRoot().getStore(NAMESPACE).put(KEY_DEPENDENT_INJECTED_FIELDS, dependentInjectedFields); + LOG.debugf("postProcessTestInstance: %s ms", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)); + } + + @SuppressWarnings("unchecked") + @Override + public void preDestroyTestInstance(ExtensionContext context) throws Exception { + long start = System.nanoTime(); + + for (InstanceHandle handle : (List>) context.getRoot().getStore(NAMESPACE) + .get(KEY_DEPENDENT_INJECTED_FIELDS, List.class)) { + try { + handle.destroy(); + } catch (Exception e) { + LOG.errorf(e, "Unable to destroy the injected %s", handle.getBean()); + } + } + + // Unset injected fields + Class testClass = context.getRequiredTestClass(); + for (Field field : testClass.getDeclaredFields()) { + if (field.isAnnotationPresent(Inject.class)) { + field.setAccessible(true); + field.set(context.getRequiredTestInstance(), null); + } + } + + LOG.debugf("preDestroyTestInstance: %s ms", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)); + } + + @Override + public void beforeAll(ExtensionContext context) throws Exception { + long start = System.nanoTime(); + + ClassLoader oldTccl = initArcContainer(context); + context.getRoot().getStore(NAMESPACE).put(KEY_OLD_TCCL, oldTccl); + + ConfigProviderResolver oldConfigProviderResolver = ConfigProviderResolver.instance(); + context.getRoot().getStore(NAMESPACE).put(KEY_OLD_CONFIG_PROVIDER_RESOLVER, oldConfigProviderResolver); + + SmallRyeConfigProviderResolver smallRyeConfigProviderResolver = new SmallRyeConfigProviderResolver(); + ConfigProviderResolver.setInstance(smallRyeConfigProviderResolver); + + // TCCL is now the QuarkusComponentTestClassLoader set during initialization + ClassLoader tccl = Thread.currentThread().getContextClassLoader(); + SmallRyeConfig config = new SmallRyeConfigBuilder().forClassLoader(tccl) + //.addDiscoveredConverters() + .addDefaultInterceptors() + .addDefaultSources() + .withSources(this) + .build(); + smallRyeConfigProviderResolver.registerConfig(config, tccl); + context.getRoot().getStore(NAMESPACE).put(KEY_CONFIG, config); + + LOG.debugf("beforeAll: %s ms", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)); + } + + @Override + public void afterAll(ExtensionContext context) throws Exception { + long start = System.nanoTime(); + + ClassLoader oldTccl = context.getRoot().getStore(NAMESPACE).get(KEY_OLD_TCCL, ClassLoader.class); + Thread.currentThread().setContextClassLoader(oldTccl); + + Arc.shutdown(); + + SmallRyeConfig config = context.getRoot().getStore(NAMESPACE).get(KEY_CONFIG, SmallRyeConfig.class); + ConfigProviderResolver.instance().releaseConfig(config); + ConfigProviderResolver + .setInstance(context.getRoot().getStore(NAMESPACE).get(KEY_OLD_CONFIG_PROVIDER_RESOLVER, + ConfigProviderResolver.class)); + + @SuppressWarnings("unchecked") + Set generatedResources = context.getRoot().getStore(NAMESPACE).get(KEY_GENERATED_RESOURCES, Set.class); + for (Path path : generatedResources) { + try { + LOG.debugf("Delete generated %s", path); + Files.deleteIfExists(path); + } catch (IOException e) { + LOG.errorf("Unable to delete the generated resource %s: ", path, e.getMessage()); + } + } + + LOG.debugf("afterAll: %s ms", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)); + } + + @Override + public void beforeEach(ExtensionContext context) throws Exception { + long start = System.nanoTime(); + + // Activate the request context + ArcContainer container = Arc.container(); + container.requestContext().activate(); + + LOG.debugf("beforeEach: %s ms", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)); + } + + @Override + public void afterEach(ExtensionContext context) throws Exception { + long start = System.nanoTime(); + + // Terminate the request context + ArcContainer container = Arc.container(); + container.requestContext().terminate(); + + LOG.debugf("afterEach: %s ms", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)); + } + + @Override + public Set getPropertyNames() { + return configProperties.keySet(); + } + + @Override + public String getValue(String propertyName) { + return configProperties.get(propertyName); + } + + @Override + public String getName() { + return QuarkusComponentTest.class.getName(); + } + + void registerMockBean(MockBeanConfiguratorImpl mock) { + this.mockConfigurators.add(mock); + } + + private BeanRegistrar registrarForMock(MockBeanConfiguratorImpl mock) { + return new BeanRegistrar() { + + @Override + public void register(RegistrationContext context) { + BeanConfigurator configurator = context.configure(mock.beanClass); + configurator.scope(mock.scope); + mock.jandexTypes().forEach(configurator::addType); + mock.jandexQualifiers().forEach(configurator::addQualifier); + if (mock.name != null) { + configurator.name(mock.name); + } + configurator.alternative(mock.alternative); + if (mock.priority != null) { + configurator.priority(mock.priority); + } + if (mock.defaultBean) { + configurator.defaultBean(); + } + String key = UUID.randomUUID().toString(); + MockBeanCreator.registerCreate(key, cast(mock.create)); + configurator.creator(MockBeanCreator.class).param("createKey", key).done(); + } + }; + } + + private static Annotation[] getQualifiers(Field field, BeanManager beanManager) { + List ret = new ArrayList<>(); + Annotation[] annotations = field.getDeclaredAnnotations(); + for (Annotation fieldAnnotation : annotations) { + if (beanManager.isQualifier(fieldAnnotation.annotationType())) { + ret.add(fieldAnnotation); + } + } + return ret.toArray(new Annotation[0]); + } + + private static Set getQualifiers(Field field, Collection qualifiers) { + Set ret = new HashSet<>(); + Annotation[] fieldAnnotations = field.getDeclaredAnnotations(); + for (Annotation annotation : fieldAnnotations) { + if (qualifiers.contains(DotName.createSimple(annotation.annotationType()))) { + ret.add(Annotations.jandexAnnotation(annotation)); + } + } + return ret; + } + + private ClassLoader initArcContainer(ExtensionContext context) { + Class testClass = context.getRequiredTestClass(); + // Collect all test class injection points to define a bean removal exclusion + List testClassInjectionPoints = new ArrayList<>(); + for (Field field : testClass.getDeclaredFields()) { + if (field.isAnnotationPresent(Inject.class)) { + testClassInjectionPoints.add(field); + } + } + + // Make sure Arc is down + Arc.shutdown(); + + // Build index + IndexView index; + try { + Indexer indexer = new Indexer(); + for (Class componentClass : componentClasses) { + // Make sure that component hierarchy and all annotations present are indexed + indexComponentClass(indexer, componentClass); + } + indexer.indexClass(ConfigProperty.class); + index = BeanArchives.buildImmutableBeanArchiveIndex(indexer.complete()); + } catch (IOException e) { + throw new IllegalStateException("Failed to create index", e); + } + + IndexView computingIndex = BeanArchives.buildComputingBeanArchiveIndex(getClass().getClassLoader(), + new ConcurrentHashMap<>(), index); + + ClassLoader old = Thread.currentThread().getContextClassLoader(); + + try { + + String testPath = testClass.getClassLoader().getResource(testClass.getName().replace(".", "/") + ".class") + .getFile(); + int targetClassesIndex = testPath.indexOf(TARGET_TEST_CLASSES); + // NOTE: continuous testing is not supported at the moment + if (targetClassesIndex == -1) { + throw new IllegalStateException("Invalid test path: " + testPath); + } + String testClassesRootPath = testPath.substring(0, targetClassesIndex); + File generatedSourcesDirectory = new File("target/generated-arc-sources"); + File testOutputDirectory = new File(testClassesRootPath + TARGET_TEST_CLASSES); + File componentsProviderFile = new File(generatedSourcesDirectory + "/" + nameToPath(testClass.getPackage() + .getName()), ComponentsProvider.class.getSimpleName()); + + // These are populated after BeanProcessor.registerCustomContexts() is called + List qualifiers = new ArrayList<>(); + AtomicReference beanResolver = new AtomicReference<>(); + + BeanProcessor.Builder builder = BeanProcessor.builder() + .setName(testClass.getName().replace('.', '_')) + .addRemovalExclusion(b -> { + // Do not remove beans injected in the test class + for (Field injectionPoint : testClassInjectionPoints) { + if (beanResolver.get().matches(b, Types.jandexType(injectionPoint.getGenericType()), + getQualifiers(injectionPoint, qualifiers))) { + return true; + } + } + return false; + }) + .setImmutableBeanArchiveIndex(index) + .setComputingBeanArchiveIndex(computingIndex) + .setRemoveUnusedBeans(true); + + // We need collect all generated resources so that we can remove them after the test + // NOTE: previously we kept the generated framework classes (to speedup subsequent test runs) but that breaks the existing @QuarkusTests + Set generatedResources = new HashSet<>(); + + builder.setOutput(new ResourceOutput() { + + @Override + public void writeResource(Resource resource) throws IOException { + switch (resource.getType()) { + case JAVA_CLASS: + generatedResources.add(resource.writeTo(testOutputDirectory).toPath()); + break; + case SERVICE_PROVIDER: + if (resource.getName() + .endsWith(ComponentsProvider.class.getName())) { + componentsProviderFile.getParentFile() + .mkdirs(); + try (FileOutputStream out = new FileOutputStream(componentsProviderFile)) { + out.write(resource.getData()); + } + } + break; + default: + throw new IllegalArgumentException(); + } + } + }); + + context.getRoot().getStore(NAMESPACE).put(KEY_GENERATED_RESOURCES, generatedResources); + + builder.addAnnotationTransformer(AnnotationsTransformer.appliedToField().whenContainsAny(qualifiers) + .whenContainsNone(DotName.createSimple(Inject.class)).thenTransform(t -> t.add(Inject.class))); + + // Register: + // 1) Dummy mock beans for all unsatisfied injection points + // 2) Synthetic beans for Config and @ConfigProperty injection points + builder.addBeanRegistrar(new BeanRegistrar() { + + @Override + public void register(RegistrationContext context) { + long start = System.nanoTime(); + List beans = context.beans().collect(); + BeanDeployment beanDeployment = context.get(Key.DEPLOYMENT); + List unsatisfied = new ArrayList<>(); + boolean configInjectionPoint = false; + Set configPropertyInjectionPoints = new HashSet<>(); + DotName configDotName = DotName.createSimple(Config.class); + DotName configPropertyDotName = DotName.createSimple(ConfigProperty.class); + + // Analyze injection points + // - find Config and @ConfigProperty injection points + // - find unsatisfied injection points + for (InjectionPointInfo injectionPoint : context.getInjectionPoints()) { + BuiltinBean builtin = BuiltinBean.resolve(injectionPoint); + if (builtin != null && builtin != BuiltinBean.INSTANCE) { + continue; + } + if (injectionPoint.getRequiredType().name().equals(configDotName)) { + configInjectionPoint = true; + continue; + } + if (injectionPoint.getRequiredQualifier(configPropertyDotName) != null) { + configPropertyInjectionPoints.add(new TypeAndQualifiers(injectionPoint.getRequiredType(), + injectionPoint.getRequiredQualifiers())); + continue; + } + if (isSatisfied(injectionPoint, beans, beanDeployment)) { + continue; + } + unsatisfied.add(injectionPoint); + LOG.debugf("Unsatisfied injection point found: %s", injectionPoint.getTargetInfo()); + } + + Set mockableInjectionPoints = new HashSet<>(); + + for (Iterator it = unsatisfied.iterator(); it.hasNext();) { + InjectionPointInfo injectionPoint = it.next(); + + Type requiredType = injectionPoint.getRequiredType(); + if (requiredType.kind() == Kind.PRIMITIVE || requiredType.kind() == Kind.ARRAY) { + continue; + } + it.remove(); + mockableInjectionPoints.add(new TypeAndQualifiers(injectionPoint.getRequiredType(), + injectionPoint.getRequiredQualifiers())); + } + + if (!unsatisfied.isEmpty()) { + throw new IllegalStateException("Found unmockable unsatisfied injection points:\n\t - " + unsatisfied + .stream().map(InjectionPointInfo::getTargetInfo).collect(Collectors.joining("\n\t - "))); + } + + for (TypeAndQualifiers typeAndQualifiers : mockableInjectionPoints) { + ClassInfo implementationClass = computingIndex.getClassByName(typeAndQualifiers.type.name()); + BeanConfigurator configurator = context.configure(implementationClass.name()) + .scope(Singleton.class) + .addType(typeAndQualifiers.type); + typeAndQualifiers.qualifiers.forEach(configurator::addQualifier); + configurator.param("implementationClass", implementationClass) + .creator(MockBeanCreator.class) + .defaultBean() + .identifier("dummy") + .done(); + } + + if (configInjectionPoint) { + context.configure(Config.class) + .addType(Config.class) + .creator(ConfigBeanCreator.class) + .done(); + } + + if (!configPropertyInjectionPoints.isEmpty()) { + BeanConfigurator configPropertyConfigurator = context.configure(Object.class) + .identifier("configProperty") + .addQualifier(ConfigProperty.class) + .param("useDefaultConfigProperties", useDefaultConfigProperties.get()) + .addInjectionPoint(ClassType.create(InjectionPoint.class)) + .creator(ConfigPropertyBeanCreator.class); + for (TypeAndQualifiers configPropertyInjectionPoint : configPropertyInjectionPoints) { + configPropertyConfigurator.addType(configPropertyInjectionPoint.type); + } + configPropertyConfigurator.done(); + } + + LOG.debugf("Test injection points analyzed in %s ms [found: %s, mocked: %s]", + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start), context.getInjectionPoints().size(), + mockableInjectionPoints.size()); + } + }); + + // Register mock beans + for (MockBeanConfiguratorImpl mockConfigurator : mockConfigurators) { + builder.addBeanRegistrar(registrarForMock(mockConfigurator)); + } + + // Process the deployment + BeanProcessor beanProcessor = builder.build(); + try { + Consumer unsupportedBytecodeTransformer = new Consumer() { + @Override + public void accept(BytecodeTransformer transformer) { + throw new UnsupportedOperationException(); + } + }; + // Populate the list of qualifiers used to simulate quarkus auto injection + ContextRegistrar.RegistrationContext registrationContext = beanProcessor.registerCustomContexts(); + qualifiers.addAll(registrationContext.get(Key.QUALIFIERS).keySet()); + beanResolver.set(registrationContext.get(Key.DEPLOYMENT).getBeanResolver()); + beanProcessor.registerScopes(); + beanProcessor.registerBeans(); + beanProcessor.getBeanDeployment().initBeanByTypeMap(); + beanProcessor.registerSyntheticObservers(); + beanProcessor.initialize(unsupportedBytecodeTransformer, Collections.emptyList()); + ValidationContext validationContext = beanProcessor.validate(unsupportedBytecodeTransformer); + beanProcessor.processValidationErrors(validationContext); + // Generate resources in parallel + ExecutorService executor = Executors.newCachedThreadPool(); + beanProcessor.generateResources(null, new HashSet<>(), unsupportedBytecodeTransformer, true, executor); + executor.shutdown(); + } catch (IOException e) { + throw new IllegalStateException("Error generating resources", e); + } + + // Use a custom ClassLoader to load the generated ComponentsProvider file + QuarkusComponentTestClassLoader testClassLoader = new QuarkusComponentTestClassLoader(old, componentsProviderFile, + null); + Thread.currentThread().setContextClassLoader(testClassLoader); + + // Now we are ready to initialize Arc + Arc.initialize(); + + } catch (Throwable e) { + if (e instanceof RuntimeException) { + throw (RuntimeException) e; + } else { + throw new RuntimeException(e); + } + } + return old; + } + + private void indexComponentClass(Indexer indexer, Class componentClass) { + try { + while (componentClass != null) { + indexer.indexClass(componentClass); + for (Annotation annotation : componentClass.getAnnotations()) { + indexer.indexClass(annotation.annotationType()); + } + for (Field field : componentClass.getDeclaredFields()) { + indexAnnotatedElement(indexer, field); + } + for (Method method : componentClass.getDeclaredMethods()) { + indexAnnotatedElement(indexer, method); + for (Parameter param : method.getParameters()) { + indexAnnotatedElement(indexer, param); + } + } + for (Class iface : componentClass.getInterfaces()) { + indexComponentClass(indexer, iface); + } + componentClass = componentClass.getSuperclass(); + } + } catch (IOException e) { + throw new IllegalStateException("Failed to index:" + componentClass, e); + } + } + + private void indexAnnotatedElement(Indexer indexer, AnnotatedElement element) throws IOException { + for (Annotation annotation : element.getAnnotations()) { + indexer.indexClass(annotation.annotationType()); + } + } + + private boolean isSatisfied(InjectionPointInfo injectionPoint, Iterable beans, BeanDeployment beanDeployment) { + for (BeanInfo bean : beans) { + if (Beans.matches(bean, injectionPoint.getRequiredType(), injectionPoint.getRequiredQualifiers())) { + LOG.debugf("Injection point %s satisfied by %s", injectionPoint.getTargetInfo(), + bean.toString()); + return true; + } + } + for (MockBeanConfiguratorImpl mock : mockConfigurators) { + if (mock.matches(beanDeployment.getBeanResolver(), injectionPoint)) { + LOG.debugf("Injection point %s satisfied by %s", injectionPoint.getTargetInfo(), + mock); + return true; + } + } + return false; + } + + private String nameToPath(String packName) { + return packName.replace('.', '/'); + } + + @SuppressWarnings("unchecked") + static T cast(Object obj) { + return (T) obj; + } + + public interface MockBeanConfigurator { + + MockBeanConfigurator types(Class... types); + + MockBeanConfigurator types(java.lang.reflect.Type types); + + MockBeanConfigurator qualifiers(Annotation... qualifiers); + + MockBeanConfigurator scope(Class scope); + + MockBeanConfigurator name(String name); + + MockBeanConfigurator alternative(boolean alternative); + + MockBeanConfigurator priority(int priority); + + MockBeanConfigurator defaultBean(boolean defaultBean); + + /** + * Set the function used to create a new bean instance and register this configurator. + * + * @param create + * @return the test extension + */ + QuarkusComponentTest create(Function, T> create); + + /** + * A Mockito mock object created from the bean class is used as a bean instance. + * + * @return the test extension + */ + QuarkusComponentTest createMockitoMock(); + + /** + * A Mockito mock object created from the bean class is used as a bean instance. + * + * @return the test extension + */ + QuarkusComponentTest createMockitoMock(Consumer mockInitializer); + + } + +} diff --git a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestClassLoader.java b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestClassLoader.java new file mode 100644 index 0000000000000..211be976cafbf --- /dev/null +++ b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestClassLoader.java @@ -0,0 +1,39 @@ +package io.quarkus.test.component; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.Collections; +import java.util.Enumeration; +import java.util.Objects; + +import io.quarkus.arc.ComponentsProvider; +import io.quarkus.arc.ResourceReferenceProvider; + +class QuarkusComponentTestClassLoader extends ClassLoader { + + private final File componentsProviderFile; + private final File resourceReferenceProviderFile; + + public QuarkusComponentTestClassLoader(ClassLoader parent, File componentsProviderFile, + File resourceReferenceProviderFile) { + super(parent); + this.componentsProviderFile = Objects.requireNonNull(componentsProviderFile); + this.resourceReferenceProviderFile = resourceReferenceProviderFile; + } + + @Override + public Enumeration getResources(String name) throws IOException { + if (("META-INF/services/" + ComponentsProvider.class.getName()).equals(name)) { + // return URL that points to the correct components provider + return Collections.enumeration(Collections.singleton(componentsProviderFile.toURI() + .toURL())); + } else if (resourceReferenceProviderFile != null + && ("META-INF/services/" + ResourceReferenceProvider.class.getName()).equals(name)) { + return Collections.enumeration(Collections.singleton(resourceReferenceProviderFile.toURI() + .toURL())); + } + return super.getResources(name); + } + +} diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/DependencyMockingTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/DependencyMockingTest.java new file mode 100644 index 0000000000000..c0d82b6e26aae --- /dev/null +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/DependencyMockingTest.java @@ -0,0 +1,39 @@ +package io.quarkus.test.component; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.Mockito; + +import io.quarkus.test.component.beans.Charlie; +import io.quarkus.test.component.beans.MyComponent; + +public class DependencyMockingTest { + + @RegisterExtension + static final QuarkusComponentTest test = new QuarkusComponentTest(MyComponent.class) + // this config property is injected into MyComponent and the value is used in the ping() method + .configProperty("foo", "BAR"); + + @Inject + MyComponent myComponent; + + @Inject + Charlie charlie; + + @Test + public void testPing1() { + Mockito.when(charlie.ping()).thenReturn("foo"); + assertEquals("foo and BAR", myComponent.ping()); + } + + @Test + public void testPing2() { + Mockito.when(charlie.ping()).thenReturn("baz"); + assertEquals("baz and BAR", myComponent.ping()); + } + +} diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/MockConfiguratorTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/MockConfiguratorTest.java new file mode 100644 index 0000000000000..37703ca6370e1 --- /dev/null +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/MockConfiguratorTest.java @@ -0,0 +1,40 @@ +package io.quarkus.test.component; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.Mockito; + +import io.quarkus.test.component.beans.Charlie; +import io.quarkus.test.component.beans.MyComponent; + +public class MockConfiguratorTest { + + @RegisterExtension + static final QuarkusComponentTest test = new QuarkusComponentTest(MyComponent.class) + .mock(Charlie.class).createMockitoMock(charlie -> { + Mockito.when(charlie.pong()).thenReturn("bar"); + }) + .configProperty("foo", "BAR"); + + @Inject + MyComponent myComponent; + + @Inject + Charlie charlie; + + @Test + public void testComponent() { + when(charlie.ping()).thenReturn("foo"); + assertEquals("foo and BAR", myComponent.ping()); + assertEquals("bar and BAR", myComponent.pong()); + + when(charlie.ping()).thenReturn("baz"); + assertEquals("baz and BAR", myComponent.ping()); + } + +} diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/MockNotSharedForClassHierarchyTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/MockNotSharedForClassHierarchyTest.java new file mode 100644 index 0000000000000..6a97a030d2700 --- /dev/null +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/MockNotSharedForClassHierarchyTest.java @@ -0,0 +1,84 @@ +package io.quarkus.test.component; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.Mockito; + +public class MockNotSharedForClassHierarchyTest { + + @RegisterExtension + static final QuarkusComponentTest test = new QuarkusComponentTest(Component.class); + + @Inject + Component component; + + @Inject + Alpha alpha; + + @Test + public void testMock() { + Mockito.when(alpha.ping()).thenReturn(42); + Mockito.when(component.baz.ping()).thenReturn(1); + assertEquals(42, component.alpha.ping()); + assertEquals(0, component.bar.ping()); + assertEquals(0, component.foo.ping()); + assertEquals(1, component.baz.ping()); + assertTrue(component.alpha != component.bar); + assertTrue(component.bar != component.baz); + assertTrue(component.baz != component.foo); + } + + @Singleton + static class Component { + + @Inject + Alpha alpha; + + @Inject + Bar bar; + + @Inject + Foo foo; + + @Inject + Baz baz; + + } + + @Singleton + static class Foo extends Bar { + + @Override + public int ping() { + return 15; + } + + } + + static class Bar extends Baz { + + @Override + public int ping() { + return 10; + } + + } + + static class Baz implements Alpha { + + } + + interface Alpha { + + default int ping() { + return 5; + } + + } +} diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/MockSharedForClassHierarchyTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/MockSharedForClassHierarchyTest.java new file mode 100644 index 0000000000000..23b996c8f1bb3 --- /dev/null +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/MockSharedForClassHierarchyTest.java @@ -0,0 +1,88 @@ +package io.quarkus.test.component; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.Mockito; + +public class MockSharedForClassHierarchyTest { + + @RegisterExtension + static final QuarkusComponentTest test = new QuarkusComponentTest(Component.class).mock(Foo.class) + .createMockitoMock(foo -> { + Mockito.when(foo.ping()).thenReturn(11); + }); + + @Inject + Component component; + + @Test + public void testMock() { + assertTrue(component.alpha == component.bar); + assertTrue(component.bar == component.baz); + assertTrue(component.baz == component.foo); + + assertEquals(11, component.alpha.ping()); + assertEquals(11, component.bar.ping()); + assertEquals(11, component.foo.ping()); + assertEquals(11, component.baz.ping()); + + Mockito.when(component.baz.ping()).thenReturn(111); + assertEquals(111, component.alpha.ping()); + assertEquals(111, component.bar.ping()); + assertEquals(111, component.foo.ping()); + } + + @Singleton + static class Component { + + @Inject + Alpha alpha; + + @Inject + Bar bar; + + @Inject + Foo foo; + + @Inject + Baz baz; + + } + + @Singleton + static class Foo extends Bar { + + @Override + public int ping() { + return 15; + } + + } + + static class Bar extends Baz { + + @Override + public int ping() { + return 10; + } + + } + + static class Baz implements Alpha { + + } + + interface Alpha { + + default int ping() { + return 5; + } + + } +} diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/ObserverInjectingMockTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/ObserverInjectingMockTest.java new file mode 100644 index 0000000000000..4dc9cbaa9e090 --- /dev/null +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/ObserverInjectingMockTest.java @@ -0,0 +1,34 @@ +package io.quarkus.test.component; + +import static org.mockito.Mockito.times; + +import jakarta.enterprise.event.Event; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.Mockito; + +import io.quarkus.test.component.beans.Delta; +import io.quarkus.test.component.beans.MyComponent; + +public class ObserverInjectingMockTest { + + @RegisterExtension + static final QuarkusComponentTest test = new QuarkusComponentTest(MyComponent.class) + .useDefaultConfigProperties(); + + @Inject + Event event; + + @Inject + Delta delta; + + @Test + public void testObserver() { + event.fire(Boolean.TRUE); + event.fire(Boolean.FALSE); + Mockito.verify(delta, times(2)).onBoolean(); + } + +} diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/ProgrammaticLookupMockTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/ProgrammaticLookupMockTest.java new file mode 100644 index 0000000000000..573a41c22d83e --- /dev/null +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/ProgrammaticLookupMockTest.java @@ -0,0 +1,43 @@ +package io.quarkus.test.component; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.Mockito; + +import io.quarkus.test.component.beans.Delta; + +public class ProgrammaticLookupMockTest { + + @RegisterExtension + static final QuarkusComponentTest test = new QuarkusComponentTest(ProgrammaticLookComponent.class); + + @Inject + ProgrammaticLookComponent component; + + @Inject + Delta delta; + + @Test + public void testMock() { + Mockito.when(delta.ping()).thenReturn(false); + assertFalse(component.ping()); + } + + @Singleton + static class ProgrammaticLookComponent { + + @Inject + Instance delta; + + boolean ping() { + return delta.get().ping(); + } + + } +} diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/UnsetConfigurationPropertiesTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/UnsetConfigurationPropertiesTest.java new file mode 100644 index 0000000000000..7cdc2668c7ff5 --- /dev/null +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/UnsetConfigurationPropertiesTest.java @@ -0,0 +1,48 @@ +package io.quarkus.test.component; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class UnsetConfigurationPropertiesTest { + + @RegisterExtension + static final QuarkusComponentTest test = new QuarkusComponentTest(Component.class) + .useDefaultConfigProperties(); + + @Inject + Component component; + + @Test + public void testComponent() { + assertNull(component.foo); + assertFalse(component.bar); + assertEquals(0, component.baz); + assertNull(component.bazzz); + } + + @Singleton + public static class Component { + + @ConfigProperty(name = "foo") + String foo; + + @ConfigProperty(name = "bar") + boolean bar; + + @ConfigProperty(name = "baz") + int baz; + + @ConfigProperty(name = "bazzz") + Integer bazzz; + + } + +} diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/beans/Alpha.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/beans/Alpha.java new file mode 100644 index 0000000000000..4193c66e37eb7 --- /dev/null +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/beans/Alpha.java @@ -0,0 +1,17 @@ +package io.quarkus.test.component.beans; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +@Singleton +public class Alpha { + + @Inject + @SimpleQualifier + Bravo bravo; + + public String ping() { + return bravo.ping() + bravo.ping(); + } + +} diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/beans/Bravo.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/beans/Bravo.java new file mode 100644 index 0000000000000..30b90764af4d6 --- /dev/null +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/beans/Bravo.java @@ -0,0 +1,23 @@ +package io.quarkus.test.component.beans; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +@SimpleQualifier +@Singleton +public class Bravo { + + @Inject + Charlie charlie; + + public String ping() { + try { + Thread.sleep(7l); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return charlie.ping(); + } + +} diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/beans/Charlie.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/beans/Charlie.java new file mode 100644 index 0000000000000..4cb8649d2d5d7 --- /dev/null +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/beans/Charlie.java @@ -0,0 +1,16 @@ +package io.quarkus.test.component.beans; + +import jakarta.enterprise.context.RequestScoped; + +@RequestScoped +public class Charlie { + + public String ping() { + return "pong"; + } + + public String pong() { + return "ping"; + } + +} diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/beans/Delta.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/beans/Delta.java new file mode 100644 index 0000000000000..1ab0e0e7cd7ce --- /dev/null +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/beans/Delta.java @@ -0,0 +1,15 @@ +package io.quarkus.test.component.beans; + +import jakarta.enterprise.context.Dependent; + +@Dependent +public class Delta { + + public boolean ping() { + return true; + } + + public void onBoolean() { + } + +} diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/beans/MyComponent.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/beans/MyComponent.java new file mode 100644 index 0000000000000..fa46c6ebba45b --- /dev/null +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/beans/MyComponent.java @@ -0,0 +1,34 @@ +package io.quarkus.test.component.beans; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.event.Observes; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.config.inject.ConfigProperty; + +@ApplicationScoped +public class MyComponent { + + @Inject + Charlie charlie; + + @Inject + @SimpleQualifier + Bravo bravo; + + @ConfigProperty(name = "foo") + String foo; + + public String ping() { + return charlie.ping() + " and " + foo; + } + + public String pong() { + return charlie.pong() + " and " + foo; + } + + void onBoolean(@Observes Boolean payload, Delta delta) { + delta.onBoolean(); + } + +} diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/beans/SimpleQualifier.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/beans/SimpleQualifier.java new file mode 100644 index 0000000000000..8b30f3b831619 --- /dev/null +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/beans/SimpleQualifier.java @@ -0,0 +1,20 @@ +package io.quarkus.test.component.beans; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import jakarta.inject.Qualifier; + +@Qualifier +@Inherited +@Target({ TYPE, METHOD, FIELD, PARAMETER }) +@Retention(RUNTIME) +public @interface SimpleQualifier { +} diff --git a/test-framework/pom.xml b/test-framework/pom.xml index 5874dd72e2448..3b5c3309b122a 100644 --- a/test-framework/pom.xml +++ b/test-framework/pom.xml @@ -28,6 +28,7 @@ junit5-internal junit5-properties junit5 + junit5-component junit5-mockito junit5-mockito-config vertx