quarkus icon indicating copy to clipboard operation
quarkus copied to clipboard

[Test][ClassLoader] Quarkus 3.22 throws when tests run with junit-platform.properties resource file

Open lloydmeta opened this issue 7 months ago • 7 comments

Describe the bug

Might be related to https://quarkus.io/blog/test-classloading-rewrite/

If you create a junit-platform.properties file in src/test/resources and run ./gradlew test it throws

java.lang.RuntimeException: Internal error. The test class class com.beachape.GreetingResourceTest should have been loaded with a QuarkusClassLoader, but instead it was loaded with jdk.internal.loader.ClassLoaders$AppClassLoader@2626b418. This is caused by the FacadeClassLoader not correctly identifying this class as a QuarkusTest.

Prior to 3.22, this wouldn't throw.

Expected behavior

It should run.

Actual behavior

It throws

java.lang.RuntimeException: Internal error. The test class class com.beachape.GreetingResourceTest should have been loaded with a QuarkusClassLoader, but instead it was loaded with jdk.internal.loader.ClassLoaders$AppClassLoader@2626b418. This is caused by the FacadeClassLoader not correctly identifying this class as a QuarkusTest.
	at io.quarkus.test.junit.QuarkusTestExtension.getClassLoaderFromTestClass(QuarkusTestExtension.java:337)
	at io.quarkus.test.junit.QuarkusTestExtension.ensureStarted(QuarkusTestExtension.java:631)
	at io.quarkus.test.junit.QuarkusTestExtension.beforeAll(QuarkusTestExtension.java:712)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1597)

How to Reproduce?

https://github.com/lloydmeta/quarkus-3.22-test-bug-repo

Output of uname -a or ver

24.4.0 Darwin Kernel Version 24.4.0: Fri Apr 11 18:28:23 PDT 2025; root:xnu-11417.101.15~117/RELEASE_X86_64 x86_64

Output of java -version

java 23.0.2 2025-01-21

Quarkus version or git rev

3.22.1

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

Gradle 8.13

Additional information

❯ ./gradlew test --no-build-cache
Discovered 2 'junit-platform.properties' configuration files on the classpath (see below); only the first (*) will be used.
- file:/Users/lloyd/Documents/djava/quarkus-test-bug-repo/build/resources/test/junit-platform.properties (*)
- jar:file:/Users/lloyd/.gradle/caches/modules-2/files-2.1/io.quarkus/quarkus-junit5-config/3.21.4/1754ab3d86388a2957df43f208fbc8f9417c47eb/quarkus-junit5-config-3.21.4.jar!/junit-platform.properties
Discovered 2 'junit-platform.properties' configuration files on the classpath (see below); only the first (*) will be used.
- file:/Users/lloyd/Documents/djava/quarkus-test-bug-repo/build/resources/test/junit-platform.properties (*)
- jar:file:/Users/lloyd/.gradle/caches/modules-2/files-2.1/io.quarkus/quarkus-junit5-config/3.21.4/1754ab3d86388a2957df43f208fbc8f9417c47eb/quarkus-junit5-config-3.21.4.jar!/junit-platform.properties

[Incubating] Problems report is available at: file:///Users/lloyd/Documents/djava/quarkus-test-bug-repo/build/reports/problems/problems-report.html

Deprecated Gradle features were used in this build, making it incompatible with Gradle 9.0.

You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins.

For more on this, please refer to https://docs.gradle.org/8.13/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation.

BUILD SUCCESSFUL in 31s
11 actionable tasks: 11 executed
❯ ./gradlew test --no-build-cache
Discovered 2 'junit-platform.properties' configuration files on the classpath (see below); only the first (*) will be used.
- file:/Users/lloyd/Documents/djava/quarkus-test-bug-repo/build/resources/test/junit-platform.properties (*)
- jar:file:/Users/lloyd/.gradle/caches/modules-2/files-2.1/io.quarkus/quarkus-junit5-config/3.22.1/d44e0e34a555e8291c2d8af1cef830283d387b85/quarkus-junit5-config-3.22.1.jar!/junit-platform.properties
Discovered 2 'junit-platform.properties' configuration files on the classpath (see below); only the first (*) will be used.
- file:/Users/lloyd/Documents/djava/quarkus-test-bug-repo/build/resources/test/junit-platform.properties (*)
- jar:file:/Users/lloyd/.gradle/caches/modules-2/files-2.1/io.quarkus/quarkus-junit5-config/3.22.1/d44e0e34a555e8291c2d8af1cef830283d387b85/quarkus-junit5-config-3.22.1.jar!/junit-platform.properties

