quarkusio / quarkus

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

`QuarkusUnitTest` throws "No config found for class" when writing extension using Kotlin #35221

Open jeffawx opened 1 year ago

jeffawx commented 1 year ago

Describe the bug

When I write a Quarkus extension using Kotlin, I could't get QuarkusUnitTest mechanism working properly if I have a config class defined in runtime module and used in a build step in deployment module.

If I changed to Java, works fine.

Current workaround is manually creating a file META-INF/quarkus-config-roots.list and list the config class there, but as described in the following link this file is not supposed to be edited manually: https://quarkus.io/guides/extension-metadata#quarkus-config-roots

Apart from this issue, defining config class using Kotlin isn't nice experience, e.g. can't use data class and Kotlin native types, has to put @JvmField on each field and make sure the field is declared using var instead of val, etc.

Expected behavior

Should not throw error

Actual behavior

Can't find config class, throw exception:

Caused by: io.quarkus.builder.BuildException: Build failure: Build failed due to errors [error]: Build step demo.DemoProcessor#helloServlet threw an exception: java.lang.IllegalStateException: No config found for class demo.HelloConfig at io.quarkus.deployment.configuration.BuildTimeConfigurationReader$ReadResult.requireObjectForClass(BuildTimeConfigurationReader.java:1274) at io.quarkus.deployment.ExtensionLoader.lambda$loadStepsFromClass$55(ExtensionLoader.java:591) at io.quarkus.deployment.ExtensionLoader$3.execute(ExtensionLoader.java:860) at io.quarkus.builder.BuildContext.run(BuildContext.java:282) at org.jboss.threads.ContextHandler$1.runWith(ContextHandler.java:18) at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2513) at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1538) at java.base/java.lang.Thread.run(Thread.java:833) at org.jboss.threads.JBossThread.run(JBossThread.java:501)

at app//io.quarkus.builder.Execution.run(Execution.java:123) at app//io.quarkus.builder.BuildExecutionBuilder.execute(BuildExecutionBuilder.java:79) at app//io.quarkus.deployment.QuarkusAugmentor.run(QuarkusAugmentor.java:160) at app//io.quarkus.runner.bootstrap.AugmentActionImpl.runAugment(AugmentActionImpl.java:332) ... 59 more Caused by: java.lang.IllegalStateException: No config found for class demo.HelloConfig at io.quarkus.deployment.configuration.BuildTimeConfigurationReader$ReadResult.requireObjectForClass(BuildTimeConfigurationReader.java:1274) at io.quarkus.deployment.ExtensionLoader.lambda$loadStepsFromClass$55(ExtensionLoader.java:591) at io.quarkus.deployment.ExtensionLoader$3.execute(ExtensionLoader.java:860) at io.quarkus.builder.BuildContext.run(BuildContext.java:282) at org.jboss.threads.ContextHandler$1.runWith(ContextHandler.java:18) at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2513) at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1538) at java.base/java.lang.Thread.run(Thread.java:833) at org.jboss.threads.JBossThread.run(JBossThread.java:501)

How to Reproduce?

Example project demo this issue:

https://github.com/jeffawx/quarkus-ext-test

If this file is removed: https://github.com/jeffawx/quarkus-ext-test/blob/main/deployment/src/main/resources/META-INF/quarkus-config-roots.list

build will fail: ./gradlew clean build

Output of uname -a or ver

No response

Output of java -version

openjdk 17.0.7 2023-04-18 OpenJDK Runtime Environment Temurin-17.0.7+7 (build 17.0.7+7) OpenJDK 64-Bit Server VM Temurin-17.0.7+7 (build 17.0.7+7, mixed mode)

GraalVM version (if different from Java)

No response

Quarkus version or git rev

3.2.2.Final

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

Gradle 8.0

Additional information

No response

quarkus-bot[bot] commented 1 year ago

/cc @evanchooly (kotlin), @geoand (kotlin)

geoand commented 1 year ago

This doesn't work because an annotation processor needs to generate metadata for Config classes. Of course that annotation processor only works for Java source.

This is issue is very similar to #35110.

geoand commented 1 year ago

Just as a note, you could overcome this issue by using a @ConfigMapping class for your configuration instead of a @ConfigRoot.

Furthermore this also applies

jeffawx commented 1 year ago

In my example the annotation processor is already applied via kapt plugin, so that there is no problem to pick up the build steps, only problem is can't find config class for QuarkusUnitTest

kapt("io.quarkus:quarkus-extension-processor:3.2.2.Final")

Just to clarify only QuarkusUnitTest doesn't work, the actual application itself works fine, just can't test via QuarkusUnitTest.

This doesn't work because an annotation processor needs to generate metadata for Config classes. Of course that annotation processor only works for Java source.

This is issue is very similar to #35110.

geoand commented 1 year ago

In that case, I propose using @ConfigMapping instead

