From cd1c0ca6ca095c729b15d53defb7206bf6a2e37c Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Mon, 17 Aug 2026 08:30:00 +0200 Subject: [PATCH] test(e2e): configs, commit, export and logs scenarios MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ImageExists and FileExists join the vocabulary, giving commit and export a real observable: the legacy tests only asserted the commands exited 0, the scenarios verify the committed image and the exported archive actually exist. Both also drop their fixed image tags and cwd-relative tar outputs for project-scoped tags and temp files, so parallel runs cannot collide and nothing is left behind in the repo. TestLocalComposeLogs waits for the echo service to exit before reading logs (Eventually), closing the race the legacy test tolerated. The follow and large-logs tests stay legacy: both drive a long-running command in the background. ps_test.go stays legacy as well — it verifies ps table/JSON formatting, which is inherently output-shaped. Fixtures configs/, commit/ and export/ are removed. Signed-off-by: Nicolas De Loof --- pkg/e2e/checks.go | 34 +++++++ pkg/e2e/commit_test.go | 93 ++++++------------- pkg/e2e/configs_test.go | 47 +++------- pkg/e2e/export_test.go | 44 +++++---- pkg/e2e/logs_test.go | 60 ++++-------- .../TestCommit}/compose.yaml | 2 +- .../TestCommitWithReplicas}/compose.yaml | 2 +- .../TestConfigFromEnv}/compose.yaml | 0 .../TestConfigFromEnv}/config.txt | 1 + pkg/e2e/testdata/TestExport/compose.yaml | 9 ++ .../TestExportWithReplicas/compose.yaml | 9 ++ .../TestLocalComposeLogs/compose.yaml | 10 ++ 12 files changed, 147 insertions(+), 164 deletions(-) rename pkg/e2e/{fixtures/commit => testdata/TestCommit}/compose.yaml (89%) rename pkg/e2e/{fixtures/export => testdata/TestCommitWithReplicas}/compose.yaml (89%) rename pkg/e2e/{fixtures/configs => testdata/TestConfigFromEnv}/compose.yaml (100%) rename pkg/e2e/{fixtures/configs => testdata/TestConfigFromEnv}/config.txt (95%) create mode 100644 pkg/e2e/testdata/TestExport/compose.yaml create mode 100644 pkg/e2e/testdata/TestExportWithReplicas/compose.yaml create mode 100644 pkg/e2e/testdata/TestLocalComposeLogs/compose.yaml diff --git a/pkg/e2e/checks.go b/pkg/e2e/checks.go index 12b6fbda6..68898b976 100644 --- a/pkg/e2e/checks.go +++ b/pkg/e2e/checks.go @@ -24,6 +24,7 @@ package e2e import ( "fmt" + "os" "runtime" "slices" "strings" @@ -376,6 +377,39 @@ func OneOffsRemoved(service string) Check { } } +// ImageExists expects an image with the given reference to be present in the +// local store. +func ImageExists(ref string) Check { + return Check{ + name: fmt.Sprintf("image %q exists", ref), + fn: func(ctx *CheckContext) error { + res := icmd.RunCmd(ctx.scenario.cli.NewDockerCmd(ctx.scenario.t, "image", "inspect", "--format", "{{.Id}}", ref)) + if res.ExitCode != 0 { + return fmt.Errorf("not found: %s", strings.TrimSpace(res.Combined())) + } + return nil + }, + } +} + +// FileExists expects a non-empty file at the given host path, e.g. the output +// of an export command. +func FileExists(path string) Check { + return Check{ + name: fmt.Sprintf("file %q exists", path), + fn: func(ctx *CheckContext) error { + info, err := os.Stat(path) + if err != nil { + return err + } + if info.Size() == 0 { + return fmt.Errorf("file is empty") + } + return nil + }, + } +} + // LabelSet expects every container of the service to carry a non-empty label. func LabelSet(service, key string) Check { return Check{ diff --git a/pkg/e2e/commit_test.go b/pkg/e2e/commit_test.go index 0daf13054..7298b6e79 100644 --- a/pkg/e2e/commit_test.go +++ b/pkg/e2e/commit_test.go @@ -21,73 +21,34 @@ import ( ) func TestCommit(t *testing.T) { - const projectName = "e2e-commit-service" - c := NewParallelCLI(t) - - cleanup := func() { - c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "--timeout=0", "--remove-orphans") - } - t.Cleanup(cleanup) - cleanup() - - c.RunDockerComposeCmd(t, "-f", "./fixtures/commit/compose.yaml", "--project-name", projectName, "up", "-d", "service") - - c.RunDockerComposeCmd( - t, - "--project-name", - projectName, - "commit", - "-a", - "John Hannibal Smith ", - "-c", - "ENV DEBUG=true", - "-m", - "sample commit", - "service", - "service:latest", - ) + s := NewScenario(t, "commit must produce an image from the service's container") + image := s.Project() + ":latest" + s.Defer(DockerCmd("image", "rm", "-f", image).MayFail()). + Step("up starts the service", + ComposeCmd("up", "-d", "service"), + ServiceState("service", "running")). + Step("commit turns the container into a tagged image", + ComposeCmd("commit", + "-a", "John Hannibal Smith ", + "-c", "ENV DEBUG=true", + "-m", "sample commit", + "service", image), + ImageExists(image)) } func TestCommitWithReplicas(t *testing.T) { - const projectName = "e2e-commit-service-with-replicas" - c := NewParallelCLI(t) - - cleanup := func() { - c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "--timeout=0", "--remove-orphans") - } - t.Cleanup(cleanup) - cleanup() - - c.RunDockerComposeCmd(t, "-f", "./fixtures/commit/compose.yaml", "--project-name", projectName, "up", "-d", "service-with-replicas") - - c.RunDockerComposeCmd( - t, - "--project-name", - projectName, - "commit", - "-a", - "John Hannibal Smith ", - "-c", - "ENV DEBUG=true", - "-m", - "sample commit", - "--index=1", - "service-with-replicas", - "service-with-replicas:1", - ) - c.RunDockerComposeCmd( - t, - "--project-name", - projectName, - "commit", - "-a", - "John Hannibal Smith ", - "-c", - "ENV DEBUG=true", - "-m", - "sample commit", - "--index=2", - "service-with-replicas", - "service-with-replicas:2", - ) + s := NewScenario(t, "commit --index must pick the requested replica of a scaled service") + image1, image2 := s.Project()+":1", s.Project()+":2" + s.Defer( + DockerCmd("image", "rm", "-f", image1).MayFail(), + DockerCmd("image", "rm", "-f", image2).MayFail()). + Step("up starts the replicas", + ComposeCmd("up", "-d", "service-with-replicas"), + ServiceScale("service-with-replicas", 3)). + Step("commit --index=1 images the first replica", + ComposeCmd("commit", "-m", "sample commit", "--index=1", "service-with-replicas", image1), + ImageExists(image1)). + Step("commit --index=2 images the second replica", + ComposeCmd("commit", "-m", "sample commit", "--index=2", "service-with-replicas", image2), + ImageExists(image2)) } diff --git a/pkg/e2e/configs_test.go b/pkg/e2e/configs_test.go index c7d86f1c7..899c5168f 100644 --- a/pkg/e2e/configs_test.go +++ b/pkg/e2e/configs_test.go @@ -18,40 +18,21 @@ package e2e import ( "testing" - - "gotest.tools/v3/icmd" ) func TestConfigFromEnv(t *testing.T) { - c := NewParallelCLI(t) - defer c.cleanupWithDown(t, "configs") - - t.Run("config from file", func(t *testing.T) { - res := icmd.RunCmd(c.NewDockerComposeCmd(t, "-f", "./fixtures/configs/compose.yaml", "run", "from_file")) - res.Assert(t, icmd.Expected{Out: "This is my config file"}) - }) - - t.Run("config from env", func(t *testing.T) { - res := icmd.RunCmd(c.NewDockerComposeCmd(t, "-f", "./fixtures/configs/compose.yaml", "run", "from_env"), - func(cmd *icmd.Cmd) { - cmd.Env = append(cmd.Env, "CONFIG=config") - }) - res.Assert(t, icmd.Expected{Out: "config"}) - }) - - t.Run("config inlined", func(t *testing.T) { - res := icmd.RunCmd(c.NewDockerComposeCmd(t, "-f", "./fixtures/configs/compose.yaml", "run", "inlined"), - func(cmd *icmd.Cmd) { - cmd.Env = append(cmd.Env, "CONFIG=config") - }) - res.Assert(t, icmd.Expected{Out: "This is my config"}) - }) - - t.Run("custom target", func(t *testing.T) { - res := icmd.RunCmd(c.NewDockerComposeCmd(t, "-f", "./fixtures/configs/compose.yaml", "run", "target"), - func(cmd *icmd.Cmd) { - cmd.Env = append(cmd.Env, "CONFIG=config") - }) - res.Assert(t, icmd.Expected{Out: "This is my config"}) - }) + NewScenario(t, "each config source — file, environment, inline content — must mount with the right content"). + Env("CONFIG=config"). + Step("a file-sourced config mounts its file's content", + ComposeCmd("run", "from_file"), + OutputContains("This is my config file")). + Step("an environment-sourced config mounts the variable's value", + ComposeCmd("run", "from_env"), + OutputContains("config")). + Step("an inline config mounts its interpolated content", + ComposeCmd("run", "inlined"), + OutputContains("This is my config")). + Step("a custom target mounts the config at the requested path", + ComposeCmd("run", "target"), + OutputContains("This is my config")) } diff --git a/pkg/e2e/export_test.go b/pkg/e2e/export_test.go index baa0dc5b9..5cceb693a 100644 --- a/pkg/e2e/export_test.go +++ b/pkg/e2e/export_test.go @@ -17,34 +17,32 @@ package e2e import ( + "path/filepath" "testing" ) func TestExport(t *testing.T) { - const projectName = "e2e-export-service" - c := NewParallelCLI(t) - - cleanup := func() { - c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "--timeout=0", "--remove-orphans") - } - t.Cleanup(cleanup) - cleanup() - - c.RunDockerComposeCmd(t, "-f", "./fixtures/export/compose.yaml", "--project-name", projectName, "up", "-d", "service") - c.RunDockerComposeCmd(t, "--project-name", projectName, "export", "-o", "service.tar", "service") + out := filepath.Join(t.TempDir(), "service.tar") + NewScenario(t, "export must write the service container's filesystem as a tar archive"). + Step("up starts the service", + ComposeCmd("up", "-d", "service"), + ServiceState("service", "running")). + Step("export writes the container filesystem to the output file", + ComposeCmd("export", "-o", out, "service"), + FileExists(out)) } func TestExportWithReplicas(t *testing.T) { - const projectName = "e2e-export-service-with-replicas" - c := NewParallelCLI(t) - - cleanup := func() { - c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "--timeout=0", "--remove-orphans") - } - t.Cleanup(cleanup) - cleanup() - - c.RunDockerComposeCmd(t, "-f", "./fixtures/export/compose.yaml", "--project-name", projectName, "up", "-d", "service-with-replicas") - c.RunDockerComposeCmd(t, "--project-name", projectName, "export", "-o", "r1.tar", "--index=1", "service-with-replicas") - c.RunDockerComposeCmd(t, "--project-name", projectName, "export", "-o", "r2.tar", "--index=2", "service-with-replicas") + dir := t.TempDir() + r1, r2 := filepath.Join(dir, "r1.tar"), filepath.Join(dir, "r2.tar") + NewScenario(t, "export --index must pick the requested replica of a scaled service"). + Step("up starts the replicas", + ComposeCmd("up", "-d", "service-with-replicas"), + ServiceScale("service-with-replicas", 3)). + Step("export --index=1 writes the first replica's filesystem", + ComposeCmd("export", "-o", r1, "--index=1", "service-with-replicas"), + FileExists(r1)). + Step("export --index=2 writes the second replica's filesystem", + ComposeCmd("export", "-o", r2, "--index=2", "service-with-replicas"), + FileExists(r2)) } diff --git a/pkg/e2e/logs_test.go b/pkg/e2e/logs_test.go index 3706f1364..afc2e0890 100644 --- a/pkg/e2e/logs_test.go +++ b/pkg/e2e/logs_test.go @@ -31,46 +31,26 @@ import ( ) func TestLocalComposeLogs(t *testing.T) { - c := NewParallelCLI(t) - - const projectName = "compose-e2e-logs" - - t.Run("up", func(t *testing.T) { - c.RunDockerComposeCmd(t, "-f", "./fixtures/logs-test/compose.yaml", "--project-name", projectName, "up", "-d") - }) - - t.Run("logs", func(t *testing.T) { - res := c.RunDockerComposeCmd(t, "--project-name", projectName, "logs") - res.Assert(t, icmd.Expected{Out: `PING localhost`}) - res.Assert(t, icmd.Expected{Out: `hello`}) - }) - - t.Run("logs ping", func(t *testing.T) { - res := c.RunDockerComposeCmd(t, "--project-name", projectName, "logs", "ping") - res.Assert(t, icmd.Expected{Out: `PING localhost`}) - assert.Assert(t, !strings.Contains(res.Stdout(), "hello")) - }) - - t.Run("logs hello", func(t *testing.T) { - res := c.RunDockerComposeCmd(t, "--project-name", projectName, "logs", "hello", "ping") - res.Assert(t, icmd.Expected{Out: `PING localhost`}) - res.Assert(t, icmd.Expected{Out: `hello`}) - }) - - t.Run("logs hello index", func(t *testing.T) { - res := c.RunDockerComposeCmd(t, "--project-name", projectName, "logs", "--index", "2", "hello") - - // docker-compose logs hello - // logs-test-hello-2 | hello - // logs-test-hello-1 | hello - t.Log(res.Stdout()) - assert.Assert(t, !strings.Contains(res.Stdout(), "hello-1")) - assert.Assert(t, strings.Contains(res.Stdout(), "hello-2")) - }) - - t.Run("down", func(t *testing.T) { - _ = c.RunDockerComposeCmd(t, "--project-name", projectName, "down") - }) + NewScenario(t, "logs must aggregate the requested services' output, and --index must select one replica"). + Step("up starts the services", + ComposeCmd("up", "-d"), + Eventually(ServiceState("hello", "exited"), 10*time.Second)). + Step("logs aggregates every service's output", + ComposeCmd("logs"), + OutputContains("PING localhost"), + OutputContains("hello")). + Step("logs on one service filters the others out", + ComposeCmd("logs", "ping"), + OutputContains("PING localhost"), + OutputNotContains("hello")). + Step("logs accepts several services", + ComposeCmd("logs", "hello", "ping"), + OutputContains("PING localhost"), + OutputContains("hello")). + Step("logs --index selects a single replica", + ComposeCmd("logs", "--index", "2", "hello"), + OutputContains("hello-2"), + OutputNotContains("hello-1")) } func TestLocalComposeLogsFollow(t *testing.T) { diff --git a/pkg/e2e/fixtures/commit/compose.yaml b/pkg/e2e/testdata/TestCommit/compose.yaml similarity index 89% rename from pkg/e2e/fixtures/commit/compose.yaml rename to pkg/e2e/testdata/TestCommit/compose.yaml index 28e4b15bd..6ab3d9093 100644 --- a/pkg/e2e/fixtures/commit/compose.yaml +++ b/pkg/e2e/testdata/TestCommit/compose.yaml @@ -6,4 +6,4 @@ services: image: alpine command: sleep infinity deploy: - replicas: 3 \ No newline at end of file + replicas: 3 diff --git a/pkg/e2e/fixtures/export/compose.yaml b/pkg/e2e/testdata/TestCommitWithReplicas/compose.yaml similarity index 89% rename from pkg/e2e/fixtures/export/compose.yaml rename to pkg/e2e/testdata/TestCommitWithReplicas/compose.yaml index 28e4b15bd..6ab3d9093 100644 --- a/pkg/e2e/fixtures/export/compose.yaml +++ b/pkg/e2e/testdata/TestCommitWithReplicas/compose.yaml @@ -6,4 +6,4 @@ services: image: alpine command: sleep infinity deploy: - replicas: 3 \ No newline at end of file + replicas: 3 diff --git a/pkg/e2e/fixtures/configs/compose.yaml b/pkg/e2e/testdata/TestConfigFromEnv/compose.yaml similarity index 100% rename from pkg/e2e/fixtures/configs/compose.yaml rename to pkg/e2e/testdata/TestConfigFromEnv/compose.yaml diff --git a/pkg/e2e/fixtures/configs/config.txt b/pkg/e2e/testdata/TestConfigFromEnv/config.txt similarity index 95% rename from pkg/e2e/fixtures/configs/config.txt rename to pkg/e2e/testdata/TestConfigFromEnv/config.txt index 58b9a4061..4e8fc5908 100644 --- a/pkg/e2e/fixtures/configs/config.txt +++ b/pkg/e2e/testdata/TestConfigFromEnv/config.txt @@ -1 +1,2 @@ This is my config file + diff --git a/pkg/e2e/testdata/TestExport/compose.yaml b/pkg/e2e/testdata/TestExport/compose.yaml new file mode 100644 index 000000000..6ab3d9093 --- /dev/null +++ b/pkg/e2e/testdata/TestExport/compose.yaml @@ -0,0 +1,9 @@ +services: + service: + image: alpine + command: sleep infinity + service-with-replicas: + image: alpine + command: sleep infinity + deploy: + replicas: 3 diff --git a/pkg/e2e/testdata/TestExportWithReplicas/compose.yaml b/pkg/e2e/testdata/TestExportWithReplicas/compose.yaml new file mode 100644 index 000000000..6ab3d9093 --- /dev/null +++ b/pkg/e2e/testdata/TestExportWithReplicas/compose.yaml @@ -0,0 +1,9 @@ +services: + service: + image: alpine + command: sleep infinity + service-with-replicas: + image: alpine + command: sleep infinity + deploy: + replicas: 3 diff --git a/pkg/e2e/testdata/TestLocalComposeLogs/compose.yaml b/pkg/e2e/testdata/TestLocalComposeLogs/compose.yaml new file mode 100644 index 000000000..0375a7446 --- /dev/null +++ b/pkg/e2e/testdata/TestLocalComposeLogs/compose.yaml @@ -0,0 +1,10 @@ +services: + ping: + image: alpine + init: true + command: ping localhost -c 1 + hello: + image: alpine + command: echo hello + deploy: + replicas: 2