> Task :test FAILED

GreetingResourceTest > initializationError FAILED
    java.lang.RuntimeException at QuarkusTestExtension.java:337

1 test completed, 1 failed

[Incubating] Problems report is available at: file:///Users/lloyd/Documents/djava/quarkus-test-bug-repo/build/reports/problems/problems-report.html

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':test'.
> There were failing tests. See the report at: file:///Users/lloyd/Documents/djava/quarkus-test-bug-repo/build/reports/tests/test/index.html

* Try:
> Run with --scan to get full insights.

Deprecated Gradle features were used in this build, making it incompatible with Gradle 9.0.

You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins.

For more on this, please refer to https://docs.gradle.org/8.13/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation.

BUILD FAILED in 5s
11 actionable tasks: 9 executed, 2 up-to-date

Workarounds

  • Going back to 3.21.4 works
  • Removing the properties file works

lloydmeta avatar May 01 '25 01:05 lloydmeta

I wonder if my own issue is related? If you have a package-private constructor or method in one (gradle) subproject (call it "A") and try to use it in another subproject (call it "tests") via extending the class (yes, split-package is discouraged, and this is probably why, but I digress), the class in "tests" is loaded in a different classloader than the parent class in "A", causing an IllegalAccessError:

project_common/src/main/java/com/example/ExampleInterface.java:

public interface ExampleInterface {
    List<String> performSomeAlgorithm(POJO object, int count);
}

project_A/src/main/java/com/example/ProductionImplementationClass.java:

public class ProductionImplementationClass implements ExampleInterface {
    public ProductionImplementationClass(String param) {
        // impl elided
    }
    @VisibleForTesting
    ProductionImplementationClass() {
        // impl elided
    }
    @VisibleForTesting
    List<String> performSomeAlgorithm(POJO object, int count) {
        // impl elided
    }
}

tests/src/main/java/com/example/TestProductionImplementationClass.java:

public class TestProductionImplementationClass extends ProductionImplementationClass {
    List<String> testPerformSomeAlgorithm(POJO object, int count) {
        return performSomeAlgorithm(object, count);
    }
}

This compiles perfectly fine, but when it's executed we find that ProductionImplementationClass is loaded in a different classloader than TestProductionImplementationClass so cannot invoke the constructor (either implicitly or explicitly) nor the method under test. Note that the test class is in the main source set: this is intentional.

For context: in our service, project_common is a subproject that contains code common to all applications contained in the root project, project_A is one self-contained application, and tests is our test framework and tests. project_A depends on project_common via a gradle implementation dependency, and tests depends on both also via a gradle implementation dependency declaration.

This exact arrangement functions as-is in 3.21.4 but breaks on upgrade to 3.22.1. I'm not opposed to changing the visibility of the methods/ctors in question, but I wonder what exact change may have led to this and whether it could be a bug or intended behaviour. And, yes, split-packages are discouraged, and I should probably make the change if only for that reason alone, but again... same question: bug or intentional.

kevinross avatar May 01 '25 15:05 kevinross

@kevinross could you maybe create a separate issue with a reproducer? I'm not sure both are related and it's probably better to track them separately.

gsmet avatar May 01 '25 16:05 gsmet

/cc @geoand (testing)

quarkus-bot[bot] avatar May 01 '25 16:05 quarkus-bot[bot]

/cc @holly-cummins

gsmet avatar May 01 '25 16:05 gsmet

Ah yes, this is definitely related to my changes. There may be a workaround, I'll have a look.

holly-cummins avatar May 01 '25 16:05 holly-cummins

