fix: make e2e tests pass reliably locally with Docker Desktop

- Fix stale image/container reuse across test runs                                                                                                                                                                                                                                                          - Add registry readiness check and async removal polling
  - Skip multi-arch test when docker driver supports it
  - Use t.Cleanup for reliable teardown, fix project name mismatches
  - Re-enable 4 previously skipped tests that now pass

Signed-off-by: Guillaume Lours <glours@users.noreply.github.com>
This commit is contained in:
Guillaume Lours 2026-04-15 21:06:36 +02:00 committed by Guillaume Lours
parent 66c21c3b6c
commit 8e0d5e17a7
12 changed files with 80 additions and 33 deletions

View file

@ -410,6 +410,25 @@ func TestBuildPlatformsStandardErrors(t *testing.T) {
})
t.Run("builder does not support multi-arch", func(t *testing.T) {
// Docker Desktop with containerd image store uses the docker driver
// but supports multi-platform builds, so this error won't occur.
inspect := c.RunDockerCmd(t, "buildx", "inspect", "--bootstrap")
output := inspect.Stdout()
isDockerDriver := false
platforms := ""
for _, line := range strings.Split(output, "\n") {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "Driver:") {
isDockerDriver = strings.TrimSpace(strings.TrimPrefix(trimmed, "Driver:")) == "docker"
}
if strings.HasPrefix(trimmed, "Platforms:") {
platforms = trimmed
}
}
if isDockerDriver && strings.Contains(platforms, "linux/amd64") && strings.Contains(platforms, "linux/arm64") {
t.Skip("docker driver supports multi-platform (containerd image store enabled)")
}
res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/build-test/platforms", "build")
res.Assert(t, icmd.Expected{
ExitCode: 1,

View file

@ -36,8 +36,11 @@ func TestRunBuildOnce(t *testing.T) {
t.Run("dependency with pull_policy build is built only once", func(t *testing.T) {
projectName := randomProjectName("build-once")
_ = c.RunDockerComposeCmd(t, "-p", projectName, "-f", "./fixtures/run-test/build-once.yaml", "down", "--rmi", "local", "--remove-orphans", "-v")
res := c.RunDockerComposeCmd(t, "-p", projectName, "-f", "./fixtures/run-test/build-once.yaml", "--verbose", "run", "--build", "--rm", "curl")
composeFile := "./fixtures/run-test/build-once.yaml"
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "-p", projectName, "-f", composeFile, "down", "--rmi", "local", "--remove-orphans", "-v")
})
res := c.RunDockerComposeCmd(t, "-p", projectName, "-f", composeFile, "--verbose", "run", "--build", "--rm", "curl")
output := res.Stdout()
@ -45,14 +48,15 @@ func TestRunBuildOnce(t *testing.T) {
assert.Equal(t, nginxBuilds, 1, "nginx should build once, built %d times\nOutput:\n%s", nginxBuilds, output)
assert.Assert(t, strings.Contains(res.Stdout(), "curl service"))
c.RunDockerComposeCmd(t, "-p", projectName, "-f", "./fixtures/run-test/build-once.yaml", "down", "--remove-orphans")
})
t.Run("nested dependencies build only once each", func(t *testing.T) {
projectName := randomProjectName("build-nested")
_ = c.RunDockerComposeCmd(t, "-p", projectName, "-f", "./fixtures/run-test/build-once-nested.yaml", "down", "--rmi", "local", "--remove-orphans", "-v")
res := c.RunDockerComposeCmd(t, "-p", projectName, "-f", "./fixtures/run-test/build-once-nested.yaml", "--verbose", "run", "--build", "--rm", "app")
composeFile := "./fixtures/run-test/build-once-nested.yaml"
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "-p", projectName, "-f", composeFile, "down", "--rmi", "local", "--remove-orphans", "-v")
})
res := c.RunDockerComposeCmd(t, "-p", projectName, "-f", composeFile, "--verbose", "run", "--build", "--rm", "app")
output := res.Stdout()
@ -64,14 +68,15 @@ func TestRunBuildOnce(t *testing.T) {
assert.Equal(t, apiBuilds, 1, "api should build once, built %d times\nOutput:\n%s", apiBuilds, output)
assert.Equal(t, appBuilds, 1, "app should build once, built %d times\nOutput:\n%s", appBuilds, output)
assert.Assert(t, strings.Contains(output, "App running"))
c.RunDockerComposeCmd(t, "-p", projectName, "-f", "./fixtures/run-test/build-once-nested.yaml", "down", "--rmi", "local", "--remove-orphans", "-v")
})
t.Run("service with no dependencies builds once", func(t *testing.T) {
projectName := randomProjectName("build-simple")
_ = c.RunDockerComposeCmd(t, "-p", projectName, "-f", "./fixtures/run-test/build-once-no-deps.yaml", "down", "--rmi", "local", "--remove-orphans")
res := c.RunDockerComposeCmd(t, "-p", projectName, "-f", "./fixtures/run-test/build-once-no-deps.yaml", "run", "--build", "--rm", "simple")
composeFile := "./fixtures/run-test/build-once-no-deps.yaml"
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "-p", projectName, "-f", composeFile, "down", "--rmi", "local", "--remove-orphans", "-v")
})
res := c.RunDockerComposeCmd(t, "-p", projectName, "-f", composeFile, "run", "--build", "--rm", "simple")
output := res.Stdout()
@ -79,8 +84,6 @@ func TestRunBuildOnce(t *testing.T) {
assert.Equal(t, simpleBuilds, 1, "simple should build once, built %d times\nOutput:\n%s", simpleBuilds, output)
assert.Assert(t, strings.Contains(res.Stdout(), "Simple service"))
c.RunDockerComposeCmd(t, "-p", projectName, "-f", "./fixtures/run-test/build-once-no-deps.yaml", "down", "--remove-orphans")
})
}

