From 429dbfd140a597374215e694968f82d190a54bca Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Mon, 4 Sep 2023 15:39:09 +0200 Subject: [PATCH] QuarkusComponentTest: register additional annotations transformers - introduce JaxrsSingletonTransformer to add Singleton to a JAX-RS component that has no scope - resolves #35313 --- test-framework/junit5-component/pom.xml | 5 ++ .../component/JaxrsSingletonTransformer.java | 38 +++++++++++ .../test/component/QuarkusComponentTest.java | 12 ++++ .../QuarkusComponentTestExtension.java | 37 +++++++++-- .../component/AnnotationsTransformerTest.java | 50 ++++++++++++++ .../AnnotationsTransformerTest.java | 65 +++++++++++++++++++ .../JaxrsSingletonTransformerTest.java | 45 +++++++++++++ 7 files changed, 248 insertions(+), 4 deletions(-) create mode 100644 test-framework/junit5-component/src/main/java/io/quarkus/test/component/JaxrsSingletonTransformer.java create mode 100644 test-framework/junit5-component/src/test/java/io/quarkus/test/component/AnnotationsTransformerTest.java create mode 100644 test-framework/junit5-component/src/test/java/io/quarkus/test/component/declarative/AnnotationsTransformerTest.java create mode 100644 test-framework/junit5-component/src/test/java/io/quarkus/test/component/declarative/JaxrsSingletonTransformerTest.java diff --git a/test-framework/junit5-component/pom.xml b/test-framework/junit5-component/pom.xml index 4c2f6090b01ea..fafa7e91c4d8b 100644 --- a/test-framework/junit5-component/pom.xml +++ b/test-framework/junit5-component/pom.xml @@ -69,6 +69,11 @@ quarkus-junit5-mockito test + + jakarta.ws.rs + jakarta.ws.rs-api + test + diff --git a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/JaxrsSingletonTransformer.java b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/JaxrsSingletonTransformer.java new file mode 100644 index 0000000000000..07780a99fd5f7 --- /dev/null +++ b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/JaxrsSingletonTransformer.java @@ -0,0 +1,38 @@ +package io.quarkus.test.component; + +import java.util.List; + +import jakarta.inject.Singleton; + +import org.jboss.jandex.AnnotationTarget.Kind; +import org.jboss.jandex.DotName; + +import io.quarkus.arc.processor.Annotations; +import io.quarkus.arc.processor.AnnotationsTransformer; +import io.quarkus.arc.processor.BuiltinScope; + +/** + * Add {@link Singleton} to a JAX-RS component that has no scope annotation. + */ +public class JaxrsSingletonTransformer implements AnnotationsTransformer { + + private final List ANNOTATIONS = List.of(DotName.createSimple("jakarta.ws.rs.Path"), + DotName.createSimple("jakarta.ws.rs.ApplicationPath"), DotName.createSimple("jakarta.ws.rs.ext.Provider")); + + @Override + public boolean appliesTo(Kind kind) { + return Kind.CLASS == kind; + } + + @Override + public void transform(TransformationContext context) { + // Note that custom scopes are not supported yet + if (BuiltinScope.isIn(context.getAnnotations())) { + return; + } + if (Annotations.containsAny(context.getAnnotations(), ANNOTATIONS)) { + context.transform().add(Singleton.class).done(); + } + } + +} 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 index 7753391c71fcd..8a9b0799f3ab8 100644 --- 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 @@ -8,6 +8,7 @@ import org.junit.jupiter.api.extension.ExtendWith; +import io.quarkus.arc.processor.AnnotationsTransformer; import io.quarkus.test.InjectMock; import io.smallrye.common.annotation.Experimental; @@ -57,4 +58,15 @@ * @see QuarkusComponentTestExtension#setConfigSourceOrdinal(int) */ int configSourceOrdinal() default QuarkusComponentTestExtension.DEFAULT_CONFIG_SOURCE_ORDINAL; + + /** + * The additional annotation transformers. + *

