test(e2e): start/stop scenarios observe container state, not CLI chatter

Migrate the state-friendly start/stop tests to the Scenario DSL:

- TestStartStop: stop halts containers in place, start brings the same
  ones back (NotRecreated); the ls checks match the project name exactly
  in the JSON output, immune to the prefix collisions the other
  start-stop project names would cause under parallel runs.
- TestStartStopWithDependencies: dependency start/stop propagation read
  from container state instead of 'Container X Started' messages.
- TestUpNoDeps replaces the 'Up no-deps links' subtest as its own
  scenario: up --no-deps must not create the linked dependency.
- TestStartSingleServiceAndDependency: create/start walk the dependency
  chain and nothing else — now asserting the unrelated services have no
  container at all, stronger than the legacy message matching.

The one-off and multi-file tests stay legacy: the vocabulary has no
one-off awareness yet and a scenario holds a single compose file.
The links and start-stop-deps fixtures become unused and are removed.

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
This commit is contained in:
Nicolas De Loof 2026-08-17 07:49:25 +02:00 committed by Nicolas De loof
parent 5baeadf929
commit 695a2a759a
7 changed files with 121 additions and 143 deletions

View file

@ -1,8 +0,0 @@
services:
foo:
image: nginx:alpine
links:
- bar
bar:
image: nginx:alpine

View file

@ -1,17 +0,0 @@
services:
another_2:
image: nginx:alpine
another:
image: nginx:alpine
depends_on:
- another_2
dep_2:
image: nginx:alpine
dep_1:
image: nginx:alpine
depends_on:
- dep_2
desired:
image: nginx:alpine
depends_on:
- dep_1

View file

