compose/pkg/e2e/start_stop_test.go
Nicolas De Loof a725a8ae04 test(e2e): one-off vocabulary — service checks skip run containers
Snapshots now record whether a container is a one-off (run) container,
plus its start time. Service-facing checks (ServiceState, NotRecreated,
label checks) observe only long-lived containers; three checks join the
vocabulary for the one-off side: OneOffState, OneOffsUntouched (same
containers, same state, same start time — catches an unwanted restart,
which message matching never could) and OneOffsRemoved (errors if there
was nothing to remove, rather than pass vacuously).

TestStartStopWithOneOffs becomes the demonstrator: stop/start/restart
act on services and leave the one-off untouched, down --remove-orphans
takes it away.

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2026-08-18 16:49:56 +02:00

212 lines
7.9 KiB
Go

/*
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.
*/
package e2e
import (
"fmt"
"strings"
"testing"
"gotest.tools/v3/assert"
"gotest.tools/v3/icmd"
)
func TestStartStop(t *testing.T) {
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) {
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"))
}
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) {
NewScenario(t, "stop, start and restart must act on services and leave one-off containers untouched").
Step("up starts the services",
ComposeCmd("up", "-d"),
ServiceState("foo", "running"),
ServiceState("bar", "running")).
Step("run adds a one-off container alongside the service",
ComposeCmd("run", "-d", "bar", "sleep", "infinity"),
OneOffState("bar", "running"),
NotRecreated("foo", "bar")).
Step("stop halts the services but not the one-off",
ComposeCmd("stop"),
ServiceState("foo", "exited"),
ServiceState("bar", "exited"),
OneOffState("bar", "running"),
OneOffsUntouched("bar")).
Step("start resumes the services without touching the one-off",
ComposeCmd("start"),
ServiceState("foo", "running"),
ServiceState("bar", "running"),
OneOffsUntouched("bar")).
Step("restart restarts the services but not the one-off",
ComposeCmd("restart"),
ServiceState("foo", "running"),
ServiceState("bar", "running"),
NotRecreated("foo", "bar"),
OneOffsUntouched("bar")).
Step("down --remove-orphans removes the one-off as well",
ComposeCmd("down", "--remove-orphans"),
ServiceNotCreated("foo"),
ServiceNotCreated("bar"),
OneOffsRemoved("bar"))
}
func TestStartAlreadyRunning(t *testing.T) {
cli := NewParallelCLI(t, WithEnv(
"COMPOSE_PROJECT_NAME=e2e-start-stop-svc-already-running",
"COMPOSE_FILE=./fixtures/start-stop/compose.yaml"))
t.Cleanup(func() {
cli.RunDockerComposeCmd(t, "down", "--remove-orphans", "-v", "-t", "0")
})
cli.RunDockerComposeCmd(t, "up", "-d", "--wait")
res := cli.RunDockerComposeCmd(t, "start", "simple")
assert.Equal(t, res.Stdout(), "", "No output should have been written to stdout")
}
func TestStopAlreadyStopped(t *testing.T) {
cli := NewParallelCLI(t, WithEnv(
"COMPOSE_PROJECT_NAME=e2e-start-stop-svc-already-stopped",
"COMPOSE_FILE=./fixtures/start-stop/compose.yaml"))
t.Cleanup(func() {
cli.RunDockerComposeCmd(t, "down", "--remove-orphans", "-v", "-t", "0")
})
cli.RunDockerComposeCmd(t, "up", "-d", "--wait")
// stop the container
cli.RunDockerComposeCmd(t, "stop", "simple")
// attempt to stop it again
res := cli.RunDockerComposeCmdNoCheck(t, "stop", "simple")
// TODO: for consistency, this should NOT write any output because the
// container is already stopped
res.Assert(t, icmd.Expected{
ExitCode: 0,
Err: "Container e2e-start-stop-svc-already-stopped-simple-1 Stopped",
})
}
func TestStartStopMultipleServices(t *testing.T) {
cli := NewParallelCLI(t, WithEnv(
"COMPOSE_PROJECT_NAME=e2e-start-stop-svc-multiple",
"COMPOSE_FILE=./fixtures/start-stop/compose.yaml"))
t.Cleanup(func() {
cli.RunDockerComposeCmd(t, "down", "--remove-orphans", "-v", "-t", "0")
})
cli.RunDockerComposeCmd(t, "up", "-d", "--wait")
res := cli.RunDockerComposeCmd(t, "stop", "simple", "another")
services := []string{"simple", "another"}
for _, svc := range services {
stopMsg := fmt.Sprintf("Container e2e-start-stop-svc-multiple-%s-1 Stopped", svc)
assert.Assert(t, strings.Contains(res.Stderr(), stopMsg),
fmt.Sprintf("Missing stop message for %s\n%s", svc, res.Combined()))
}
res = cli.RunDockerComposeCmd(t, "start", "simple", "another")
for _, svc := range services {
startMsg := fmt.Sprintf("Container e2e-start-stop-svc-multiple-%s-1 Started", svc)
assert.Assert(t, strings.Contains(res.Stderr(), startMsg),
fmt.Sprintf("Missing start message for %s\n%s", svc, res.Combined()))
}
}
func TestStartSingleServiceAndDependency(t *testing.T) {
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) {
cli := NewParallelCLI(t, WithEnv("COMPOSE_PROJECT_NAME=e2e-start-stop-svc-multiple-files"))
t.Cleanup(func() {
cli.RunDockerComposeCmd(t, "-p", "e2e-start-stop-svc-multiple-files", "down", "--remove-orphans")
})
cli.RunDockerComposeCmd(t, "-f", "./fixtures/start-stop/compose.yaml", "up", "-d")
cli.RunDockerComposeCmd(t, "-f", "./fixtures/start-stop/other.yaml", "up", "-d")
res := cli.RunDockerComposeCmd(t, "-f", "./fixtures/start-stop/compose.yaml", "stop")
assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-files-simple-1 Stopped"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-files-another-1 Stopped"), res.Combined())
assert.Assert(t, !strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-files-a-different-one-1 Stopped"), res.Combined())
assert.Assert(t, !strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-files-and-another-one-1 Stopped"), res.Combined())
}