From a7293130c6cb7d1a574a723cb1da43db1dcd6f2d Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Mon, 17 Aug 2026 08:56:12 +0200 Subject: [PATCH] =?UTF-8?q?test(e2e):=20compose=20command=20scenarios=20?= =?UTF-8?q?=E2=80=94=20rm,=20compatibility,=20config,=20orphan=20scoping?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TestRm, TestRemoveOrphaned and TestUnnecessaryResources observe which containers exist instead of grepping docker ps. - TestCompatibility drops the heavyweight sentences fixture: underscore naming shows on any model. - TestConfig/TestConfigInterpolate build their expected rendering from the scenario's own project name and directory; OutputMatches joins the vocabulary for TestInitContainer's ordering assertion. - TestStopWithDependenciesAttached inlines its model. - Kept legacy: TestLocalComposeUp (fixed port, HTTP, shared sentences fixture), TestDownComposefileInParentFolder (project name inferred from a fixture-relative directory), TestAttachRestart (long-running attached up), the COMPOSE_FILE-from-.env family and TestNestedDotEnv (they exercise invocation without -f, which ComposeCmd always sets). - Fixtures init-container/, external/ and dependencies/ are removed. Signed-off-by: Nicolas De Loof --- pkg/e2e/checks.go | 14 ++ pkg/e2e/compose_test.go | 192 ++++++------------ pkg/e2e/fixtures/dependencies/compose.yaml | 10 - pkg/e2e/fixtures/external/compose.yaml | 14 -- .../testdata/TestCompatibility/compose.yaml | 5 + pkg/e2e/testdata/TestConfig/compose.yaml | 5 + .../TestConfig/nginx-build/Dockerfile | 2 + .../TestConfigInterpolate/compose.yaml | 5 + .../nginx-build/Dockerfile | 2 + .../TestInitContainer}/compose.yaml | 0 .../testdata/TestRemoveOrphaned/compose.yaml | 13 ++ pkg/e2e/testdata/TestRm/compose.yaml | 9 + .../compose.yaml | 10 + .../TestUnnecessaryResources/compose.yaml | 18 ++ 14 files changed, 145 insertions(+), 154 deletions(-) delete mode 100644 pkg/e2e/fixtures/dependencies/compose.yaml delete mode 100644 pkg/e2e/fixtures/external/compose.yaml create mode 100644 pkg/e2e/testdata/TestCompatibility/compose.yaml create mode 100644 pkg/e2e/testdata/TestConfig/compose.yaml create mode 100644 pkg/e2e/testdata/TestConfig/nginx-build/Dockerfile create mode 100644 pkg/e2e/testdata/TestConfigInterpolate/compose.yaml create mode 100644 pkg/e2e/testdata/TestConfigInterpolate/nginx-build/Dockerfile rename pkg/e2e/{fixtures/init-container => testdata/TestInitContainer}/compose.yaml (100%) create mode 100644 pkg/e2e/testdata/TestRemoveOrphaned/compose.yaml create mode 100644 pkg/e2e/testdata/TestRm/compose.yaml create mode 100644 pkg/e2e/testdata/TestStopWithDependenciesAttached/compose.yaml create mode 100644 pkg/e2e/testdata/TestUnnecessaryResources/compose.yaml diff --git a/pkg/e2e/checks.go b/pkg/e2e/checks.go index efe0a2718..6572f0d0d 100644 --- a/pkg/e2e/checks.go +++ b/pkg/e2e/checks.go @@ -89,6 +89,20 @@ func StdoutContains(sub string) Check { } } +// OutputMatches expects the regular expression to match the command's stdout, +// for expectations OutputContains cannot express (e.g. ordering). +func OutputMatches(pattern string) Check { + return Check{ + name: fmt.Sprintf("output matches %q", pattern), + fn: func(ctx *CheckContext) error { + if !regexp.MustCompile(pattern).MatchString(ctx.result.Stdout()) { + return fmt.Errorf("no match in stdout") + } + return nil + }, + } +} + // OutputMatchesCount expects the regular expression to match the command's // stdout exactly n times, e.g. counting how many times a service was built. func OutputMatchesCount(pattern string, n int) Check { diff --git a/pkg/e2e/compose_test.go b/pkg/e2e/compose_test.go index 41e2a89d7..db4810d63 100644 --- a/pkg/e2e/compose_test.go +++ b/pkg/e2e/compose_test.go @@ -26,7 +26,6 @@ import ( "time" "gotest.tools/v3/assert" - is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/icmd" ) @@ -159,88 +158,46 @@ func TestAttachRestart(t *testing.T) { } func TestInitContainer(t *testing.T) { - c := NewParallelCLI(t) - - res := c.RunDockerComposeCmd(t, "--ansi=never", "--project-directory", "./fixtures/init-container", "up", "--menu=false") - defer c.RunDockerComposeCmd(t, "-p", "init-container", "down") - assert.Assert(t, is.Regexp("foo-1 | hello(?m:.*)bar-1 | world", res.Stdout())) + NewScenario(t, "a service_completed_successfully dependency must run to completion before its dependent starts"). + Step("up runs the init container first, then the dependent", + ComposeCmd("--ansi=never", "up", "--menu=false"), + OutputMatches("foo-1 | hello(?m:.*)bar-1 | world")) } func TestRm(t *testing.T) { - c := NewParallelCLI(t) - - const projectName = "compose-e2e-rm" - - t.Run("up", func(t *testing.T) { - c.RunDockerComposeCmd(t, "-f", "./fixtures/simple-composefile/compose.yaml", "-p", projectName, "up", "-d") - }) - - t.Run("rm --stop --force simple", func(t *testing.T) { - res := c.RunDockerComposeCmd(t, "-f", "./fixtures/simple-composefile/compose.yaml", "-p", projectName, "rm", - "--stop", "--force", "simple") - res.Assert(t, icmd.Expected{Err: "Removed", ExitCode: 0}) - }) - - t.Run("check containers after rm", func(t *testing.T) { - res := c.RunDockerCmd(t, "ps", "--all") - assert.Assert(t, !strings.Contains(res.Combined(), projectName+"-simple"), res.Combined()) - assert.Assert(t, strings.Contains(res.Combined(), projectName+"-another"), res.Combined()) - }) - - t.Run("up (again)", func(t *testing.T) { - c.RunDockerComposeCmd(t, "-f", "./fixtures/simple-composefile/compose.yaml", "-p", projectName, "up", "-d") - }) - - t.Run("rm ---stop --force ", func(t *testing.T) { - res := c.RunDockerComposeCmd(t, "-f", "./fixtures/simple-composefile/compose.yaml", "-p", projectName, "rm", - "--stop", "--force") - res.Assert(t, icmd.Expected{ExitCode: 0}) - }) - - t.Run("check containers after rm", func(t *testing.T) { - res := c.RunDockerCmd(t, "ps", "--all") - assert.Assert(t, !strings.Contains(res.Combined(), projectName+"-simple"), res.Combined()) - assert.Assert(t, !strings.Contains(res.Combined(), projectName+"-another"), res.Combined()) - }) - - t.Run("down", func(t *testing.T) { - c.RunDockerComposeCmd(t, "-p", projectName, "down") - }) + NewScenario(t, "rm --stop --force must remove the selected service's containers, or every service's without selection"). + Step("up starts both services", + ComposeCmd("up", "-d"), + ServiceState("simple", "running"), + ServiceState("another", "running")). + Step("rm on one service removes only its container", + ComposeCmd("rm", "--stop", "--force", "simple"), + ServiceNotCreated("simple"), + ServiceState("another", "running")). + Step("up brings the removed service back", + ComposeCmd("up", "-d"), + ServiceState("simple", "running")). + Step("rm without selection removes every container", + ComposeCmd("rm", "--stop", "--force"), + ServiceNotCreated("simple"), + ServiceNotCreated("another")) } func TestCompatibility(t *testing.T) { - // this test shares a fixture with TestLocalComposeUp and can't run at the same time - c := NewCLI(t) - - const projectName = "compose-e2e-compatibility" - - t.Run("up", func(t *testing.T) { - c.RunDockerComposeCmd(t, "--compatibility", "-f", "./fixtures/sentences/compose.yaml", "--project-name", - projectName, "up", "-d") - }) - - t.Run("check container names", func(t *testing.T) { - res := c.RunDockerCmd(t, "ps", "--format", "{{.Names}}") - res.Assert(t, icmd.Expected{Out: "compose-e2e-compatibility_web_1"}) - res.Assert(t, icmd.Expected{Out: "compose-e2e-compatibility_words_1"}) - res.Assert(t, icmd.Expected{Out: "compose-e2e-compatibility_db_1"}) - }) - - t.Run("down", func(t *testing.T) { - c.RunDockerComposeCmd(t, "-p", projectName, "down") - }) + s := NewScenario(t, "--compatibility must name containers with underscore separators") + s.Step("up names the container the v1 way", + ComposeCmd("--compatibility", "up", "-d"), + ServiceState("simple", "running")). + Step("the container name uses underscores", + DockerCmd("ps", "--filter", "label=com.docker.compose.project="+s.Project(), "--format", "{{.Names}}"), + OutputContains(s.Project()+"_simple_1")) } func TestConfig(t *testing.T) { - const projectName = "compose-e2e-config" - c := NewParallelCLI(t) - - wd, err := os.Getwd() - assert.NilError(t, err) - - t.Run("up", func(t *testing.T) { - res := c.RunDockerComposeCmd(t, "-f", "./fixtures/simple-build-test/compose.yaml", "-p", projectName, "config") - res.Assert(t, icmd.Expected{Out: fmt.Sprintf(`name: %s + s := NewScenario(t, "config must render the canonical model, resolving the build context to an absolute path") + s.Step("the rendering resolves the context and names the default network", + ComposeCmd("config"), + OutputContains(fmt.Sprintf(`name: %s services: nginx: build: @@ -250,24 +207,18 @@ services: default: null networks: default: - name: compose-e2e-config_default -`, projectName, filepath.Join(wd, "fixtures", "simple-build-test", "nginx-build")), ExitCode: 0}) - }) + name: %s_default +`, s.Project(), filepath.Join(s.Dir(), "nginx-build"), s.Project()))) } func TestConfigInterpolate(t *testing.T) { - const projectName = "compose-e2e-config-interpolate" - c := NewParallelCLI(t) - - wd, err := os.Getwd() - assert.NilError(t, err) - - t.Run("config", func(t *testing.T) { - res := c.RunDockerComposeCmd(t, "-f", "./fixtures/simple-build-test/compose-interpolate.yaml", "-p", projectName, "config", "--no-interpolate") - res.Assert(t, icmd.Expected{Out: fmt.Sprintf(`name: %s + s := NewScenario(t, "config --no-interpolate must keep variable expressions while still resolving paths") + s.Step("the rendering keeps the dockerfile variable", + ComposeCmd("config", "--no-interpolate"), + OutputContains(fmt.Sprintf(`name: %s networks: default: - name: compose-e2e-config-interpolate_default + name: %s_default services: nginx: build: @@ -275,43 +226,28 @@ services: dockerfile: ${MYVAR} networks: default: null -`, projectName, filepath.Join(wd, "fixtures", "simple-build-test", "nginx-build")), ExitCode: 0}) - }) +`, s.Project(), s.Project(), filepath.Join(s.Dir(), "nginx-build")))) } func TestStopWithDependenciesAttached(t *testing.T) { - const projectName = "compose-e2e-stop-with-deps" - c := NewParallelCLI(t, WithEnv("COMMAND=echo hello")) - - cleanup := func() { - c.RunDockerComposeCmd(t, "-p", projectName, "down", "--remove-orphans", "--timeout=0") - } - cleanup() - t.Cleanup(cleanup) - - res := c.RunDockerComposeCmd(t, "-f", "./fixtures/dependencies/compose.yaml", "-p", projectName, "up", "--attach-dependencies", "foo", "--menu=false") - res.Assert(t, icmd.Expected{Out: "exited with code 0"}) + NewScenario(t, "up --attach-dependencies must stop with the target service, reporting its exit"). + Step("up returns when the attached service exits", + ComposeCmd("up", "--attach-dependencies", "foo", "--menu=false").Within(60*time.Second), + OutputContains("exited with code 0")) } func TestRemoveOrphaned(t *testing.T) { - const projectName = "compose-e2e-remove-orphaned" - c := NewParallelCLI(t) - - cleanup := func() { - c.RunDockerComposeCmd(t, "-p", projectName, "down", "--remove-orphans", "--timeout=0") - } - cleanup() - t.Cleanup(cleanup) - - // run stack - c.RunDockerComposeCmd(t, "-f", "./fixtures/sentences/compose.yaml", "-p", projectName, "up", "-d") - - // down "web" service with orphaned removed - c.RunDockerComposeCmd(t, "-f", "./fixtures/sentences/compose.yaml", "-p", projectName, "down", "--remove-orphans", "web") - - // check "words" service has not been considered orphaned - res := c.RunDockerComposeCmd(t, "-f", "./fixtures/sentences/compose.yaml", "-p", projectName, "ps", "--format", "{{.Name}}") - res.Assert(t, icmd.Expected{Out: fmt.Sprintf("%s-words-1", projectName)}) + NewScenario(t, "down --remove-orphans scoped to a service must not touch the other services"). + Step("up starts the stack", + ComposeCmd("up", "-d"), + ServiceState("web", "running"), + ServiceState("words", "running"), + ServiceState("db", "running")). + Step("down scoped to one service leaves the others alone", + ComposeCmd("down", "--remove-orphans", "web"), + ServiceNotCreated("web"), + ServiceState("words", "running"), + ServiceState("db", "running")) } func TestComposeFileSetByDotEnv(t *testing.T) { @@ -390,16 +326,12 @@ func TestNestedDotEnv(t *testing.T) { } func TestUnnecessaryResources(t *testing.T) { - const projectName = "compose-e2e-unnecessary-resources" - c := NewParallelCLI(t) - defer c.cleanupWithDown(t, projectName) - - res := c.RunDockerComposeCmdNoCheck(t, "-f", "./fixtures/external/compose.yaml", "-p", projectName, "up", "-d") - res.Assert(t, icmd.Expected{ - ExitCode: 1, - Err: "network foo_bar declared as external, but could not be found", - }) - - c.RunDockerComposeCmd(t, "-f", "./fixtures/external/compose.yaml", "-p", projectName, "up", "-d", "test") - // Should not fail as missing external network is not used + NewScenario(t, "a missing external network must only fail the services that use it"). + Step("up with every service is rejected over the missing network", + ComposeCmd("up", "-d").MayFail(), + ExitCode(1), + OutputContains("network foo_bar declared as external, but could not be found")). + Step("up scoped to the unaffected service succeeds", + ComposeCmd("up", "-d", "test"), + ServiceState("test", "running")) } diff --git a/pkg/e2e/fixtures/dependencies/compose.yaml b/pkg/e2e/fixtures/dependencies/compose.yaml deleted file mode 100644 index 099c5f18a..000000000 --- a/pkg/e2e/fixtures/dependencies/compose.yaml +++ /dev/null @@ -1,10 +0,0 @@ -services: - foo: - image: nginx:alpine - command: "${COMMAND}" - depends_on: - - bar - - bar: - image: nginx:alpine - scale: 2 diff --git a/pkg/e2e/fixtures/external/compose.yaml b/pkg/e2e/fixtures/external/compose.yaml deleted file mode 100644 index 29b8b74a1..000000000 --- a/pkg/e2e/fixtures/external/compose.yaml +++ /dev/null @@ -1,14 +0,0 @@ -services: - test: - image: nginx:alpine - - other: - image: nginx:alpine - networks: - test_network: - ipv4_address: 8.8.8.8 - -networks: - test_network: - external: true - name: foo_bar \ No newline at end of file diff --git a/pkg/e2e/testdata/TestCompatibility/compose.yaml b/pkg/e2e/testdata/TestCompatibility/compose.yaml new file mode 100644 index 000000000..c1179460b --- /dev/null +++ b/pkg/e2e/testdata/TestCompatibility/compose.yaml @@ -0,0 +1,5 @@ +services: + simple: + image: alpine + init: true + command: top diff --git a/pkg/e2e/testdata/TestConfig/compose.yaml b/pkg/e2e/testdata/TestConfig/compose.yaml new file mode 100644 index 000000000..7e72f1c20 --- /dev/null +++ b/pkg/e2e/testdata/TestConfig/compose.yaml @@ -0,0 +1,5 @@ +services: + nginx: + build: + context: nginx-build + dockerfile: Dockerfile diff --git a/pkg/e2e/testdata/TestConfig/nginx-build/Dockerfile b/pkg/e2e/testdata/TestConfig/nginx-build/Dockerfile new file mode 100644 index 000000000..2f54902fd --- /dev/null +++ b/pkg/e2e/testdata/TestConfig/nginx-build/Dockerfile @@ -0,0 +1,2 @@ +FROM alpine + diff --git a/pkg/e2e/testdata/TestConfigInterpolate/compose.yaml b/pkg/e2e/testdata/TestConfigInterpolate/compose.yaml new file mode 100644 index 000000000..57d092a99 --- /dev/null +++ b/pkg/e2e/testdata/TestConfigInterpolate/compose.yaml @@ -0,0 +1,5 @@ +services: + nginx: + build: + context: nginx-build + dockerfile: ${MYVAR} diff --git a/pkg/e2e/testdata/TestConfigInterpolate/nginx-build/Dockerfile b/pkg/e2e/testdata/TestConfigInterpolate/nginx-build/Dockerfile new file mode 100644 index 000000000..2f54902fd --- /dev/null +++ b/pkg/e2e/testdata/TestConfigInterpolate/nginx-build/Dockerfile @@ -0,0 +1,2 @@ +FROM alpine + diff --git a/pkg/e2e/fixtures/init-container/compose.yaml b/pkg/e2e/testdata/TestInitContainer/compose.yaml similarity index 100% rename from pkg/e2e/fixtures/init-container/compose.yaml rename to pkg/e2e/testdata/TestInitContainer/compose.yaml diff --git a/pkg/e2e/testdata/TestRemoveOrphaned/compose.yaml b/pkg/e2e/testdata/TestRemoveOrphaned/compose.yaml new file mode 100644 index 000000000..e8d07d49b --- /dev/null +++ b/pkg/e2e/testdata/TestRemoveOrphaned/compose.yaml @@ -0,0 +1,13 @@ +services: + web: + image: alpine + init: true + command: sleep infinity + words: + image: alpine + init: true + command: sleep infinity + db: + image: alpine + init: true + command: sleep infinity diff --git a/pkg/e2e/testdata/TestRm/compose.yaml b/pkg/e2e/testdata/TestRm/compose.yaml new file mode 100644 index 000000000..88154e296 --- /dev/null +++ b/pkg/e2e/testdata/TestRm/compose.yaml @@ -0,0 +1,9 @@ +services: + simple: + image: alpine + init: true + command: top + another: + image: alpine + init: true + command: top diff --git a/pkg/e2e/testdata/TestStopWithDependenciesAttached/compose.yaml b/pkg/e2e/testdata/TestStopWithDependenciesAttached/compose.yaml new file mode 100644 index 000000000..8933d2829 --- /dev/null +++ b/pkg/e2e/testdata/TestStopWithDependenciesAttached/compose.yaml @@ -0,0 +1,10 @@ +services: + foo: + image: alpine + command: echo hello + depends_on: + - bar + bar: + image: alpine + init: true + command: sleep infinity diff --git a/pkg/e2e/testdata/TestUnnecessaryResources/compose.yaml b/pkg/e2e/testdata/TestUnnecessaryResources/compose.yaml new file mode 100644 index 000000000..a38513ffa --- /dev/null +++ b/pkg/e2e/testdata/TestUnnecessaryResources/compose.yaml @@ -0,0 +1,18 @@ +services: + test: + image: alpine + init: true + command: sleep infinity + + other: + image: alpine + init: true + command: sleep infinity + networks: + test_network: + ipv4_address: 8.8.8.8 + +networks: + test_network: + external: true + name: foo_bar