@ -22,111 +22,62 @@ import (
"testing"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/icmd"
)
func TestStartStop(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "e2e-start-stop-no-dependencies"
getProjectRegx := func(status string) string {
// match output with random spaces like:
// e2e-start-stop running(3)
return fmt.Sprintf("%s\\s+%s\\(%d\\)", projectName, status, 2)
}
t.Run("Up a project", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/start-stop/compose.yaml", "--project-name", projectName, "up",
"-d")
assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-no-dependencies-simple-1 Started"), res.Combined())
res = c.RunDockerComposeCmd(t, "ls", "--all")
assert.Assert(t, is.Regexp(getProjectRegx("running"), res.Stdout()))
res = c.RunDockerComposeCmd(t, "--project-name", projectName, "ps")
assertServiceStatus(t, projectName, "simple", "Up", res.Stdout())
assertServiceStatus(t, projectName, "another", "Up", res.Stdout())
})
t.Run("stop project", func(t *testing.T) {
c.RunDockerComposeCmd(t, "-f", "./fixtures/start-stop/compose.yaml", "--project-name", projectName, "stop")
res := c.RunDockerComposeCmd(t, "ls")
assert.Assert(t, !strings.Contains(res.Combined(), "e2e-start-stop-no-dependencies"), res.Combined())
res = c.RunDockerComposeCmd(t, "ls", "--all")
assert.Assert(t, is.Regexp(getProjectRegx("exited"), res.Stdout()))
res = c.RunDockerComposeCmd(t, "--project-name", projectName, "ps")
assert.Assert(t, !strings.Contains(res.Combined(), "e2e-start-stop-no-dependencies-words-1"), res.Combined())
res = c.RunDockerComposeCmd(t, "--project-name", projectName, "ps", "--all")
assertServiceStatus(t, projectName, "simple", "Exited", res.Stdout())
assertServiceStatus(t, projectName, "another", "Exited", res.Stdout())
})
t.Run("start project", func(t *testing.T) {
c.RunDockerComposeCmd(t, "-f", "./fixtures/start-stop/compose.yaml", "--project-name", projectName, "start")
res := c.RunDockerComposeCmd(t, "ls")
assert.Assert(t, is.Regexp(getProjectRegx("running"), res.Stdout()))
})
t.Run("down", func(t *testing.T) {
_ = c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
})
s := NewScenario(t, "stop must halt the project's containers in place, start must bring the same ones back")
s.Step("up starts every service",
ComposeCmd("up", "-d"),
ServiceState("simple", "running"),
ServiceState("another", "running")).
Step("ls reports the project as running",
ComposeCmd("ls", "--format", "json"),
OutputContains(`"Name":"`+s.Project()+`"`)).
Step("stop halts the containers without removing them",
ComposeCmd("stop"),
ServiceState("simple", "exited"),
ServiceState("another", "exited")).
Step("a stopped project is hidden from ls",
ComposeCmd("ls", "--format", "json"),
OutputNotContains(`"Name":"`+s.Project()+`"`)).
Step("ls --all still lists the stopped project",
ComposeCmd("ls", "--all", "--format", "json"),
OutputContains(`"Name":"`+s.Project()+`"`)).
Step("start brings the same containers back",
ComposeCmd("start"),
ServiceState("simple", "running"),
ServiceState("another", "running"),
NotRecreated("simple", "another"))
}
func TestStartStopWithDependencies(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "e2e-start-stop-with-dependencies"
NewScenario(t, "stop must only halt the requested service, start must also start its dependencies").
Step("up starts the service and its dependency",
ComposeCmd("up", "-d"),
ServiceState("foo", "running"),
ServiceState("bar", "running")).
Step("stop foo leaves the dependency running",
ComposeCmd("stop", "foo"),
ServiceState("foo", "exited"),
ServiceState("bar", "running")).
Step("stop halts the whole project",
ComposeCmd("stop"),
ServiceState("foo", "exited"),
ServiceState("bar", "exited")).
Step("start foo brings its dependency back too",
ComposeCmd("start", "foo"),
ServiceState("foo", "running"),
ServiceState("bar", "running"),
NotRecreated("foo", "bar"))
}
defer c.RunDockerComposeCmd(t, "--project-name", projectName, "rm", "-fsv")
t.Run("Up", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/dependencies/compose.yaml", "--project-name", projectName,
"up", "-d")
assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-with-dependencies-foo-1 Started"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-with-dependencies-bar-1 Started"), res.Combined())
})
t.Run("stop foo", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "--project-name", projectName, "stop", "foo")
assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-with-dependencies-foo-1 Stopped"), res.Combined())
res = c.RunDockerComposeCmd(t, "--project-name", projectName, "ps", "--status", "running")
assert.Assert(t, strings.Contains(res.Combined(), "e2e-start-stop-with-dependencies-bar-1"), res.Combined())
assert.Assert(t, !strings.Contains(res.Combined(), "e2e-start-stop-with-dependencies-foo-1"), res.Combined())
})
t.Run("start foo", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "--project-name", projectName, "stop")
assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-with-dependencies-bar-1 Stopped"), res.Combined())
res = c.RunDockerComposeCmd(t, "--project-name", projectName, "start", "foo")
out := res.Combined()
assert.Assert(t, strings.Contains(out, "Container e2e-start-stop-with-dependencies-bar-1 Started"), out)
assert.Assert(t, strings.Contains(out, "Container e2e-start-stop-with-dependencies-foo-1 Started"), out)
res = c.RunDockerComposeCmd(t, "--project-name", projectName, "ps", "--status", "running")
out = res.Combined()
assert.Assert(t, strings.Contains(out, "e2e-start-stop-with-dependencies-bar-1"), out)
assert.Assert(t, strings.Contains(out, "e2e-start-stop-with-dependencies-foo-1"), out)
})
t.Run("Up no-deps links", func(t *testing.T) {
_ = c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/links/compose.yaml", "--project-name", projectName, "up",
"--no-deps", "-d", "foo")
assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-with-dependencies-foo-1 Started"), res.Combined())
assert.Assert(t, !strings.Contains(res.Combined(), "Container e2e-start-stop-with-dependencies-bar-1 Started"), res.Combined())
})
t.Run("down", func(t *testing.T) {
_ = c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
})
func TestUpNoDeps(t *testing.T) {
NewScenario(t, "up --no-deps must not create the service's dependencies").
Step("up --no-deps starts only the requested service",
ComposeCmd("up", "--no-deps", "-d", "foo"),
ServiceState("foo", "running"),
ServiceNotCreated("bar"))
}
func TestStartStopWithOneOffs(t *testing.T) {
@ -244,27 +195,21 @@ func TestStartStopMultipleServices(t *testing.T) {
}
func TestStartSingleServiceAndDependency(t *testing.T) {
cli := NewParallelCLI(t, WithEnv(
"COMPOSE_PROJECT_NAME=e2e-start-single-deps",
"COMPOSE_FILE=./fixtures/start-stop/start-stop-deps.yaml"))
t.Cleanup(func() {
cli.RunDockerComposeCmd(t, "down", "--remove-orphans", "-v", "-t", "0")
})
cli.RunDockerComposeCmd(t, "create", "desired")
res := cli.RunDockerComposeCmd(t, "start", "desired")
desiredServices := []string{"desired", "dep_1", "dep_2"}
for _, s := range desiredServices {
startMsg := fmt.Sprintf("Container e2e-start-single-deps-%s-1 Started", s)
assert.Assert(t, strings.Contains(res.Combined(), startMsg),
fmt.Sprintf("Missing start message for service: %s\n%s", s, res.Combined()))
}
undesiredServices := []string{"another", "another_2"}
for _, s := range undesiredServices {
assert.Assert(t, !strings.Contains(res.Combined(), s),
fmt.Sprintf("Shouldn't have message for service: %s\n%s", s, res.Combined()))
}
NewScenario(t, "start of a created service must start its dependency chain and nothing else").
Step("create prepares the desired service and its dependencies only",
ComposeCmd("create", "desired"),
ServiceState("desired", "created"),
ServiceState("dep_1", "created"),
ServiceState("dep_2", "created"),
ServiceNotCreated("another"),
ServiceNotCreated("another_2")).
Step("start brings up the desired service with its dependencies, nothing else",
ComposeCmd("start", "desired"),
ServiceState("desired", "running"),
ServiceState("dep_1", "running"),
ServiceState("dep_2", "running"),
ServiceNotCreated("another"),
ServiceNotCreated("another_2"))
}
func TestStartStopMultipleFiles(t *testing.T) {

View file

@ -0,0 +1,27 @@
services:
desired:
image: alpine
init: true
command: sleep infinity
depends_on:
- dep_1
dep_1:
image: alpine
init: true
command: sleep infinity
depends_on:
- dep_2
dep_2:
image: alpine
init: true
command: sleep infinity
another:
image: alpine
init: true
command: sleep infinity
depends_on:
- another_2
another_2:
image: alpine
init: true
command: sleep infinity

View file

@ -0,0 +1,9 @@
services:
simple:
image: alpine
init: true
command: sleep infinity
another:
image: alpine
init: true
command: sleep infinity

View file

@ -0,0 +1,11 @@
services:
foo:
image: alpine
init: true
command: sleep infinity
depends_on:
- bar
bar:
image: alpine
init: true
command: sleep infinity

View file

@ -0,0 +1,11 @@
services:
foo:
image: alpine
init: true
command: sleep infinity
links:
- bar
bar:
image: alpine
init: true
command: sleep infinity