spring-projects / spring-boot

Spring Boot
https://spring.io/projects/spring-boot
Apache License 2.0
75.01k stars 40.65k forks source link

Using @ServiceConnection on a Container @Bean method does not work with AOT-processed tests #42851

Open wyhasany opened 1 day ago

wyhasany commented 1 day ago

Spring Boot application fails to run the nativeTest task with Testcontainers.

I generated a simple project using start.spring.io:

image

On the first run, it fails due to a reflection issue:

...
o.t.d.DockerClientProviderStrategy       : Could not find a valid Docker environment. Please check configuration. Attempted configurations were:
    UnixSocketClientProviderStrategy: failed with exception RuntimeException (org.testcontainers.shaded.com.fasterxml.jackson.databind.JsonMappingException: Can not construct instance of com.github.dockerjava.api.model.RuntimeInfo: no suitable constructor found, can not deserialize from Object value (missing default constructor or creator, or perhaps need to add/enable type information?)
 at [Source: N/A; line: -1, column: -1] (through reference chain: com.github.dockerjava.api.model.Info["Runtimes"]->java.util.LinkedHashMap["io.containerd.runc.v2"])). Root cause JsonMappingException (Can not construct instance of com.github.dockerjava.api.model.RuntimeInfo: no suitable constructor found, can not deserialize from Object value (missing default constructor or creator, or perhaps need to add/enable type information?)
 at [Source: N/A; line: -1, column: -1] (through reference chain: com.github.dockerjava.api.model.Info["Runtimes"]->java.util.LinkedHashMap["io.containerd.runc.v2"]))
    DockerDesktopClientProviderStrategy: failed with exception RuntimeException (org.testcontainers.shaded.com.fasterxml.jackson.databind.JsonMappingException: Can not construct instance of com.github.dockerjava.api.model.RuntimeInfo: no suitable constructor found, can not deserialize from Object value (missing default constructor or creator, or perhaps need to add/enable type information?)
 at [Source: N/A; line: -1, column: -1] (through reference chain: com.github.dockerjava.api.model.Info["Runtimes"]->java.util.LinkedHashMap["io.containerd.runc.v2"])). Root cause JsonMappingException (Can not construct instance of com.github.dockerjava.api.model.RuntimeInfo: no suitable constructor found, can not deserialize from Object value (missing default constructor or creator, or perhaps need to add/enable type information?)
 at [Source: N/A; line: -1, column: -1] (through reference chain: com.github.dockerjava.api.model.Info["Runtimes"]->java.util.LinkedHashMap["io.containerd.runc.v2"]))As no valid configuration was found, execution cannot continue.
See https://java.testcontainers.org/on_failure.html for more details.
...

I solved this issue by adding a reflect.json file in src/test/resources/META-INF/native-image/ with the following content::

[
  {
    "name":"com.github.dockerjava.api.model.RuntimeInfo",
    "allDeclaredFields":true,
    "queryAllDeclaredMethods":true,
    "queryAllDeclaredConstructors":true,
    "methods":[{"name":"<init>","parameterTypes":[] }, {"name":"setPath","parameterTypes":["java.lang.String"] }]
  },
  {
    "name":"com.github.dockerjava.api.model.Info",
    "allDeclaredFields":true,
    "queryAllDeclaredMethods":true,
    "queryAllDeclaredConstructors":true,
    "methods":[{"name":"<init>","parameterTypes":[] }]
  }
]

However, I'm confused because Testcontainers seems to be supported by GraalVM as indicated here: https://www.graalvm.org/native-image/libraries-and-frameworks/

With that configuration, I encountered another issue that I'm unsure how to resolve:

> Task :nativeTest
JUnit Platform on Native Image - report
----------------------------------------

12:37:58.794 [main] INFO org.springframework.test.context.support.AnnotationConfigContextLoaderUtils -- Could not detect default configuration classes for test class [com.example.demo.DemoApplicationTests]: DemoApplicationTests does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
12:37:58.797 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper -- Found @SpringBootConfiguration com.example.demo.DemoApplication for test class com.example.demo.DemoApplicationTests

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v3.3.4)