Should I create a new ticket then...? I commented on this one due to the clear connection to classloaders in the stacktrace - but I'm happy to create a new one and attach a proper reproducer.

kevinross avatar May 01 '25 19:05 kevinross

Yes please, @kevinross. The root cause is most likely the same (https://github.com/quarkusio/quarkus/pull/34681), but I suspect the fix for your issue will be very different. The changes in 3.22 are intended to avoid the sort of "tests in wrong classloader" problem you're seeing but obviously didn't have the intended effect for your scenario.

holly-cummins avatar May 01 '25 21:05 holly-cummins

@holly-cummins here you go!

kevinross avatar May 05 '25 12:05 kevinross

This is still an issue in 3.22.3 but now the stack trace is different:

org.gradle.api.internal.tasks.testing.TestSuiteExecutionException: Could not complete execution for Gradle Test Executor 21.
	at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.stop(SuiteTestClassProcessor.java:65)
	at [email protected]/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
	at [email protected]/java.lang.reflect.Method.invoke(Method.java:580)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
	at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
	at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:92)
	at jdk.proxy1/jdk.proxy1.$Proxy4.stop(Unknown Source)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker$3.run(TestWorker.java:200)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:132)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63)
	at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
	at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:121)
	at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:71)
	at app//worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
	at app//worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
Caused by: java.lang.NullPointerException: Cannot invoke "io.quarkus.test.junit.classloading.FacadeClassLoader.isServiceLoaderMechanism()" because "io.quarkus.test.junit.launcher.CustomLauncherInterceptor.facadeLoader" is null
	at io.quarkus.test.junit.launcher.CustomLauncherInterceptor.launcherDiscoveryFinished(CustomLauncherInterceptor.java:101)
	at org.junit.platform.launcher.listeners.discovery.CompositeLauncherDiscoveryListener.lambda$launcherDiscoveryFinished$1(CompositeLauncherDiscoveryListener.java:45)
	at org.junit.platform.commons.util.CollectionUtils.forEachInReverseOrder(CollectionUtils.java:243)
	at org.junit.platform.launcher.listeners.discovery.CompositeLauncherDiscoveryListener.launcherDiscoveryFinished(CompositeLauncherDiscoveryListener.java:45)
	at org.junit.platform.launcher.core.EngineDiscoveryOrchestrator.discover(EngineDiscoveryOrchestrator.java:110)
	at org.junit.platform.launcher.core.EngineDiscoveryOrchestrator.discover(EngineDiscoveryOrchestrator.java:78)
	at org.junit.platform.launcher.core.DefaultLauncher.discover(DefaultLauncher.java:99)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:85)
	at org.junit.platform.launcher.core.DelegatingLauncher.execute(DelegatingLauncher.java:47)
	at org.junit.platform.launcher.core.InterceptingLauncher.lambda$execute$1(InterceptingLauncher.java:39)
	at org.junit.platform.launcher.core.ClasspathAlignmentCheckingLauncherInterceptor.intercept(ClasspathAlignmentCheckingLauncherInterceptor.java:25)
	at org.junit.platform.launcher.core.InterceptingLauncher.execute(InterceptingLauncher.java:38)
	at org.junit.platform.launcher.core.DelegatingLauncher.execute(DelegatingLauncher.java:47)
	at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.processAllTestClasses(JUnitPlatformTestClassProcessor.java:124)
	at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.access$000(JUnitPlatformTestClassProcessor.java:99)
	at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor.stop(JUnitPlatformTestClassProcessor.java:94)
	at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.stop(SuiteTestClassProcessor.java:63)
	... 16 more

rodcheater avatar May 23 '25 02:05 rodcheater

This issue is not just related to gralde, maven has the same issue.

Reproducer:

quarkus create app org.acme:getting-started
cd getting-started
mkdir -p src/test/resources/
touch src/test/resources/junit-platform.properties
mvn clean verify

Running Apache Maven 3.9.9 with jdk 21

jagodevreede avatar May 23 '25 06:05 jagodevreede

Thanks for the update, @rodcheater! And thanks for letting us know about maven, @jagodevreede. I've updated the blog to reflect the broader scope of the issue.

