diff --git a/pkg/e2e/testdata/TestUpInteractiveDependencyTimeout/compose.yaml b/pkg/e2e/testdata/TestUpInteractiveDependencyTimeout/compose.yaml new file mode 100644 index 00000000000..114b50c6418 --- /dev/null +++ b/pkg/e2e/testdata/TestUpInteractiveDependencyTimeout/compose.yaml @@ -0,0 +1,14 @@ +services: + db: + image: alpine + command: sleep infinity + healthcheck: + test: ["CMD", "false"] + interval: 1s + retries: 100 + web: + image: alpine + command: echo web-started + depends_on: + db: + condition: service_healthy diff --git a/pkg/e2e/testdata/TestUpInteractiveDryRun/compose.yaml b/pkg/e2e/testdata/TestUpInteractiveDryRun/compose.yaml new file mode 100644 index 00000000000..a35fee06453 --- /dev/null +++ b/pkg/e2e/testdata/TestUpInteractiveDryRun/compose.yaml @@ -0,0 +1,3 @@ +services: + app: + image: alpine diff --git a/pkg/e2e/testdata/TestUpInteractiveHookLogs/compose.yaml b/pkg/e2e/testdata/TestUpInteractiveHookLogs/compose.yaml new file mode 100644 index 00000000000..fc7cf9fc2b6 --- /dev/null +++ b/pkg/e2e/testdata/TestUpInteractiveHookLogs/compose.yaml @@ -0,0 +1,17 @@ +services: + app: + image: alpine + # the sleep keeps the container running while post_start execs into it and + # the session streams its output + command: sh -c 'echo "app-read:$(cat /shared/init.txt)"; sleep 5' + volumes: + - data:/shared + pre_start: + - image: alpine + # prints a marker the service never logs, so its presence in the + # session can only come from the hook's own output stream + command: sh -c 'echo pre-start-hook-stdout; echo "pre-start-file-content" > /shared/init.txt' + post_start: + - command: echo post-start-hook-ran +volumes: + data: diff --git a/pkg/e2e/testdata/TestUpInteractiveImmediateExit/compose.yaml b/pkg/e2e/testdata/TestUpInteractiveImmediateExit/compose.yaml new file mode 100644 index 00000000000..c0bcba59920 --- /dev/null +++ b/pkg/e2e/testdata/TestUpInteractiveImmediateExit/compose.yaml @@ -0,0 +1,8 @@ +services: + quick: + image: alpine + command: echo hi + long: + image: alpine + command: sleep infinity + init: true diff --git a/pkg/e2e/testdata/TestUpInteractiveRecreateStreamsNewContainer/compose.yaml b/pkg/e2e/testdata/TestUpInteractiveRecreateStreamsNewContainer/compose.yaml new file mode 100644 index 00000000000..bf0ece21b24 --- /dev/null +++ b/pkg/e2e/testdata/TestUpInteractiveRecreateStreamsNewContainer/compose.yaml @@ -0,0 +1,7 @@ +services: + app: + image: alpine + # the sleep keeps the container running while the session attaches to it + command: sh -c 'echo "value=$VALUE"; sleep 2' + environment: + VALUE: ${VALUE:-one} diff --git a/pkg/e2e/testdata/TestUpInteractiveRestartsExitedContainer/compose.yaml b/pkg/e2e/testdata/TestUpInteractiveRestartsExitedContainer/compose.yaml new file mode 100644 index 00000000000..8f80cc9df5c --- /dev/null +++ b/pkg/e2e/testdata/TestUpInteractiveRestartsExitedContainer/compose.yaml @@ -0,0 +1,4 @@ +services: + app: + image: alpine + command: sh -c 'if [ -f /tmp/ran ]; then echo second-run; else touch /tmp/ran; fi' diff --git a/pkg/e2e/testdata/TestUpInteractiveWaitsForHealthy/compose.yaml b/pkg/e2e/testdata/TestUpInteractiveWaitsForHealthy/compose.yaml new file mode 100644 index 00000000000..a940773b8dd --- /dev/null +++ b/pkg/e2e/testdata/TestUpInteractiveWaitsForHealthy/compose.yaml @@ -0,0 +1,22 @@ +services: + db: + image: alpine + command: sh -c 'sleep 3; touch /shared/ready; sleep 60' + init: true + volumes: + - shared:/shared + healthcheck: + test: ["CMD", "test", "-f", "/shared/ready"] + interval: 1s + timeout: 1s + retries: 30 + web: + image: alpine + command: sh -c 'if [ -f /shared/ready ]; then echo web-saw-ready; else echo web-too-early; fi' + volumes: + - shared:/shared + depends_on: + db: + condition: service_healthy +volumes: + shared: diff --git a/pkg/e2e/up_interactive_test.go b/pkg/e2e/up_interactive_test.go new file mode 100644 index 00000000000..00b6d3a7d78 --- /dev/null +++ b/pkg/e2e/up_interactive_test.go @@ -0,0 +1,105 @@ +//go:build e2e + +/* + Copyright 2026 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. +*/ + +package e2e + +import ( + "testing" + "time" +) + +// Scenarios locking the observable behavior of the foreground (non-detached) +// `compose up`. Most e2e coverage of the start phase runs detached. + +func TestUpInteractiveHookLogs(t *testing.T) { + NewScenario(t, "a foreground up must run the hooks around the service and stream their output into its session"). + Step("up shows both hooks' output, and the service reads what pre_start wrote before it started", + ComposeCmd("up", "--menu=false").Within(120*time.Second), + StdoutContains("pre-start-hook-stdout"), + StdoutContains("app-read:pre-start-file-content"), + StdoutContains("post-start-hook-ran"), + ServiceState("app", "exited")) +} + +func TestUpInteractiveImmediateExit(t *testing.T) { + NewScenario(t, "a foreground up --abort-on-container-exit must notice a container that exits as soon as it starts"). + Step("up aborts on the instantly exiting container instead of waiting for the long-running one", + ComposeCmd("up", "--menu=false", "--abort-on-container-exit").Within(60*time.Second), + OutputContains("quick-1 exited with code 0"), + ServiceState("quick", "exited"), + ServiceState("long", "exited")) +} + +func TestUpInteractiveRestartsExitedContainer(t *testing.T) { + NewScenario(t, "a foreground up must start a container that exited earlier, reusing it"). + Step("up -d leaves the service exited after its first run", + ComposeCmd("up", "-d"), + Eventually(ServiceState("app", "exited"), 15*time.Second)). + Step("a foreground up runs the same container a second time", + ComposeCmd("up", "--menu=false").Within(60*time.Second), + NotRecreated("app"), + StdoutContains("second-run"), + ServiceState("app", "exited")) +} + +func TestUpInteractiveWaitsForHealthy(t *testing.T) { + NewScenario(t, "a foreground up must not start a service before its service_healthy dependency is healthy"). + Step("up starts web only once db reports healthy", + ComposeCmd("up", "--menu=false", "--abort-on-container-exit").Within(120*time.Second), + StdoutContains("web-saw-ready"), + OutputNotContains("web-too-early"), + ServiceState("web", "exited")) +} + +func TestUpInteractiveRecreateStreamsNewContainer(t *testing.T) { + NewScenario(t, "a foreground up after a config change must recreate the service and stream the new container's output"). + Step("up -d creates the service with its first configuration", + ComposeCmd("up", "-d"), + Eventually(ServiceState("app", "exited"), 15*time.Second)). + Step("a foreground up with another value recreates it and shows the new output", + ComposeCmd("up", "--menu=false").WithEnv("VALUE=two").Within(60*time.Second), + Recreated("app"), + ContainerEnv("app", "VALUE", "two"), + StdoutContains("value=two"), + OutputNotContains("value=one")) +} + +func TestUpInteractiveDependencyTimeout(t *testing.T) { + NewScenario(t, "a foreground up with --wait-timeout must fail once a dependency stays unhealthy, without starting the dependent"). + Step("up fails on the dependency timeout; web exists but was never started", + ComposeCmd("up", "--menu=false", "--wait-timeout", "3").MayFail().Within(60*time.Second), + ExitCode(1), + OutputContains("timeout waiting for dependencies"), + OutputNotContains("deadline exceeded"), + OutputNotContains("web-started"), + ServiceState("db", "running"), + // web's container is created in the create phase, before the start + // phase blocks on db's health: "created" (not "no container") is + // what says it was prepared but never started. + ServiceState("web", "created")) +} + +func TestUpInteractiveDryRun(t *testing.T) { + // the progress lines name the container after the project + s := NewScenario(t, "a foreground up --dry-run must report the container creation, never start it and leave nothing behind") + s.Step("up --dry-run reports the creation, no start, and leaves no container behind", + ComposeCmd("up", "--menu=false", "--dry-run").Within(60*time.Second), + OutputContains("Container "+s.Project()+"-app-1 Created"), + OutputNotContains(s.Project()+"-app-1 Start"), + ServiceNotCreated("app")) +}