jeffawx commented 1 year ago

In that case, I propose using @ConfigMapping instead

I switched to use @ConfigMapping, now a bit more Kotlin friendly (no more val/var limitation and @JvmField), but I get same result as previously, i.e. extension still works properly in real application, but QuarkusUnitTest can't find config which can be fixed by manually put in META-INF/quarkus-config-roots.list

@ConfigRoot(phase = ConfigPhase.BUILD_TIME)
@ConfigMapping(prefix = "quarkus.my.ext")
interface HelloConfig {
    /**
     * test
     */
    @WithDefault("Hello World")
    fun text(): String
}

btw: My config class is root level, has to put @ConfigRoot or else doesn't work, I see the same pattern in Quarkus code base, e.g. https://github.com/quarkusio/quarkus/blob/main/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveServerRuntimeConfig.java

geoand commented 1 year ago

Can you update your sample with ConfigMapping?

Thanks

geoand commented 1 year ago

cc @radcortez

jeffawx commented 1 year ago

Can you update your sample with ConfigMapping?

Yep pushed the changes here: https://github.com/jeffawx/quarkus-ext-test

if you run ./gradlew test it should pass test, but if you remove META-INF/quarkus-config-roots.list will fail

Also confirmed this test extension works properly in real application.

radcortez commented 1 year ago

For what I've seen, the processor is not being executed at all. It requires:

kapt {
    keepJavacAnnotationProcessors = true
}

Now the processor executes as expected, but it does not generate the files in the standard folders. These are generated in build/tmp/kapt3/classes/. Check https://youtrack.jetbrains.com/issue/KT-22263.

I've tried to add the sources on the runtime project, with

sourceSets {
    main {
        resources {
            srcDir("build/tmp/kapt3/classes/main")
        }
    }
}

The expected config file is now in the JAR, but it seems it is not visible in the deployment project. It seems that additional source sets are not added in the module dependency. I'm not a big expert in Gradle, so maybe someone else can help here?

jeffawx commented 1 year ago

source sets are not added in the module dependency.

Thanks for the feedback!

Sorry previous example has some missing piece in build script causing it can't work in actual application.

Now I updated the example with the demo application that uses this extension, also added a README file for easy reproducing the issue: https://github.com/jeffawx/quarkus-ext-test

./gradlew test fails: No config found for interface demo.HelloConfig, not expected!!

But if publish the jar by ./gradlew publishToMavenLocal -x test, go to test-app folder, run

./gradlew quarkuDev

followed by curl localhost:8080/sayHello proves the actual application is working.

in deployment module, rename META-INF/quarkus-config-roots.list_test to META-INF/quarkus-config-roots.list, then ./gradlew test, works!
jeffawx commented 1 year ago

FYI: it seems to be some ClassLoader related problem for QuarkusUnitTest (for kapt generated resources)

Because if I wrote a simple test method adding the lines below it can find and print my config file:

val file = ServiceUtil.classNamesNamedIn(
    Thread.currentThread().contextClassLoader,
    "META-INF/quarkus-config-roots.list"
)
println(file)

Under the covers Quarkus uses ServiceUtil.classNamesNamedIn to find the resources so the only difference is classloader.

I have also played with setAllowTestClassOutsideDeployment on QuarkusUnitTest but no luck.

radcortez commented 1 year ago

It works when running the test-app because you are referencing the resulting jars of the extension, which contains the quarkus-config-root.list.

The issue here is kapt is generating the output in a different folder from build/classes or build/resources as you would expect. For a Gradle dependency project these are the expected directories to find each project module output. There should be a way to change the output path.

Another easier alternative is copy the generated file to the main build output. Adding this to the runtime module, the test passes as expected:

tasks.register<Copy>("copyConfigRoots") {
    from(layout.buildDirectory.file("tmp/kapt3/classes/main/META-INF/quarkus-config-roots.list"))
    into(layout.buildDirectory.dir("resources/main/META-INF/"))
}

tasks.named("build") {
    finalizedBy("copyConfigRoots")
}
jeffawx commented 1 year ago

Thanks @radcortez, I tried the snippet above it works!

I also found out the test code below can find config because kotlin plugin appended the directory into classpath.

In System.getProperty("java.class.path") result I can see tmp/kapt3/classes/main

Question: is it possible in future to make QuarkusUnitTest respect java.class.path so that no need to do special configuration for kotlin?

@Test
fun test() {
    println(System.getProperty("java.class.path"))

    val names = ServiceUtil.classNamesNamedIn(
        Thread.currentThread().contextClassLoader,
        "META-INF/quarkus-config-roots.list"
     )
     println(names)
}
radcortez commented 1 year ago

@aloubyansky I think this is needs to be handled in https://github.com/quarkusio/quarkus/blob/main/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/tooling/GradleApplicationModelBuilder.java