+ * The initial set includes the {@link JaxrsSingletonTransformer}. + * + * @see AnnotationsTransformer + * @see QuarkusComponentTestExtension#addAnnotationsTransformer(AnnotationsTransformer) + */ + Class[] annotationsTransformers() default {}; + } 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 index 00960605b882a..3aa420659512b 100644 --- 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 @@ -171,12 +171,14 @@ public class QuarkusComponentTestExtension private final AtomicBoolean useDefaultConfigProperties = new AtomicBoolean(); private final AtomicBoolean addNestedClassesAsComponents = new AtomicBoolean(true); private final AtomicInteger configSourceOrdinal = new AtomicInteger(DEFAULT_CONFIG_SOURCE_ORDINAL); + private final List additionalAnnotationsTransformers; // Used for declarative registration public QuarkusComponentTestExtension() { this.additionalComponentClasses = List.of(); this.configProperties = new HashMap<>(); this.mockConfigurators = new ArrayList<>(); + this.additionalAnnotationsTransformers = new ArrayList<>(); } /** @@ -189,6 +191,7 @@ public QuarkusComponentTestExtension(Class... additionalComponentClasses) { this.additionalComponentClasses = List.of(additionalComponentClasses); this.configProperties = new HashMap<>(); this.mockConfigurators = new ArrayList<>(); + this.additionalAnnotationsTransformers = new ArrayList<>(); } /** @@ -210,7 +213,7 @@ public MockBeanConfigurator mock(Class beanClass) { * * @param key * @param value - * @return the extension + * @return self */ public QuarkusComponentTestExtension configProperty(String key, String value) { this.configProperties.put(key, value); @@ -222,7 +225,7 @@ public QuarkusComponentTestExtension configProperty(String key, String value) { *

