quarkusio / quarkus

Quarkus: Supersonic Subatomic Java.
https://quarkus.io
Apache License 2.0
13.41k stars 2.57k forks source link

QuarkusComponentTest does not register Quarkus Config Converters #41709

Closed diversit closed 1 week ago

diversit commented 2 weeks ago

Describe the bug

A QuarkusComponentTest fails for components which rely on MicroProfile Config Converters provided by Quarkus. E.g. io.quarkus.runtime.configuration.DurationConverter.

Testing a component using

@ConfigProperty(name = "my.duration", defaultValue = "60s")
private Duration myDuration;

fails with an exception: java.lang.RuntimeException: Error injecting java.time.Duration io.quarkus.issue.MyComponent.myDuration.

Running the application works fine since then the Quarkus Config Converters are available.

Expected behavior

Expected the Quarkus Config Converters to be available and used when running a QuarkusComponentTest test or to have some kind of mechanism with which custom converters can be registered.

Actual behavior

None of the Quarkus Config Converters are registered.

The exception is caused by the default Duration converter provided by SmallRye's io.smallrye.config.ImplicitConverters which tries to use the java.time.Duration.parse function to parse the value but this does not support the syntax: 60s.

Full stack trace:

2024-07-05 13:31:32,203 INFO  [io.qua.arc.pro.BeanProcessor] (main) Found unrecommended usage of private members (use package-private instead) in application beans:
    - @Inject field io.quarkus.issue.MyComponent#myDuration

java.lang.RuntimeException: Error injecting java.time.Duration io.quarkus.issue.MyComponent.myDuration

    at io.quarkus.issue.MyComponent_Bean.doCreate(Unknown Source)
    at io.quarkus.issue.MyComponent_Bean.create(Unknown Source)
    at io.quarkus.issue.MyComponent_Bean.create(Unknown Source)
    at io.quarkus.arc.impl.AbstractSharedContext.createInstanceHandle(AbstractSharedContext.java:119)
    at io.quarkus.arc.impl.AbstractSharedContext$1.get(AbstractSharedContext.java:38)
    at io.quarkus.arc.impl.AbstractSharedContext$1.get(AbstractSharedContext.java:35)
    at io.quarkus.arc.impl.LazyValue.get(LazyValue.java:32)
    at io.quarkus.arc.impl.ComputingCache.computeIfAbsent(ComputingCache.java:69)
    at io.quarkus.arc.impl.ComputingCacheContextInstances.computeIfAbsent(ComputingCacheContextInstances.java:19)
    at io.quarkus.arc.impl.AbstractSharedContext.get(AbstractSharedContext.java:35)
    at io.quarkus.arc.impl.ClientProxies.getApplicationScopedDelegate(ClientProxies.java:21)
    at io.quarkus.issue.MyComponent_ClientProxy.arc$delegate(Unknown Source)
    at io.quarkus.issue.MyComponent_ClientProxy.getMyDuration(Unknown Source)
    at io.quarkus.issue.MyComponentTest.testMyDuration(MyComponentTest.java:17)
    at java.base/java.lang.reflect.Method.invoke(Method.java:580)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1597)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1597)
Caused by: jakarta.enterprise.inject.CreationException: Error creating synthetic bean [WtUE35eXet8tKWS0XAkVy-_OpGY]: java.lang.IllegalArgumentException: SRCFG00039: The config property sco.maxcurrentlistener.down.duration with the config value "60s" threw an Exception whilst being converted SRCFG00020: Failed to convert value with static method
    at io.quarkus.arc.generator.Object_WtUE35eXet8tKWS0XAkVy-_OpGY_Synthetic_Bean.doCreate(Unknown Source)
    at io.quarkus.arc.generator.Object_WtUE35eXet8tKWS0XAkVy-_OpGY_Synthetic_Bean.create(Unknown Source)
    at io.quarkus.arc.generator.Object_WtUE35eXet8tKWS0XAkVy-_OpGY_Synthetic_Bean.get(Unknown Source)
    at io.quarkus.arc.impl.CurrentInjectionPointProvider.get(CurrentInjectionPointProvider.java:48)
    ... 17 more
