From 887708b759397669f7d3f2482eff0e46af0d624c Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Sun, 7 May 2023 12:37:02 +0200 Subject: [PATCH] Introduce QuarkusComponentTest - a JUnit extension to ease the testing of components and mocking of their dependencies == Lifecycle - the CDI container is started and a dedicated SmallRyeConfig is registered 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 jakarta.inject.Inject are injected after a test instance is created and unset before a test instance is destroyed - the dependent beans injected into fields annotated with jakarta.inject.Inject are correctly destroyed before a test instance is destroyed - the CDI request context is activated and terminated per each test method == Auto Mocking Unsatisfied Dependencies - the test does not fail if a component injects an unsatisfied dependency - instead, a synthetic bean is registered automatically for each combination of required type and qualifiers of an injection point that resolves to an unsatisfied dependency - the bean has the Singleton scope so it's shared across all injection points with the same required type and qualifiers - the injected reference is an unconfigured Mockito mock. You can inject the mock in your test and leverage the Mockito API to configure the behavior == Custom Mocks For Unsatisfied Dependencies Sometimes you need the full control over the bean attributes and maybe even configure the default mock behavior. You can use the mock configurator API via the QuarkusComponentTest#mock(Class) method. == Configuration A dedicated SmallRyeConfig is registered during the before all test phase. Moreover, it's possible to set the configuration properties via the configProperty(String, String) method. If you only need to use the default values for missing config properties, then the useDefaultConfigProperties() might come in useful. Co-authored-by: Stuart Douglas stuart.w.douglas@gmail.com --- bom/application/pom.xml | 5 + .../dev/testing/JunitTestRunner.java | 4 +- .../asciidoc/getting-started-testing.adoc | 132 +++ .../io/quarkus/arc/processor/Annotations.java | 163 ++++ .../arc/processor/BeanConfigurator.java | 1 + .../arc/processor/BeanConfiguratorBase.java | 17 + .../quarkus/arc/processor/BeanDeployment.java | 37 +- .../io/quarkus/arc/processor/BeanInfo.java | 15 +- .../quarkus/arc/processor/BeanProcessor.java | 11 +- .../quarkus/arc/processor/BeanResolver.java | 9 + .../arc/processor/BeanResolverImpl.java | 5 + .../java/io/quarkus/arc/processor/Beans.java | 2 +- .../quarkus/arc/processor/BuildExtension.java | 1 + .../io/quarkus/arc/processor/BuiltinBean.java | 8 +- .../java/io/quarkus/arc/processor/Types.java | 55 ++ .../bcextensions/AnnotationBuilderImpl.java | 11 +- .../bcextensions/AnnotationsReflection.java | 170 ---- .../AnnotationsTransformation.java | 4 +- .../SyntheticBeanBuilderImpl.java | 7 +- .../SyntheticComponentBuilderBase.java | 6 +- .../bcextensions/SyntheticComponentsImpl.java | 4 +- .../SyntheticObserverBuilderImpl.java | 4 +- .../bcextensions/TypesReflection.java | 43 - .../quarkus/arc/impl/HierarchyDiscovery.java | 6 +- test-framework/junit5-component/pom.xml | 70 ++ .../test/component/ConfigBeanCreator.java | 33 + .../component/ConfigPropertyBeanCreator.java | 65 ++ .../quarkus/test/component/ConfigureMock.java | 16 + .../test/component/MockBeanConfigurator.java | 55 ++ .../component/MockBeanConfiguratorImpl.java | 215 +++++ .../test/component/MockBeanCreator.java | 43 + .../test/component/QuarkusComponentTest.java | 42 + .../QuarkusComponentTestClassLoader.java | 39 + .../QuarkusComponentTestExtension.java | 831 ++++++++++++++++++ .../test/component/TestConfigProperty.java | 35 + .../src/main/resources/application.properties | 2 + ...ApplicationPropertiesConfigSourceTest.java | 37 + .../test/component/DependencyMockingTest.java | 39 + .../test/component/MockConfiguratorTest.java | 40 + .../MockNotSharedForClassHierarchyTest.java | 84 ++ .../MockSharedForClassHierarchyTest.java | 88 ++ .../component/ObserverInjectingMockTest.java | 34 + .../component/ProgrammaticLookupMockTest.java | 43 + .../UnsetConfigurationPropertiesTest.java | 48 + .../quarkus/test/component/beans/Alpha.java | 17 + .../quarkus/test/component/beans/Bravo.java | 23 + .../quarkus/test/component/beans/Charlie.java | 16 + .../quarkus/test/component/beans/Delta.java | 15 + .../test/component/beans/MyComponent.java | 34 + .../test/component/beans/SimpleQualifier.java | 20 + .../DeclarativeDependencyMockingTest.java | 38 + ...ativeUnsetConfigurationPropertiesTest.java | 47 + .../component/declarative/InjectMockTest.java | 32 + test-framework/pom.xml | 1 + 54 files changed, 2559 insertions(+), 263 deletions(-) delete mode 100644 independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/AnnotationsReflection.java delete mode 100644 independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/TypesReflection.java create mode 100644 test-framework/junit5-component/pom.xml create mode 100644 test-framework/junit5-component/src/main/java/io/quarkus/test/component/ConfigBeanCreator.java create mode 100644 test-framework/junit5-component/src/main/java/io/quarkus/test/component/ConfigPropertyBeanCreator.java create mode 100644 test-framework/junit5-component/src/main/java/io/quarkus/test/component/ConfigureMock.java create mode 100644 test-framework/junit5-component/src/main/java/io/quarkus/test/component/MockBeanConfigurator.java create mode 100644 test-framework/junit5-component/src/main/java/io/quarkus/test/component/MockBeanConfiguratorImpl.java create mode 100644 test-framework/junit5-component/src/main/java/io/quarkus/test/component/MockBeanCreator.java create mode 100644 test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTest.java create mode 100644 test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestClassLoader.java create mode 100644 test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestExtension.java create mode 100644 test-framework/junit5-component/src/main/java/io/quarkus/test/component/TestConfigProperty.java create mode 100644 test-framework/junit5-component/src/main/resources/application.properties create mode 100644 test-framework/junit5-component/src/test/java/io/quarkus/test/component/ApplicationPropertiesConfigSourceTest.java create mode 100644 test-framework/junit5-component/src/test/java/io/quarkus/test/component/DependencyMockingTest.java create mode 100644 test-framework/junit5-component/src/test/java/io/quarkus/test/component/MockConfiguratorTest.java create mode 100644 test-framework/junit5-component/src/test/java/io/quarkus/test/component/MockNotSharedForClassHierarchyTest.java create mode 100644 test-framework/junit5-component/src/test/java/io/quarkus/test/component/MockSharedForClassHierarchyTest.java create mode 100644 test-framework/junit5-component/src/test/java/io/quarkus/test/component/ObserverInjectingMockTest.java create mode 100644 test-framework/junit5-component/src/test/java/io/quarkus/test/component/ProgrammaticLookupMockTest.java create mode 100644 test-framework/junit5-component/src/test/java/io/quarkus/test/component/UnsetConfigurationPropertiesTest.java create mode 100644 test-framework/junit5-component/src/test/java/io/quarkus/test/component/beans/Alpha.java create mode 100644 test-framework/junit5-component/src/test/java/io/quarkus/test/component/beans/Bravo.java create mode 100644 test-framework/junit5-component/src/test/java/io/quarkus/test/component/beans/Charlie.java create mode 100644 test-framework/junit5-component/src/test/java/io/quarkus/test/component/beans/Delta.java create mode 100644 test-framework/junit5-component/src/test/java/io/quarkus/test/component/beans/MyComponent.java create mode 100644 test-framework/junit5-component/src/test/java/io/quarkus/test/component/beans/SimpleQualifier.java create mode 100644 test-framework/junit5-component/src/test/java/io/quarkus/test/component/declarative/DeclarativeDependencyMockingTest.java create mode 100644 test-framework/junit5-component/src/test/java/io/quarkus/test/component/declarative/DeclarativeUnsetConfigurationPropertiesTest.java create mode 100644 test-framework/junit5-component/src/test/java/io/quarkus/test/component/declarative/InjectMockTest.java diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 19e4e36821055..dcd8470e60363 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 51fddadc5f53a..ad877b4e3767a 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 @@ -566,6 +566,7 @@ private DiscoveryResult discoverTestClasses() { } } } + Set allTestAnnotations = collectTestAnnotations(index); Set allTestClasses = new HashSet<>(); Map enclosingClasses = new HashMap<>(); @@ -597,7 +598,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