holly-cummins avatar May 23 '25 08:05 holly-cummins

This is still an issue in 3.22.3 but now the stack trace is different:

org.gradle.api.internal.tasks.testing.TestSuiteExecutionException: Could not complete execution for Gradle Test Executor 21.
	at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.stop(SuiteTestClassProcessor.java:65)
	at [email protected]/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
	at [email protected]/java.lang.reflect.Method.invoke(Method.java:580)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
	at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
	at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:92)
	at jdk.proxy1/jdk.proxy1.$Proxy4.stop(Unknown Source)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker$3.run(TestWorker.java:200)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:132)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63)
	at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
	at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:121)
	at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:71)
	at app//worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
	at app//worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
Caused by: java.lang.NullPointerException: Cannot invoke "io.quarkus.test.junit.classloading.FacadeClassLoader.isServiceLoaderMechanism()" because "io.quarkus.test.junit.launcher.CustomLauncherInterceptor.facadeLoader" is null
	at io.quarkus.test.junit.launcher.CustomLauncherInterceptor.launcherDiscoveryFinished(CustomLauncherInterceptor.java:101)
	at org.junit.platform.launcher.listeners.discovery.CompositeLauncherDiscoveryListener.lambda$launcherDiscoveryFinished$1(CompositeLauncherDiscoveryListener.java:45)
	at org.junit.platform.commons.util.CollectionUtils.forEachInReverseOrder(CollectionUtils.java:243)
	at org.junit.platform.launcher.listeners.discovery.CompositeLauncherDiscoveryListener.launcherDiscoveryFinished(CompositeLauncherDiscoveryListener.java:45)
	at org.junit.platform.launcher.core.EngineDiscoveryOrchestrator.discover(EngineDiscoveryOrchestrator.java:110)
	at org.junit.platform.launcher.core.EngineDiscoveryOrchestrator.discover(EngineDiscoveryOrchestrator.java:78)
	at org.junit.platform.launcher.core.DefaultLauncher.discover(DefaultLauncher.java:99)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:85)
	at org.junit.platform.launcher.core.DelegatingLauncher.execute(DelegatingLauncher.java:47)
	at org.junit.platform.launcher.core.InterceptingLauncher.lambda$execute$1(InterceptingLauncher.java:39)
	at org.junit.platform.launcher.core.ClasspathAlignmentCheckingLauncherInterceptor.intercept(ClasspathAlignmentCheckingLauncherInterceptor.java:25)
	at org.junit.platform.launcher.core.InterceptingLauncher.execute(InterceptingLauncher.java:38)
	at org.junit.platform.launcher.core.DelegatingLauncher.execute(DelegatingLauncher.java:47)
	at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.processAllTestClasses(JUnitPlatformTestClassProcessor.java:124)
	at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.access$000(JUnitPlatformTestClassProcessor.java:99)
	at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor.stop(JUnitPlatformTestClassProcessor.java:94)
	at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.stop(SuiteTestClassProcessor.java:63)
	... 16 more

We are experiencing this same issue when trying to upgrade to 3.22.3. I gave 3.23.0.CR1 a shot as well but it's still there. 3.20.1 works fine.

lspahija avatar May 27 '25 16:05 lspahija

As a workaround for this, putting the following into your project's junit-platform.properties file should get things working:

junit.jupiter.extensions.autodetection.enabled=true
junit.jupiter.testclass.order.default=io.quarkus.test.config.QuarkusClassOrderer
junit.platform.launcher.interceptors.enabled=true

holly-cummins avatar May 27 '25 20:05 holly-cummins

@holly-cummins that seems to work. thanks!

lspahija avatar May 27 '25 21:05 lspahija

I've got a PR now which fixes this. But in general, having a junit-platform.properties can still cause problems in some cases. @lspahija, if you're using the properties to register a class orderer, it's better to list that orderer in the Quarkus application properties (see https://quarkus.io/guides/getting-started-testing#testing_different_profiles).

holly-cummins avatar May 29 '25 13:05 holly-cummins