diff --git a/pkg/e2e/checks.go b/pkg/e2e/checks.go index b7c792f20..a4ceb10b3 100644 --- a/pkg/e2e/checks.go +++ b/pkg/e2e/checks.go @@ -88,6 +88,20 @@ func StdoutContains(sub string) Check { } } +// StderrContains expects the command's stderr to contain a string, e.g. a +// container's stderr stream relayed by up. +func StderrContains(sub string) Check { + return Check{ + name: fmt.Sprintf("stderr contains %q", sub), + fn: func(ctx *CheckContext) error { + if !strings.Contains(ctx.result.Stderr(), sub) { + return fmt.Errorf("not found in stderr") + } + return nil + }, + } +} + // OutputNotContains expects the command output not to contain a string. func OutputNotContains(sub string) Check { return Check{ diff --git a/pkg/e2e/compose_run_test.go b/pkg/e2e/compose_run_test.go index 242ef38b9..3a80fdc86 100644 --- a/pkg/e2e/compose_run_test.go +++ b/pkg/e2e/compose_run_test.go @@ -17,227 +17,157 @@ package e2e import ( - "os" "strings" "testing" "time" - "gotest.tools/v3/assert" "gotest.tools/v3/icmd" "gotest.tools/v3/poll" ) -func TestLocalComposeRun(t *testing.T) { +func TestComposeRun(t *testing.T) { + s := NewScenario(t, "run must execute a one-off with the service command or an override, starting its dependencies") + s.Step("run executes the service's own command and starts its dependency", + ComposeCmd("run", "back"), + StdoutContains("Hello there!!"), + OutputNotContains("orphan"), + ServiceState("db", "running"), + OneOffState("back", "exited"), + ServiceNotCreated("front")). + Step("run with an override command warns about the previous one-off, now an orphan", + ComposeCmd("run", "back", "echo", "Hello one more time"), + StdoutContains("Hello one more time"), + OutputContains("orphan")). + Step("COMPOSE_IGNORE_ORPHANS silences the warning", + ComposeCmd("run", "back", "echo", "Hello again").WithEnv("COMPOSE_IGNORE_ORPHANS=True"), + StdoutContains("Hello again"), + OutputNotContains("orphan")). + Step("run --rm leaves the earlier one-offs alone and removes its own container", + ComposeCmd("run", "--rm", "back", "echo", "Hello and gone").WithEnv("COMPOSE_IGNORE_ORPHANS=True"), + StdoutContains("Hello and gone"), + OneOffsUntouched("back")). + Step("run --volumes bind-mounts the requested host path", + ComposeCmd("run", "--volumes", s.Dir()+":/foo", "back", "/bin/sh", "-c", "ls /foo").WithEnv("COMPOSE_IGNORE_ORPHANS=True"), + StdoutContains("compose.yaml")). + Step("run --env-from-file injects the file's variables", + ComposeCmd("run", "--env-from-file", s.Dir()+"/run.env", "front", "env").WithEnv("COMPOSE_IGNORE_ORPHANS=True"), + StdoutContains("FOO=BAR")). + Step("run --env injects the variable", + ComposeCmd("run", "--env", "FOO=BAR", "front", "env").WithEnv("COMPOSE_IGNORE_ORPHANS=True"), + StdoutContains("FOO=BAR")) +} + +func TestComposeRunPorts(t *testing.T) { + s := NewScenario(t, "run must only publish ports when asked: --publish for ad-hoc, --service-ports for the model's") + s.Step("run --publish maps the requested port, not the model's", + ComposeCmd("run", "--publish", "8081:80", "-d", "back", "/bin/sh", "-c", "sleep 30")). + Step("the ad-hoc mapping is live", + DockerCmd("ps", "--filter", "label=com.docker.compose.project="+s.Project()), + OutputContains("8081->80/tcp"), + OutputNotContains("8082->80/tcp")). + Step("run --service-ports maps the model's ports", + ComposeCmd("run", "--service-ports", "-d", "back", "/bin/sh", "-c", "sleep 30")). + Step("the model's mapping is live", + DockerCmd("ps", "--filter", "label=com.docker.compose.project="+s.Project()), + OutputContains("8082->80/tcp")) +} + +func TestComposeRunDeps(t *testing.T) { + // Regression test for https://github.com/docker/compose/issues/9459 + // run used to start other services of the project beyond the target's + // dependency chain. + NewScenario(t, "run must start the target's dependencies and nothing else, unless --no-deps"). + Step("run starts the shared dependency but not the sibling service", + ComposeCmd("run", "service_a"), + OutputContains("shared_dep"), + OutputNotContains("service_b"), + ServiceNotCreated("service_b")). + Step("run --no-deps starts nothing but the one-off", + ComposeCmd("run", "--no-deps", "service_a").WithEnv("COMPOSE_IGNORE_ORPHANS=True"), + OutputNotContains("service_b"), + OutputNotContains("shared_dep"), + NotRecreated("shared_dep"), + ServiceNotCreated("service_b")) +} + +func TestComposeRunNotRequiredDeps(t *testing.T) { + NewScenario(t, "run must skip a dependency marked required: false when its profile is inactive"). + Step("run executes the service without materializing the optional dependency", + ComposeCmd("run", "foo"), + OutputContains("foo"), + ServiceNotCreated("bar")) +} + +func TestComposeRunQuietPull(t *testing.T) { + NewScenario(t, "run --quiet-pull and COMPOSE_PROGRESS=quiet must silence pull progress at two levels"). + Step("start without the image locally", + ComposeCmd("down", "--rmi", "all")). + Step("--quiet-pull keeps the decision but drops the layer progress", + ComposeCmd("run", "--quiet-pull", "backend"), + OutputNotContains("Pull complete"), + OutputContains("Pulled")). + Step("remove the image again", + ComposeCmd("down", "--rmi", "all")). + Step("COMPOSE_PROGRESS=quiet silences the pull entirely", + ComposeCmd("run", "backend").WithEnv("COMPOSE_PROGRESS=quiet"), + OutputNotContains("Pull complete"), + OutputNotContains("Pulled")) +} + +func TestComposeRunPullAlways(t *testing.T) { + NewScenario(t, "run --pull always must pull the image even when present locally"). + Step("run reports the pull it was asked to always perform", + ComposeCmd("run", "--pull", "always", "backend"), + OutputContains("Image nginx Pulling"), + OutputContains("Image nginx Pulled")) +} + +func TestComposeRunBuild(t *testing.T) { + s := NewScenario(t, "run must build a service whose image comes from another service's build context") + s.Defer( + DockerCmd("image", "rm", "-f", s.Project()+"-build").MayFail(), + DockerCmd("image", "rm", "-f", s.Project()+"-build_base").MayFail()). + Step("run builds the chained images and executes the command", + ComposeCmd("run", "build", "echo", "hello world"), + StdoutContains("hello world")) +} + +func TestComposeRunRmStopSignal(t *testing.T) { c := NewParallelCLI(t) - defer c.cleanupWithDown(t, "run-test") - - t.Run("compose run", func(t *testing.T) { - res := c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/compose.yaml", "run", "back") - lines := Lines(res.Stdout()) - assert.Equal(t, lines[len(lines)-1], "Hello there!!", res.Stdout()) - assert.Assert(t, !strings.Contains(res.Combined(), "orphan")) - res = c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/compose.yaml", "run", "back", "echo", - "Hello one more time") - lines = Lines(res.Stdout()) - assert.Equal(t, lines[len(lines)-1], "Hello one more time", res.Stdout()) - assert.Assert(t, strings.Contains(res.Combined(), "orphan")) + projectName := "run-test" + t.Cleanup(func() { + c.cleanupWithDown(t, projectName) }) - t.Run("check run container exited", func(t *testing.T) { - res := c.RunDockerCmd(t, "ps", "--all") - lines := Lines(res.Stdout()) - var runContainerID string - var truncatedSlug string - for _, line := range lines { - fields := strings.Fields(line) - containerID := fields[len(fields)-1] - assert.Assert(t, !strings.HasPrefix(containerID, "run-test-front")) - if strings.HasPrefix(containerID, "run-test-back") { - // only the one-off container for back service - assert.Assert(t, strings.HasPrefix(containerID, "run-test-back-run-"), containerID) - truncatedSlug = strings.Replace(containerID, "run-test-back-run-", "", 1) - runContainerID = containerID - } - if strings.HasPrefix(containerID, "run-test-db-1") { - assert.Assert(t, strings.Contains(line, "Up"), line) - } + res := c.RunDockerComposeCmd(t, "--project-name", projectName, "-f", "./fixtures/ps-test/compose.yaml", "run", "--rm", "-d", "nginx") + res.Assert(t, icmd.Success) + + res = c.RunDockerCmd(t, "ps", "--quiet", "--filter", "name=run-test-nginx") + containerID := strings.TrimSpace(res.Stdout()) + + res = c.RunDockerCmd(t, "stop", containerID) + res.Assert(t, icmd.Success) + // --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()) } - assert.Assert(t, runContainerID != "") - res = c.RunDockerCmd(t, "inspect", runContainerID) - res.Assert(t, icmd.Expected{Out: ` "Status": "exited"`}) - res.Assert(t, icmd.Expected{Out: `"com.docker.compose.project": "run-test"`}) - res.Assert(t, icmd.Expected{Out: `"com.docker.compose.oneoff": "True",`}) - res.Assert(t, icmd.Expected{Out: `"com.docker.compose.slug": "` + truncatedSlug}) - }) + return poll.Success() + }, poll.WithTimeout(10*time.Second), poll.WithDelay(500*time.Millisecond)) +} - t.Run("compose run --rm", func(t *testing.T) { - res := c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/compose.yaml", "run", "--rm", "back", "echo", - "Hello again") - lines := Lines(res.Stdout()) - assert.Equal(t, lines[len(lines)-1], "Hello again", res.Stdout()) - - res = c.RunDockerCmd(t, "ps", "--all") - assert.Assert(t, strings.Contains(res.Stdout(), "run-test-back"), res.Stdout()) - }) - - t.Run("down", func(t *testing.T) { - c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/compose.yaml", "down", "--remove-orphans") - res := c.RunDockerCmd(t, "ps", "--all") - assert.Assert(t, !strings.Contains(res.Stdout(), "run-test"), res.Stdout()) - }) - - t.Run("compose run --volumes", func(t *testing.T) { - wd, err := os.Getwd() - assert.NilError(t, err) - res := c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/compose.yaml", "run", "--volumes", wd+":/foo", - "back", "/bin/sh", "-c", "ls /foo") - res.Assert(t, icmd.Expected{Out: "compose_run_test.go"}) - - res = c.RunDockerCmd(t, "ps", "--all") - assert.Assert(t, strings.Contains(res.Stdout(), "run-test-back"), res.Stdout()) - }) - - t.Run("compose run --publish", func(t *testing.T) { - c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/ports.yaml", "run", "--publish", "8081:80", "-d", "back", - "/bin/sh", "-c", "sleep 1") - res := c.RunDockerCmd(t, "ps") - assert.Assert(t, strings.Contains(res.Stdout(), "8081->80/tcp"), res.Stdout()) - assert.Assert(t, !strings.Contains(res.Stdout(), "8082->80/tcp"), res.Stdout()) - }) - - t.Run("compose run --service-ports", func(t *testing.T) { - c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/ports.yaml", "run", "--service-ports", "-d", "back", - "/bin/sh", "-c", "sleep 1") - res := c.RunDockerCmd(t, "ps") - assert.Assert(t, strings.Contains(res.Stdout(), "8082->80/tcp"), res.Stdout()) - }) - - t.Run("compose run orphan", func(t *testing.T) { - // Use different compose files to get an orphan container - c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/orphan.yaml", "run", "simple") - res := c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/compose.yaml", "run", "back", "echo", "Hello") - assert.Assert(t, strings.Contains(res.Combined(), "orphan")) - - cmd := c.NewDockerComposeCmd(t, "-f", "./fixtures/run-test/compose.yaml", "run", "back", "echo", "Hello") - res = icmd.RunCmd(cmd, func(cmd *icmd.Cmd) { - cmd.Env = append(cmd.Env, "COMPOSE_IGNORE_ORPHANS=True") - }) - assert.Assert(t, !strings.Contains(res.Combined(), "orphan")) - }) - - t.Run("down", func(t *testing.T) { - cmd := c.NewDockerComposeCmd(t, "-f", "./fixtures/run-test/compose.yaml", "down") - icmd.RunCmd(cmd, func(c *icmd.Cmd) { - c.Env = append(c.Env, "COMPOSE_REMOVE_ORPHANS=True") - }) - res := c.RunDockerCmd(t, "ps", "--all") - - assert.Assert(t, !strings.Contains(res.Stdout(), "run-test"), res.Stdout()) - }) - - t.Run("run starts only container and dependencies", func(t *testing.T) { - // ensure that even if another service is up run does not start it: https://github.com/docker/compose/issues/9459 - res := c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/deps.yaml", "up", "service_b", "--menu=false") - res.Assert(t, icmd.Success) - - res = c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/deps.yaml", "run", "service_a") - assert.Assert(t, strings.Contains(res.Combined(), "shared_dep"), res.Combined()) - assert.Assert(t, !strings.Contains(res.Combined(), "service_b"), res.Combined()) - - c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/deps.yaml", "down", "--remove-orphans") - }) - - t.Run("run without dependencies", func(t *testing.T) { - res := c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/deps.yaml", "run", "--no-deps", "service_a") - assert.Assert(t, !strings.Contains(res.Combined(), "shared_dep"), res.Combined()) - assert.Assert(t, !strings.Contains(res.Combined(), "service_b"), res.Combined()) - - c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/deps.yaml", "down", "--remove-orphans") - }) - - t.Run("run with not required dependency", func(t *testing.T) { - res := c.RunDockerComposeCmd(t, "-f", "./fixtures/dependencies/deps-not-required.yaml", "run", "foo") - assert.Assert(t, strings.Contains(res.Combined(), "foo"), res.Combined()) - assert.Assert(t, !strings.Contains(res.Combined(), "bar"), res.Combined()) - - c.RunDockerComposeCmd(t, "-f", "./fixtures/dependencies/deps-not-required.yaml", "down", "--remove-orphans") - }) - - t.Run("--quiet-pull", func(t *testing.T) { - res := c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/quiet-pull.yaml", "down", "--remove-orphans", "--rmi", "all") - res.Assert(t, icmd.Success) - - res = c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/quiet-pull.yaml", "run", "--quiet-pull", "backend") - assert.Assert(t, !strings.Contains(res.Combined(), "Pull complete"), res.Combined()) - assert.Assert(t, strings.Contains(res.Combined(), "Pulled"), res.Combined()) - }) - - t.Run("COMPOSE_PROGRESS quiet", func(t *testing.T) { - res := c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/quiet-pull.yaml", "down", "--remove-orphans", "--rmi", "all") - res.Assert(t, icmd.Success) - - cmd := c.NewDockerComposeCmd(t, "-f", "./fixtures/run-test/quiet-pull.yaml", "run", "backend") - res = icmd.RunCmd(cmd, func(c *icmd.Cmd) { - c.Env = append(c.Env, "COMPOSE_PROGRESS=quiet") - }) - assert.Assert(t, !strings.Contains(res.Combined(), "Pull complete"), res.Combined()) - assert.Assert(t, !strings.Contains(res.Combined(), "Pulled"), res.Combined()) - }) - - t.Run("--pull", func(t *testing.T) { - res := c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/pull.yaml", "down", "--remove-orphans", "--rmi", "all") - res.Assert(t, icmd.Success) - - res = c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/pull.yaml", "run", "--pull", "always", "backend") - assert.Assert(t, strings.Contains(res.Combined(), "Image nginx Pulling"), res.Combined()) - assert.Assert(t, strings.Contains(res.Combined(), "Image nginx Pulled"), res.Combined()) - }) - - t.Run("compose run --env-from-file", func(t *testing.T) { - res := c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/compose.yaml", "run", "--env-from-file", "./fixtures/run-test/run.env", - "front", "env") - res.Assert(t, icmd.Expected{Out: "FOO=BAR"}) - }) - - t.Run("compose run -rm with stop signal", func(t *testing.T) { - projectName := "run-test" - res := c.RunDockerComposeCmd(t, "--project-name", projectName, "-f", "./fixtures/ps-test/compose.yaml", "run", "--rm", "-d", "nginx") - res.Assert(t, icmd.Success) - - res = c.RunDockerCmd(t, "ps", "--quiet", "--filter", "name=run-test-nginx") - containerID := strings.TrimSpace(res.Stdout()) - - res = c.RunDockerCmd(t, "stop", containerID) - res.Assert(t, icmd.Success) - // --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) { - res := c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/compose.yaml", "run", "--env", "FOO=BAR", - "front", "env") - res.Assert(t, icmd.Expected{Out: "FOO=BAR"}) - }) - - t.Run("compose run --build", func(t *testing.T) { - c.cleanupWithDown(t, "run-test", "--rmi=local") - res := c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/compose.yaml", "run", "build", "echo", "hello world") - res.Assert(t, icmd.Expected{Out: "hello world"}) - }) +func TestComposeRunPipedInput(t *testing.T) { + if composeStandaloneMode { + t.Skip("Skipping test compose with piped input detection in standalone mode") + } + c := NewParallelCLI(t) + defer c.cleanupWithDown(t, "run-piped-test") t.Run("compose run with piped input detection", func(t *testing.T) { - if composeStandaloneMode { - t.Skip("Skipping test compose with piped input detection in standalone mode") - } // Test that piped input is properly detected and TTY is automatically disabled // This tests the logic added in run.go that checks dockerCli.In().IsTerminal() - cmd := c.NewCmd("sh", "-c", "echo 'piped-content' | docker compose -f ./fixtures/run-test/piped-test.yaml run --rm piped-test") + cmd := c.NewCmd("sh", "-c", "echo 'piped-content' | docker compose -p run-piped-test -f ./fixtures/run-test/piped-test.yaml run --rm piped-test") res := icmd.RunCmd(cmd) res.Assert(t, icmd.Expected{Out: "piped-content"}) @@ -245,12 +175,9 @@ func TestLocalComposeRun(t *testing.T) { }) t.Run("compose run piped input should not allocate TTY", func(t *testing.T) { - if composeStandaloneMode { - t.Skip("Skipping test compose with piped input detection in standalone mode") - } // Test that when stdin is piped, the container correctly detects no TTY // This verifies that the automatic noTty=true setting works correctly - cmd := c.NewCmd("sh", "-c", "echo '' | docker compose -f ./fixtures/run-test/piped-test.yaml run --rm tty-test") + cmd := c.NewCmd("sh", "-c", "echo '' | docker compose -p run-piped-test -f ./fixtures/run-test/piped-test.yaml run --rm tty-test") res := icmd.RunCmd(cmd) res.Assert(t, icmd.Expected{Out: "No TTY detected"}) @@ -258,12 +185,9 @@ func TestLocalComposeRun(t *testing.T) { }) t.Run("compose run piped input with explicit --tty should fail", func(t *testing.T) { - if composeStandaloneMode { - t.Skip("Skipping test compose with piped input detection in standalone mode") - } // Test that explicitly requesting TTY with piped input fails with proper error message // This should trigger the "input device is not a TTY" error - cmd := c.NewCmd("sh", "-c", "echo 'test' | docker compose -f ./fixtures/run-test/piped-test.yaml run --rm --tty piped-test") + cmd := c.NewCmd("sh", "-c", "echo 'test' | docker compose -p run-piped-test -f ./fixtures/run-test/piped-test.yaml run --rm --tty piped-test") res := icmd.RunCmd(cmd) res.Assert(t, icmd.Expected{ @@ -273,12 +197,9 @@ func TestLocalComposeRun(t *testing.T) { }) t.Run("compose run piped input with --no-tty=false should fail", func(t *testing.T) { - if composeStandaloneMode { - t.Skip("Skipping test compose with piped input detection in standalone mode") - } // Test that explicitly disabling --no-tty (i.e., requesting TTY) with piped input fails // This should also trigger the "input device is not a TTY" error - cmd := c.NewCmd("sh", "-c", "echo 'test' | docker compose -f ./fixtures/run-test/piped-test.yaml run --rm --no-tty=false piped-test") + cmd := c.NewCmd("sh", "-c", "echo 'test' | docker compose -p run-piped-test -f ./fixtures/run-test/piped-test.yaml run --rm --no-tty=false piped-test") res := icmd.RunCmd(cmd) res.Assert(t, icmd.Expected{ diff --git a/pkg/e2e/compose_up_test.go b/pkg/e2e/compose_up_test.go index b00cb3c18..1acaf7393 100644 --- a/pkg/e2e/compose_up_test.go +++ b/pkg/e2e/compose_up_test.go @@ -20,95 +20,61 @@ import ( "strings" "testing" "time" - - "gotest.tools/v3/assert" - "gotest.tools/v3/icmd" ) func TestUpWait(t *testing.T) { - c := NewParallelCLI(t) - const projectName = "e2e-deps-wait" - - timeout := time.After(30 * time.Second) - done := make(chan bool) - go func() { - //nolint:nolintlint,testifylint // helper asserts inside goroutine; acceptable in this e2e test - res := c.RunDockerComposeCmd(t, "-f", "fixtures/dependencies/deps-completed-successfully.yaml", "--project-name", projectName, "up", "--wait", "-d") - assert.Assert(t, strings.Contains(res.Combined(), "e2e-deps-wait-oneshot-1"), res.Combined()) - done <- true - }() - - select { - case <-timeout: - t.Fatal("test did not finish in time") - case <-done: - break - } - - c.RunDockerComposeCmd(t, "--project-name", projectName, "down") + s := NewScenario(t, "up --wait must return once dependencies completed and services run") + s.Step("up --wait returns with the long-running service up and the oneshot completed", + ComposeCmd("up", "--wait", "-d").Within(30*time.Second), + OutputContains(s.Project()+"-oneshot-1"), + ServiceState("longrunning", "running"), + ServiceState("oneshot", "exited")) } func TestUpExitCodeFrom(t *testing.T) { - c := NewParallelCLI(t) - const projectName = "e2e-exit-code-from" - - res := c.RunDockerComposeCmdNoCheck(t, "-f", "fixtures/start-fail/start-depends_on-long-lived.yaml", "--project-name", projectName, "up", "--menu=false", "--exit-code-from=failure", "failure") - res.Assert(t, icmd.Expected{ExitCode: 42}) - - c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "--remove-orphans") + NewScenario(t, "up --exit-code-from must return the selected service's exit code"). + Step("up returns the failing service's code once it exits", + ComposeCmd("up", "--menu=false", "--exit-code-from=failure", "failure").MayFail().Within(60*time.Second), + ExitCode(42)) } func TestUpExitCodeFromContainerKilled(t *testing.T) { - c := NewParallelCLI(t) - const projectName = "e2e-exit-code-from-kill" - - res := c.RunDockerComposeCmdNoCheck(t, "-f", "fixtures/start-fail/start-depends_on-long-lived.yaml", "--project-name", projectName, "up", "--menu=false", "--exit-code-from=test") - res.Assert(t, icmd.Expected{ExitCode: 143}) - - c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "--remove-orphans") + NewScenario(t, "up --exit-code-from must report 143 for a service stopped by the abort"). + Step("the watched long-lived service is stopped when another exits", + ComposeCmd("up", "--menu=false", "--exit-code-from=test").MayFail().Within(60*time.Second), + ExitCode(143)) } func TestPortRange(t *testing.T) { - c := NewParallelCLI(t) - const projectName = "e2e-port-range" - - reset := func() { - c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "--remove-orphans", "--timeout=0") - } - reset() - t.Cleanup(reset) - - res := c.RunDockerComposeCmdNoCheck(t, "-f", "fixtures/port-range/compose.yaml", "--project-name", projectName, "up", "-d") - res.Assert(t, icmd.Success) + NewScenario(t, "a published port range must accommodate scaled replicas and single ports alike"). + Step("up binds every replica within the range", + ComposeCmd("up", "-d"), + ServiceScale("a", 5), + ServiceState("b", "running"), + ServiceState("c", "running")) } func TestStdoutStderr(t *testing.T) { - c := NewParallelCLI(t) - const projectName = "e2e-stdout-stderr" - - res := c.RunDockerComposeCmdNoCheck(t, "-f", "fixtures/stdout-stderr/compose.yaml", "--project-name", projectName, "up", "--menu=false") - res.Assert(t, icmd.Expected{Out: "log to stdout", Err: "log to stderr"}) - - c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "--remove-orphans") + NewScenario(t, "up must relay each container stream to its own: stdout to stdout, stderr to stderr"). + Step("the two streams arrive separated", + ComposeCmd("up", "--menu=false"), + StdoutContains("log to stdout"), + StderrContains("log to stderr")) } func TestLoggingDriver(t *testing.T) { - c := NewCLI(t) - const projectName = "e2e-logging-driver" - defer c.cleanupWithDown(t, projectName) - - host := "HOST=127.0.0.1" - res := c.RunDockerCmd(t, "info", "-f", "{{.OperatingSystem}}") - os := res.Stdout() - if strings.TrimSpace(os) == "Docker Desktop" { - host = "HOST=host.docker.internal" + s := NewScenario(t, "a logging-driver address change must reconfigure the service on the next up") + host := "127.0.0.1" + if strings.Contains(s.CLI().RunDockerCmd(t, "info", "-f", "{{.OperatingSystem}}").Stdout(), "Docker Desktop") { + host = "host.docker.internal" } - - cmd := c.NewDockerComposeCmd(t, "-f", "fixtures/logging-driver/compose.yaml", "--project-name", projectName, "up", "-d") - cmd.Env = append(cmd.Env, host, "BAR=foo") - icmd.RunCmd(cmd).Assert(t, icmd.Success) - - cmd = c.NewDockerComposeCmd(t, "-f", "fixtures/logging-driver/compose.yaml", "--project-name", projectName, "up", "-d") - cmd.Env = append(cmd.Env, host, "BAR=zot") - icmd.RunCmd(cmd).Assert(t, icmd.Success) + s.Env("HOST="+host). + Step("up starts the log collector and the app", + ComposeCmd("up", "-d").WithEnv("BAR=foo"), + ServiceState("fluentbit", "running"), + ServiceState("app", "running")). + Step("a collector config change is applied by recreating it", + ComposeCmd("up", "-d").WithEnv("BAR=zot"), + Recreated("fluentbit"), + ServiceState("app", "running")) } diff --git a/pkg/e2e/fixtures/dependencies/Dockerfile b/pkg/e2e/fixtures/dependencies/Dockerfile deleted file mode 100644 index fe5992a8d..000000000 --- a/pkg/e2e/fixtures/dependencies/Dockerfile +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright 2020 Docker Compose CLI authors - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -FROM busybox:1.35.0 -RUN echo "hello" \ No newline at end of file diff --git a/pkg/e2e/fixtures/port-range/compose.yaml b/pkg/e2e/fixtures/port-range/compose.yaml deleted file mode 100644 index 65f6fde61..000000000 --- a/pkg/e2e/fixtures/port-range/compose.yaml +++ /dev/null @@ -1,16 +0,0 @@ -services: - a: - image: nginx:alpine - scale: 5 - ports: - - "6005-6015:80" - - b: - image: nginx:alpine - ports: - - 80 - - c: - image: nginx:alpine - ports: - - 80 \ No newline at end of file diff --git a/pkg/e2e/fixtures/run-test/compose.yaml b/pkg/e2e/fixtures/run-test/compose.yaml deleted file mode 100644 index aef801190..000000000 --- a/pkg/e2e/fixtures/run-test/compose.yaml +++ /dev/null @@ -1,31 +0,0 @@ -services: - back: - image: alpine - command: echo "Hello there!!" - depends_on: - - db - networks: - - backnet - db: - image: nginx:alpine - networks: - - backnet - volumes: - - data:/test - front: - image: nginx:alpine - networks: - - frontnet - build: - build: - dockerfile_inline: "FROM base" - additional_contexts: - base: "service:build_base" - build_base: - build: - dockerfile_inline: "FROM alpine" -networks: - frontnet: - backnet: -volumes: - data: diff --git a/pkg/e2e/fixtures/run-test/orphan.yaml b/pkg/e2e/fixtures/run-test/orphan.yaml deleted file mode 100644 index b059acc97..000000000 --- a/pkg/e2e/fixtures/run-test/orphan.yaml +++ /dev/null @@ -1,4 +0,0 @@ -services: - simple: - image: alpine - command: echo "Hi there!!" diff --git a/pkg/e2e/fixtures/run-test/quiet-pull.yaml b/pkg/e2e/fixtures/run-test/quiet-pull.yaml deleted file mode 100644 index 922676363..000000000 --- a/pkg/e2e/fixtures/run-test/quiet-pull.yaml +++ /dev/null @@ -1,3 +0,0 @@ -services: - backend: - image: hello-world \ No newline at end of file diff --git a/pkg/e2e/fixtures/stdout-stderr/log_to_stderr.sh b/pkg/e2e/fixtures/stdout-stderr/log_to_stderr.sh deleted file mode 100755 index f015ca89b..000000000 --- a/pkg/e2e/fixtures/stdout-stderr/log_to_stderr.sh +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright 2020 Docker Compose CLI authors - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - ->&2 echo "log to stderr" -echo "log to stdout" diff --git a/pkg/e2e/testdata/TestComposeRun/compose.yaml b/pkg/e2e/testdata/TestComposeRun/compose.yaml new file mode 100644 index 000000000..ffff77d3a --- /dev/null +++ b/pkg/e2e/testdata/TestComposeRun/compose.yaml @@ -0,0 +1,14 @@ +services: + back: + image: alpine + command: echo "Hello there!!" + depends_on: + - db + db: + image: alpine + init: true + command: sleep infinity + front: + image: alpine + init: true + command: sleep infinity diff --git a/pkg/e2e/fixtures/run-test/run.env b/pkg/e2e/testdata/TestComposeRun/run.env similarity index 88% rename from pkg/e2e/fixtures/run-test/run.env rename to pkg/e2e/testdata/TestComposeRun/run.env index 6ac867af7..c61e4f427 100644 --- a/pkg/e2e/fixtures/run-test/run.env +++ b/pkg/e2e/testdata/TestComposeRun/run.env @@ -1 +1,2 @@ FOO=BAR + diff --git a/pkg/e2e/testdata/TestComposeRunBuild/compose.yaml b/pkg/e2e/testdata/TestComposeRunBuild/compose.yaml new file mode 100644 index 000000000..04fc119ad --- /dev/null +++ b/pkg/e2e/testdata/TestComposeRunBuild/compose.yaml @@ -0,0 +1,9 @@ +services: + build: + build: + dockerfile_inline: "FROM base" + additional_contexts: + base: "service:build_base" + build_base: + build: + dockerfile_inline: "FROM alpine" diff --git a/pkg/e2e/fixtures/run-test/deps.yaml b/pkg/e2e/testdata/TestComposeRunDeps/compose.yaml similarity index 92% rename from pkg/e2e/fixtures/run-test/deps.yaml rename to pkg/e2e/testdata/TestComposeRunDeps/compose.yaml index 6e0e394a3..8f31f39b6 100644 --- a/pkg/e2e/fixtures/run-test/deps.yaml +++ b/pkg/e2e/testdata/TestComposeRunDeps/compose.yaml @@ -10,4 +10,4 @@ services: depends_on: - shared_dep shared_dep: - image: bash \ No newline at end of file + image: bash diff --git a/pkg/e2e/fixtures/dependencies/deps-not-required.yaml b/pkg/e2e/testdata/TestComposeRunNotRequiredDeps/compose.yaml similarity index 62% rename from pkg/e2e/fixtures/dependencies/deps-not-required.yaml rename to pkg/e2e/testdata/TestComposeRunNotRequiredDeps/compose.yaml index 44286846b..52d44df5f 100644 --- a/pkg/e2e/fixtures/dependencies/deps-not-required.yaml +++ b/pkg/e2e/testdata/TestComposeRunNotRequiredDeps/compose.yaml @@ -7,5 +7,7 @@ services: required: false condition: service_healthy bar: - image: nginx:alpine - profiles: [not-required] \ No newline at end of file + image: alpine + init: true + command: sleep infinity + profiles: [not-required] diff --git a/pkg/e2e/fixtures/run-test/ports.yaml b/pkg/e2e/testdata/TestComposeRunPorts/compose.yaml similarity index 100% rename from pkg/e2e/fixtures/run-test/ports.yaml rename to pkg/e2e/testdata/TestComposeRunPorts/compose.yaml diff --git a/pkg/e2e/fixtures/run-test/pull.yaml b/pkg/e2e/testdata/TestComposeRunPullAlways/compose.yaml similarity index 63% rename from pkg/e2e/fixtures/run-test/pull.yaml rename to pkg/e2e/testdata/TestComposeRunPullAlways/compose.yaml index 223fecd50..f29c9fc24 100644 --- a/pkg/e2e/fixtures/run-test/pull.yaml +++ b/pkg/e2e/testdata/TestComposeRunPullAlways/compose.yaml @@ -1,4 +1,4 @@ services: backend: image: nginx - command: nginx -t \ No newline at end of file + command: nginx -t diff --git a/pkg/e2e/testdata/TestComposeRunQuietPull/compose.yaml b/pkg/e2e/testdata/TestComposeRunQuietPull/compose.yaml new file mode 100644 index 000000000..87418dfde --- /dev/null +++ b/pkg/e2e/testdata/TestComposeRunQuietPull/compose.yaml @@ -0,0 +1,3 @@ +services: + backend: + image: hello-world diff --git a/pkg/e2e/fixtures/logging-driver/compose.yaml b/pkg/e2e/testdata/TestLoggingDriver/compose.yaml similarity index 85% rename from pkg/e2e/fixtures/logging-driver/compose.yaml rename to pkg/e2e/testdata/TestLoggingDriver/compose.yaml index 37b3e8b3e..f3bf703b7 100644 --- a/pkg/e2e/fixtures/logging-driver/compose.yaml +++ b/pkg/e2e/testdata/TestLoggingDriver/compose.yaml @@ -8,7 +8,9 @@ services: FOO: ${BAR} app: - image: nginx + image: alpine + init: true + command: sleep infinity depends_on: fluentbit: condition: service_started diff --git a/pkg/e2e/testdata/TestPortRange/compose.yaml b/pkg/e2e/testdata/TestPortRange/compose.yaml new file mode 100644 index 000000000..9a1266d06 --- /dev/null +++ b/pkg/e2e/testdata/TestPortRange/compose.yaml @@ -0,0 +1,22 @@ +services: + a: + image: alpine + init: true + command: sleep infinity + scale: 5 + ports: + - "6005-6015:80" + + b: + image: alpine + init: true + command: sleep infinity + ports: + - 80 + + c: + image: alpine + init: true + command: sleep infinity + ports: + - 80 diff --git a/pkg/e2e/testdata/TestScaleDoesntRecreate/compose.yaml b/pkg/e2e/testdata/TestScaleDoesntRecreate/compose.yaml new file mode 100644 index 000000000..23e61c820 --- /dev/null +++ b/pkg/e2e/testdata/TestScaleDoesntRecreate/compose.yaml @@ -0,0 +1,5 @@ +services: + simple: + image: alpine + init: true + command: sleep infinity diff --git a/pkg/e2e/fixtures/stdout-stderr/compose.yaml b/pkg/e2e/testdata/TestStdoutStderr/compose.yaml similarity index 100% rename from pkg/e2e/fixtures/stdout-stderr/compose.yaml rename to pkg/e2e/testdata/TestStdoutStderr/compose.yaml diff --git a/pkg/e2e/testdata/TestStdoutStderr/log_to_stderr.sh b/pkg/e2e/testdata/TestStdoutStderr/log_to_stderr.sh new file mode 100644 index 000000000..6e328e08b --- /dev/null +++ b/pkg/e2e/testdata/TestStdoutStderr/log_to_stderr.sh @@ -0,0 +1,3 @@ +>&2 echo "log to stderr" +echo "log to stdout" + diff --git a/pkg/e2e/fixtures/start-fail/start-depends_on-long-lived.yaml b/pkg/e2e/testdata/TestUpExitCodeFrom/compose.yaml similarity index 100% rename from pkg/e2e/fixtures/start-fail/start-depends_on-long-lived.yaml rename to pkg/e2e/testdata/TestUpExitCodeFrom/compose.yaml diff --git a/pkg/e2e/testdata/TestUpExitCodeFromContainerKilled/compose.yaml b/pkg/e2e/testdata/TestUpExitCodeFromContainerKilled/compose.yaml new file mode 100644 index 000000000..a3c920e0f --- /dev/null +++ b/pkg/e2e/testdata/TestUpExitCodeFromContainerKilled/compose.yaml @@ -0,0 +1,14 @@ +services: + safe: + image: 'alpine' + init: true + command: ['/bin/sh', '-c', 'sleep infinity'] # never exiting + failure: + image: 'alpine' + init: true + command: ['/bin/sh', '-c', 'sleep 1 ; echo "exiting with error" ; exit 42'] + test: + image: 'alpine' + init: true + command: ['/bin/sh', '-c', 'sleep 99999 ; echo "tests are OK"'] # very long job + depends_on: [safe] diff --git a/pkg/e2e/fixtures/simple-composefile/id.yaml b/pkg/e2e/testdata/TestUpImageID/compose.yaml similarity index 100% rename from pkg/e2e/fixtures/simple-composefile/id.yaml rename to pkg/e2e/testdata/TestUpImageID/compose.yaml diff --git a/pkg/e2e/fixtures/profiles/docker-compose.yaml b/pkg/e2e/testdata/TestUpProfile/compose.yaml similarity index 58% rename from pkg/e2e/fixtures/profiles/docker-compose.yaml rename to pkg/e2e/testdata/TestUpProfile/compose.yaml index 134d7bbc9..4d5f0d04a 100644 --- a/pkg/e2e/fixtures/profiles/docker-compose.yaml +++ b/pkg/e2e/testdata/TestUpProfile/compose.yaml @@ -1,15 +1,12 @@ services: foo: - container_name: foo_c profiles: [ test ] image: alpine depends_on: [ db ] bar: - container_name: bar_c profiles: [ test ] image: alpine - + db: - container_name: db_c - image: alpine \ No newline at end of file + image: alpine diff --git a/pkg/e2e/fixtures/start-fail/compose.yaml b/pkg/e2e/testdata/TestUpServiceUnhealthy/compose.yaml similarity index 100% rename from pkg/e2e/fixtures/start-fail/compose.yaml rename to pkg/e2e/testdata/TestUpServiceUnhealthy/compose.yaml diff --git a/pkg/e2e/fixtures/stop/compose.yaml b/pkg/e2e/testdata/TestUpStopWithLogsMixed/compose.yaml similarity index 100% rename from pkg/e2e/fixtures/stop/compose.yaml rename to pkg/e2e/testdata/TestUpStopWithLogsMixed/compose.yaml diff --git a/pkg/e2e/fixtures/dependencies/deps-completed-successfully.yaml b/pkg/e2e/testdata/TestUpWait/compose.yaml similarity index 100% rename from pkg/e2e/fixtures/dependencies/deps-completed-successfully.yaml rename to pkg/e2e/testdata/TestUpWait/compose.yaml diff --git a/pkg/e2e/fixtures/resources/compose.yaml b/pkg/e2e/testdata/TestUpWithAllResources/compose.yaml similarity index 71% rename from pkg/e2e/fixtures/resources/compose.yaml rename to pkg/e2e/testdata/TestUpWithAllResources/compose.yaml index 482368880..667b1d054 100644 --- a/pkg/e2e/fixtures/resources/compose.yaml +++ b/pkg/e2e/testdata/TestUpWithAllResources/compose.yaml @@ -2,4 +2,4 @@ volumes: my_vol: {} networks: - my_net: {} \ No newline at end of file + my_net: {} diff --git a/pkg/e2e/testdata/TestUpWithBuildDependencies/Dockerfile b/pkg/e2e/testdata/TestUpWithBuildDependencies/Dockerfile new file mode 100644 index 000000000..3f4056da0 --- /dev/null +++ b/pkg/e2e/testdata/TestUpWithBuildDependencies/Dockerfile @@ -0,0 +1,3 @@ +FROM busybox:1.35.0 +RUN echo "hello" + diff --git a/pkg/e2e/fixtures/dependencies/service-image-depends-on.yaml b/pkg/e2e/testdata/TestUpWithBuildDependencies/compose.yaml similarity index 54% rename from pkg/e2e/fixtures/dependencies/service-image-depends-on.yaml rename to pkg/e2e/testdata/TestUpWithBuildDependencies/compose.yaml index 313997836..c64d6dae0 100644 --- a/pkg/e2e/fixtures/dependencies/service-image-depends-on.yaml +++ b/pkg/e2e/testdata/TestUpWithBuildDependencies/compose.yaml @@ -1,9 +1,9 @@ services: foo: - image: built-image-dependency + image: ${BUILT_IMAGE} build: context: . bar: - image: built-image-dependency + image: ${BUILT_IMAGE} depends_on: - foo diff --git a/pkg/e2e/fixtures/dependencies/dependency-exit.yaml b/pkg/e2e/testdata/TestUpWithDependencyExit/compose.yaml similarity index 68% rename from pkg/e2e/fixtures/dependencies/dependency-exit.yaml rename to pkg/e2e/testdata/TestUpWithDependencyExit/compose.yaml index 7ba02ba79..214804abd 100644 --- a/pkg/e2e/fixtures/dependencies/dependency-exit.yaml +++ b/pkg/e2e/testdata/TestUpWithDependencyExit/compose.yaml @@ -1,10 +1,11 @@ services: web: - image: nginx:alpine + image: alpine + init: true + command: sleep infinity depends_on: db: condition: service_healthy db: image: alpine command: sh -c "exit 1" - diff --git a/pkg/e2e/testdata/TestUpWithDependencyNotRequired/compose.yaml b/pkg/e2e/testdata/TestUpWithDependencyNotRequired/compose.yaml new file mode 100644 index 000000000..52d44df5f --- /dev/null +++ b/pkg/e2e/testdata/TestUpWithDependencyNotRequired/compose.yaml @@ -0,0 +1,13 @@ +services: + foo: + image: bash + command: echo "foo" + depends_on: + bar: + required: false + condition: service_healthy + bar: + image: alpine + init: true + command: sleep infinity + profiles: [not-required] diff --git a/pkg/e2e/up_test.go b/pkg/e2e/up_test.go index 8575be9e3..4a93203ed 100644 --- a/pkg/e2e/up_test.go +++ b/pkg/e2e/up_test.go @@ -21,7 +21,6 @@ package e2e import ( "context" "errors" - "fmt" "os/exec" "strings" "syscall" @@ -29,19 +28,17 @@ import ( "time" "gotest.tools/v3/assert" - "gotest.tools/v3/icmd" "github.com/docker/compose/v5/pkg/utils" ) func TestUpServiceUnhealthy(t *testing.T) { - c := NewParallelCLI(t) - const projectName = "e2e-start-fail" - - res := c.RunDockerComposeCmdNoCheck(t, "-f", "fixtures/start-fail/compose.yaml", "--project-name", projectName, "up", "-d") - res.Assert(t, icmd.Expected{ExitCode: 1, Err: `container e2e-start-fail-fail-1 is unhealthy`}) - - c.RunDockerComposeCmd(t, "--project-name", projectName, "down") + s := NewScenario(t, "up must fail when a service never turns healthy") + s.Step("up reports the unhealthy container and fails", + ComposeCmd("up", "-d").MayFail().Within(60*time.Second), + ExitCode(1), + OutputContains("container "+s.Project()+"-fail-1 is unhealthy"), + ServiceState("depends", "created")) } func TestUpDependenciesNotStopped(t *testing.T) { @@ -107,118 +104,74 @@ func TestUpDependenciesNotStopped(t *testing.T) { } func TestUpWithBuildDependencies(t *testing.T) { - c := NewParallelCLI(t) - - t.Run("up with service using image build by an another service", func(t *testing.T) { - // ensure local test run does not reuse previously build image - c.RunDockerOrExitError(t, "rmi", "built-image-dependency") - - res := c.RunDockerComposeCmd(t, "--project-directory", "fixtures/dependencies", - "-f", "fixtures/dependencies/service-image-depends-on.yaml", "up", "-d") - - t.Cleanup(func() { - c.RunDockerComposeCmd(t, "--project-directory", "fixtures/dependencies", - "-f", "fixtures/dependencies/service-image-depends-on.yaml", "down", "--rmi", "all") - }) - - res.Assert(t, icmd.Success) - }) + s := NewScenario(t, "up must build a service's image before starting another service that reuses it") + image := s.Project() + "-built" + s.Env("BUILT_IMAGE="+image). + Defer(DockerCmd("image", "rm", "-f", image).MayFail()). + Step("up builds once and starts both services from the built image", + ComposeCmd("up", "-d"), + ImageExists(image)) } func TestUpWithDependencyExit(t *testing.T) { - c := NewParallelCLI(t) - - t.Run("up with dependency to exit before being healthy", func(t *testing.T) { - res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/dependencies", - "-f", "fixtures/dependencies/dependency-exit.yaml", "up", "-d") - - t.Cleanup(func() { - c.RunDockerComposeCmd(t, "--project-name", "dependencies", "down") - }) - - res.Assert(t, icmd.Expected{ExitCode: 1, Err: "dependency failed to start: container dependencies-db-1 exited (1)"}) - }) + s := NewScenario(t, "up must fail when a dependency exits before turning healthy") + s.Step("up reports the exited dependency and fails", + ComposeCmd("up", "-d").MayFail(), + ExitCode(1), + OutputContains("dependency failed to start: container "+s.Project()+"-db-1 exited (1)"), + ServiceState("web", "created")) } func TestScaleDoesntRecreate(t *testing.T) { - c := NewCLI(t) - const projectName = "compose-e2e-scale" - t.Cleanup(func() { - c.RunDockerComposeCmd(t, "--project-name", projectName, "down") - }) - - c.RunDockerComposeCmd(t, "-f", "fixtures/simple-composefile/compose.yaml", "--project-name", projectName, "up", "-d") - - res := c.RunDockerComposeCmd(t, "-f", "fixtures/simple-composefile/compose.yaml", "--project-name", projectName, "up", "--scale", "simple=2", "-d") - assert.Check(t, !strings.Contains(res.Combined(), "Recreated")) + NewScenario(t, "scaling up must add a replica without recreating the existing one"). + Step("up starts the first replica", + ComposeCmd("up", "-d"), + ReplicaNumbers("simple", 1)). + Step("up --scale adds the second replica, keeping the first", + ComposeCmd("up", "--scale", "simple=2", "-d"), + ReplicaNumbers("simple", 1, 2), + OutputNotContains("Recreated")) } func TestUpWithDependencyNotRequired(t *testing.T) { - c := NewCLI(t) - const projectName = "compose-e2e-dependency-not-required" - t.Cleanup(func() { - c.RunDockerComposeCmd(t, "--project-name", projectName, "down") - }) - - res := c.RunDockerComposeCmd(t, "-f", "./fixtures/dependencies/deps-not-required.yaml", "--project-name", projectName, - "--profile", "not-required", "up", "-d") - assert.Assert(t, strings.Contains(res.Combined(), "foo"), res.Combined()) - assert.Assert(t, strings.Contains(res.Combined(), " optional dependency \"bar\" failed to start"), res.Combined()) + NewScenario(t, "up must start the service even when an optional dependency cannot"). + Step("up succeeds, reporting the optional dependency failure", + ComposeCmd("--profile", "not-required", "up", "-d"), + OutputContains("foo"), + OutputContains(`optional dependency "bar" failed to start`)) } func TestUpWithAllResources(t *testing.T) { - c := NewCLI(t) - const projectName = "compose-e2e-all-resources" - t.Cleanup(func() { - c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "-v") - }) - - res := c.RunDockerComposeCmd(t, "-f", "./fixtures/resources/compose.yaml", "--all-resources", "--project-name", projectName, "up") - assert.Assert(t, strings.Contains(res.Combined(), fmt.Sprintf(`Volume %s_my_vol Created`, projectName)), res.Combined()) - assert.Assert(t, strings.Contains(res.Combined(), fmt.Sprintf(`Network %s_my_net Created`, projectName)), res.Combined()) + s := NewScenario(t, "up --all-resources must create volumes and networks no service uses") + s.Step("up creates the unused volume and network", + ComposeCmd("--all-resources", "up"), + OutputContains("Volume "+s.Project()+"_my_vol Created"), + OutputContains("Network "+s.Project()+"_my_net Created")) } func TestUpProfile(t *testing.T) { - c := NewCLI(t) - const projectName = "compose-e2e-up-profile" - t.Cleanup(func() { - c.RunDockerComposeCmd(t, "--project-name", projectName, "--profile", "test", "down", "-v") - }) - - res := c.RunDockerComposeCmd(t, "-f", "./fixtures/profiles/docker-compose.yaml", "--project-name", projectName, "up", "foo") - assert.Assert(t, strings.Contains(res.Combined(), `Container db_c Created`), res.Combined()) - assert.Assert(t, strings.Contains(res.Combined(), `Container foo_c Created`), res.Combined()) - assert.Assert(t, !strings.Contains(res.Combined(), `Container bar_c Created`), res.Combined()) + NewScenario(t, "up on a profiled service must start it and its dependencies, not its profile siblings"). + Step("up starts the target and its dependency only", + ComposeCmd("up", "foo"), + ServiceState("foo", "exited"), + ServiceState("db", "exited"), + ServiceNotCreated("bar")) } func TestUpImageID(t *testing.T) { - c := NewCLI(t) - const projectName = "compose-e2e-up-image-id" - - digest := strings.TrimSpace(c.RunDockerCmd(t, "image", "inspect", "alpine", "-f", "{{ .ID }}").Stdout()) + s := NewScenario(t, "a service image referenced by its bare ID must be usable") + digest := strings.TrimSpace(s.CLI().RunDockerCmd(t, "image", "inspect", "alpine", "-f", "{{ .ID }}").Stdout()) _, id, _ := strings.Cut(digest, ":") - - t.Cleanup(func() { - c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "-v") - }) - - c = NewCLI(t, WithEnv(fmt.Sprintf("ID=%s", id))) - c.RunDockerComposeCmd(t, "-f", "./fixtures/simple-composefile/id.yaml", "--project-name", projectName, "up") + s.Env("ID="+id). + Step("up runs the container from the image ID", + ComposeCmd("up")) } func TestUpStopWithLogsMixed(t *testing.T) { - c := NewCLI(t) - const projectName = "compose-e2e-stop-logs" - - t.Cleanup(func() { - c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "-v") - }) - - res := c.RunDockerComposeCmd(t, "-f", "./fixtures/stop/compose.yaml", "--project-name", projectName, "up", "--abort-on-container-exit") - // assert we still get service2 logs after service 1 Stopped event - res.Assert(t, icmd.Expected{ - Err: "Container compose-e2e-stop-logs-service1-1 Stopped", - }) - // assert we get stop hook logs - res.Assert(t, icmd.Expected{Out: "service2-1 -> | stop hook running...\nservice2-1 | 64 bytes"}) + s := NewScenario(t, "on abort, logs of surviving services must keep flowing while others stop, hooks included") + s.Step("up aborts on the first exit but still relays service2's logs and stop hook", + ComposeCmd("up", "--abort-on-container-exit").Within(60*time.Second), + StderrContains("Container "+s.Project()+"-service1-1 Stopped"), + StdoutContains("stop hook running..."), + StdoutContains("64 bytes")) }