test(e2e): batch snapshot inspect with per-container fallback, explicit suite timeout

The per-container inspect of the previous fix doubled the suite's
wall-clock on the containerd CI job — one docker CLI process per
container per snapshot — pushing it past go test's default 10m and
killing the run mid-flight. Snapshots go back to one batch inspect,
falling back to individual inspects only when the batch fails because
a helper container vanished. The e2e make targets also pass an
explicit -timeout 20m so a slow job fails on its own results, not on
the default timeout.

TestUpStopWithLogsMixed becomes deterministic: with a bounded ping the
service could exit on its own before the abort reached it and the
pre_stop hook never ran; it now pings forever so the abort always
interrupts it.

Signed-off-by: Nicolas De Loof <[email protected]>
This commit is contained in:
Nicolas De Loof
2026-08-18 11:37:35 +02:00
parent d3838a73be
commit 07a364d18e
2 changed files with 55 additions and 37 deletions
+49 -35
View File
@@ -324,47 +324,27 @@ func (s *Scenario) command(action Action) icmd.Cmd {
}
// snapshot captures the current state of the project's containers.
// Containers are inspected one by one: helper containers (e.g. the ones
// running lifecycle hooks) carry the project label but can vanish between the
// listing and the inspect, and one gone container must not blank the whole
// The containers are inspected in one batch; if that fails — helper
// containers (e.g. the ones running lifecycle hooks) carry the project label
// but can vanish between the listing and the inspect — each container is
// retried individually so one gone container does not blank the whole
// snapshot. Helpers are also excluded from the snapshot itself: a container
// belongs to a service if it is numbered as a replica or marked one-off.
func (s *Scenario) snapshot() snapshot {
s.t.Helper()
snap := snapshot{}
for _, id := range s.projectContainerIDs() {
res := icmd.RunCmd(s.cli.NewDockerCmd(s.t, "inspect", id))
if res.ExitCode != 0 {
continue
}
var containers []struct {
ID string `json:"Id"`
Name string `json:"Name"`
State struct {
Status string `json:"Status"`
StartedAt string `json:"StartedAt"`
} `json:"State"`
Config struct {
Labels map[string]string `json:"Labels"`
} `json:"Config"`
}
if err := json.Unmarshal([]byte(res.Stdout()), &containers); err != nil {
continue
}
for _, c := range containers {
oneOff := c.Config.Labels["com.docker.compose.oneoff"] == "True"
if !oneOff && c.Config.Labels["com.docker.compose.container-number"] == "" {
continue
ids := s.projectContainerIDs()
if len(ids) == 0 {
return snap
}
res := icmd.RunCmd(s.cli.NewDockerCmd(s.t, append([]string{"inspect"}, ids...)...))
if res.ExitCode == 0 {
s.collect(snap, res.Stdout())
} else {
for _, id := range ids {
if res := icmd.RunCmd(s.cli.NewDockerCmd(s.t, "inspect", id)); res.ExitCode == 0 {
s.collect(snap, res.Stdout())
}
service := c.Config.Labels["com.docker.compose.service"]
snap[service] = append(snap[service], containerState{
ID: c.ID,
Name: strings.TrimPrefix(c.Name, "/"),
State: c.State.Status,
StartedAt: c.State.StartedAt,
OneOff: oneOff,
Labels: c.Config.Labels,
})
}
}
for service := range snap {
@@ -373,6 +353,40 @@ func (s *Scenario) snapshot() snapshot {
return snap
}
// collect parses a docker inspect output into the snapshot, skipping helper
// containers (neither numbered as a replica nor one-off).
func (s *Scenario) collect(snap snapshot, inspectOutput string) {
var containers []struct {
ID string `json:"Id"`
Name string `json:"Name"`
State struct {
Status string `json:"Status"`
StartedAt string `json:"StartedAt"`
} `json:"State"`
Config struct {
Labels map[string]string `json:"Labels"`
} `json:"Config"`
}
if err := json.Unmarshal([]byte(inspectOutput), &containers); err != nil {
return
}
for _, c := range containers {
oneOff := c.Config.Labels["com.docker.compose.oneoff"] == "True"
if !oneOff && c.Config.Labels["com.docker.compose.container-number"] == "" {
continue
}
service := c.Config.Labels["com.docker.compose.service"]
snap[service] = append(snap[service], containerState{
ID: c.ID,
Name: strings.TrimPrefix(c.Name, "/"),
State: c.State.Status,
StartedAt: c.State.StartedAt,
OneOff: oneOff,
Labels: c.Config.Labels,
})
}
}
func (s *Scenario) projectContainerIDs() []string {
res := icmd.RunCmd(s.cli.NewDockerCmd(s.t, "ps", "-a", "--no-trunc",
"--filter", "label=com.docker.compose.project="+s.project, "--format", "{{.ID}}"))
+6 -2
View File
@@ -262,15 +262,19 @@ services:
}
func TestUpStopWithLogsMixed(t *testing.T) {
// service2 pings forever so the abort always interrupts it: with a bounded
// ping, on a fast machine it can exit on its own before the abort reaches
// it, and the pre_stop hook never runs.
s := NewScenario(t, "on abort, logs of surviving services must keep flowing while others stop, hooks included")
s.Compose(`
services:
service1:
image: alpine
command: /bin/true
command: sh -c "sleep 2"
service2:
image: alpine
command: ping -c 2 localhost
init: true
command: ping localhost
pre_stop:
- command: echo "stop hook running..."
`).