diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 918b288e88613..930aea6d93867 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -2955,6 +2955,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 88453151ec334..eca7628f8960f 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 @@ -569,6 +569,7 @@ private DiscoveryResult discoverTestClasses() { } } } + Set allTestAnnotations = collectTestAnnotations(index); Set allTestClasses = new HashSet<>(); Map enclosingClasses = new HashMap<>(); @@ -600,7 +601,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) + || 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 31421ab89e6f2..cf621d16d3c22 100644 --- a/docs/src/main/asciidoc/getting-started-testing.adoc +++ b/docs/src/main/asciidoc/getting-started-testing.adoc @@ -1507,3 +1507,135 @@ 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 + +IMPORTANT: This feature is experimental and the API may change in the future. + +In Quarkus, the component model is built on top CDI. +Therefore, Quarkus provides the `QuarkusComponentTestExtension`, a JUnit extension to ease the testing of components and mocking of their dependencies. +This extension is available in the `quarkus-junit5-component` dependency. + +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> + + @ConfigProperty(name = "bar") + boolean bar; <3> + + public String ping() { + return bar ? charlie.ping() : "nok"; + } +} +---- +<1> `Foo` is an `@ApplicationScoped` CDI bean. +<2> `Foo` depends on `Charlie` which declares a method `ping()`. +<3> `Foo` depends on the config property `bar`. + +Then a component test could look like: + +[source, java] +---- +import static org.junit.jupiter.api.Assertions.assertEquals; + +import jakarta.inject.Inject; +import io.quarkus.test.component.TestConfigProperty; +import io.quarkus.test.component.TestMock; +import io.quarkus.test.component.QuarkusComponentTest; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +@QuarkusComponentTest <1> +@TestConfigProperty(key = "bar", value = "true") <2> +public class FooTest { + + @Inject + Foo foo; <3> + + @ConfigureMock + Charlie charlieMock; <4> + + @Test + public void testPing() { + Mockito.when(charlieMock.ping()).thenReturn("OK"); <5> + assertEquals("OK", foo.ping()); + } +} +---- +<1> The `QuarkusComponentTest` annotation registers the JUnit extension. +<2> Set a configuration property for the test. +<3> The test injects the component under the test. The types of all fields annotated with `@Inject` are considered the component types under test. You can also specify additional component classes via `@QuarkusComponentTest#value()`. +<4> The test also injects `Charlie`, a dependency for which a synthetic `@Singleton` bean is registered automatically. The injected reference is an "unconfigured" Mockito mock. +<5> We can leverage the Mockito API in a test method to configure the behavior. + +If you need the full control over the `QuarkusComponentTestExtension` configuration then you can use the `@RegisterExtension` annotation and configure the extension programatically. +The test above could be rewritten like: + +[source, java] +---- +import static org.junit.jupiter.api.Assertions.assertEquals; + +import jakarta.inject.Inject; +import io.quarkus.test.component.TestMock; +import io.quarkus.test.component.QuarkusComponentTestExtension; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +public class FooTest { + + @RegisterExtension <1> + static final QuarkusComponentTestExtension extension = new QuarkusComponentTestExtension().configProperty("bar","true"); + + @Inject + Foo foo; + + @ConfigureMock + Charlie charlieMock; + + @Test + public void testPing() { + Mockito.when(charlieMock.ping()).thenReturn("OK"); + assertEquals("OK", foo.ping()); + } +} +---- +<1> The `QuarkusComponentTestExtension` is configured in a static field. + +=== Lifecycle + +So what exactly does the `QuarkusComponentTest` do? +It starts the CDI container and registers a dedicated xref:config-reference.adoc[configuration object] 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` and `@ConfigureMock` 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. + +NOTE: By default, a new test instance is created for each test method. Therefore, a new CDI container is started for each test method. However, if the test class is annotated with `@org.junit.jupiter.api.TestInstance` and the test instance lifecycle is set to `org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS` then the CDI container will be shared across all test method executions of a given test class. + +=== 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 `QuarkusComponentTestExtension#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 `QuarkusComponentTestExtension#configProperty(String, String)` method or the `@TestConfigProperty` annotation. +If you only need to use the default values for missing config properties, then the `QuarkusComponentTestExtension#useDefaultConfigProperties()` or `@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 8af6b4fefca8d..d1e16194e5311 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 @@ -129,7 +129,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) { @@ -142,9 +142,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); @@ -176,7 +175,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(); @@ -197,7 +196,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) { @@ -206,7 +205,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); @@ -273,14 +272,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); @@ -326,12 +325,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)); } @@ -728,12 +725,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 94b5567a1df75..92838d59b4fb7 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(); @@ -956,6 +956,8 @@ boolean isEmpty() { static class Builder { + private String identifier; + private ClassInfo implClazz; private Type providerType; @@ -1003,6 +1005,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; @@ -1115,7 +1122,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 31ba850b8f231..df4ec28a3a5b0 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..934ec6194b6df --- /dev/null +++ b/test-framework/junit5-component/pom.xml @@ -0,0 +1,70 @@ + + + 4.0.0 + + + io.quarkus + quarkus-test-framework + 999-SNAPSHOT + + + quarkus-junit5-component + Quarkus - Test Framework - JUnit 5 Component Test Framework + This feature is experimental and the API may change in the future + + + + 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 + + + io.quarkus + quarkus-bootstrap-core + + + io.quarkus + quarkus-core + + + io.smallrye.common + smallrye-common-annotation + + + + org.jboss.logmanager + jboss-logmanager-embedded + test + + + io.quarkus + quarkus-junit5-mockito + 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..d0939c6f32e2e --- /dev/null +++ b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/ConfigBeanCreator.java @@ -0,0 +1,33 @@ +package io.quarkus.test.component; + +import java.util.concurrent.atomic.AtomicReference; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; + +import io.quarkus.arc.BeanCreator; +import io.quarkus.arc.SyntheticCreationalContext; + +public class ConfigBeanCreator implements BeanCreator { + + // we need to keep a reference to the CL used to register the config object in order to support the continuous testing where TCCL does not work for us + private static final AtomicReference configClassLoader = new AtomicReference<>(); + + @Override + public Config create(SyntheticCreationalContext context) { + return getConfig(); + } + + static Config getConfig() { + return ConfigProvider.getConfig(configClassLoader.get()); + } + + static void setClassLoader(ClassLoader classLoader) { + configClassLoader.set(classLoader); + } + + static void clear() { + configClassLoader.set(null); + } + +} 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/ConfigureMock.java b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/ConfigureMock.java new file mode 100644 index 0000000000000..b545c0b30362f --- /dev/null +++ b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/ConfigureMock.java @@ -0,0 +1,16 @@ +package io.quarkus.test.component; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Marks a field of a test class as a target of a mock dependency injection. + */ +@Retention(RUNTIME) +@Target(ElementType.FIELD) +public @interface ConfigureMock { + +} diff --git a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/MockBeanConfigurator.java b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/MockBeanConfigurator.java new file mode 100644 index 0000000000000..c2f3850bfd31c --- /dev/null +++ b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/MockBeanConfigurator.java @@ -0,0 +1,55 @@ +package io.quarkus.test.component; + +import java.lang.annotation.Annotation; +import java.util.function.Consumer; +import java.util.function.Function; + +import io.quarkus.arc.SyntheticCreationalContext; + +/** + * Configures a mock of a bean. + * + * @param + * @see QuarkusComponentTestExtension#mock(Class) + */ +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 + */ + QuarkusComponentTestExtension create(Function, T> create); + + /** + * A Mockito mock object created from the bean class is used as a bean instance. + * + * @return the test extension + */ + QuarkusComponentTestExtension createMockitoMock(); + + /** + * A Mockito mock object created from the bean class is used as a bean instance. + * + * @return the test extension + */ + QuarkusComponentTestExtension createMockitoMock(Consumer mockInitializer); + +} \ No newline at end of file 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..e7d62a0fa6904 --- /dev/null +++ b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/MockBeanConfiguratorImpl.java @@ -0,0 +1,215 @@ +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.annotation.Priority; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.context.RequestScoped; +import jakarta.enterprise.inject.Alternative; +import jakarta.enterprise.inject.Default; +import jakarta.inject.Named; +import jakarta.inject.Qualifier; +import jakarta.inject.Singleton; + +import org.jboss.jandex.AnnotationInstance; +import org.mockito.Mockito; + +import io.quarkus.arc.DefaultBean; +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; + +class MockBeanConfiguratorImpl implements MockBeanConfigurator { + + final QuarkusComponentTestExtension 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(QuarkusComponentTestExtension test, Class beanClass) { + this.test = test; + this.beanClass = beanClass; + this.types = new HierarchyDiscovery(beanClass).getTypeClosure(); + + 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); + } + + if (beanClass.isAnnotationPresent(Alternative.class)) { + this.alternative = true; + } + Named named = beanClass.getAnnotation(Named.class); + if (named != null) { + String val = named.value(); + if (!val.isBlank()) { + this.name = val; + } else { + StringBuilder defaultName = new StringBuilder(); + defaultName.append(beanClass.getSimpleName()); + // URLMatcher becomes uRLMatcher + defaultName.setCharAt(0, Character.toLowerCase(defaultName.charAt(0))); + this.name = defaultName.toString(); + } + } + Priority priority = beanClass.getAnnotation(Priority.class); + if (priority != null) { + this.priority = priority.value(); + } + if (beanClass.isAnnotationPresent(DefaultBean.class)) { + this.defaultBean = true; + } + } + + @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 QuarkusComponentTestExtension create(Function, T> create) { + this.create = create; + return register(); + } + + @Override + public QuarkusComponentTestExtension createMockitoMock() { + this.create = c -> QuarkusComponentTestExtension.cast(Mockito.mock(beanClass)); + return register(); + } + + @Override + public QuarkusComponentTestExtension createMockitoMock(Consumer mockInitializer) { + this.create = c -> { + T mock = QuarkusComponentTestExtension.cast(Mockito.mock(beanClass)); + mockInitializer.accept(mock); + return mock; + }; + return register(); + } + + public QuarkusComponentTestExtension 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..cce7bfaa6e07b --- /dev/null +++ b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/MockBeanCreator.java @@ -0,0 +1,43 @@ +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); + } + + static void clear() { + createFunctions.clear(); + } + +} 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..5b26337f7966d --- /dev/null +++ b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTest.java @@ -0,0 +1,42 @@ +package io.quarkus.test.component; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.extension.ExtendWith; + +import io.smallrye.common.annotation.Experimental; + +/** + * Registers the {@link QuarkusComponentTestExtension} that makes it easy to test Quarkus components. + */ +@Experimental("This feature is experimental and the API may change in the future") +@ExtendWith(QuarkusComponentTestExtension.class) +@Retention(RUNTIME) +@Target({ TYPE }) +public @interface QuarkusComponentTest { + + /** + * The set of additional components under test. + *