* For primitives the default values as defined in the JLS are used. For any other type {@code null} is injected. * - * @return the extension + * @return self */ public QuarkusComponentTestExtension useDefaultConfigProperties() { this.useDefaultConfigProperties.set(true); @@ -235,7 +238,7 @@ public QuarkusComponentTestExtension useDefaultConfigProperties() { * By default, all static nested classes declared on the test class are added to the set of additional components under * test. * - * @return the extension + * @return self */ public QuarkusComponentTestExtension ignoreNestedClasses() { this.addNestedClassesAsComponents.set(false); @@ -247,13 +250,24 @@ public QuarkusComponentTestExtension ignoreNestedClasses() { * {@value #DEFAULT_CONFIG_SOURCE_ORDINAL} is used. * * @param val - * @return the extension + * @return self */ public QuarkusComponentTestExtension setConfigSourceOrdinal(int val) { this.configSourceOrdinal.set(val); return this; } + /** + * Add an additional {@link AnnotationsTransformer}. + * + * @param transformer + * @return self + */ + public QuarkusComponentTestExtension addAnnotationsTransformer(AnnotationsTransformer transformer) { + this.additionalAnnotationsTransformers.add(transformer); + return this; + } + @Override public void postProcessTestInstance(Object testInstance, ExtensionContext context) throws Exception { long start = System.nanoTime(); @@ -295,6 +309,16 @@ public void beforeAll(ExtensionContext context) throws Exception { } this.addNestedClassesAsComponents.set(testAnnotation.addNestedClassesAsComponents()); this.configSourceOrdinal.set(testAnnotation.configSourceOrdinal()); + Class[] transformers = testAnnotation.annotationsTransformers(); + if (transformers.length > 0) { + for (Class transformerClass : transformers) { + try { + this.additionalAnnotationsTransformers.add(transformerClass.getDeclaredConstructor().newInstance()); + } catch (Exception e) { + LOG.errorf("Unable to instantiate %s", transformerClass); + } + } + } } // All fields annotated with @Inject represent component classes Class current = testClass; @@ -607,6 +631,11 @@ public void writeResource(Resource resource) throws IOException { builder.addAnnotationTransformer(AnnotationsTransformer.appliedToField().whenContainsAny(qualifiers) .whenContainsNone(DotName.createSimple(Inject.class)).thenTransform(t -> t.add(Inject.class))); + builder.addAnnotationTransformer(new JaxrsSingletonTransformer()); + for (AnnotationsTransformer transformer : additionalAnnotationsTransformers) { + builder.addAnnotationTransformer(transformer); + } + // Register: // 1) Dummy mock beans for all unsatisfied injection points // 2) Synthetic beans for Config and @ConfigProperty injection points diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/AnnotationsTransformerTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/AnnotationsTransformerTest.java new file mode 100644 index 0000000000000..54f8d591db162 --- /dev/null +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/AnnotationsTransformerTest.java @@ -0,0 +1,50 @@ +package io.quarkus.test.component; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import org.jboss.jandex.DotName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.Mockito; + +import io.quarkus.arc.processor.AnnotationsTransformer; +import io.quarkus.test.InjectMock; +import io.quarkus.test.component.beans.Charlie; + +public class AnnotationsTransformerTest { + + @RegisterExtension + static final QuarkusComponentTestExtension extension = new QuarkusComponentTestExtension() + .addAnnotationsTransformer(AnnotationsTransformer.appliedToClass() + .whenClass(c -> c.declaredAnnotations().isEmpty() + && c.annotationsMap().containsKey(DotName.createSimple(Inject.class))) + .thenTransform(t -> t.add(Singleton.class))); + + @Inject + NotABean bean; + + @InjectMock + Charlie charlie; + + @Test + public void testPing() { + Mockito.when(charlie.ping()).thenReturn("foo"); + assertEquals("foo", bean.ping()); + } + + // @Singleton should be added automatically + public static class NotABean { + + @Inject + Charlie charlie; + + public String ping() { + return charlie.ping(); + } + + } + +} diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/declarative/AnnotationsTransformerTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/declarative/AnnotationsTransformerTest.java new file mode 100644 index 0000000000000..2c2cfaa90a4d2 --- /dev/null +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/declarative/AnnotationsTransformerTest.java @@ -0,0 +1,65 @@ +package io.quarkus.test.component.declarative; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import org.jboss.jandex.AnnotationTarget.Kind; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import io.quarkus.arc.processor.AnnotationsTransformer; +import io.quarkus.test.InjectMock; +import io.quarkus.test.component.QuarkusComponentTest; +import io.quarkus.test.component.beans.Charlie; +import io.quarkus.test.component.declarative.AnnotationsTransformerTest.MyAnnotationsTransformer; + +@QuarkusComponentTest(annotationsTransformers = MyAnnotationsTransformer.class) +public class AnnotationsTransformerTest { + + @Inject + NotABean bean; + + @InjectMock + Charlie charlie; + + @Test + public void testPing() { + Mockito.when(charlie.ping()).thenReturn("foo"); + assertEquals("foo", bean.ping()); + } + + // @Singleton should be added automatically + public static class NotABean { + + @Inject + Charlie charlie; + + public String ping() { + return charlie.ping(); + } + + } + + public static class MyAnnotationsTransformer implements AnnotationsTransformer { + + @Override + public boolean appliesTo(Kind kind) { + return Kind.CLASS == kind; + } + + @Override + public void transform(TransformationContext context) { + ClassInfo c = context.getTarget().asClass(); + if (c.declaredAnnotations().isEmpty() + && c.annotationsMap().containsKey(DotName.createSimple(Inject.class))) { + context.transform().add(Singleton.class).done(); + } + } + + } + +} diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/declarative/JaxrsSingletonTransformerTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/declarative/JaxrsSingletonTransformerTest.java new file mode 100644 index 0000000000000..7d487178b190d --- /dev/null +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/declarative/JaxrsSingletonTransformerTest.java @@ -0,0 +1,45 @@ +package io.quarkus.test.component.declarative; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import io.quarkus.test.InjectMock; +import io.quarkus.test.component.QuarkusComponentTest; +import io.quarkus.test.component.beans.Charlie; + +@QuarkusComponentTest +public class JaxrsSingletonTransformerTest { + + @Inject + MyResource resource; + + @InjectMock + Charlie charlie; + + @Test + public void testPing() { + Mockito.when(charlie.ping()).thenReturn("foo"); + assertEquals("foo", resource.ping()); + } + + // @Singleton should be added automatically + @Path("my") + public static class MyResource { + + @Inject + Charlie charlie; + + @GET + public String ping() { + return charlie.ping(); + } + + } + +}