2024-10-23T12:37:58.813+02:00  INFO 9667 --- [demo] [           main] com.example.demo.DemoApplicationTests    : Starting AOT-processed DemoApplicationTests using Java 21.0.4 with PID 9667 (started by rowickim in /private/tmp/demo)
2024-10-23T12:37:58.813+02:00  INFO 9667 --- [demo] [           main] com.example.demo.DemoApplicationTests    : No active profile set, falling back to 1 default profile: "default"
2024-10-23T12:37:58.816+02:00  INFO 9667 --- [demo] [           main] org.testcontainers.images.PullPolicy     : Image pull policy will be performed by: DefaultPullPolicy()
2024-10-23T12:37:58.816+02:00  INFO 9667 --- [demo] [           main] o.t.utility.ImageNameSubstitutor         : Image name substitution will be performed by: DefaultImageNameSubstitutor (composite of 'ConfigurationFileImageNameSubstitutor' and 'PrefixingImageNameSubstitutor')
2024-10-23T12:37:58.825+02:00  INFO 9667 --- [demo] [           main] o.t.d.DockerClientProviderStrategy       : Loaded org.testcontainers.dockerclient.UnixSocketClientProviderStrategy from ~/.testcontainers.properties, will try it first
2024-10-23T12:37:58.846+02:00  INFO 9667 --- [demo] [           main] o.t.d.DockerClientProviderStrategy       : Found Docker environment with local Unix socket (unix:///var/run/docker.sock)
2024-10-23T12:37:58.847+02:00  INFO 9667 --- [demo] [           main] org.testcontainers.DockerClientFactory   : Docker host IP address is localhost
2024-10-23T12:37:58.852+02:00  INFO 9667 --- [demo] [           main] org.testcontainers.DockerClientFactory   : Connected to docker:
  Server Version: 27.2.0
  API Version: 1.47
  Operating System: Docker Desktop
  Total Memory: 7838 MB
2024-10-23T12:37:58.852+02:00  WARN 9667 --- [demo] [           main] o.testcontainers.utility.ResourceReaper  :
********************************************************************************
Ryuk has been disabled. This can cause unexpected behavior in your environment.
********************************************************************************
2024-10-23T12:37:58.852+02:00  INFO 9667 --- [demo] [           main] org.testcontainers.DockerClientFactory   : Checking the system...
2024-10-23T12:37:58.852+02:00  INFO 9667 --- [demo] [           main] org.testcontainers.DockerClientFactory   : ✔︎ Docker server version should be at least 1.6.0
2024-10-23T12:37:58.859+02:00  INFO 9667 --- [demo] [           main] tc.mongo:latest                          : Creating container for image: mongo:latest
2024-10-23T12:37:59.004+02:00  INFO 9667 --- [demo] [           main] tc.mongo:latest                          : Container mongo:latest is starting: cb2dd662645f26c2cb8ffdf5dd73d4332628b60c2a5e6487fcfe101c4480456e
2024-10-23T12:37:59.877+02:00  INFO 9667 --- [demo] [           main] tc.mongo:latest                          : Container mongo:latest started in PT1.018256S
2024-10-23T12:38:00.339+02:00  WARN 9667 --- [demo] [           main] o.s.c.support.GenericApplicationContext  : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'standardMongoSettingsCustomizer': Unsatisfied dependency expressed through method 'standardMongoSettingsCustomizer' parameter 1: No qualifying bean of type 'org.springframework.boot.autoconfigure.mongo.MongoConnectionDetails' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}

It looks like the task aotTestClasses doesn't add any MongoConnectionDetails to the application context.

Here is the zipped project for reference: demo.zip

Environment Details

I'm using the following version of GraalVM:

❯ java -version
java version "21.0.4" 2024-07-16 LTS
Java(TM) SE Runtime Environment Oracle GraalVM 21.0.4+8.1 (build 21.0.4+8-LTS-jvmci-23.1-b41)
Java HotSpot(TM) 64-Bit Server VM Oracle GraalVM 21.0.4+8.1 (build 21.0.4+8-LTS-jvmci-23.1-b41, mixed mode, sharing)
wilkinsona commented 1 day ago

This is similar to https://github.com/spring-projects/spring-boot/issues/35663, although that issue was using @ServiceConnection on a field in a test class whereas this issue's using @ServiceConnection on a @Bean method. I wonder if that difference may be where the problem lies with the fix for #35663 possibly going too far and adversely affecting the @Bean case as well as fixing the field case.

wyhasany commented 1 day ago

@wilkinsona it works with following configuration:

@SpringBootTest
@Testcontainers
class DemoApplicationTests {

    @Container
    @ServiceConnection
    static MongoDBContainer mongoDbContainer = new MongoDBContainer(DockerImageName.parse("mongo:latest"));

    @Test
    void contextLoads() {
    }

}
wilkinsona commented 1 day ago

That's good to know, @wyhasany. Thank you. We'll see what we can do for the @Bean approach.

nosan commented 1 day ago

@wilkinsona I've been trying to use this way:

@SpringBootTest
@ImportTestcontainers
class DemoApplicationTests {

    @ServiceConnection
    private static final MongoDBContainer mongoDbContainer =
            new MongoDBContainer(DockerImageName.parse("mongo:latest"));

    @Test
    void contextLoads() {
    }

}

it also fails due to the following:

Exception in thread "main" org.springframework.test.context.aot.TestContextAotException: Failed to generate AOT artifacts for test classes [com.example.demo.DemoApplicationTests]
    at org.springframework.test.context.aot.TestContextAotGenerator.lambda$processAheadOfTime$5(TestContextAotGenerator.java:286)
    at java.base/java.util.LinkedHashMap.forEach(LinkedHashMap.java:721)
    at org.springframework.util.MultiValueMapAdapter.forEach(MultiValueMapAdapter.java:179)
    at org.springframework.test.context.aot.TestContextAotGenerator.processAheadOfTime(TestContextAotGenerator.java:244)
    at org.springframework.test.context.aot.TestContextAotGenerator.processAheadOfTime(TestContextAotGenerator.java:205)
    at org.springframework.test.context.aot.TestAotProcessor.performAotProcessing(TestAotProcessor.java:91)
    at org.springframework.test.context.aot.TestAotProcessor.doProcess(TestAotProcessor.java:72)
    at org.springframework.test.context.aot.TestAotProcessor.doProcess(TestAotProcessor.java:39)
    at org.springframework.context.aot.AbstractAotProcessor.process(AbstractAotProcessor.java:82)
    at org.springframework.boot.test.context.SpringBootTestAotProcessor.main(SpringBootTestAotProcessor.java:63)
Caused by: org.springframework.test.context.aot.TestContextAotException: Failed to process test class [com.example.demo.DemoApplicationTests] for AOT
    at org.springframework.test.context.aot.TestContextAotGenerator.processAheadOfTime(TestContextAotGenerator.java:323)
Caused by: org.springframework.test.context.aot.TestContextAotException: Failed to process test class [com.example.demo.DemoApplicationTests] for AOT

    at org.springframework.test.context.aot.TestContextAotGenerator.lambda$processAheadOfTime$5(TestContextAotGenerator.java:277)
    ... 9 more
Caused by: java.lang.IllegalStateException: Error processing bean with name 'importTestContainer.com.example.demo.DemoApplicationTests.mongoDbContainer': instance supplier is not supported
    at org.springframework.beans.factory.aot.DefaultBeanRegistrationCodeFragments.getTarget(DefaultBeanRegistrationCodeFragments.java:83)
Caused by: java.lang.IllegalStateException: Error processing bean with name 'importTestContainer.com.example.demo.DemoApplicationTests.mongoDbContainer': instance supplier is not supported

    at org.springframework.beans.factory.aot.BeanDefinitionMethodGenerator.generateBeanDefinitionMethod(BeanDefinitionMethodGenerator.java:85)
    at org.springframework.beans.factory.aot.BeanRegistrationsAotContribution.lambda$generateRegisterBeanDefinitionsMethod$2(BeanRegistrationsAotContribution.java:90)
    at java.base/java.util.LinkedHashMap.forEach(LinkedHashMap.java:721)
    at org.springframework.beans.factory.aot.BeanRegistrationsAotContribution.generateRegisterBeanDefinitionsMethod(BeanRegistrationsAotContribution.java:88)
    at org.springframework.beans.factory.aot.BeanRegistrationsAotContribution.lambda$applyTo$1(BeanRegistrationsAotContribution.java:73)
    at org.springframework.aot.generate.GeneratedMethod.<init>(GeneratedMethod.java:54)
    at org.springframework.aot.generate.GeneratedMethods.add(GeneratedMethods.java:112)
    at org.springframework.aot.generate.GeneratedMethods.add(GeneratedMethods.java:89)
    at org.springframework.beans.factory.aot.BeanRegistrationsAotContribution.applyTo(BeanRegistrationsAotContribution.java:72)
    at org.springframework.context.aot.BeanFactoryInitializationAotContributions.applyTo(BeanFactoryInitializationAotContributions.java:78)
    at org.springframework.context.aot.ApplicationContextAotGenerator.lambda$processAheadOfTime$0(ApplicationContextAotGenerator.java:58)
    at org.springframework.context.aot.ApplicationContextAotGenerator.withCglibClassHandler(ApplicationContextAotGenerator.java:67)
    at org.springframework.context.aot.ApplicationContextAotGenerator.processAheadOfTime(ApplicationContextAotGenerator.java:53)
    at org.springframework.test.context.aot.TestContextAotGenerator.processAheadOfTime(TestContextAotGenerator.java:319)
    ... 10 more
wilkinsona commented 1 day ago

That's interesting. Thanks, @nosan. That looks like another variant of #35663 as AOT processing is occurring but it's trying to produce something that isn't valid. We might need a separate issue for that, but let's just stick with this one for now until we have a complete understanding of what works, what doesn't work, and why.

wilkinsona commented 20 hours ago

