testcontainers / testcontainers-java

Testcontainers is a Java library that supports JUnit tests, providing lightweight, throwaway instances of common databases, Selenium web browsers, or anything else that can run in a Docker container.
https://testcontainers.org
MIT License
8.02k stars 1.65k forks source link

[Bug]: Compile errors with class annoteted with @CompileStatic #5785

Open piotrminkina opened 2 years ago

piotrminkina commented 2 years ago

Module

Core

Testcontainers version

1.17.3

Using the latest Testcontainers version?

Yes

Host OS

Linux

Host Arch

x86_64

Docker version

Client: Docker Engine - Community
 Version:           20.10.17
 API version:       1.41
 Go version:        go1.17.11
 Git commit:        100c701
 Built:             Mon Jun  6 23:02:57 2022
 OS/Arch:           linux/amd64
 Context:           default
 Experimental:      true

Server: Docker Engine - Community
 Engine:
  Version:          20.10.17
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.17.11
  Git commit:       a89b842
  Built:            Mon Jun  6 23:01:03 2022
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.6.7
  GitCommit:        0197261a30bf81f1ee8e6a4dd2dea0ef95d67ccb
 runc:
  Version:          1.1.3
  GitCommit:        v1.1.3-0-g6724737
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

What happened?

I'm not sure if this is a problem on the testcontainers-java side or a specificity of the Groovy language. If this is not your problem, I apologise for the wasted time.

Anyway, I've noticed a problem when calling the GenericContainer.withNetworkMode() method when it is called from a class, with the @CompileStatic annotation, immediately after calling the GenericContainer.withCommand() method. The compilation ends with an error as follows.

[Static type checking] - Cannot find matching method org.testcontainers.containers.GenericContainer#withNetworkMode(java.lang.String). Please check if the declared type is correct and if the method exists.
 @ line 9, column 25.
           def container = new GenericContainer(imageName)

The source of the problematic class is as follows:

import groovy.transform.CompileStatic
import org.testcontainers.containers.GenericContainer
import org.testcontainers.utility.DockerImageName

@CompileStatic
abstract class GenericContainerDriver {
    GenericContainerDriver() {
        def imageName = DockerImageName.parse('debian')
        def container = new GenericContainer(imageName)
            .withCommand('sleep', 'infinity') // after commenting out this line, the compilation succeeds
            .withNetworkMode('host')
    }
}

Relevant log output

$ ./gradlew assemble

> Task :compileGroovy FAILED
startup failed:
/tmp/stupid/src/main/groovy/GenericContainerDriver.groovy: 9: [Static type checking] - Cannot find matching method org.testcontainers.containers.GenericContainer#withNetworkMode(java.lang.String). Please check if the declared type is correct and if the method exists.
 @ line 9, column 25.
           def container = new GenericContainer(imageName)
                           ^

1 error

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':compileGroovy'.
> Compilation failed; see the compiler error output for details.

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.

* Get more help at https://help.gradle.org

BUILD FAILED in 2s
1 actionable task: 1 executed

Additional Information

Full example attached: stupid.tar.gz

Regards Piotr Minkina

aidando73 commented 2 years ago

@piotrminkina Idk much about Groovy or Java Generics but this seems to compile for me:

    GenericContainerDriver() {
        def imageName = DockerImageName.parse('debian')
        def container = new GenericContainer(imageName)
            .withCommand('sleep', 'infinity') // after commenting out this line, the compilation succeeds
        container = container
            .withNetworkMode('host')
    }

Does this work as a workaround for now?

This also works:

    GenericContainerDriver() {
        def imageName = DockerImageName.parse('debian')
        GenericContainer<GenericContainer> container = new GenericContainer<GenericContainer>(imageName)
            .withCommand('sleep', 'infinity') // after commenting out this line, the compilation succeeds
            .withCommand("test")
    }
aidando73 commented 2 years ago

Also, what were your plans with host network mode? I'm interested to know your use case. We're currently discussing whether or not to support host network mode.

piotrminkina commented 2 years ago

@REslim30 Thanks for reply.

Currently I'm using something like:

def container = new GenericContainer(imageName)
container.withCommand('sleep', 'infinity')
container.withNetworkMode('host')

or something like:

def container = new GenericContainer(imageName).tap {
    withCommand('sleep', 'infinity')
    withNetworkMode('host')
}

Regarding the following comment:

Also, what were your plans with host network mode? I'm interested to know your use case. We're currently discussing whether or not to support host network mode.

I am using testcontainers-java to test my Artifactory instance. This instance is run by the docker-compose plugin for Gradle. All traffic to Artifactory goes through a reverse proxy server (in my case Nginx). The Artifactory container and Nginx are on a private network, which Docker Compose configures.

Once the service stack is started, Terraform provisioning is performed (Artifactory is configured). Since it takes a very long time to start Artifactory and Terraform provisioning, I only do this once, for all test cases, and I already ensure in the test implementation that this one instance can be tested in parallel.

My test cases check that the reverse proxy server correctly forwards traffic to the Artifactory, and that the configuration of the Artifactory is done as intended and that, in general, everything works. This form of e2e testing. I check, for example, whether it is possible to download a package from an artifactory repository, for different types of repositories (e.g. Debian, NPM, Alpine, etc.).

The testcontainers-java library helps me to create a container from an image containing the necessary tools (e.g. apt, npm, apk, etc.). At the time I wrote this submission, I needed the host network to get a connection to Artifactory (via Nginx), via exposed ports 80 and 443. From the container network created by testcontainers-java, this possibility was not available, but I don't remember anymore for what reason.

I am currently using my own implementation of ExternalNetwork. This allows the containers created by testcontainers-java to directly access the Artifactory and Nginx containers, as they are on the same private network. It would be useful to have official support in testcontainers-java for external networks.

To make things more interesting, I have to use the *.example.com domain when testing, because I am testing different variants of sub-domains, for different types of repositories, e.g. debian.example.com (virtual), debian-remote.example.com (remote), debian-local.example.com (local), because downloading a package with the same name may behave differently when I download it from a virtual repository and a remote one. Docker's default DNS server implementation can't resolve subdomains, so I had to implement my own DnsmasqContainer class that implements dnsmasq and makes it configurable.

To illustrate my situation, I've included the full, but simplified code in the attachment example.tar.gz. I will be pleased if certain parts become officially supported by testcontainers-java.

aidando73 commented 2 years ago

Thanks for the detailed response @piotrminkina . We'll keep your use case in mind