From f1415e659a134c9e5a70bfcb737152b3ae1ff5bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20de=20la=20Pe=C3=B1a?= Date: Fri, 30 May 2025 05:10:16 +0200 Subject: [PATCH 1/5] feat: make exposing ports configurable by config --- internal/config/config.go | 10 ++ internal/config/config_test.go | 216 +++++++++++++++++++-------------- 2 files changed, 135 insertions(+), 91 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index 64f2f7fb13..ef8d2f5d46 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -85,6 +85,11 @@ type Config struct { // // Environment variable: TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE TestcontainersHost string `properties:"tc.host,default="` + + // TestcontainersPortMappingTimeout is the time to wait before all the exposed ports are mapped. + // + // Environment variable: TESTCONTAINERS_PORT_MAPPING_TIMEOUT + TestcontainersPortMappingTimeout time.Duration `properties:"tc.port.mapping.timeout,default=5s"` } // } @@ -141,6 +146,11 @@ func read() Config { config.RyukConnectionTimeout = timeout } + testcontainersPortMappingTimeoutEnv := readTestcontainersEnv("TESTCONTAINERS_PORT_MAPPING_TIMEOUT") + if timeout, err := time.ParseDuration(testcontainersPortMappingTimeoutEnv); err == nil { + config.TestcontainersPortMappingTimeout = timeout + } + return config } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 591fcff11c..7b090bcc6a 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -26,6 +26,7 @@ func resetTestEnv(t *testing.T) { t.Setenv("RYUK_VERBOSE", "") t.Setenv("RYUK_RECONNECTION_TIMEOUT", "") t.Setenv("RYUK_CONNECTION_TIMEOUT", "") + t.Setenv("TESTCONTAINERS_PORT_MAPPING_TIMEOUT", "") } func TestReadConfig(t *testing.T) { @@ -79,16 +80,17 @@ func TestReadTCConfig(t *testing.T) { t.Setenv("TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED", "true") t.Setenv("RYUK_RECONNECTION_TIMEOUT", "13s") t.Setenv("RYUK_CONNECTION_TIMEOUT", "12s") - + t.Setenv("TESTCONTAINERS_PORT_MAPPING_TIMEOUT", "11s") config := read() expected := Config{ - HubImageNamePrefix: defaultHubPrefix, - RyukDisabled: true, - RyukPrivileged: true, - Host: "", // docker socket is empty at the properties file - RyukReconnectionTimeout: 13 * time.Second, - RyukConnectionTimeout: 12 * time.Second, + HubImageNamePrefix: defaultHubPrefix, + RyukDisabled: true, + RyukPrivileged: true, + Host: "", // docker socket is empty at the properties file + RyukReconnectionTimeout: 13 * time.Second, + RyukConnectionTimeout: 12 * time.Second, + TestcontainersPortMappingTimeout: 11 * time.Second, } assert.Equal(t, expected, config) @@ -128,15 +130,17 @@ func TestReadTCConfig(t *testing.T) { t.Setenv("RYUK_VERBOSE", "true") t.Setenv("RYUK_RECONNECTION_TIMEOUT", "13s") t.Setenv("RYUK_CONNECTION_TIMEOUT", "12s") + t.Setenv("TESTCONTAINERS_PORT_MAPPING_TIMEOUT", "11s") config := read() expected := Config{ - HubImageNamePrefix: defaultHubPrefix, - RyukDisabled: true, - RyukPrivileged: true, - RyukVerbose: true, - RyukReconnectionTimeout: 13 * time.Second, - RyukConnectionTimeout: 12 * time.Second, + HubImageNamePrefix: defaultHubPrefix, + RyukDisabled: true, + RyukPrivileged: true, + RyukVerbose: true, + RyukReconnectionTimeout: 13 * time.Second, + RyukConnectionTimeout: 12 * time.Second, + TestcontainersPortMappingTimeout: 11 * time.Second, } assert.Equal(t, expected, config) @@ -145,9 +149,11 @@ func TestReadTCConfig(t *testing.T) { t.Run("HOME contains TC properties file", func(t *testing.T) { defaultRyukConnectionTimeout := 60 * time.Second defaultRyukReconnectionTimeout := 10 * time.Second + defaultTestcontainersPortMappingTimeout := 5 * time.Second defaultConfig := Config{ - RyukConnectionTimeout: defaultRyukConnectionTimeout, - RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + RyukConnectionTimeout: defaultRyukConnectionTimeout, + RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + TestcontainersPortMappingTimeout: defaultTestcontainersPortMappingTimeout, } tests := []struct { @@ -161,9 +167,10 @@ func TestReadTCConfig(t *testing.T) { "docker.host = " + tcpDockerHost33293, map[string]string{}, Config{ - Host: tcpDockerHost33293, - RyukConnectionTimeout: defaultRyukConnectionTimeout, - RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + Host: tcpDockerHost33293, + RyukConnectionTimeout: defaultRyukConnectionTimeout, + RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + TestcontainersPortMappingTimeout: defaultTestcontainersPortMappingTimeout, }, }, { @@ -173,9 +180,10 @@ func TestReadTCConfig(t *testing.T) { `, map[string]string{}, Config{ - Host: tcpDockerHost4711, - RyukConnectionTimeout: defaultRyukConnectionTimeout, - RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + Host: tcpDockerHost4711, + RyukConnectionTimeout: defaultRyukConnectionTimeout, + RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + TestcontainersPortMappingTimeout: defaultTestcontainersPortMappingTimeout, }, }, { @@ -187,10 +195,11 @@ func TestReadTCConfig(t *testing.T) { `, map[string]string{}, Config{ - Host: tcpDockerHost1234, - TLSVerify: 1, - RyukConnectionTimeout: defaultRyukConnectionTimeout, - RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + Host: tcpDockerHost1234, + TLSVerify: 1, + RyukConnectionTimeout: defaultRyukConnectionTimeout, + RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + TestcontainersPortMappingTimeout: defaultTestcontainersPortMappingTimeout, }, }, { @@ -198,8 +207,9 @@ func TestReadTCConfig(t *testing.T) { "", map[string]string{}, Config{ - RyukConnectionTimeout: defaultRyukConnectionTimeout, - RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + RyukConnectionTimeout: defaultRyukConnectionTimeout, + RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + TestcontainersPortMappingTimeout: defaultTestcontainersPortMappingTimeout, }, }, { @@ -209,9 +219,10 @@ func TestReadTCConfig(t *testing.T) { `, map[string]string{}, Config{ - Host: tcpDockerHost1234, - RyukConnectionTimeout: defaultRyukConnectionTimeout, - RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + Host: tcpDockerHost1234, + RyukConnectionTimeout: defaultRyukConnectionTimeout, + RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + TestcontainersPortMappingTimeout: defaultTestcontainersPortMappingTimeout, }, }, { @@ -219,9 +230,10 @@ func TestReadTCConfig(t *testing.T) { "docker.host=" + tcpDockerHost33293, map[string]string{}, Config{ - Host: tcpDockerHost33293, - RyukConnectionTimeout: defaultRyukConnectionTimeout, - RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + Host: tcpDockerHost33293, + RyukConnectionTimeout: defaultRyukConnectionTimeout, + RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + TestcontainersPortMappingTimeout: defaultTestcontainersPortMappingTimeout, }, }, { @@ -238,10 +250,11 @@ func TestReadTCConfig(t *testing.T) { docker.cert.path=/tmp/certs`, map[string]string{}, Config{ - Host: tcpDockerHost1234, - CertPath: "/tmp/certs", - RyukConnectionTimeout: defaultRyukConnectionTimeout, - RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + Host: tcpDockerHost1234, + CertPath: "/tmp/certs", + RyukConnectionTimeout: defaultRyukConnectionTimeout, + RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + TestcontainersPortMappingTimeout: defaultTestcontainersPortMappingTimeout, }, }, { @@ -249,9 +262,10 @@ func TestReadTCConfig(t *testing.T) { `ryuk.disabled=true`, map[string]string{}, Config{ - RyukDisabled: true, - RyukConnectionTimeout: defaultRyukConnectionTimeout, - RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + RyukDisabled: true, + RyukConnectionTimeout: defaultRyukConnectionTimeout, + RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + TestcontainersPortMappingTimeout: defaultTestcontainersPortMappingTimeout, }, }, { @@ -259,44 +273,52 @@ func TestReadTCConfig(t *testing.T) { `ryuk.container.privileged=true`, map[string]string{}, Config{ - RyukPrivileged: true, - RyukConnectionTimeout: defaultRyukConnectionTimeout, - RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + RyukPrivileged: true, + RyukConnectionTimeout: defaultRyukConnectionTimeout, + RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + TestcontainersPortMappingTimeout: defaultTestcontainersPortMappingTimeout, }, }, { "With Ryuk container timeouts configured using properties", `ryuk.connection.timeout=12s - ryuk.reconnection.timeout=13s`, + ryuk.reconnection.timeout=13s + tc.port.mapping.timeout=11s`, map[string]string{}, Config{ - RyukReconnectionTimeout: 13 * time.Second, - RyukConnectionTimeout: 12 * time.Second, + RyukReconnectionTimeout: 13 * time.Second, + RyukConnectionTimeout: 12 * time.Second, + TestcontainersPortMappingTimeout: 11 * time.Second, }, }, { "With Ryuk container timeouts configured using env vars", ``, map[string]string{ - "RYUK_RECONNECTION_TIMEOUT": "13s", - "RYUK_CONNECTION_TIMEOUT": "12s", + "RYUK_RECONNECTION_TIMEOUT": "13s", + "RYUK_CONNECTION_TIMEOUT": "12s", + "TESTCONTAINERS_PORT_MAPPING_TIMEOUT": "11s", }, Config{ - RyukReconnectionTimeout: 13 * time.Second, - RyukConnectionTimeout: 12 * time.Second, + RyukReconnectionTimeout: 13 * time.Second, + RyukConnectionTimeout: 12 * time.Second, + TestcontainersPortMappingTimeout: 11 * time.Second, }, }, { "With Ryuk container timeouts configured using env vars and properties. Env var wins", `ryuk.connection.timeout=22s - ryuk.reconnection.timeout=23s`, + ryuk.reconnection.timeout=23s + tc.port.mapping.timeout=21s`, map[string]string{ - "RYUK_RECONNECTION_TIMEOUT": "13s", - "RYUK_CONNECTION_TIMEOUT": "12s", + "RYUK_RECONNECTION_TIMEOUT": "13s", + "RYUK_CONNECTION_TIMEOUT": "12s", + "TESTCONTAINERS_PORT_MAPPING_TIMEOUT": "11s", }, Config{ - RyukReconnectionTimeout: 13 * time.Second, - RyukConnectionTimeout: 12 * time.Second, + RyukReconnectionTimeout: 13 * time.Second, + RyukConnectionTimeout: 12 * time.Second, + TestcontainersPortMappingTimeout: 11 * time.Second, }, }, { @@ -304,9 +326,10 @@ func TestReadTCConfig(t *testing.T) { `ryuk.verbose=true`, map[string]string{}, Config{ - RyukVerbose: true, - RyukConnectionTimeout: defaultRyukConnectionTimeout, - RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + RyukVerbose: true, + RyukConnectionTimeout: defaultRyukConnectionTimeout, + RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + TestcontainersPortMappingTimeout: defaultTestcontainersPortMappingTimeout, }, }, { @@ -316,9 +339,10 @@ func TestReadTCConfig(t *testing.T) { "TESTCONTAINERS_RYUK_DISABLED": "true", }, Config{ - RyukDisabled: true, - RyukConnectionTimeout: defaultRyukConnectionTimeout, - RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + RyukDisabled: true, + RyukConnectionTimeout: defaultRyukConnectionTimeout, + RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + TestcontainersPortMappingTimeout: defaultTestcontainersPortMappingTimeout, }, }, { @@ -328,9 +352,10 @@ func TestReadTCConfig(t *testing.T) { "TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED": "true", }, Config{ - RyukPrivileged: true, - RyukConnectionTimeout: defaultRyukConnectionTimeout, - RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + RyukPrivileged: true, + RyukConnectionTimeout: defaultRyukConnectionTimeout, + RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + TestcontainersPortMappingTimeout: defaultTestcontainersPortMappingTimeout, }, }, { @@ -340,9 +365,10 @@ func TestReadTCConfig(t *testing.T) { "TESTCONTAINERS_RYUK_DISABLED": "true", }, Config{ - RyukDisabled: true, - RyukConnectionTimeout: defaultRyukConnectionTimeout, - RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + RyukDisabled: true, + RyukConnectionTimeout: defaultRyukConnectionTimeout, + RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + TestcontainersPortMappingTimeout: defaultTestcontainersPortMappingTimeout, }, }, { @@ -352,9 +378,10 @@ func TestReadTCConfig(t *testing.T) { "TESTCONTAINERS_RYUK_DISABLED": "true", }, Config{ - RyukDisabled: true, - RyukConnectionTimeout: defaultRyukConnectionTimeout, - RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + RyukDisabled: true, + RyukConnectionTimeout: defaultRyukConnectionTimeout, + RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + TestcontainersPortMappingTimeout: defaultTestcontainersPortMappingTimeout, }, }, { @@ -380,9 +407,10 @@ func TestReadTCConfig(t *testing.T) { "RYUK_VERBOSE": "true", }, Config{ - RyukVerbose: true, - RyukConnectionTimeout: defaultRyukConnectionTimeout, - RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + RyukVerbose: true, + RyukConnectionTimeout: defaultRyukConnectionTimeout, + RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + TestcontainersPortMappingTimeout: defaultTestcontainersPortMappingTimeout, }, }, { @@ -392,9 +420,10 @@ func TestReadTCConfig(t *testing.T) { "RYUK_VERBOSE": "true", }, Config{ - RyukVerbose: true, - RyukConnectionTimeout: defaultRyukConnectionTimeout, - RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + RyukVerbose: true, + RyukConnectionTimeout: defaultRyukConnectionTimeout, + RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + TestcontainersPortMappingTimeout: defaultTestcontainersPortMappingTimeout, }, }, { @@ -420,9 +449,10 @@ func TestReadTCConfig(t *testing.T) { "TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED": "true", }, Config{ - RyukPrivileged: true, - RyukConnectionTimeout: defaultRyukConnectionTimeout, - RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + RyukPrivileged: true, + RyukConnectionTimeout: defaultRyukConnectionTimeout, + RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + TestcontainersPortMappingTimeout: defaultTestcontainersPortMappingTimeout, }, }, { @@ -432,9 +462,10 @@ func TestReadTCConfig(t *testing.T) { "TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED": "true", }, Config{ - RyukPrivileged: true, - RyukConnectionTimeout: defaultRyukConnectionTimeout, - RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + RyukPrivileged: true, + RyukConnectionTimeout: defaultRyukConnectionTimeout, + RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + TestcontainersPortMappingTimeout: defaultTestcontainersPortMappingTimeout, }, }, { @@ -487,9 +518,10 @@ func TestReadTCConfig(t *testing.T) { `hub.image.name.prefix=` + defaultHubPrefix + `/props/`, map[string]string{}, Config{ - HubImageNamePrefix: defaultHubPrefix + "/props/", - RyukConnectionTimeout: defaultRyukConnectionTimeout, - RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + HubImageNamePrefix: defaultHubPrefix + "/props/", + RyukConnectionTimeout: defaultRyukConnectionTimeout, + RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + TestcontainersPortMappingTimeout: defaultTestcontainersPortMappingTimeout, }, }, { @@ -499,9 +531,10 @@ func TestReadTCConfig(t *testing.T) { "TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX": defaultHubPrefix + "/env/", }, Config{ - HubImageNamePrefix: defaultHubPrefix + "/env/", - RyukConnectionTimeout: defaultRyukConnectionTimeout, - RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + HubImageNamePrefix: defaultHubPrefix + "/env/", + RyukConnectionTimeout: defaultRyukConnectionTimeout, + RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + TestcontainersPortMappingTimeout: defaultTestcontainersPortMappingTimeout, }, }, { @@ -511,9 +544,10 @@ func TestReadTCConfig(t *testing.T) { "TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX": defaultHubPrefix + "/env/", }, Config{ - HubImageNamePrefix: defaultHubPrefix + "/env/", - RyukConnectionTimeout: defaultRyukConnectionTimeout, - RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + HubImageNamePrefix: defaultHubPrefix + "/env/", + RyukConnectionTimeout: defaultRyukConnectionTimeout, + RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + TestcontainersPortMappingTimeout: defaultTestcontainersPortMappingTimeout, }, }, } From e6ea3649ffb549bf97bad8a31ee2429a71bb5e18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20de=20la=20Pe=C3=B1a?= Date: Fri, 30 May 2025 05:13:50 +0200 Subject: [PATCH 2/5] chore: apply configuration to the port mapping check --- lifecycle.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lifecycle.go b/lifecycle.go index 72363cccca..cffcfee310 100644 --- a/lifecycle.go +++ b/lifecycle.go @@ -14,6 +14,7 @@ import ( "github.com/docker/docker/api/types/network" "github.com/docker/go-connections/nat" + "github.com/testcontainers/testcontainers-go/internal/config" "github.com/testcontainers/testcontainers-go/log" ) @@ -249,13 +250,16 @@ var defaultReadinessHook = func() ContainerLifecycleHooks { // wait until all the exposed ports are mapped: // it will be ready when all the exposed ports are mapped, // checking every 50ms, up to 1s, and failing if all the - // exposed ports are not mapped in 5s. + // exposed ports are not mapped in the interval defined by the + // [config.TestcontainersPortMappingTimeout] config. dockerContainer := c.(*DockerContainer) + cfg := config.Read() + b := backoff.NewExponentialBackOff() b.InitialInterval = 50 * time.Millisecond - b.MaxElapsedTime = 5 * time.Second + b.MaxElapsedTime = cfg.TestcontainersPortMappingTimeout b.MaxInterval = time.Duration(float64(time.Second) * backoff.DefaultRandomizationFactor) err := backoff.RetryNotify( @@ -273,7 +277,7 @@ var defaultReadinessHook = func() ContainerLifecycleHooks { }, ) if err != nil { - return fmt.Errorf("all exposed ports, %s, were not mapped in 5s: %w", dockerContainer.exposedPorts, err) + return fmt.Errorf("all exposed ports, %s, were not mapped in %s: %w", dockerContainer.exposedPorts, cfg.TestcontainersPortMappingTimeout, err) } return nil From c603baf225afa65181d049183f4817f96aaacda1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20de=20la=20Pe=C3=B1a?= Date: Fri, 30 May 2025 06:01:42 +0200 Subject: [PATCH 3/5] chore: refactor checkPortsMapped to make it testable --- lifecycle.go | 99 +++++++++++++++++++++++------------------------ lifecycle_test.go | 66 ++++++++++++++++++++++++++++--- 2 files changed, 110 insertions(+), 55 deletions(-) diff --git a/lifecycle.go b/lifecycle.go index cffcfee310..012b73884e 100644 --- a/lifecycle.go +++ b/lifecycle.go @@ -214,29 +214,57 @@ var defaultLogConsumersHook = func(cfg *LogConsumerConfig) ContainerLifecycleHoo } } -func checkPortsMapped(exposedAndMappedPorts nat.PortMap, exposedPorts []string) error { - portMap, _, err := nat.ParsePortSpecs(exposedPorts) - if err != nil { - return fmt.Errorf("parse exposed ports: %w", err) - } +// checkPortsMapped waits until all the exposed ports are mapped: +// it will be ready when all the exposed ports are mapped, checking every 50ms, up to 1s, +// and failing if all the exposed ports are not mapped in the interval defined by the +// [config.TestcontainersPortMappingTimeout] config. +func checkPortsMapped(ctx context.Context, inspectRawContainer func(context.Context) (*container.InspectResponse, error), exposedPorts []string, timeout time.Duration) error { + b := backoff.NewExponentialBackOff() + + b.InitialInterval = 50 * time.Millisecond + b.MaxElapsedTime = timeout + b.MaxInterval = time.Duration(float64(time.Second) * backoff.DefaultRandomizationFactor) + + err := backoff.RetryNotify( + func() error { + jsonRaw, err := inspectRawContainer(ctx) + if err != nil { + return err + } - for exposedPort := range portMap { - // having entries in exposedAndMappedPorts, where the key is the exposed port, - // and the value is the mapped port, means that the port has been already mapped. - if _, ok := exposedAndMappedPorts[exposedPort]; ok { - continue - } + portMap, _, err := nat.ParsePortSpecs(exposedPorts) + if err != nil { + return fmt.Errorf("parse exposed ports: %w", err) + } - // check if the port is mapped with the protocol (default is TCP) - if strings.Contains(string(exposedPort), "/") { - return fmt.Errorf("port %s is not mapped yet", exposedPort) - } + for exposedPort := range portMap { + // having entries in exposedAndMappedPorts, where the key is the exposed port, + // and the value is the mapped port, means that the port has been already mapped. + if _, ok := jsonRaw.NetworkSettings.Ports[exposedPort]; ok { + continue + } - // Port didn't have a type, default to tcp and retry. - exposedPort += "/tcp" - if _, ok := exposedAndMappedPorts[exposedPort]; !ok { - return fmt.Errorf("port %s is not mapped yet", exposedPort) - } + // check if the port is mapped with the protocol (default is TCP) + if strings.Contains(string(exposedPort), "/") { + return fmt.Errorf("port %s is not mapped yet", exposedPort) + } + + // Port didn't have a type, default to tcp and retry. + exposedPort += "/tcp" + if _, ok := jsonRaw.NetworkSettings.Ports[exposedPort]; !ok { + return fmt.Errorf("port %s is not mapped yet", exposedPort) + } + } + + return nil + }, + b, + func(err error, _ time.Duration) { + log.Printf("All requested ports were not exposed: %v", err) + }, + ) + if err != nil { + return fmt.Errorf("all exposed ports, %s, were not mapped in %s: %w", exposedPorts, timeout, err) } return nil @@ -247,40 +275,11 @@ var defaultReadinessHook = func() ContainerLifecycleHooks { return ContainerLifecycleHooks{ PostStarts: []ContainerHook{ func(ctx context.Context, c Container) error { - // wait until all the exposed ports are mapped: - // it will be ready when all the exposed ports are mapped, - // checking every 50ms, up to 1s, and failing if all the - // exposed ports are not mapped in the interval defined by the - // [config.TestcontainersPortMappingTimeout] config. dockerContainer := c.(*DockerContainer) cfg := config.Read() - b := backoff.NewExponentialBackOff() - - b.InitialInterval = 50 * time.Millisecond - b.MaxElapsedTime = cfg.TestcontainersPortMappingTimeout - b.MaxInterval = time.Duration(float64(time.Second) * backoff.DefaultRandomizationFactor) - - err := backoff.RetryNotify( - func() error { - jsonRaw, err := dockerContainer.inspectRawContainer(ctx) - if err != nil { - return err - } - - return checkPortsMapped(jsonRaw.NetworkSettings.Ports, dockerContainer.exposedPorts) - }, - b, - func(err error, _ time.Duration) { - dockerContainer.logger.Printf("All requested ports were not exposed: %v", err) - }, - ) - if err != nil { - return fmt.Errorf("all exposed ports, %s, were not mapped in %s: %w", dockerContainer.exposedPorts, cfg.TestcontainersPortMappingTimeout, err) - } - - return nil + return checkPortsMapped(ctx, dockerContainer.inspectRawContainer, dockerContainer.exposedPorts, cfg.TestcontainersPortMappingTimeout) }, // wait for the container to be ready func(ctx context.Context, c Container) error { diff --git a/lifecycle_test.go b/lifecycle_test.go index 02de785103..b903d83616 100644 --- a/lifecycle_test.go +++ b/lifecycle_test.go @@ -475,6 +475,29 @@ func TestMergePortBindings(t *testing.T) { } } +type mockPortMappingCheckContainer struct { + ports nat.PortMap +} + +func (m *mockPortMappingCheckContainer) InspectRawContainer(ctx context.Context) (*container.InspectResponse, error) { + // check if the context has a timeout or if is cancelled + // used to simulate a timeout in the container inspection, + // so the exposed ports are not mapped. + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + return &container.InspectResponse{ + NetworkSettings: &container.NetworkSettings{ + NetworkSettingsBase: container.NetworkSettingsBase{ + Ports: m.ports, + }, + }, + }, nil +} + func TestPortMappingCheck(t *testing.T) { makePortMap := func(ports ...string) nat.PortMap { out := make(nat.PortMap) @@ -490,6 +513,15 @@ func TestPortMappingCheck(t *testing.T) { exposedPorts []string expectError bool }{ + "empty-ports": { + exposedAndMappedPorts: makePortMap(), + exposedPorts: []string{}, + }, + "invalid-port-format": { + exposedAndMappedPorts: makePortMap(), + exposedPorts: []string{"invalid"}, + expectError: true, + }, "no-protocol": { exposedAndMappedPorts: makePortMap("1024/tcp"), exposedPorts: []string{"1024"}, @@ -510,6 +542,10 @@ func TestPortMappingCheck(t *testing.T) { exposedAndMappedPorts: makePortMap("1024/tcp", "1025/tcp", "1026/tcp"), exposedPorts: []string{"1024", "25:1025/tcp", "1026:1026"}, }, + "multiple-protocols": { + exposedAndMappedPorts: makePortMap("1024/tcp", "1024/udp"), + exposedPorts: []string{"1024/tcp", "1024/udp"}, + }, "only-ipv4": { exposedAndMappedPorts: makePortMap("1024/tcp"), exposedPorts: []string{"0.0.0.0::1024/tcp"}, @@ -530,14 +566,34 @@ func TestPortMappingCheck(t *testing.T) { expectError: true, }, } + for name, tt := range tests { t.Run(name, func(t *testing.T) { - err := checkPortsMapped(tt.exposedAndMappedPorts, tt.exposedPorts) - if tt.expectError { - require.Error(t, err) - return + mock := &mockPortMappingCheckContainer{ + ports: tt.exposedAndMappedPorts, } - require.NoError(t, err) + + t.Run("high-timeout", func(t *testing.T) { + // the context will not be cancelled, so the container inspection will not timeout while waiting for the ports to be mapped + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + // simulate the configuration of the exposed ports timeout is 1 second + err := checkPortsMapped(ctx, mock.InspectRawContainer, tt.exposedPorts, 1*time.Second) + if tt.expectError { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + + t.Run("low-timeout", func(t *testing.T) { + // the context is cancelled, so the container inspection will timeout while waiting for the ports to be mapped + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + err := checkPortsMapped(ctx, mock.InspectRawContainer, tt.exposedPorts, 1*time.Second) + require.Error(t, err) + }) }) } } From 240b622edd2a1f4fe5fc8aff8de300c150e01182 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20de=20la=20Pe=C3=B1a?= Date: Fri, 30 May 2025 07:20:54 +0200 Subject: [PATCH 4/5] chore: refactor config tests for maintainability --- internal/config/config_test.go | 889 ++++++++++++++++----------------- 1 file changed, 429 insertions(+), 460 deletions(-) diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 7b090bcc6a..fafe42a558 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -6,7 +6,6 @@ import ( "testing" "time" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -16,23 +15,166 @@ const ( tcpDockerHost4711 = "tcp://127.0.0.1:4711" ) +// setupTestDir creates a temporary directory and sets it as HOME and USERPROFILE +func setupTestDir(t *testing.T) string { + t.Helper() + tmpDir := t.TempDir() + t.Setenv("HOME", tmpDir) + t.Setenv("USERPROFILE", tmpDir) // Windows support + return tmpDir +} + +// setupTestProperties writes the given content to a .testcontainers.properties file in the given directory +func setupTestProperties(t *testing.T, dir, content string) { + t.Helper() + err := os.WriteFile(filepath.Join(dir, ".testcontainers.properties"), []byte(content), 0o600) + require.NoErrorf(t, err, "Failed to create the properties file") +} + +// setupTestEnv sets up the test environment with the given environment variables +func setupTestEnv(t *testing.T, env map[string]string) { + t.Helper() + for k, v := range env { + t.Setenv(k, v) + } +} + // unset environment variables to avoid side effects // execute this function before each test func resetTestEnv(t *testing.T) { t.Helper() - t.Setenv("TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX", "") - t.Setenv("TESTCONTAINERS_RYUK_DISABLED", "") - t.Setenv("TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED", "") - t.Setenv("RYUK_VERBOSE", "") - t.Setenv("RYUK_RECONNECTION_TIMEOUT", "") - t.Setenv("RYUK_CONNECTION_TIMEOUT", "") - t.Setenv("TESTCONTAINERS_PORT_MAPPING_TIMEOUT", "") + + setupTestEnv(t, map[string]string{ + "TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX": "", + "TESTCONTAINERS_RYUK_DISABLED": "", + "TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED": "", + "RYUK_VERBOSE": "", + "RYUK_RECONNECTION_TIMEOUT": "", + "RYUK_CONNECTION_TIMEOUT": "", + "TESTCONTAINERS_PORT_MAPPING_TIMEOUT": "", + }) +} + +// defaultConfig returns a Config with default timeout values used in tests. +// The default values are: +// - RyukConnectionTimeout: 60 seconds +// - RyukReconnectionTimeout: 10 seconds +// - TestcontainersPortMappingTimeout: 5 seconds +func defaultConfig() Config { + return Config{ + RyukConnectionTimeout: 60 * time.Second, + RyukReconnectionTimeout: 10 * time.Second, + TestcontainersPortMappingTimeout: 5 * time.Second, + } +} + +// defaultTestConfig creates a new Config with default values and applies the given overrides. +// It is the main helper function for creating test configurations. Each override function +// modifies a specific aspect of the default configuration. +// +// Example usage: +// +// config := defaultTestConfig( +// withHost("tcp://localhost:1234"), +// withRyukDisabled(true), +// withTimeouts(12*time.Second, 13*time.Second, 11*time.Second), +// ) +// +// For custom modifications not covered by the standard helpers, you can use an inline function: +// +// config := defaultTestConfig( +// withHost("tcp://localhost:1234"), +// func(c *Config) { c.TLSVerify = 1 }, +// ) +func defaultTestConfig(overrides ...func(*Config)) Config { + config := defaultConfig() + for _, override := range overrides { + override(&config) + } + return config +} + +// withHost returns a function that sets the Docker host URL in a Config. +// This is used to specify the Docker daemon connection URL. +// +// Example: +// +// config := defaultTestConfig(withHost("tcp://localhost:1234")) +func withHost(host string) func(*Config) { + return func(c *Config) { + c.Host = host + } +} + +// withRyukDisabled returns a function that sets whether Ryuk (the resource reaper) is disabled. +// When disabled, Ryuk will not be started and containers will not be automatically removed. +// +// Example: +// +// config := defaultTestConfig(withRyukDisabled(true)) +func withRyukDisabled(disabled bool) func(*Config) { + return func(c *Config) { + c.RyukDisabled = disabled + } +} + +// withRyukPrivileged returns a function that sets whether the Ryuk container should run in privileged mode. +// Privileged mode is required for some Docker operations but may have security implications. +// +// Example: +// +// config := defaultTestConfig(withRyukPrivileged(true)) +func withRyukPrivileged(privileged bool) func(*Config) { + return func(c *Config) { + c.RyukPrivileged = privileged + } +} + +// withRyukVerbose returns a function that sets whether Ryuk should run in verbose mode. +// Verbose mode enables additional logging for debugging purposes. +// +// Example: +// +// config := defaultTestConfig(withRyukVerbose(true)) +func withRyukVerbose(verbose bool) func(*Config) { + return func(c *Config) { + c.RyukVerbose = verbose + } +} + +// withTimeouts returns a function that sets all timeout-related fields in a Config. +// The timeouts control various aspects of container and resource management: +// - connection: Time to wait for initial connection to Ryuk +// - reconnection: Time to wait between reconnection attempts to Ryuk +// - portMapping: Time to wait for port mapping operations +// +// Example: +// +// config := defaultTestConfig(withTimeouts(12*time.Second, 13*time.Second, 11*time.Second)) +func withTimeouts(connection, reconnection, portMapping time.Duration) func(*Config) { + return func(c *Config) { + c.RyukConnectionTimeout = connection + c.RyukReconnectionTimeout = reconnection + c.TestcontainersPortMappingTimeout = portMapping + } +} + +// withHubImagePrefix returns a function that sets the prefix for Docker Hub image names. +// This is used to configure a custom registry or mirror for pulling Docker images. +// +// Example: +// +// config := defaultTestConfig(withHubImagePrefix("registry.mycompany.com/mirror")) +func withHubImagePrefix(prefix string) func(*Config) { + return func(c *Config) { + c.HubImageNamePrefix = prefix + } } func TestReadConfig(t *testing.T) { resetTestEnv(t) - t.Run("Config is read just once", func(t *testing.T) { + t.Run("config-read-once", func(t *testing.T) { t.Cleanup(Reset) t.Setenv("HOME", "") @@ -50,9 +192,8 @@ func TestReadConfig(t *testing.T) { require.Equal(t, expected, config) t.Setenv("TESTCONTAINERS_RYUK_DISABLED", "false") - config = Read() - assert.Equal(t, expected, config) + require.Equal(t, expected, config) }) } @@ -61,101 +202,249 @@ func TestReadTCConfig(t *testing.T) { const defaultHubPrefix string = "registry.mycompany.com/mirror" - t.Run("HOME is not set", func(t *testing.T) { - t.Setenv("HOME", "") - t.Setenv("USERPROFILE", "") // Windows support - - config := read() - - expected := Config{} - - assert.Equal(t, expected, config) - }) - - t.Run("HOME is not set - TESTCONTAINERS_ env is set", func(t *testing.T) { - t.Setenv("HOME", "") - t.Setenv("USERPROFILE", "") // Windows support - t.Setenv("TESTCONTAINERS_RYUK_DISABLED", "true") - t.Setenv("TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX", defaultHubPrefix) - t.Setenv("TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED", "true") - t.Setenv("RYUK_RECONNECTION_TIMEOUT", "13s") - t.Setenv("RYUK_CONNECTION_TIMEOUT", "12s") - t.Setenv("TESTCONTAINERS_PORT_MAPPING_TIMEOUT", "11s") - config := read() - - expected := Config{ - HubImageNamePrefix: defaultHubPrefix, - RyukDisabled: true, - RyukPrivileged: true, - Host: "", // docker socket is empty at the properties file - RyukReconnectionTimeout: 13 * time.Second, - RyukConnectionTimeout: 12 * time.Second, - TestcontainersPortMappingTimeout: 11 * time.Second, - } - - assert.Equal(t, expected, config) - }) - - t.Run("HOME does not contain TC props file", func(t *testing.T) { - tmpDir := t.TempDir() - t.Setenv("HOME", tmpDir) - t.Setenv("USERPROFILE", tmpDir) // Windows support - - config := read() - - expected := Config{} - - assert.Equal(t, expected, config) + // Group 1: Basic environment setup tests + t.Run("environment-setup", func(t *testing.T) { + t.Run("HOME-not-set", func(t *testing.T) { + t.Setenv("HOME", "") + t.Setenv("USERPROFILE", "") // Windows support + + config := read() + require.Equal(t, Config{}, config) + }) + + t.Run("HOME-does-not-contain-TC-props-file", func(t *testing.T) { + setupTestDir(t) + config := read() + require.Equal(t, Config{}, config) + }) + + t.Run("HOME-does-not-contain-TC-props-file-DOCKER_HOST-env-is-set", func(t *testing.T) { + setupTestDir(t) + t.Setenv("DOCKER_HOST", tcpDockerHost33293) + config := read() + require.Equal(t, Config{}, config) // the config does not read DOCKER_HOST + }) }) - t.Run("HOME does not contain TC props file - DOCKER_HOST env is set", func(t *testing.T) { - tmpDir := t.TempDir() - t.Setenv("HOME", tmpDir) - t.Setenv("USERPROFILE", tmpDir) // Windows support - t.Setenv("DOCKER_HOST", tcpDockerHost33293) - - config := read() - expected := Config{} // the config does not read DOCKER_HOST, that's why it's empty - - assert.Equal(t, expected, config) + // Group 2: Environment variables tests + t.Run("environment-variables", func(t *testing.T) { + t.Run("HOME-is-not-set-TESTCONTAINERS_env-is-set", func(t *testing.T) { + t.Setenv("HOME", "") + t.Setenv("USERPROFILE", "") // Windows support + env := map[string]string{ + "TESTCONTAINERS_RYUK_DISABLED": "true", + "TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX": defaultHubPrefix, + "TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED": "true", + "RYUK_RECONNECTION_TIMEOUT": "13s", + "RYUK_CONNECTION_TIMEOUT": "12s", + "TESTCONTAINERS_PORT_MAPPING_TIMEOUT": "11s", + } + setupTestEnv(t, env) + + expected := defaultTestConfig( + withHubImagePrefix(defaultHubPrefix), + withRyukDisabled(true), + withRyukPrivileged(true), + withTimeouts(12*time.Second, 13*time.Second, 11*time.Second), + ) + require.Equal(t, expected, read()) + }) + + t.Run("HOME-does-not-contain-TC-props-file-TESTCONTAINERS_env-is-set", func(t *testing.T) { + setupTestDir(t) + env := map[string]string{ + "TESTCONTAINERS_RYUK_DISABLED": "true", + "TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX": defaultHubPrefix, + "TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED": "true", + "RYUK_VERBOSE": "true", + "RYUK_RECONNECTION_TIMEOUT": "13s", + "RYUK_CONNECTION_TIMEOUT": "12s", + "TESTCONTAINERS_PORT_MAPPING_TIMEOUT": "11s", + } + setupTestEnv(t, env) + + expected := defaultTestConfig( + withHubImagePrefix(defaultHubPrefix), + withRyukDisabled(true), + withRyukPrivileged(true), + withRyukVerbose(true), + withTimeouts(12*time.Second, 13*time.Second, 11*time.Second), + ) + require.Equal(t, expected, read()) + }) }) - t.Run("HOME does not contain TC props file - TESTCONTAINERS_ env is set", func(t *testing.T) { - tmpDir := t.TempDir() - t.Setenv("HOME", tmpDir) - t.Setenv("USERPROFILE", tmpDir) // Windows support - t.Setenv("TESTCONTAINERS_RYUK_DISABLED", "true") - t.Setenv("TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX", defaultHubPrefix) - t.Setenv("TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED", "true") - t.Setenv("RYUK_VERBOSE", "true") - t.Setenv("RYUK_RECONNECTION_TIMEOUT", "13s") - t.Setenv("RYUK_CONNECTION_TIMEOUT", "12s") - t.Setenv("TESTCONTAINERS_PORT_MAPPING_TIMEOUT", "11s") - - config := read() - expected := Config{ - HubImageNamePrefix: defaultHubPrefix, - RyukDisabled: true, - RyukPrivileged: true, - RyukVerbose: true, - RyukReconnectionTimeout: 13 * time.Second, - RyukConnectionTimeout: 12 * time.Second, - TestcontainersPortMappingTimeout: 11 * time.Second, - } - - assert.Equal(t, expected, config) + // Group 3: Properties file tests + t.Run("properties-file", func(t *testing.T) { + // Group 3.1: Docker host configuration + t.Run("docker-host", func(t *testing.T) { + tests := []struct { + name string + content string + expected Config + }{ + { + "single-docker-host-with-spaces", + "docker.host = " + tcpDockerHost33293, + defaultTestConfig(withHost(tcpDockerHost33293)), + }, + { + "single-docker-host-without-spaces", + "docker.host=" + tcpDockerHost33293, + defaultTestConfig(withHost(tcpDockerHost33293)), + }, + { + "multiple-docker-host-entries-last-one-wins", + `docker.host = ` + tcpDockerHost33293 + ` +docker.host = ` + tcpDockerHost4711, + defaultTestConfig(withHost(tcpDockerHost4711)), + }, + { + "multiple-docker-host-entries-with-TLS", + `docker.host = ` + tcpDockerHost33293 + ` +docker.host = ` + tcpDockerHost4711 + ` +docker.host = ` + tcpDockerHost1234 + ` +docker.tls.verify = 1`, + defaultTestConfig( + withHost(tcpDockerHost1234), + func(c *Config) { c.TLSVerify = 1 }, + ), + }, + { + "multiple-docker-host-entries-with-TLS-and-cert-path", + `#docker.host = ` + tcpDockerHost33293 + ` +docker.host = ` + tcpDockerHost4711 + ` +docker.host = ` + tcpDockerHost1234 + ` +docker.cert.path=/tmp/certs`, + defaultTestConfig( + withHost(tcpDockerHost1234), + func(c *Config) { c.CertPath = "/tmp/certs" }, + ), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpDir := setupTestDir(t) + setupTestProperties(t, tmpDir, tt.content) + require.Equal(t, tt.expected, read()) + }) + } + }) + + // Group 3.2: Ryuk configuration + t.Run("ryuk", func(t *testing.T) { + tests := []struct { + name string + content string + expected Config + }{ + { + "ryuk-disabled", + "ryuk.disabled=true", + defaultTestConfig(withRyukDisabled(true)), + }, + { + "ryuk-privileged", + "ryuk.container.privileged=true", + defaultTestConfig(withRyukPrivileged(true)), + }, + { + "ryuk-verbose", + "ryuk.verbose=true", + defaultTestConfig(withRyukVerbose(true)), + }, + { + "ryuk-timeouts", + `ryuk.connection.timeout=12s +ryuk.reconnection.timeout=13s +tc.port.mapping.timeout=11s`, + defaultTestConfig(withTimeouts(12*time.Second, 13*time.Second, 11*time.Second)), + }, + { + "port-mapping-timeout", + "tc.port.mapping.timeout=14s", + defaultTestConfig(withTimeouts(60*time.Second, 10*time.Second, 14*time.Second)), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpDir := setupTestDir(t) + setupTestProperties(t, tmpDir, tt.content) + require.Equal(t, tt.expected, read()) + }) + } + }) + + // Group 3.3: Hub image configuration + t.Run("hub-image", func(t *testing.T) { + tests := []struct { + name string + content string + expected Config + }{ + { + "hub-image-prefix", + "hub.image.name.prefix=" + defaultHubPrefix + "/props/", + defaultTestConfig(withHubImagePrefix(defaultHubPrefix + "/props/")), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpDir := setupTestDir(t) + setupTestProperties(t, tmpDir, tt.content) + require.Equal(t, tt.expected, read()) + }) + } + }) + + // Group 3.4: Edge cases + t.Run("edge-cases", func(t *testing.T) { + tests := []struct { + name string + content string + expected Config + }{ + { + "empty-file", + "", + defaultConfig(), + }, + { + "comments-are-ignored", + "#docker.host=" + tcpDockerHost33293, + defaultConfig(), + }, + { + "non-valid-properties-are-ignored", + `foo = bar +docker.host = ` + tcpDockerHost1234, + defaultTestConfig(withHost(tcpDockerHost1234)), + }, + { + "invalid-TLS-verify-value", + `ryuk.container.privileged=false +docker.tls.verify = ERROR`, + Config{ + // read() doesn't set default values, so timeouts should be zero + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpDir := setupTestDir(t) + setupTestProperties(t, tmpDir, tt.content) + require.Equal(t, tt.expected, read()) + }) + } + }) }) - t.Run("HOME contains TC properties file", func(t *testing.T) { - defaultRyukConnectionTimeout := 60 * time.Second - defaultRyukReconnectionTimeout := 10 * time.Second - defaultTestcontainersPortMappingTimeout := 5 * time.Second - defaultConfig := Config{ - RyukConnectionTimeout: defaultRyukConnectionTimeout, - RyukReconnectionTimeout: defaultRyukReconnectionTimeout, - TestcontainersPortMappingTimeout: defaultTestcontainersPortMappingTimeout, - } - + // Group 4: environment-variables-vs-properties-precedence + t.Run("precedence", func(t *testing.T) { tests := []struct { name string content string @@ -163,409 +452,89 @@ func TestReadTCConfig(t *testing.T) { expected Config }{ { - "Single Docker host with spaces", - "docker.host = " + tcpDockerHost33293, - map[string]string{}, - Config{ - Host: tcpDockerHost33293, - RyukConnectionTimeout: defaultRyukConnectionTimeout, - RyukReconnectionTimeout: defaultRyukReconnectionTimeout, - TestcontainersPortMappingTimeout: defaultTestcontainersPortMappingTimeout, - }, - }, - { - "Multiple docker host entries, last one wins", - `docker.host = ` + tcpDockerHost33293 + ` - docker.host = ` + tcpDockerHost4711 + ` - `, - map[string]string{}, - Config{ - Host: tcpDockerHost4711, - RyukConnectionTimeout: defaultRyukConnectionTimeout, - RyukReconnectionTimeout: defaultRyukReconnectionTimeout, - TestcontainersPortMappingTimeout: defaultTestcontainersPortMappingTimeout, - }, - }, - { - "Multiple docker host entries, last one wins, with TLS", - `docker.host = ` + tcpDockerHost33293 + ` - docker.host = ` + tcpDockerHost4711 + ` - docker.host = ` + tcpDockerHost1234 + ` - docker.tls.verify = 1 - `, - map[string]string{}, - Config{ - Host: tcpDockerHost1234, - TLSVerify: 1, - RyukConnectionTimeout: defaultRyukConnectionTimeout, - RyukReconnectionTimeout: defaultRyukReconnectionTimeout, - TestcontainersPortMappingTimeout: defaultTestcontainersPortMappingTimeout, - }, - }, - { - "Empty file", - "", - map[string]string{}, - Config{ - RyukConnectionTimeout: defaultRyukConnectionTimeout, - RyukReconnectionTimeout: defaultRyukReconnectionTimeout, - TestcontainersPortMappingTimeout: defaultTestcontainersPortMappingTimeout, - }, - }, - { - "Non-valid properties are ignored", - `foo = bar - docker.host = ` + tcpDockerHost1234 + ` - `, - map[string]string{}, - Config{ - Host: tcpDockerHost1234, - RyukConnectionTimeout: defaultRyukConnectionTimeout, - RyukReconnectionTimeout: defaultRyukReconnectionTimeout, - TestcontainersPortMappingTimeout: defaultTestcontainersPortMappingTimeout, - }, - }, - { - "Single Docker host without spaces", - "docker.host=" + tcpDockerHost33293, - map[string]string{}, - Config{ - Host: tcpDockerHost33293, - RyukConnectionTimeout: defaultRyukConnectionTimeout, - RyukReconnectionTimeout: defaultRyukReconnectionTimeout, - TestcontainersPortMappingTimeout: defaultTestcontainersPortMappingTimeout, - }, - }, - { - "Comments are ignored", - `#docker.host=` + tcpDockerHost33293, - map[string]string{}, - defaultConfig, - }, - { - "Multiple docker host entries, last one wins, with TLS and cert path", - `#docker.host = ` + tcpDockerHost33293 + ` - docker.host = ` + tcpDockerHost4711 + ` - docker.host = ` + tcpDockerHost1234 + ` - docker.cert.path=/tmp/certs`, - map[string]string{}, - Config{ - Host: tcpDockerHost1234, - CertPath: "/tmp/certs", - RyukConnectionTimeout: defaultRyukConnectionTimeout, - RyukReconnectionTimeout: defaultRyukReconnectionTimeout, - TestcontainersPortMappingTimeout: defaultTestcontainersPortMappingTimeout, - }, - }, - { - "With Ryuk disabled using properties", - `ryuk.disabled=true`, - map[string]string{}, - Config{ - RyukDisabled: true, - RyukConnectionTimeout: defaultRyukConnectionTimeout, - RyukReconnectionTimeout: defaultRyukReconnectionTimeout, - TestcontainersPortMappingTimeout: defaultTestcontainersPortMappingTimeout, - }, - }, - { - "With Ryuk container privileged using properties", - `ryuk.container.privileged=true`, - map[string]string{}, - Config{ - RyukPrivileged: true, - RyukConnectionTimeout: defaultRyukConnectionTimeout, - RyukReconnectionTimeout: defaultRyukReconnectionTimeout, - TestcontainersPortMappingTimeout: defaultTestcontainersPortMappingTimeout, - }, - }, - { - "With Ryuk container timeouts configured using properties", - `ryuk.connection.timeout=12s - ryuk.reconnection.timeout=13s - tc.port.mapping.timeout=11s`, - map[string]string{}, - Config{ - RyukReconnectionTimeout: 13 * time.Second, - RyukConnectionTimeout: 12 * time.Second, - TestcontainersPortMappingTimeout: 11 * time.Second, - }, - }, - { - "With Ryuk container timeouts configured using env vars", - ``, - map[string]string{ - "RYUK_RECONNECTION_TIMEOUT": "13s", - "RYUK_CONNECTION_TIMEOUT": "12s", - "TESTCONTAINERS_PORT_MAPPING_TIMEOUT": "11s", - }, - Config{ - RyukReconnectionTimeout: 13 * time.Second, - RyukConnectionTimeout: 12 * time.Second, - TestcontainersPortMappingTimeout: 11 * time.Second, - }, - }, - { - "With Ryuk container timeouts configured using env vars and properties. Env var wins", - `ryuk.connection.timeout=22s - ryuk.reconnection.timeout=23s - tc.port.mapping.timeout=21s`, - map[string]string{ - "RYUK_RECONNECTION_TIMEOUT": "13s", - "RYUK_CONNECTION_TIMEOUT": "12s", - "TESTCONTAINERS_PORT_MAPPING_TIMEOUT": "11s", - }, - Config{ - RyukReconnectionTimeout: 13 * time.Second, - RyukConnectionTimeout: 12 * time.Second, - TestcontainersPortMappingTimeout: 11 * time.Second, - }, - }, - { - "With Ryuk verbose configured using properties", - `ryuk.verbose=true`, - map[string]string{}, - Config{ - RyukVerbose: true, - RyukConnectionTimeout: defaultRyukConnectionTimeout, - RyukReconnectionTimeout: defaultRyukReconnectionTimeout, - TestcontainersPortMappingTimeout: defaultTestcontainersPortMappingTimeout, - }, - }, - { - "With Ryuk disabled using an env var", - ``, + "ryuk-disabled-env-var-wins", + "ryuk.disabled=false", map[string]string{ "TESTCONTAINERS_RYUK_DISABLED": "true", }, - Config{ - RyukDisabled: true, - RyukConnectionTimeout: defaultRyukConnectionTimeout, - RyukReconnectionTimeout: defaultRyukReconnectionTimeout, - TestcontainersPortMappingTimeout: defaultTestcontainersPortMappingTimeout, - }, + defaultTestConfig(withRyukDisabled(true)), }, { - "With Ryuk container privileged using an env var", - ``, + "ryuk-privileged-env-var-wins", + "ryuk.container.privileged=false", map[string]string{ "TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED": "true", }, - Config{ - RyukPrivileged: true, - RyukConnectionTimeout: defaultRyukConnectionTimeout, - RyukReconnectionTimeout: defaultRyukReconnectionTimeout, - TestcontainersPortMappingTimeout: defaultTestcontainersPortMappingTimeout, - }, - }, - { - "With Ryuk disabled using an env var and properties. Env var wins (0)", - `ryuk.disabled=true`, - map[string]string{ - "TESTCONTAINERS_RYUK_DISABLED": "true", - }, - Config{ - RyukDisabled: true, - RyukConnectionTimeout: defaultRyukConnectionTimeout, - RyukReconnectionTimeout: defaultRyukReconnectionTimeout, - TestcontainersPortMappingTimeout: defaultTestcontainersPortMappingTimeout, - }, - }, - { - "With Ryuk disabled using an env var and properties. Env var wins (1)", - `ryuk.disabled=false`, - map[string]string{ - "TESTCONTAINERS_RYUK_DISABLED": "true", - }, - Config{ - RyukDisabled: true, - RyukConnectionTimeout: defaultRyukConnectionTimeout, - RyukReconnectionTimeout: defaultRyukReconnectionTimeout, - TestcontainersPortMappingTimeout: defaultTestcontainersPortMappingTimeout, - }, - }, - { - "With Ryuk disabled using an env var and properties. Env var wins (2)", - `ryuk.disabled=true`, - map[string]string{ - "TESTCONTAINERS_RYUK_DISABLED": "false", - }, - defaultConfig, - }, - { - "With Ryuk disabled using an env var and properties. Env var wins (3)", - `ryuk.disabled=false`, - map[string]string{ - "TESTCONTAINERS_RYUK_DISABLED": "false", - }, - defaultConfig, - }, - { - "With Ryuk verbose using an env var and properties. Env var wins (0)", - `ryuk.verbose=true`, - map[string]string{ - "RYUK_VERBOSE": "true", - }, - Config{ - RyukVerbose: true, - RyukConnectionTimeout: defaultRyukConnectionTimeout, - RyukReconnectionTimeout: defaultRyukReconnectionTimeout, - TestcontainersPortMappingTimeout: defaultTestcontainersPortMappingTimeout, - }, + defaultTestConfig(withRyukPrivileged(true)), }, { - "With Ryuk verbose using an env var and properties. Env var wins (1)", - `ryuk.verbose=false`, + "ryuk-verbose-env-var-wins", + "ryuk.verbose=false", map[string]string{ "RYUK_VERBOSE": "true", }, - Config{ - RyukVerbose: true, - RyukConnectionTimeout: defaultRyukConnectionTimeout, - RyukReconnectionTimeout: defaultRyukReconnectionTimeout, - TestcontainersPortMappingTimeout: defaultTestcontainersPortMappingTimeout, - }, - }, - { - "With Ryuk verbose using an env var and properties. Env var wins (2)", - `ryuk.verbose=true`, - map[string]string{ - "RYUK_VERBOSE": "false", - }, - defaultConfig, - }, - { - "With Ryuk verbose using an env var and properties. Env var wins (3)", - `ryuk.verbose=false`, - map[string]string{ - "RYUK_VERBOSE": "false", - }, - defaultConfig, - }, - { - "With Ryuk container privileged using an env var and properties. Env var wins (0)", - `ryuk.container.privileged=true`, - map[string]string{ - "TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED": "true", - }, - Config{ - RyukPrivileged: true, - RyukConnectionTimeout: defaultRyukConnectionTimeout, - RyukReconnectionTimeout: defaultRyukReconnectionTimeout, - TestcontainersPortMappingTimeout: defaultTestcontainersPortMappingTimeout, - }, - }, - { - "With Ryuk container privileged using an env var and properties. Env var wins (1)", - `ryuk.container.privileged=false`, - map[string]string{ - "TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED": "true", - }, - Config{ - RyukPrivileged: true, - RyukConnectionTimeout: defaultRyukConnectionTimeout, - RyukReconnectionTimeout: defaultRyukReconnectionTimeout, - TestcontainersPortMappingTimeout: defaultTestcontainersPortMappingTimeout, - }, + defaultTestConfig(withRyukVerbose(true)), }, { - "With Ryuk container privileged using an env var and properties. Env var wins (2)", - `ryuk.container.privileged=true`, - map[string]string{ - "TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED": "false", - }, - defaultConfig, - }, - { - "With Ryuk container privileged using an env var and properties. Env var wins (3)", - `ryuk.container.privileged=false`, + "ryuk-timeouts-env-vars-win", + `ryuk.connection.timeout=22s +ryuk.reconnection.timeout=23s +tc.port.mapping.timeout=21s`, map[string]string{ - "TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED": "false", + "RYUK_RECONNECTION_TIMEOUT": "13s", + "RYUK_CONNECTION_TIMEOUT": "12s", + "TESTCONTAINERS_PORT_MAPPING_TIMEOUT": "11s", }, - defaultConfig, + defaultTestConfig(withTimeouts(12*time.Second, 13*time.Second, 11*time.Second)), }, { - "With TLS verify using properties when value is wrong", - `ryuk.container.privileged=false - docker.tls.verify = ERROR`, + "hub-image-prefix-env-var-wins", + "hub.image.name.prefix=" + defaultHubPrefix + "/props/", map[string]string{ - "TESTCONTAINERS_RYUK_DISABLED": "true", - "TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED": "true", - }, - Config{ - RyukDisabled: true, - RyukPrivileged: true, + "TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX": defaultHubPrefix + "/env/", }, + defaultTestConfig(withHubImagePrefix(defaultHubPrefix + "/env/")), }, { - "With Ryuk disabled using an env var and properties. Env var does not win because it's not a boolean value", - `ryuk.disabled=false`, + "invalid-boolean-env-var-is-ignored", + "ryuk.disabled=false", map[string]string{ "TESTCONTAINERS_RYUK_DISABLED": "foo", }, - defaultConfig, + defaultConfig(), }, { - "With Ryuk container privileged using an env var and properties. Env var does not win because it's not a boolean value", - `ryuk.container.privileged=false`, + "invalid-privileged-env-var-is-ignored", + "ryuk.container.privileged=false", map[string]string{ "TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED": "foo", }, - defaultConfig, - }, - { - "With Hub image name prefix set as a property", - `hub.image.name.prefix=` + defaultHubPrefix + `/props/`, - map[string]string{}, - Config{ - HubImageNamePrefix: defaultHubPrefix + "/props/", - RyukConnectionTimeout: defaultRyukConnectionTimeout, - RyukReconnectionTimeout: defaultRyukReconnectionTimeout, - TestcontainersPortMappingTimeout: defaultTestcontainersPortMappingTimeout, - }, + defaultConfig(), }, { - "With Hub image name prefix set as env var", - ``, + "port-mapping-timeout-env-var-wins", + "tc.port.mapping.timeout=22s", map[string]string{ - "TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX": defaultHubPrefix + "/env/", - }, - Config{ - HubImageNamePrefix: defaultHubPrefix + "/env/", - RyukConnectionTimeout: defaultRyukConnectionTimeout, - RyukReconnectionTimeout: defaultRyukReconnectionTimeout, - TestcontainersPortMappingTimeout: defaultTestcontainersPortMappingTimeout, + "TESTCONTAINERS_PORT_MAPPING_TIMEOUT": "14s", }, + defaultTestConfig(withTimeouts(60*time.Second, 10*time.Second, 14*time.Second)), }, { - "With Hub image name prefix set as env var and properties: Env var wins", - `hub.image.name.prefix=` + defaultHubPrefix + `/props/`, + "invalid-port-mapping-timeout-env-var-is-ignored", + "tc.port.mapping.timeout=22s", map[string]string{ - "TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX": defaultHubPrefix + "/env/", - }, - Config{ - HubImageNamePrefix: defaultHubPrefix + "/env/", - RyukConnectionTimeout: defaultRyukConnectionTimeout, - RyukReconnectionTimeout: defaultRyukReconnectionTimeout, - TestcontainersPortMappingTimeout: defaultTestcontainersPortMappingTimeout, + "TESTCONTAINERS_PORT_MAPPING_TIMEOUT": "invalid", }, + defaultTestConfig(withTimeouts(60*time.Second, 10*time.Second, 22*time.Second)), }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - tmpDir := t.TempDir() - t.Setenv("HOME", tmpDir) - t.Setenv("USERPROFILE", tmpDir) // Windows support - for k, v := range tt.env { - t.Setenv(k, v) - } - err := os.WriteFile(filepath.Join(tmpDir, ".testcontainers.properties"), []byte(tt.content), 0o600) - require.NoErrorf(t, err, "Failed to create the file") - - // - config := read() - - assert.Equal(t, tt.expected, config, "Configuration doesn't not match") + tmpDir := setupTestDir(t) + setupTestEnv(t, tt.env) + setupTestProperties(t, tmpDir, tt.content) + require.Equal(t, tt.expected, read()) }) } }) From 613b0abe8b9bd7a6b625e211cd1b12ed147d3519 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20de=20la=20Pe=C3=B1a?= Date: Fri, 30 May 2025 14:51:34 +0200 Subject: [PATCH 5/5] chore: enforce value at CI --- .github/workflows/ci-test-go.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci-test-go.yml b/.github/workflows/ci-test-go.yml index ab698022f6..5f526a0c1b 100644 --- a/.github/workflows/ci-test-go.yml +++ b/.github/workflows/ci-test-go.yml @@ -46,6 +46,7 @@ jobs: env: TESTCONTAINERS_RYUK_DISABLED: "${{ inputs.ryuk-disabled }}" RYUK_CONNECTION_TIMEOUT: "${{ inputs.project-directory == 'modules/compose' && '5m' || '60s' }}" + TESTCONTAINERS_PORT_MAPPING_TIMEOUT: "5s" RYUK_RECONNECTION_TIMEOUT: "${{ inputs.project-directory == 'modules/compose' && '30s' || '10s' }}" SHOULD_RUN_SONAR: "false" strategy: