From 42d994bb189288564f912faee6c8fce106930972 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Tue, 10 Oct 2023 15:15:35 +0200 Subject: [PATCH] QuarkusComponentTest: initial support for ConfigMapping --- .../asciidoc/getting-started-testing.adoc | 2 + .../component/ConfigMappingBeanCreator.java | 25 +++++++ .../QuarkusComponentTestExtension.java | 65 ++++++++++++++++--- .../component/config/ConfigMappingTest.java | 56 ++++++++++++++++ 4 files changed, 139 insertions(+), 9 deletions(-) create mode 100644 test-framework/junit5-component/src/main/java/io/quarkus/test/component/ConfigMappingBeanCreator.java create mode 100644 test-framework/junit5-component/src/test/java/io/quarkus/test/component/config/ConfigMappingTest.java diff --git a/docs/src/main/asciidoc/getting-started-testing.adoc b/docs/src/main/asciidoc/getting-started-testing.adoc index f2536c3b73ec8..520d80ed8095c 100644 --- a/docs/src/main/asciidoc/getting-started-testing.adoc +++ b/docs/src/main/asciidoc/getting-started-testing.adoc @@ -1648,6 +1648,8 @@ If you only need to use the default values for missing config properties, then t It is also possible to set configuration properties for a test method with the `@io.quarkus.test.component.TestConfigProperty` annotation. However, if the test instance lifecycle is `Lifecycle#_PER_CLASS` this annotation can only be used on the test class and is ignored on test methods. +CDI beans are also automatically registered for all injected https://smallrye.io/smallrye-config/Main/config/mappings/[Config Mappings]. The mappings are populated with the test configuration properties. + === Mocking CDI Interceptors If a tested component class declares an interceptor binding then you might need to mock the interception too. diff --git a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/ConfigMappingBeanCreator.java b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/ConfigMappingBeanCreator.java new file mode 100644 index 0000000000000..0cb8f2725eab2 --- /dev/null +++ b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/ConfigMappingBeanCreator.java @@ -0,0 +1,25 @@ +package io.quarkus.test.component; + +import io.quarkus.arc.BeanCreator; +import io.quarkus.arc.SyntheticCreationalContext; +import io.smallrye.config.SmallRyeConfig; + +public class ConfigMappingBeanCreator implements BeanCreator { + + @Override + public Object create(SyntheticCreationalContext context) { + String prefix = context.getParams().get("prefix").toString(); + Class mappingClass = tryLoad(context.getParams().get("mappingClass").toString()); + SmallRyeConfig config = ConfigBeanCreator.getConfig().unwrap(SmallRyeConfig.class); + return config.getConfigMapping(mappingClass, prefix); + } + + static Class tryLoad(String name) { + try { + return Thread.currentThread().getContextClassLoader().loadClass(name); + } catch (ClassNotFoundException e) { + throw new IllegalStateException("Unable to load type: " + name, e); + } + } + +} 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 1b34ebcd78b81..15a2f6e2d7fc0 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 @@ -20,6 +20,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Optional; import java.util.Set; import java.util.UUID; @@ -48,6 +49,7 @@ import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipse.microprofile.config.spi.ConfigProviderResolver; import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationValue; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.ClassType; import org.jboss.jandex.DotName; @@ -95,6 +97,8 @@ import io.quarkus.runtime.configuration.ApplicationPropertiesConfigSourceLoader; import io.quarkus.test.InjectMock; import io.smallrye.common.annotation.Experimental; +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.ConfigMappings.ConfigClassWithPrefix; import io.smallrye.config.SmallRyeConfig; import io.smallrye.config.SmallRyeConfigBuilder; import io.smallrye.config.SmallRyeConfigProviderResolver; @@ -150,6 +154,7 @@ public static QuarkusComponentTestExtensionBuilder builder() { private static final String KEY_TEST_INSTANCE = "testInstance"; private static final String KEY_CONFIG = "config"; private static final String KEY_TEST_CLASS_CONFIG = "testClassConfig"; + private static final String KEY_CONFIG_MAPPINGS = "configMappings"; private static final String QUARKUS_TEST_COMPONENT_OUTPUT_DIRECTORY = "quarkus.test.component.output-directory"; @@ -229,7 +234,7 @@ private void buildContainer(ExtensionContext context) { private void cleanup(ExtensionContext context) { ClassLoader oldTccl = context.getRoot().getStore(NAMESPACE).get(KEY_OLD_TCCL, ClassLoader.class); Thread.currentThread().setContextClassLoader(oldTccl); - + context.getRoot().getStore(NAMESPACE).remove(KEY_CONFIG_MAPPINGS); Set generatedResources = context.getRoot().getStore(NAMESPACE).get(KEY_GENERATED_RESOURCES, Set.class); for (Path path : generatedResources) { try { @@ -285,15 +290,24 @@ private void startContainer(ExtensionContext context, Lifecycle testInstanceLife // TCCL is now the QuarkusComponentTestClassLoader set during initialization ClassLoader tccl = Thread.currentThread().getContextClassLoader(); - SmallRyeConfig config = new SmallRyeConfigBuilder().forClassLoader(tccl) + SmallRyeConfigBuilder configBuilder = new SmallRyeConfigBuilder().forClassLoader(tccl) .addDefaultInterceptors() .addDefaultSources() .withSources(new ApplicationPropertiesConfigSourceLoader.InFileSystem()) .withSources(new ApplicationPropertiesConfigSourceLoader.InClassPath()) .withSources( new QuarkusComponentTestConfigSource(configuration.configProperties, - configuration.configSourceOrdinal)) - .build(); + configuration.configSourceOrdinal)); + @SuppressWarnings("unchecked") + Set configMappings = context.getRoot().getStore(NAMESPACE).get(KEY_CONFIG_MAPPINGS, + Set.class); + if (configMappings != null) { + // Register the mappings found during bean discovery + for (ConfigClassWithPrefix mapping : configMappings) { + configBuilder.withMapping(mapping.getKlass(), mapping.getPrefix()); + } + } + SmallRyeConfig config = configBuilder.build(); smallRyeConfigProviderResolver.registerConfig(config, tccl); context.getRoot().getStore(NAMESPACE).put(KEY_CONFIG, config); ConfigBeanCreator.setClassLoader(tccl); @@ -503,17 +517,15 @@ public void register(RegistrationContext registrationContext) { Set unsatisfiedInjectionPoints = new HashSet<>(); boolean configInjectionPoint = false; Set configPropertyInjectionPoints = new HashSet<>(); + Map> prefixToConfigMappings = new HashMap<>(); DotName configDotName = DotName.createSimple(Config.class); DotName configPropertyDotName = DotName.createSimple(ConfigProperty.class); + DotName configMappingDotName = DotName.createSimple(ConfigMapping.class); // Analyze injection points - // - find Config and @ConfigProperty injection points + // - find Config, @ConfigProperty and config mappings injection points // - find unsatisfied injection points for (InjectionPointInfo injectionPoint : registrationContext.getInjectionPoints()) { - BuiltinBean builtin = BuiltinBean.resolve(injectionPoint); - if (builtin != null && builtin != BuiltinBean.INSTANCE && builtin != BuiltinBean.LIST) { - continue; - } if (injectionPoint.getRequiredType().name().equals(configDotName) && injectionPoint.hasDefaultedQualifier()) { configInjectionPoint = true; @@ -524,6 +536,10 @@ public void register(RegistrationContext registrationContext) { injectionPoint.getRequiredQualifiers())); continue; } + BuiltinBean builtin = BuiltinBean.resolve(injectionPoint); + if (builtin != null && builtin != BuiltinBean.INSTANCE && builtin != BuiltinBean.LIST) { + continue; + } Type requiredType = injectionPoint.getRequiredType(); Set requiredQualifiers = injectionPoint.getRequiredQualifiers(); if (builtin == BuiltinBean.LIST) { @@ -535,6 +551,19 @@ public void register(RegistrationContext registrationContext) { requiredQualifiers.add(AnnotationInstance.builder(DotNames.DEFAULT).build()); } } + if (requiredType.kind() == Kind.CLASS) { + ClassInfo clazz = computingIndex.getClassByName(requiredType.name()); + if (clazz != null && clazz.isInterface()) { + AnnotationInstance configMapping = clazz.declaredAnnotation(configMappingDotName); + if (configMapping != null) { + AnnotationValue prefixValue = configMapping.value("prefix"); + String prefix = prefixValue == null ? "" : prefixValue.asString(); + Set mappingClasses = prefixToConfigMappings.computeIfAbsent(prefix, + k -> new HashSet<>()); + mappingClasses.add(clazz.name().toString()); + } + } + } if (isSatisfied(requiredType, requiredQualifiers, injectionPoint, beans, beanDeployment, configuration)) { continue; @@ -591,6 +620,24 @@ public void register(RegistrationContext registrationContext) { configPropertyConfigurator.done(); } + if (!prefixToConfigMappings.isEmpty()) { + Set configMappings = new HashSet<>(); + for (Entry> e : prefixToConfigMappings.entrySet()) { + for (String mapping : e.getValue()) { + DotName mappingName = DotName.createSimple(mapping); + registrationContext.configure(mappingName) + .addType(mappingName) + .creator(ConfigMappingBeanCreator.class) + .param("mappingClass", mapping) + .param("prefix", e.getKey()) + .done(); + configMappings.add(ConfigClassWithPrefix + .configClassWithPrefix(ConfigMappingBeanCreator.tryLoad(mapping), e.getKey())); + } + } + extensionContext.getRoot().getStore(NAMESPACE).put(KEY_CONFIG_MAPPINGS, configMappings); + } + LOG.debugf("Test injection points analyzed in %s ms [found: %s, mocked: %s]", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start), registrationContext.getInjectionPoints().size(), diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/config/ConfigMappingTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/config/ConfigMappingTest.java new file mode 100644 index 0000000000000..b51761d34c953 --- /dev/null +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/config/ConfigMappingTest.java @@ -0,0 +1,56 @@ +package io.quarkus.test.component.config; + +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 io.quarkus.test.component.QuarkusComponentTest; +import io.quarkus.test.component.TestConfigProperty; +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithDefault; + +@QuarkusComponentTest +@TestConfigProperty(key = "foo.bar", value = "true") +public class ConfigMappingTest { + + @Inject + Foo foo; + + @TestConfigProperty(key = "foo.oof", value = "boom") + @Test + public void testMapping() { + assertTrue(foo.config.bar()); + assertEquals("loom", foo.config.baz()); + assertEquals("boom", foo.config.oof()); + } + + @TestConfigProperty(key = "foo.oof", value = "boomboom") + @Test + public void testAnotherMapping() { + assertTrue(foo.config.bar()); + assertEquals("loom", foo.config.baz()); + assertEquals("boomboom", foo.config.oof()); + } + + @Singleton + public static class Foo { + + @Inject + FooConfig config; + } + + @ConfigMapping(prefix = "foo") + interface FooConfig { + + boolean bar(); + + @WithDefault("loom") + String baz(); + + String oof(); + } +}