I've opened https://github.com/spring-projects/spring-boot/issues/42875 for the problem with @ImportTestcontainers.

wilkinsona commented 19 hours ago

Undoing the fix for #35663 results in the following failure during AOT processing:

Exception in thread "main" org.springframework.test.context.aot.TestContextAotException: Failed to generate AOT artifacts for test classes [com.example.demo.DemoApplicationTests]
        at org.springframework.test.context.aot.TestContextAotGenerator.lambda$processAheadOfTime$5(TestContextAotGenerator.java:286)
        at java.base/java.util.LinkedHashMap.forEach(LinkedHashMap.java:721)
        at org.springframework.util.MultiValueMapAdapter.forEach(MultiValueMapAdapter.java:179)
        at org.springframework.test.context.aot.TestContextAotGenerator.processAheadOfTime(TestContextAotGenerator.java:244)
        at org.springframework.test.context.aot.TestContextAotGenerator.processAheadOfTime(TestContextAotGenerator.java:205)
        at org.springframework.test.context.aot.TestAotProcessor.performAotProcessing(TestAotProcessor.java:91)
        at org.springframework.test.context.aot.TestAotProcessor.doProcess(TestAotProcessor.java:72)
        at org.springframework.test.context.aot.TestAotProcessor.doProcess(TestAotProcessor.java:39)
        at org.springframework.context.aot.AbstractAotProcessor.process(AbstractAotProcessor.java:81)
        at org.springframework.boot.test.context.SpringBootTestAotProcessor.main(SpringBootTestAotProcessor.java:63)
Caused by: org.springframework.test.context.aot.TestContextAotException: Failed to process test class [com.example.demo.DemoApplicationTests] for AOT
        at org.springframework.test.context.aot.TestContextAotGenerator.processAheadOfTime(TestContextAotGenerator.java:323)
        at org.springframework.test.context.aot.TestContextAotGenerator.lambda$processAheadOfTime$5(TestContextAotGenerator.java:277)
        ... 9 more
Caused by: java.lang.IllegalStateException: Error processing bean with name 'mongoContainerConnectionDetailsForMongoDbContainer': instance supplier is not supported
        at org.springframework.beans.factory.aot.DefaultBeanRegistrationCodeFragments.getTarget(DefaultBeanRegistrationCodeFragments.java:83)
        at org.springframework.beans.factory.aot.BeanDefinitionMethodGenerator.generateBeanDefinitionMethod(BeanDefinitionMethodGenerator.java:85)
        at org.springframework.beans.factory.aot.BeanRegistrationsAotContribution.lambda$generateRegisterBeanDefinitionsMethod$2(BeanRegistrationsAotContribution.java:90)
        at java.base/java.util.LinkedHashMap.forEach(LinkedHashMap.java:721)
        at org.springframework.beans.factory.aot.BeanRegistrationsAotContribution.generateRegisterBeanDefinitionsMethod(BeanRegistrationsAotContribution.java:88)
        at org.springframework.beans.factory.aot.BeanRegistrationsAotContribution.lambda$applyTo$1(BeanRegistrationsAotContribution.java:73)
        at org.springframework.aot.generate.GeneratedMethod.<init>(GeneratedMethod.java:54)
        at org.springframework.aot.generate.GeneratedMethods.add(GeneratedMethods.java:112)
        at org.springframework.aot.generate.GeneratedMethods.add(GeneratedMethods.java:89)
        at org.springframework.beans.factory.aot.BeanRegistrationsAotContribution.applyTo(BeanRegistrationsAotContribution.java:72)
        at org.springframework.context.aot.BeanFactoryInitializationAotContributions.applyTo(BeanFactoryInitializationAotContributions.java:78)
        at org.springframework.context.aot.ApplicationContextAotGenerator.lambda$processAheadOfTime$0(ApplicationContextAotGenerator.java:58)
        at org.springframework.context.aot.ApplicationContextAotGenerator.withCglibClassHandler(ApplicationContextAotGenerator.java:67)
        at org.springframework.context.aot.ApplicationContextAotGenerator.processAheadOfTime(ApplicationContextAotGenerator.java:53)
        at org.springframework.test.context.aot.TestContextAotGenerator.processAheadOfTime(TestContextAotGenerator.java:319)
        ... 10 more

This is caused by ConnectionDetailsRegistrar defining a bean that uses an instance supplier:

https://github.com/spring-projects/spring-boot/blob/8ff30843fd71940d674dfddb78f5528fa1157d09/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ConnectionDetailsRegistrar.java#L105

Unlike #35663, where there is a test context customizer that will register the beans when the tests are being run, skipping these beans results in them not being defined at all. It looks like we need to find a way to register them during AOT processing without using an instance supplier.