+ * The initial set of components is derived from the test class. The types of all fields annotated with + * {@link jakarta.inject.Inject} are considered the component types. + * + * @return the components under test + */ + Class[] value() default {}; + + /** + * Indicates that the default values should be used for missing config properties. + *

+ * If not used then a 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. + * + * @see QuarkusComponentTestExtension#useDefaultConfigProperties() + */ + boolean useDefaultConfigProperties() default false; +} 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/main/java/io/quarkus/test/component/QuarkusComponentTestExtension.java b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestExtension.java new file mode 100644 index 0000000000000..4abdad8044d14 --- /dev/null +++ b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestExtension.java @@ -0,0 +1,831 @@ +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.event.Event; +import jakarta.enterprise.inject.Instance; +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.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.quarkus.bootstrap.classloading.QuarkusClassLoader; +import io.quarkus.runtime.configuration.ApplicationPropertiesConfigSourceLoader; +import io.smallrye.common.annotation.Experimental; +import io.smallrye.config.SmallRyeConfig; +import io.smallrye.config.SmallRyeConfigBuilder; +import io.smallrye.config.SmallRyeConfigProviderResolver; + +/** + * JUnit extension that makes it easy 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. + */ +@Experimental("This feature is experimental and the API may change in the future") +public class QuarkusComponentTestExtension + implements BeforeAllCallback, AfterAllCallback, BeforeEachCallback, AfterEachCallback, TestInstancePostProcessor, + TestInstancePreDestroyCallback, ConfigSource { + + private static final Logger LOG = Logger.getLogger(QuarkusComponentTestExtension.class); + + private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace + .create(QuarkusComponentTestExtension.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_INJECTED_FIELDS = "injectedFields"; + private static final String KEY_CONFIG = "config"; + + private static final String TARGET_TEST_CLASSES = "target/test-classes"; + + private final Map configProperties; + private final List> additionalComponentClasses; + private final List> mockConfigurators; + private final AtomicBoolean useDefaultConfigProperties = new AtomicBoolean(); + + // Used for declarative registration + public QuarkusComponentTestExtension() { + this.additionalComponentClasses = List.of(); + this.configProperties = new HashMap<>(); + this.mockConfigurators = new ArrayList<>(); + } + + /** + * The initial set of components under test is derived from the test class. The types of all fields annotated with + * {@link jakarta.inject.Inject} are considered the component types. + * + * @param additionalComponentClasses + */ + public QuarkusComponentTestExtension(Class... additionalComponentClasses) { + this.additionalComponentClasses = List.of(additionalComponentClasses); + 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 QuarkusComponentTestExtension 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 QuarkusComponentTestExtension 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 + context.getRoot().getStore(NAMESPACE).put(KEY_INJECTED_FIELDS, + injectFields(context.getRequiredTestClass(), testInstance)); + + 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 (FieldInjector fieldInjector : (List) context.getRoot().getStore(NAMESPACE) + .get(KEY_INJECTED_FIELDS, List.class)) { + fieldInjector.unset(context.getRequiredTestInstance()); + } + + LOG.debugf("preDestroyTestInstance: %s ms", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)); + } + + @Override + public void beforeAll(ExtensionContext context) throws Exception { + long start = System.nanoTime(); + + Class testClass = context.getRequiredTestClass(); + + // Extension may be registered declaratively + Set> componentClasses = new HashSet<>(this.additionalComponentClasses); + QuarkusComponentTest testAnnotation = testClass.getAnnotation(QuarkusComponentTest.class); + if (testAnnotation != null) { + Collections.addAll(componentClasses, testAnnotation.value()); + if (testAnnotation.useDefaultConfigProperties()) { + this.useDefaultConfigProperties.set(true); + } + } + // All fields annotated with @Inject represent component classes + for (Field field : testClass.getDeclaredFields()) { + if (field.isAnnotationPresent(Inject.class) && !resolvesToBuiltinBean(field.getType())) { + componentClasses.add(field.getType()); + } + } + + TestConfigProperty[] testConfigProperties = testClass.getAnnotationsByType(TestConfigProperty.class); + for (TestConfigProperty testConfigProperty : testConfigProperties) { + this.configProperties.put(testConfigProperty.key(), testConfigProperty.value()); + } + + ClassLoader oldTccl = initArcContainer(context, componentClasses); + 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) + .addDefaultInterceptors() + .addDefaultSources() + .withSources(new ApplicationPropertiesConfigSourceLoader.InFileSystem()) + .withSources(new ApplicationPropertiesConfigSourceLoader.InClassPath()) + .withSources(this) + .build(); + smallRyeConfigProviderResolver.registerConfig(config, tccl); + context.getRoot().getStore(NAMESPACE).put(KEY_CONFIG, config); + ConfigBeanCreator.setClassLoader(tccl); + + 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); + + try { + Arc.shutdown(); + } catch (Exception e) { + LOG.error("An error occured during ArC shutdown: " + e); + } + MockBeanCreator.clear(); + ConfigBeanCreator.clear(); + + 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 QuarkusComponentTestExtension.class.getName(); + } + + @Override + public int getOrdinal() { + // System properties (400) and ENV variables (300) take precedence but application.properties has lower priority (250) + return 275; + } + + 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, Collection> componentClasses) { + 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); + } + } + + if (componentClasses.isEmpty()) { + throw new IllegalStateException("No component classes to test"); + } + + // Make sure Arc is down + try { + Arc.shutdown(); + } catch (Exception e) { + throw new IllegalStateException("An error occured during ArC shutdown: " + e); + } + + // 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); + } + + ClassLoader oldTccl = Thread.currentThread().getContextClassLoader(); + + IndexView computingIndex = BeanArchives.buildComputingBeanArchiveIndex(oldTccl, + new ConcurrentHashMap<>(), index); + + try { + + // 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<>(); + + File generatedSourcesDirectory = new File("target/generated-arc-sources"); + File componentsProviderFile = new File(generatedSourcesDirectory + "/" + nameToPath(testClass.getPackage() + .getName()), ComponentsProvider.class.getSimpleName()); + if (testClass.getClassLoader() instanceof QuarkusClassLoader) { + //continuous testing environment + Map classes = new HashMap<>(); + builder.setOutput(new ResourceOutput() { + @Override + public void writeResource(Resource resource) throws IOException { + switch (resource.getType()) { + case JAVA_CLASS: + classes.put(resource.getName() + ".class", resource.getData()); + ((QuarkusClassLoader) testClass.getClassLoader()).reset(classes, Map.of()); + 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(); + } + } + }); + } else { + 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 testOutputDirectory = new File(testClassesRootPath + TARGET_TEST_CLASSES); + + 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(oldTccl, + 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 oldTccl; + } + + 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; + } + + private List injectFields(Class testClass, Object testInstance) throws Exception { + List> injectAnnotations; + Class injectMock = loadInjectMock(); + if (injectMock != null) { + injectAnnotations = List.of(Inject.class, ConfigureMock.class, injectMock); + } else { + injectAnnotations = List.of(Inject.class, ConfigureMock.class); + } + List injectedFields = new ArrayList<>(); + for (Field field : testClass.getDeclaredFields()) { + for (Class annotation : injectAnnotations) { + if (field.isAnnotationPresent(annotation)) { + injectedFields.add(new FieldInjector(field, testInstance)); + break; + } + } + } + return injectedFields; + } + + static class FieldInjector { + + private final Field field; + private final InstanceHandle handle; + + public FieldInjector(Field field, Object testInstance) throws Exception { + ArcContainer container = Arc.container(); + BeanManager beanManager = container.beanManager(); + + this.field = field; + this.handle = container.instance(field.getGenericType(), getQualifiers(field, beanManager)); + + if (field.isAnnotationPresent(Inject.class)) { + if (handle.getBean().getKind() == io.quarkus.arc.InjectableBean.Kind.SYNTHETIC) { + throw new IllegalStateException(String + .format("The injected field %s expects a real component; but obtained: %s", field, + handle.getBean())); + } + } else { + if (handle.getBean().getKind() != io.quarkus.arc.InjectableBean.Kind.SYNTHETIC) { + throw new IllegalStateException(String + .format("The injected field %s expects a mocked bean; but obtained: %s", field, handle.getBean())); + } + } + + field.setAccessible(true); + field.set(testInstance, handle.get()); + } + + void unset(Object testInstance) throws Exception { + if (handle.getBean() != null && handle.getBean().getScope().equals(Dependent.class)) { + try { + handle.destroy(); + } catch (Exception e) { + LOG.errorf(e, "Unable to destroy the injected %s", handle.getBean()); + } + } + field.setAccessible(true); + field.set(testInstance, null); + } + + } + + @SuppressWarnings("unchecked") + private Class loadInjectMock() { + try { + return (Class) Class.forName("io.quarkus.test.junit.mockito.InjectMock"); + } catch (Throwable e) { + return null; + } + } + + private boolean resolvesToBuiltinBean(Class rawType) { + return Instance.class.isAssignableFrom(rawType) || Event.class.equals(rawType) || BeanManager.class.equals(rawType); + } + +} diff --git a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/TestConfigProperty.java b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/TestConfigProperty.java new file mode 100644 index 0000000000000..f493d669c2881 --- /dev/null +++ b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/TestConfigProperty.java @@ -0,0 +1,35 @@ +package io.quarkus.test.component; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import io.quarkus.test.component.TestConfigProperty.TestConfigProperties; + +/** + * Set a configuration property for the test. + * + * @see QuarkusComponentTest + * @see QuarkusComponentTestExtension#configProperty(String, String) + */ +@Retention(RUNTIME) +@Target(TYPE) +@Repeatable(TestConfigProperties.class) +public @interface TestConfigProperty { + + String key(); + + String value(); + + @Retention(RUNTIME) + @Target(TYPE) + @interface TestConfigProperties { + + TestConfigProperty[] value(); + + } + +} diff --git a/test-framework/junit5-component/src/main/resources/application.properties b/test-framework/junit5-component/src/main/resources/application.properties new file mode 100644 index 0000000000000..6e74a44df9348 --- /dev/null +++ b/test-framework/junit5-component/src/main/resources/application.properties @@ -0,0 +1,2 @@ +org.acme.foo=rocket +org.acme.bar=grut \ No newline at end of file diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/ApplicationPropertiesConfigSourceTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/ApplicationPropertiesConfigSourceTest.java new file mode 100644 index 0000000000000..da6b5d8c36fd5 --- /dev/null +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/ApplicationPropertiesConfigSourceTest.java @@ -0,0 +1,37 @@ +package io.quarkus.test.component; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +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 ApplicationPropertiesConfigSourceTest { + + @RegisterExtension + static final QuarkusComponentTestExtension extension = new QuarkusComponentTestExtension() + .configProperty("org.acme.bar", "GRUT"); + + @Inject + Component component; + + @Test + public void testComponent() { + assertEquals("rocket", component.foo); + assertEquals("GRUT", component.bar); + } + + @Singleton + public static class Component { + + @ConfigProperty(name = "org.acme.foo") + String foo; + + @ConfigProperty(name = "org.acme.bar") + String bar; + } + +} 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..017307b14bca3 --- /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 QuarkusComponentTestExtension extension = new QuarkusComponentTestExtension(MyComponent.class) + // this config property is injected into MyComponent and the value is used in the ping() method + .configProperty("foo", "BAR"); + + @Inject + MyComponent myComponent; + + @ConfigureMock + 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..f8daa8eb1b172 --- /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 QuarkusComponentTestExtension extension = new QuarkusComponentTestExtension(MyComponent.class) + .mock(Charlie.class).createMockitoMock(charlie -> { + Mockito.when(charlie.pong()).thenReturn("bar"); + }) + .configProperty("foo", "BAR"); + + @Inject + MyComponent myComponent; + + @ConfigureMock + 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..68901c0a6cfc6 --- /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 QuarkusComponentTestExtension extension = new QuarkusComponentTestExtension(Component.class); + + @Inject + Component component; + + @ConfigureMock + 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..2e523a427a398 --- /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 QuarkusComponentTestExtension extension = new QuarkusComponentTestExtension(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..3abeb552d0f5f --- /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 QuarkusComponentTestExtension extension = new QuarkusComponentTestExtension(MyComponent.class) + .useDefaultConfigProperties(); + + @Inject + Event event; + + @ConfigureMock + 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..7bd43fcf3aae1 --- /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 QuarkusComponentTestExtension extension = new QuarkusComponentTestExtension(ProgrammaticLookComponent.class); + + @Inject + ProgrammaticLookComponent component; + + @ConfigureMock + 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..a8dbc8b8c6226 --- /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 QuarkusComponentTestExtension extension = new QuarkusComponentTestExtension(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/junit5-component/src/test/java/io/quarkus/test/component/declarative/DeclarativeDependencyMockingTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/declarative/DeclarativeDependencyMockingTest.java new file mode 100644 index 0000000000000..d71eeef0a2b9a --- /dev/null +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/declarative/DeclarativeDependencyMockingTest.java @@ -0,0 +1,38 @@ +package io.quarkus.test.component.declarative; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import io.quarkus.test.component.ConfigureMock; +import io.quarkus.test.component.QuarkusComponentTest; +import io.quarkus.test.component.TestConfigProperty; +import io.quarkus.test.component.beans.Charlie; +import io.quarkus.test.component.beans.MyComponent; + +@QuarkusComponentTest +@TestConfigProperty(key = "foo", value = "BAR") +public class DeclarativeDependencyMockingTest { + + @Inject + MyComponent myComponent; + + @ConfigureMock + 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/declarative/DeclarativeUnsetConfigurationPropertiesTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/declarative/DeclarativeUnsetConfigurationPropertiesTest.java new file mode 100644 index 0000000000000..0f26c6e99ecca --- /dev/null +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/declarative/DeclarativeUnsetConfigurationPropertiesTest.java @@ -0,0 +1,47 @@ +package io.quarkus.test.component.declarative; + +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 io.quarkus.test.component.QuarkusComponentTest; +import io.quarkus.test.component.declarative.DeclarativeUnsetConfigurationPropertiesTest.Component; + +@QuarkusComponentTest(value = Component.class, useDefaultConfigProperties = true) +public class DeclarativeUnsetConfigurationPropertiesTest { + + @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/declarative/InjectMockTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/declarative/InjectMockTest.java new file mode 100644 index 0000000000000..a3ec60dbf0213 --- /dev/null +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/declarative/InjectMockTest.java @@ -0,0 +1,32 @@ +package io.quarkus.test.component.declarative; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import io.quarkus.test.component.QuarkusComponentTest; +import io.quarkus.test.component.TestConfigProperty; +import io.quarkus.test.component.beans.Charlie; +import io.quarkus.test.component.beans.MyComponent; +import io.quarkus.test.junit.mockito.InjectMock; + +@QuarkusComponentTest(MyComponent.class) +@TestConfigProperty(key = "foo", value = "BAR") +public class InjectMockTest { + + @Inject + MyComponent myComponent; + + @InjectMock + Charlie charlie; + + @Test + public void testPing() { + Mockito.when(charlie.ping()).thenReturn("foo"); + assertEquals("foo and BAR", myComponent.ping()); + } + +} 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