mirror of
https://github.com/docker/compose.git
synced 2026-10-12 02:13:08 +00:00
e2e: fix test resource isolation and make standalone parallelism configurable
Several e2e tests used fixed host ports (8070, 8080, 8090, etc.), global buildx builders registered with --use, and a hardcoded IPC container name. Under parallel execution these shared daemon-level resources caused port conflicts, buildkitd container-name collisions, and intermittent failures. Separately, the standalone make target hardcoded -parallel=1, forcing the entire standalone suite to run serially even though the same tests run in parallel in plugin mode. Changes: - Convert fixed host ports in four fixtures (network-test, sentences, build-test, volume-test) to ephemeral bindings; add ServicePublishedPort helper to framework.go to resolve the actual mapped port at runtime. - Give each buildx-builder test a unique daemon-scoped name via BuilderName helper, preventing container-name collisions between parallel goroutines. Applies to TestBuildPlatformsWithCorrectBuildxConfig, TestBuildPrivileged, TestBuildBuilder, TestBuildEntitlements, TestBuildTLS. - Rename the fixed ipc_mode_container to a project-scoped name (ipc_e2e-src) and update the ipc-test fixture accordingly. - Replace the hardcoded -parallel=1 in make e2e-compose-standalone with E2E_PARALLEL_PLUGIN?=4 and E2E_STANDALONE_PARALLEL?=4, aligning both modes with the effective GOMAXPROCS on 4-core GitHub Actions runners. Both variables are overridable (E2E_STANDALONE_PARALLEL=1 restores serial local debugging). - Raise the poll delay in watch_test.go long-running polls (90s/120s timeout) from the default 100ms to 1s; bump cascade_test.go and publish_test.go from 100ms to 500ms. Reduces CPU pressure without affecting effective test duration. The logging-driver fixture retains its fixed port 24224: the Docker fluentd log driver encodes fluentd-address at service-start time, making ephemeral resolution non-trivial. TestLoggingDriver is serial so there is no collision risk in practice. Signed-off-by: Guillaume Lours <[email protected]>
This commit is contained in:
committed by
Nicolas De loof
parent
f8ed846e4d
commit
8bd79da4bc
@@ -32,6 +32,8 @@ endif
|
||||
BUILD_FLAGS?=
|
||||
TEST_FLAGS?=
|
||||
E2E_TEST?=
|
||||
E2E_PARALLEL_PLUGIN?=4
|
||||
E2E_STANDALONE_PARALLEL?=4
|
||||
ifneq ($(E2E_TEST),)
|
||||
TEST_FLAGS:=$(TEST_FLAGS) -run '$(E2E_TEST)'
|
||||
endif
|
||||
@@ -75,11 +77,11 @@ install: binary
|
||||
|
||||
.PHONY: e2e-compose
|
||||
e2e-compose: example-provider ## Run end to end local tests in plugin mode. Set E2E_TEST=TestName to run a single test
|
||||
go run gotest.tools/gotestsum@latest --format testname --junitfile "/tmp/report/report.xml" -- -v $(TEST_FLAGS) -count=1 -timeout 20m ./pkg/e2e
|
||||
go run gotest.tools/gotestsum@latest --format testname --junitfile "/tmp/report/report.xml" -- -v $(TEST_FLAGS) -count=1 -parallel=$(E2E_PARALLEL_PLUGIN) -timeout 20m ./pkg/e2e
|
||||
|
||||
.PHONY: e2e-compose-standalone
|
||||
e2e-compose-standalone: ## Run End to end local tests in standalone mode. Set E2E_TEST=TestName to run a single test
|
||||
go run gotest.tools/gotestsum@latest --format testname --junitfile "/tmp/report/report.xml" -- $(TEST_FLAGS) -v -count=1 -parallel=1 -timeout 20m --tags=standalone ./pkg/e2e
|
||||
go run gotest.tools/gotestsum@latest --format testname --junitfile "/tmp/report/report.xml" -- $(TEST_FLAGS) -v -count=1 -parallel=$(E2E_STANDALONE_PARALLEL) -timeout 20m --tags=standalone ./pkg/e2e
|
||||
|
||||
.PHONY: build-and-e2e-compose
|
||||
build-and-e2e-compose: build e2e-compose ## Compile the compose cli-plugin and run end to end local tests in plugin mode. Set E2E_TEST=TestName to run a single test
|
||||
|
||||
+22
-13
@@ -105,7 +105,7 @@ func TestLocalComposeBuild(t *testing.T) {
|
||||
res.Assert(t, icmd.Expected{Out: "COPY static /usr/share/nginx/html"})
|
||||
res.Assert(t, icmd.Expected{Out: "COPY static2 /usr/share/nginx/html"})
|
||||
|
||||
output := HTTPGetWithRetry(t, "http://localhost:8070", http.StatusOK, 2*time.Second, 20*time.Second)
|
||||
output := HTTPGetWithRetry(t, fmt.Sprintf("http://localhost:%d", c.ServicePublishedPort(t, "build-test", "nginx", 80)), http.StatusOK, 2*time.Second, 20*time.Second)
|
||||
assert.Assert(t, strings.Contains(output, "Hello from Nginx container"))
|
||||
|
||||
c.RunDockerCmd(t, "image", "inspect", "build-test-nginx")
|
||||
@@ -294,13 +294,15 @@ func TestBuildPlatformsWithCorrectBuildxConfig(t *testing.T) {
|
||||
}
|
||||
c := NewParallelCLI(t)
|
||||
|
||||
// declare builder
|
||||
result := c.RunDockerCmd(t, "buildx", "create", "--name", "build-platform", "--use", "--bootstrap")
|
||||
// declare a per-test-unique builder to avoid container-name collisions
|
||||
// when tests run in parallel on the same Docker daemon.
|
||||
builderName := BuilderName(t, "build-platform")
|
||||
result := c.RunDockerCmd(t, "buildx", "create", "--name", builderName, "--use", "--bootstrap")
|
||||
assert.NilError(t, result.Error)
|
||||
|
||||
t.Cleanup(func() {
|
||||
c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test/platforms", "down")
|
||||
_ = c.RunDockerCmd(t, "buildx", "rm", "-f", "build-platform")
|
||||
_ = c.RunDockerCmd(t, "buildx", "rm", "-f", builderName)
|
||||
})
|
||||
|
||||
t.Run("platform not supported by builder", func(t *testing.T) {
|
||||
@@ -358,14 +360,16 @@ func TestBuildPlatformsWithCorrectBuildxConfig(t *testing.T) {
|
||||
func TestBuildPrivileged(t *testing.T) {
|
||||
c := NewParallelCLI(t)
|
||||
|
||||
// declare builder
|
||||
result := c.RunDockerCmd(t, "buildx", "create", "--name", "build-privileged", "--use", "--bootstrap", "--buildkitd-flags",
|
||||
// declare a per-test-unique builder to avoid container-name collisions
|
||||
// when tests run in parallel on the same Docker daemon.
|
||||
builderName := BuilderName(t, "build-privileged")
|
||||
result := c.RunDockerCmd(t, "buildx", "create", "--name", builderName, "--use", "--bootstrap", "--buildkitd-flags",
|
||||
`'--allow-insecure-entitlement=security.insecure'`)
|
||||
assert.NilError(t, result.Error)
|
||||
|
||||
t.Cleanup(func() {
|
||||
c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test/privileged", "down")
|
||||
_ = c.RunDockerCmd(t, "buildx", "rm", "-f", "build-privileged")
|
||||
_ = c.RunDockerCmd(t, "buildx", "rm", "-f", builderName)
|
||||
})
|
||||
|
||||
t.Run("use build privileged mode to run insecure build command", func(t *testing.T) {
|
||||
@@ -473,8 +477,9 @@ func TestBuildPlatformsStandardErrors(t *testing.T) {
|
||||
|
||||
func TestBuildBuilder(t *testing.T) {
|
||||
c := NewParallelCLI(t)
|
||||
builderName := "build-with-builder"
|
||||
// declare builder
|
||||
// declare a per-test-unique builder to avoid container-name collisions
|
||||
// when tests run in parallel on the same Docker daemon.
|
||||
builderName := BuilderName(t, "build-with-builder")
|
||||
result := c.RunDockerCmd(t, "buildx", "create", "--name", builderName, "--use", "--bootstrap")
|
||||
assert.NilError(t, result.Error)
|
||||
|
||||
@@ -500,14 +505,16 @@ func TestBuildBuilder(t *testing.T) {
|
||||
func TestBuildEntitlements(t *testing.T) {
|
||||
c := NewParallelCLI(t)
|
||||
|
||||
// declare builder
|
||||
result := c.RunDockerCmd(t, "buildx", "create", "--name", "build-insecure", "--use", "--bootstrap", "--buildkitd-flags",
|
||||
// declare a per-test-unique builder to avoid container-name collisions
|
||||
// when tests run in parallel on the same Docker daemon.
|
||||
builderName := BuilderName(t, "build-insecure")
|
||||
result := c.RunDockerCmd(t, "buildx", "create", "--name", builderName, "--use", "--bootstrap", "--buildkitd-flags",
|
||||
`'--allow-insecure-entitlement=security.insecure'`)
|
||||
assert.NilError(t, result.Error)
|
||||
|
||||
t.Cleanup(func() {
|
||||
c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test/entitlements", "down")
|
||||
_ = c.RunDockerCmd(t, "buildx", "rm", "-f", "build-insecure")
|
||||
_ = c.RunDockerCmd(t, "buildx", "rm", "-f", builderName)
|
||||
})
|
||||
|
||||
t.Run("use build privileged mode to run insecure build command", func(t *testing.T) {
|
||||
@@ -599,7 +606,9 @@ func TestBuildTLS(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
c := NewParallelCLI(t)
|
||||
const dindBuilder = "e2e-dind-builder"
|
||||
// Use a per-test-unique name to avoid container/context collisions when
|
||||
// tests run in parallel on the same Docker daemon.
|
||||
dindBuilder := BuilderName(t, "e2e-dind-builder")
|
||||
tmp := t.TempDir()
|
||||
|
||||
t.Cleanup(func() {
|
||||
|
||||
@@ -61,7 +61,7 @@ func TestCascadeIgnoresOneOffContainer(t *testing.T) {
|
||||
})
|
||||
|
||||
poll.WaitOn(t, expectOutput(res, "Attaching to running-1"),
|
||||
poll.WithDelay(100*time.Millisecond), poll.WithTimeout(30*time.Second))
|
||||
poll.WithDelay(500*time.Millisecond), poll.WithTimeout(30*time.Second))
|
||||
|
||||
c.RunDockerComposeCmd(t, "-f", "./fixtures/cascade/compose.yaml",
|
||||
"run", "--rm", "--no-deps", "running", "/bin/true")
|
||||
|
||||
@@ -43,7 +43,8 @@ func TestLocalComposeUp(t *testing.T) {
|
||||
res := c.RunDockerComposeCmd(t, "-p", projectName, "ps")
|
||||
res.Assert(t, icmd.Expected{Out: `web`})
|
||||
|
||||
endpoint := "http://localhost:90"
|
||||
webPort := c.ServicePublishedPort(t, projectName, "web", 80)
|
||||
endpoint := fmt.Sprintf("http://localhost:%d", webPort)
|
||||
output := HTTPGetWithRetry(t, endpoint+"/words/noun", http.StatusOK, 2*time.Second, 20*time.Second)
|
||||
assert.Assert(t, strings.Contains(output, `"word":`))
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ services:
|
||||
nginx:
|
||||
build: nginx-build
|
||||
ports:
|
||||
- 8070:80
|
||||
- 80
|
||||
|
||||
nginx2:
|
||||
build: nginx-build2
|
||||
|
||||
@@ -15,7 +15,7 @@ services:
|
||||
image: gtardif/sentences-api
|
||||
init: true
|
||||
ports:
|
||||
- "8080:8080"
|
||||
- "8080"
|
||||
networks:
|
||||
- dbnet
|
||||
- servicenet
|
||||
@@ -23,7 +23,7 @@ services:
|
||||
image: gtardif/sentences-web
|
||||
init: true
|
||||
ports:
|
||||
- "80:80"
|
||||
- "80"
|
||||
labels:
|
||||
- "my-label=test"
|
||||
networks:
|
||||
|
||||
@@ -6,12 +6,12 @@ services:
|
||||
image: gtardif/sentences-api
|
||||
init: true
|
||||
ports:
|
||||
- "95:8080"
|
||||
- "8080"
|
||||
web:
|
||||
image: gtardif/sentences-web
|
||||
init: true
|
||||
ports:
|
||||
- "90:80"
|
||||
- "80"
|
||||
labels:
|
||||
- "my-label=test"
|
||||
healthcheck:
|
||||
|
||||
@@ -4,7 +4,7 @@ services:
|
||||
volumes:
|
||||
- ./static:/usr/share/nginx/html
|
||||
ports:
|
||||
- 8090:80
|
||||
- 80
|
||||
|
||||
nginx2:
|
||||
build: nginx-build
|
||||
@@ -13,7 +13,7 @@ services:
|
||||
- /usr/src/app/node_modules
|
||||
- otherVol:/usr/share/nginx/test
|
||||
ports:
|
||||
- 9090:80
|
||||
- 80
|
||||
configs:
|
||||
- myconfig
|
||||
secrets:
|
||||
|
||||
@@ -22,10 +22,12 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -510,3 +512,42 @@ func (c *CLI) cleanupWithDown(t testing.TB, project string, args ...string) {
|
||||
t.Helper()
|
||||
c.RunDockerComposeCmd(t, append([]string{"-p", project, "down", "-v", "--remove-orphans"}, args...)...)
|
||||
}
|
||||
|
||||
// ServicePublishedPort returns the ephemeral host port mapped to targetPort
|
||||
// on the named service in the given compose project. It requires the service
|
||||
// to be already running. The test fails immediately if the mapping cannot be
|
||||
// resolved.
|
||||
func (c *CLI) ServicePublishedPort(t testing.TB, project, service string, targetPort int) int {
|
||||
t.Helper()
|
||||
res := c.RunDockerComposeCmd(t, "-p", project, "port", service, strconv.Itoa(targetPort))
|
||||
addr := strings.TrimSpace(res.Stdout())
|
||||
_, portStr, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
t.Fatalf("ServicePublishedPort: cannot parse compose port output %q: %v", addr, err)
|
||||
}
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil {
|
||||
t.Fatalf("ServicePublishedPort: invalid port number %q: %v", portStr, err)
|
||||
}
|
||||
return port
|
||||
}
|
||||
|
||||
// BuilderName returns a buildx builder name that is unique to the test,
|
||||
// preventing container-name collisions when tests run in parallel on the
|
||||
// same Docker daemon.
|
||||
func BuilderName(t testing.TB, base string) string {
|
||||
t.Helper()
|
||||
safe := strings.Map(func(r rune) rune {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '-':
|
||||
return r
|
||||
default:
|
||||
return '-'
|
||||
}
|
||||
}, t.Name())
|
||||
name := base + "-" + safe
|
||||
if len(name) > 60 {
|
||||
name = strings.TrimRight(name[:60], "-")
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
@@ -42,7 +42,8 @@ func TestNetworks(t *testing.T) {
|
||||
res := c.RunDockerComposeCmd(t, "ps")
|
||||
res.Assert(t, icmd.Expected{Out: `web`})
|
||||
|
||||
endpoint := "http://localhost:80"
|
||||
webPort := c.ServicePublishedPort(t, projectName, "web", 80)
|
||||
endpoint := fmt.Sprintf("http://localhost:%d", webPort)
|
||||
output := HTTPGetWithRetry(t, endpoint+"/words/noun", http.StatusOK, 2*time.Second, 20*time.Second)
|
||||
assert.Assert(t, strings.Contains(output, `"word":`))
|
||||
|
||||
@@ -51,7 +52,7 @@ func TestNetworks(t *testing.T) {
|
||||
res.Assert(t, icmd.Expected{Out: "microservices"})
|
||||
|
||||
res = c.RunDockerComposeCmd(t, "port", "words", "8080")
|
||||
res.Assert(t, icmd.Expected{Out: `0.0.0.0:8080`})
|
||||
res.Assert(t, icmd.Expected{Out: `0.0.0.0:`})
|
||||
|
||||
c.RunDockerComposeCmd(t, "down", "-t0", "-v")
|
||||
res = c.RunDockerCmd(t, "network", "ls")
|
||||
|
||||
@@ -155,7 +155,7 @@ func TestPublish(t *testing.T) {
|
||||
return poll.Success()
|
||||
}
|
||||
return poll.Continue("registry not ready, status %d", resp.StatusCode)
|
||||
}, poll.WithTimeout(10*time.Second), poll.WithDelay(100*time.Millisecond))
|
||||
}, poll.WithTimeout(10*time.Second), poll.WithDelay(500*time.Millisecond))
|
||||
|
||||
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/publish/oci/compose.yaml", "-f", "./fixtures/publish/oci/compose-override.yaml",
|
||||
"-p", projectName, "publish", "--with-env", "--yes", "--insecure-registry", registry+"/test:test")
|
||||
|
||||
@@ -2,6 +2,11 @@ services:
|
||||
fluentbit:
|
||||
image: fluent/fluent-bit:3.1.7-debug
|
||||
ports:
|
||||
# Port 24224 intentionally fixed: the Docker fluentd log driver sets
|
||||
# fluentd-address at service-start time (see compose.yaml and
|
||||
# TestLoggingDriver in compose_up_test.go). Dynamic resolution would
|
||||
# require a two-phase compose up which is unnecessarily invasive.
|
||||
# TestLoggingDriver is serial (no t.Parallel()), so collision risk is nil.
|
||||
- "24224:24224"
|
||||
- "24224:24224/udp"
|
||||
environment:
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
@@ -43,7 +44,8 @@ func TestLocalComposeVolume(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("access bind mount data", func(t *testing.T) {
|
||||
output := HTTPGetWithRetry(t, "http://localhost:8090", http.StatusOK, 2*time.Second, 20*time.Second)
|
||||
port := c.ServicePublishedPort(t, projectName, "nginx", 80)
|
||||
output := HTTPGetWithRetry(t, fmt.Sprintf("http://localhost:%d", port), http.StatusOK, 2*time.Second, 20*time.Second)
|
||||
assert.Assert(t, strings.Contains(output, "Hello from Nginx container"))
|
||||
})
|
||||
|
||||
|
||||
@@ -345,7 +345,7 @@ func TestWatchMultiServices(t *testing.T) {
|
||||
return poll.Success()
|
||||
}
|
||||
return poll.Continue("%v", watch.Stdout())
|
||||
}, poll.WithTimeout(90*time.Second))
|
||||
}, poll.WithTimeout(90*time.Second), poll.WithDelay(time.Second))
|
||||
|
||||
waitRebuild := func(service string, expected string) {
|
||||
poll.WaitOn(t, func(l poll.LogT) poll.Result {
|
||||
@@ -354,7 +354,7 @@ func TestWatchMultiServices(t *testing.T) {
|
||||
return poll.Success()
|
||||
}
|
||||
return poll.Continue("%v", cat.Combined())
|
||||
}, poll.WithTimeout(90*time.Second))
|
||||
}, poll.WithTimeout(90*time.Second), poll.WithDelay(time.Second))
|
||||
}
|
||||
waitRebuild("a", "test")
|
||||
waitRebuild("b", "test")
|
||||
@@ -412,7 +412,7 @@ func TestWatchRebuildIgnoresDependencies(t *testing.T) {
|
||||
return poll.Success()
|
||||
}
|
||||
return poll.Continue("waiting for watch to start: %v", buffer.String())
|
||||
}, poll.WithTimeout(120*time.Second))
|
||||
}, poll.WithTimeout(120*time.Second), poll.WithDelay(time.Second))
|
||||
|
||||
// Record the cutoff point in the log buffer so we only inspect output
|
||||
// produced AFTER the file change triggers the rebuild.
|
||||
|
||||
Reference in New Issue
Block a user