Caused by: java.lang.IllegalArgumentException: SRCFG00039: The config property sco.maxcurrentlistener.down.duration with the config value "60s" threw an Exception whilst being converted SRCFG00020: Failed to convert value with static method
    at io.smallrye.config.SmallRyeConfig.convertValue(SmallRyeConfig.java:421)
    at io.smallrye.config.inject.ConfigProducerUtil.getValue(ConfigProducerUtil.java:100)
    at io.smallrye.config.inject.ConfigProducerUtil.getValue(ConfigProducerUtil.java:60)
    at io.quarkus.test.component.ConfigPropertyBeanCreator.create(ConfigPropertyBeanCreator.java:40)
    at io.quarkus.arc.generator.Object_WtUE35eXet8tKWS0XAkVy-_OpGY_Synthetic_Bean.createSynthetic(Unknown Source)
    ... 21 more
Caused by: java.lang.IllegalArgumentException: SRCFG00020: Failed to convert value with static method
    at io.smallrye.config.ImplicitConverters$StaticMethodConverter.convert(ImplicitConverters.java:133)
    at io.smallrye.config.SmallRyeConfig.convertValue(SmallRyeConfig.java:419)
    ... 25 more
Caused by: java.lang.reflect.InvocationTargetException
    at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:118)
    at java.base/java.lang.reflect.Method.invoke(Method.java:580)
    at io.smallrye.config.ImplicitConverters$StaticMethodConverter.convert(ImplicitConverters.java:131)
    ... 26 more
Caused by: java.time.format.DateTimeParseException: Text cannot be parsed to a Duration
    at java.base/java.time.Duration.parse(Duration.java:419)
    at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
    ... 28 more

Process finished with exit code 255

How to Reproduce?

Component:

package io.quarkus.issue;

import jakarta.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.config.inject.ConfigProperty;

import java.time.Duration;

@ApplicationScoped
public class MyComponent {

    @ConfigProperty(name = "sco.maxcurrentlistener.down.duration", defaultValue = "60s")
    private Duration myDuration;

    public String getMyDuration() {
        return myDuration.toString();
    }
}

Component Test:

package io.quarkus.issue;

import io.quarkus.test.component.QuarkusComponentTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

@QuarkusComponentTest
class MyComponentTest {

    @Inject
    MyComponent myComponent;

    @Test
    public void testMyDuration() {
        assertEquals("60s", myComponent.getMyDuration());
    }
}

Output of uname -a or ver

Darwin MacBook-Pro-8.local 22.5.0 Darwin Kernel Version 22.5.0: Mon Apr 24 20:53:19 PDT 2023; root:xnu-8796.121.2~5/RELEASE_ARM64_T6020 arm64

Output of java -version

openjdk version "22" 2024-03-19 OpenJDK Runtime Environment Temurin-22+36 (build 22+36) OpenJDK 64-Bit Server VM Temurin-22+36 (build 22+36, mixed mode)

Quarkus version or git rev

3.11.1

Build tool (ie. output of mvnw --version or gradlew --version)

Maven home: /Users/user/.m2/wrapper/dists/apache-maven-3.9.3-bin/326f10f4/apache-maven-3.9.3 Java version: 22, vendor: Eclipse Adoptium, runtime: /Users/user/.sdkman/candidates/java/22-tem Default locale: en_GB, platform encoding: UTF-8 OS name: "mac os x", version: "13.4", arch: "aarch64", family: "mac"

Additional information

I tried adding the converter manually in test code by

    @BeforeEach
    public void registerConverter() {
        var instance = ConfigProviderResolver.instance();
        var builder = instance.getBuilder()
                .withConverters(new DurationConverter())
                .build();
        instance.registerConfig(builder, MyComponentTest.class.getClassLoader());
    }

or adding in the test method itself but this does not work. The actual used Config from which the Converter is obtained is different than the one created in the test. When trying to use the correct ClassLoader

        var contextClassLoader = Thread.currentThread().getContextClassLoader();
        instance.registerConfig(builder, contextClassLoader);

the test failed since a Config is already registered for that class loader.

quarkus-bot[bot] commented 2 weeks ago

/cc @radcortez (config)

geoand commented 2 weeks ago

cc @mkouba

mkouba commented 2 weeks ago

Expected the Quarkus Config Converters to be available and used when running a QuarkusComponentTest test or to have some kind of mechanism with which custom converters can be registered.

@diversit You're right that we don't register any Quarkus-specific converter and there's also no way to modify the underlying SmallRyeConfigBuilder. And I think that we should support both.