From 12288a7fffad3262c536c55800268ef4ae752de2 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Mon, 17 Aug 2026 07:49:06 +0200 Subject: [PATCH] =?UTF-8?q?test(e2e):=20pause=20scenarios=20=E2=80=94=20pa?= =?UTF-8?q?used=20is=20a=20container=20state,=20ExitCode=20locks=20error?= =?UTF-8?q?=20paths?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrate the four pause tests to the Scenario DSL: the invariant is the container state (paused/running), not an HTTP timeout, so the scenarios observe state and drop the port-mapping helpers and the pause fixture. ExitCode joins the check vocabulary for error-path steps, paired with MayFail. TestPauseServiceDoesNotExist now locks the current behavior — pause of an unknown service is rejected with 'no such service' — which resolves the TODO the legacy test carried: it asserted exit 0 while wishing for an error, and the CLI has since started validating selected services. Signed-off-by: Nicolas De Loof --- pkg/e2e/checks.go | 15 ++ pkg/e2e/fixtures/pause/compose.yaml | 17 -- pkg/e2e/pause_test.go | 158 ++++-------------- pkg/e2e/testdata/TestPause/compose.yaml | 9 + .../compose.yaml | 5 + .../TestPauseServiceDoesNotExist/compose.yaml | 5 + .../TestPauseServiceNotRunning/compose.yaml | 5 + 7 files changed, 72 insertions(+), 142 deletions(-) delete mode 100644 pkg/e2e/fixtures/pause/compose.yaml create mode 100644 pkg/e2e/testdata/TestPause/compose.yaml create mode 100644 pkg/e2e/testdata/TestPauseServiceAlreadyPaused/compose.yaml create mode 100644 pkg/e2e/testdata/TestPauseServiceDoesNotExist/compose.yaml create mode 100644 pkg/e2e/testdata/TestPauseServiceNotRunning/compose.yaml diff --git a/pkg/e2e/checks.go b/pkg/e2e/checks.go index 88a6bb0f8..8f3d9d326 100644 --- a/pkg/e2e/checks.go +++ b/pkg/e2e/checks.go @@ -85,6 +85,21 @@ func OutputNotContains(sub string) Check { } } +// ExitCode expects the step's command to have exited with the given code. +// Only meaningful on a MayFail action: without MayFail, any non-zero exit +// already fails the step before checks run. +func ExitCode(code int) Check { + return Check{ + name: fmt.Sprintf("command exits with code %d", code), + fn: func(ctx *CheckContext) error { + if ctx.result.ExitCode != code { + return fmt.Errorf("exit code is %d", ctx.result.ExitCode) + } + return nil + }, + } +} + // Eventually retries a state-based check until it holds or the timeout // expires, re-observing the project state between attempts. Output-based // checks are not meaningful here: the step's output never changes. diff --git a/pkg/e2e/fixtures/pause/compose.yaml b/pkg/e2e/fixtures/pause/compose.yaml deleted file mode 100644 index 615fcad58..000000000 --- a/pkg/e2e/fixtures/pause/compose.yaml +++ /dev/null @@ -1,17 +0,0 @@ -services: - a: - image: nginx:alpine - ports: [80] - healthcheck: - test: wget --spider -S -T1 http://localhost:80 - interval: 1s - timeout: 1s - b: - image: nginx:alpine - ports: [80] - depends_on: - - a - healthcheck: - test: wget --spider -S -T1 http://localhost:80 - interval: 1s - timeout: 1s diff --git a/pkg/e2e/pause_test.go b/pkg/e2e/pause_test.go index 8b94e4653..d8b037c26 100644 --- a/pkg/e2e/pause_test.go +++ b/pkg/e2e/pause_test.go @@ -17,143 +17,51 @@ package e2e import ( - "encoding/json" - "errors" - "fmt" - "net" - "net/http" "testing" - "time" - - "gotest.tools/v3/assert" - "gotest.tools/v3/icmd" ) func TestPause(t *testing.T) { - cli := NewParallelCLI(t, WithEnv( - "COMPOSE_PROJECT_NAME=e2e-pause", - "COMPOSE_FILE=./fixtures/pause/compose.yaml")) - - cleanup := func() { - cli.RunDockerComposeCmd(t, "down", "-v", "--remove-orphans", "-t", "0") - } - cleanup() - t.Cleanup(cleanup) - - // launch both services and verify that they are accessible - cli.RunDockerComposeCmd(t, "up", "-d") - urls := map[string]string{ - "a": urlForService(t, cli, "a", 80), - "b": urlForService(t, cli, "b", 80), - } - for _, url := range urls { - HTTPGetWithRetry(t, url, http.StatusOK, 50*time.Millisecond, 20*time.Second) - } - - // pause a and verify that it can no longer be hit but b still can - cli.RunDockerComposeCmd(t, "pause", "a") - httpClient := http.Client{Timeout: 250 * time.Millisecond} - resp, err := httpClient.Get(urls["a"]) - if resp != nil { - _ = resp.Body.Close() - } - assert.Assert(t, err != nil, "a should no longer respond") - var netErr net.Error - assert.ErrorType(t, err, &netErr, "expected a network error") - errors.As(err, &netErr) - assert.Assert(t, netErr.Timeout(), "Error should have indicated a timeout") - HTTPGetWithRetry(t, urls["b"], http.StatusOK, 50*time.Millisecond, 5*time.Second) - - // unpause a and verify that both containers work again - cli.RunDockerComposeCmd(t, "unpause", "a") - for _, url := range urls { - HTTPGetWithRetry(t, url, http.StatusOK, 50*time.Millisecond, 5*time.Second) - } + NewScenario(t, "pause must freeze only the targeted service, unpause must resume it in place"). + Step("up starts both services", + ComposeCmd("up", "-d"), + ServiceState("a", "running"), + ServiceState("b", "running")). + Step("pause freezes the targeted service, the other keeps running", + ComposeCmd("pause", "a"), + ServiceState("a", "paused"), + ServiceState("b", "running")). + Step("unpause resumes the paused service without recreating anything", + ComposeCmd("unpause", "a"), + ServiceState("a", "running"), + ServiceState("b", "running"), + NotRecreated("a", "b")) } func TestPauseServiceNotRunning(t *testing.T) { - cli := NewParallelCLI(t, WithEnv( - "COMPOSE_PROJECT_NAME=e2e-pause-svc-not-running", - "COMPOSE_FILE=./fixtures/pause/compose.yaml")) - - cleanup := func() { - cli.RunDockerComposeCmd(t, "down", "-v", "--remove-orphans", "-t", "0") - } - cleanup() - t.Cleanup(cleanup) - - // pause a and verify that it can no longer be hit but b still can - res := cli.RunDockerComposeCmdNoCheck(t, "pause", "a") - // TODO: `docker pause` errors in this case, should Compose be consistent? - res.Assert(t, icmd.Expected{ExitCode: 0}) + NewScenario(t, "pause of a service with no container must succeed as a no-op"). + Step("pause without any container is accepted", + ComposeCmd("pause", "a")) } func TestPauseServiceAlreadyPaused(t *testing.T) { - cli := NewParallelCLI(t, WithEnv( - "COMPOSE_PROJECT_NAME=e2e-pause-svc-already-paused", - "COMPOSE_FILE=./fixtures/pause/compose.yaml")) - - cleanup := func() { - cli.RunDockerComposeCmd(t, "down", "-v", "--remove-orphans", "-t", "0") - } - cleanup() - t.Cleanup(cleanup) - - // launch a and wait for it to come up - cli.RunDockerComposeCmd(t, "up", "--menu=false", "--wait", "a") - HTTPGetWithRetry(t, urlForService(t, cli, "a", 80), http.StatusOK, 50*time.Millisecond, 10*time.Second) - - // pause a twice - first time should pass, second time fail - cli.RunDockerComposeCmd(t, "pause", "a") - res := cli.RunDockerComposeCmdNoCheck(t, "pause", "a") - res.Assert(t, icmd.Expected{ExitCode: 1, Err: "already paused"}) + NewScenario(t, "pausing an already-paused service must fail"). + Step("up starts the service", + ComposeCmd("up", "-d"), + ServiceState("a", "running")). + Step("a first pause freezes the service", + ComposeCmd("pause", "a"), + ServiceState("a", "paused")). + Step("a second pause is rejected", + ComposeCmd("pause", "a").MayFail(), + ExitCode(1), + OutputContains("already paused")) } func TestPauseServiceDoesNotExist(t *testing.T) { - cli := NewParallelCLI(t, WithEnv( - "COMPOSE_PROJECT_NAME=e2e-pause-svc-not-exist", - "COMPOSE_FILE=./fixtures/pause/compose.yaml")) - - cleanup := func() { - cli.RunDockerComposeCmd(t, "down", "-v", "--remove-orphans", "-t", "0") - } - cleanup() - t.Cleanup(cleanup) - - // pause a and verify that it can no longer be hit but b still can - res := cli.RunDockerComposeCmdNoCheck(t, "pause", "does_not_exist") - // TODO: `compose down does_not_exist` and similar error, this should too - res.Assert(t, icmd.Expected{ExitCode: 0}) -} - -func urlForService(t testing.TB, cli *CLI, service string, targetPort int) string { - t.Helper() - return fmt.Sprintf( - "http://localhost:%d", - publishedPortForService(t, cli, service, targetPort), - ) -} - -func publishedPortForService(t testing.TB, cli *CLI, service string, targetPort int) int { - t.Helper() - res := cli.RunDockerComposeCmd(t, "ps", "--format=json", service) - var svc struct { - Publishers []struct { - TargetPort int - PublishedPort int - } - } - assert.NilError(t, json.Unmarshal([]byte(res.Stdout()), &svc), - "Failed to parse `%s` output", res.Cmd.String()) - var found bool - var port int - for _, pp := range svc.Publishers { - if pp.TargetPort == targetPort { - found = true - port = pp.PublishedPort - } - } - assert.Assert(t, found, "No published port for target port %d\nService: %s", targetPort, res.Combined()) - return port + NewScenario(t, "pause of a service unknown to the model must be rejected"). + Step("pause of an unknown service is rejected", + ComposeCmd("pause", "does_not_exist").MayFail(), + ExitCode(1), + OutputContains("no such service: does_not_exist")) } diff --git a/pkg/e2e/testdata/TestPause/compose.yaml b/pkg/e2e/testdata/TestPause/compose.yaml new file mode 100644 index 000000000..23f2acbf1 --- /dev/null +++ b/pkg/e2e/testdata/TestPause/compose.yaml @@ -0,0 +1,9 @@ +services: + a: + image: alpine + init: true + command: sleep infinity + b: + image: alpine + init: true + command: sleep infinity diff --git a/pkg/e2e/testdata/TestPauseServiceAlreadyPaused/compose.yaml b/pkg/e2e/testdata/TestPauseServiceAlreadyPaused/compose.yaml new file mode 100644 index 000000000..2e684be98 --- /dev/null +++ b/pkg/e2e/testdata/TestPauseServiceAlreadyPaused/compose.yaml @@ -0,0 +1,5 @@ +services: + a: + image: alpine + init: true + command: sleep infinity diff --git a/pkg/e2e/testdata/TestPauseServiceDoesNotExist/compose.yaml b/pkg/e2e/testdata/TestPauseServiceDoesNotExist/compose.yaml new file mode 100644 index 000000000..2e684be98 --- /dev/null +++ b/pkg/e2e/testdata/TestPauseServiceDoesNotExist/compose.yaml @@ -0,0 +1,5 @@ +services: + a: + image: alpine + init: true + command: sleep infinity diff --git a/pkg/e2e/testdata/TestPauseServiceNotRunning/compose.yaml b/pkg/e2e/testdata/TestPauseServiceNotRunning/compose.yaml new file mode 100644 index 000000000..2e684be98 --- /dev/null +++ b/pkg/e2e/testdata/TestPauseServiceNotRunning/compose.yaml @@ -0,0 +1,5 @@ +services: + a: + image: alpine + init: true + command: sleep infinity