View file

@ -20,9 +20,11 @@ import (
"os"
"strings"
"testing"
"time"
"gotest.tools/v3/assert"
"gotest.tools/v3/icmd"
"gotest.tools/v3/poll"
)
func TestLocalComposeRun(t *testing.T) {
@ -207,8 +209,14 @@ func TestLocalComposeRun(t *testing.T) {
res = c.RunDockerCmd(t, "stop", containerID)
res.Assert(t, icmd.Success)
res = c.RunDockerCmd(t, "ps", "--all", "--filter", "name=run-test-nginx", "--format", "'{{.Names}}'")
assert.Assert(t, !strings.Contains(res.Stdout(), "run-test-nginx"), res.Stdout())
// --rm auto-removal is async, wait for the container to be removed
poll.WaitOn(t, func(l poll.LogT) poll.Result {
res = c.RunDockerCmd(t, "ps", "--all", "--filter", "name=run-test-nginx", "--format", "'{{.Names}}'")
if strings.Contains(res.Stdout(), "run-test-nginx") {
return poll.Continue("container still present: %s", res.Stdout())
}
return poll.Success()
}, poll.WithTimeout(10*time.Second), poll.WithDelay(500*time.Millisecond))
})
t.Run("compose run --env", func(t *testing.T) {

View file

@ -140,16 +140,13 @@ func TestDownComposefileInParentFolder(t *testing.T) {
}
func TestAttachRestart(t *testing.T) {
t.Skip("Skipping test until we can fix it")
if _, ok := os.LookupEnv("CI"); ok {
t.Skip("Skipping test on CI... flaky")
}
c := NewParallelCLI(t)
cmd := c.NewDockerComposeCmd(t, "--ansi=never", "--project-directory", "./fixtures/attach-restart", "up")
res := icmd.StartCmd(cmd)
defer c.RunDockerComposeCmd(t, "-p", "attach-restart", "down")
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "-p", "attach-restart", "down")
})
c.WaitForCondition(t, func() (bool, string) {
debug := res.Combined()

View file

@ -46,8 +46,9 @@ func TestUnusedMissingEnvFile(t *testing.T) {
func TestRunEnvFile(t *testing.T) {
c := NewParallelCLI(t)
defer c.cleanupWithDown(t, "run_dotenv")
const projectName = "run-dotenv"
defer c.cleanupWithDown(t, projectName)
res := c.RunDockerComposeCmd(t, "--project-directory", "./fixtures/env_file", "run", "serviceC", "env")
res := c.RunDockerComposeCmd(t, "-p", projectName, "--project-directory", "./fixtures/env_file", "run", "--rm", "serviceC", "env")
res.Assert(t, icmd.Expected{Out: "FOO=BAR"})
}

View file

@ -21,9 +21,11 @@ import (
)
func TestComposeModel(t *testing.T) {
t.Skip("waiting for docker-model release")
if _, err := findPluginExecutable(DockerModelExecutableName); err != nil {
t.Skip("docker-model plugin not available")
}
c := NewParallelCLI(t)
defer c.cleanupWithDown(t, "model-test")
c.RunDockerComposeCmd(t, "-f", "./fixtures/model/compose.yaml", "run", "test", "sh", "-c", "curl ${FOO_URL}")
c.RunDockerComposeCmd(t, "-p", "model-test", "-f", "./fixtures/model/compose.yaml", "run", "--rm", "test", "sh", "-c", "curl ${FOO_URL}")
}

View file

@ -147,7 +147,6 @@ func TestNetworkModes(t *testing.T) {
}
func TestNetworkConfigChanged(t *testing.T) {
t.Skip("unstable")
// fixture is shared with TestNetworks and is not safe to run concurrently
c := NewCLI(t)
const projectName = "network_config_change"

View file

@ -22,7 +22,6 @@ import (
"fmt"
"net"
"net/http"
"os"
"testing"
"time"
@ -31,9 +30,6 @@ import (
)
func TestPause(t *testing.T) {
if _, ok := os.LookupEnv("CI"); ok {
t.Skip("Skipping test on CI... flaky")
}
cli := NewParallelCLI(t, WithEnv(
"COMPOSE_PROJECT_NAME=e2e-pause",
"COMPOSE_FILE=./fixtures/pause/compose.yaml"))

View file

@ -32,6 +32,9 @@ func TestPs(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "e2e-ps"
// ensure clean state from any previous failed run
c.RunDockerComposeCmdNoCheck(t, "--project-name", projectName, "down", "--remove-orphans")
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/ps-test/compose.yaml", "--project-name", projectName, "up", "-d")
assert.NilError(t, res.Error)
t.Cleanup(func() {

View file

@ -18,11 +18,14 @@ package e2e
import (
"fmt"
"net/http"
"strings"
"testing"
"time"
"gotest.tools/v3/assert"
"gotest.tools/v3/icmd"
"gotest.tools/v3/poll"
)
func TestPublishChecks(t *testing.T) {
@ -136,6 +139,20 @@ func TestPublish(t *testing.T) {
c.RunDockerCmd(t, "rm", "--force", registryName)
})
// Wait for registry to be ready
registryURL := "http://" + registry + "/v2/"
poll.WaitOn(t, func(l poll.LogT) poll.Result {
resp, err := http.Get(registryURL) //nolint:gosec,noctx
if err != nil {
return poll.Continue("registry not ready: %v", err)
}
_ = resp.Body.Close()
if resp.StatusCode < 500 {
return poll.Success()
}
return poll.Continue("registry not ready, status %d", resp.StatusCode)
}, poll.WithTimeout(10*time.Second), poll.WithDelay(100*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")
res.Assert(t, icmd.Expected{ExitCode: 0})

View file

@ -335,7 +335,7 @@ func TestWatchMultiServices(t *testing.T) {
testFile := filepath.Join(tmpdir, "test")
assert.NilError(t, os.WriteFile(testFile, []byte("test"), 0o600))
cmd := c.NewDockerComposeCmd(t, "-p", projectName, "-f", composeFilePath, "up", "--watch")
cmd := c.NewDockerComposeCmd(t, "-p", projectName, "-f", composeFilePath, "up", "--build", "--watch")
buffer := bytes.NewBuffer(nil)
cmd.Stdout = buffer
watch := icmd.StartCmd(cmd)
@ -345,7 +345,7 @@ func TestWatchMultiServices(t *testing.T) {
return poll.Success()
}
return poll.Continue("%v", watch.Stdout())
})
}, poll.WithTimeout(90*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))
}
waitRebuild("a", "test")
waitRebuild("b", "test")

View file

@ -79,7 +79,9 @@ func (b *SafeBuffer) RequireEventuallyContains(t testing.TB, v string) {
}
return poll.Success()
},
poll.WithTimeout(2*time.Second),
// 10s: container startup on Docker Desktop (macOS) with VM overhead
// can take 3-8s vs <1s on native Linux CI
poll.WithTimeout(10*time.Second),
poll.WithDelay(20*time.Millisecond),
)
}