Skip to content

Commit da0d37a

Browse files
ndeloofclaude
andcommitted
Add e2e tests for convergence edge cases
Add targeted e2e tests covering specific behaviors that must be preserved across convergence refactoring: - TestReplaceLabelOnRecreate (compose_test.go): verify com.docker.compose.replace label is set on recreated containers - TestNetworkConfigChangeReconnectsContainers (networks_test.go): verify network config change recreates the network and reconnects containers to the new network ID - TestUpRecreateVolumesAlsoRecreatContainers (volumes_test.go): verify volume config change with -y also recreates containers - TestDependentContainerReplacedOnRecreate (restart_test.go): verify dependent services with restart:true get a new container when their dependency is recreated - TestUpIdempotent (up_test.go): verify running up twice with no changes produces no recreations and preserves container IDs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
1 parent baaaaa3 commit da0d37a

6 files changed

Lines changed: 147 additions & 0 deletions

File tree

pkg/e2e/compose_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,3 +406,30 @@ func TestUnnecessaryResources(t *testing.T) {
406406
c.RunDockerComposeCmd(t, "-f", "./fixtures/external/compose.yaml", "-p", projectName, "up", "-d", "test")
407407
// Should not fail as missing external network is not used
408408
}
409+
410+
func TestReplaceLabelOnRecreate(t *testing.T) {
411+
c := NewCLI(t)
412+
const projectName = "replace-label"
413+
t.Cleanup(func() {
414+
c.cleanupWithDown(t, projectName)
415+
})
416+
417+
// First up: fresh container has no replace label
418+
c.RunDockerComposeCmd(t, "-f", "./fixtures/replace-label/compose.yaml",
419+
"--project-name", projectName, "up", "-d")
420+
res := c.RunDockerCmd(t, "inspect", fmt.Sprintf("%s-svc-1", projectName),
421+
"-f", `{{ index .Config.Labels "com.docker.compose.replace" }}`)
422+
res.Assert(t, icmd.Expected{Out: ""})
423+
424+
// Second up with changed env triggers recreate
425+
cli := NewCLI(t, WithEnv("TAG=v2"))
426+
res = cli.RunDockerComposeCmd(t, "-f", "./fixtures/replace-label/compose.yaml",
427+
"--project-name", projectName, "up", "-d")
428+
assert.Assert(t, strings.Contains(res.Stderr(), "Recreated"), res.Stderr())
429+
430+
// Recreated container should have replace label pointing to old name
431+
res = c.RunDockerCmd(t, "inspect", fmt.Sprintf("%s-svc-1", projectName),
432+
"-f", `{{ index .Config.Labels "com.docker.compose.replace" }}`)
433+
assert.Assert(t, strings.Contains(res.Stdout(), "svc-1"),
434+
"expected replace label with 'svc-1', got: %s", res.Stdout())
435+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
services:
2+
svc:
3+
image: alpine
4+
command: sleep infinity
5+
environment:
6+
- TAG=${TAG:-v1}

pkg/e2e/networks_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,37 @@ func TestInterfaceName(t *testing.T) {
201201
res.Assert(t, icmd.Expected{Out: "foobar@"})
202202
}
203203

204+
func TestNetworkConfigChangeReconnectsContainers(t *testing.T) {
205+
c := NewCLI(t)
206+
const projectName = "network_config_reconnect"
207+
t.Cleanup(func() {
208+
c.cleanupWithDown(t, projectName)
209+
})
210+
211+
c.RunDockerComposeCmd(t, "-f", "./fixtures/network-recreate/compose.yaml",
212+
"--project-name", projectName, "up", "-d")
213+
214+
netName := fmt.Sprintf("%s_test", projectName)
215+
res := c.RunDockerCmd(t, "network", "inspect", netName, "-f", "{{ .Id }}")
216+
initialNetID := strings.TrimSpace(res.Stdout())
217+
218+
// Change network config (label) -> network recreated
219+
cli := NewCLI(t, WithEnv("FOO=changed"))
220+
cli.RunDockerComposeCmd(t, "-f", "./fixtures/network-recreate/compose.yaml",
221+
"--project-name", projectName, "up", "-d")
222+
223+
// Network ID must have changed
224+
res = c.RunDockerCmd(t, "network", "inspect", netName, "-f", "{{ .Id }}")
225+
newNetID := strings.TrimSpace(res.Stdout())
226+
assert.Assert(t, newNetID != initialNetID, "expected network to be recreated with new ID")
227+
228+
// Container must be connected to the new network
229+
res = c.RunDockerCmd(t, "inspect", fmt.Sprintf("%s-web-1", projectName),
230+
"-f", `{{ range .NetworkSettings.Networks }}{{ .NetworkID }}{{ end }}`)
231+
assert.Assert(t, strings.Contains(res.Stdout(), newNetID),
232+
"expected container on new network %s, got: %s", newNetID, res.Stdout())
233+
}
234+
204235
func TestNetworkRecreate(t *testing.T) {
205236
c := NewCLI(t)
206237
const projectName = "network_recreate"

pkg/e2e/restart_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,37 @@ func TestRestartWithDependencies(t *testing.T) {
9898
assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Running", depNoRestart)), out)
9999
}
100100

101+
func TestDependentContainerRestartedOnRecreate(t *testing.T) {
102+
c := NewCLI(t, WithEnv(
103+
"COMPOSE_PROJECT_NAME=e2e-dep-restart",
104+
))
105+
t.Cleanup(func() {
106+
c.RunDockerComposeCmd(t, "down", "--remove-orphans")
107+
})
108+
109+
// First up
110+
c.RunDockerComposeCmd(t, "-f", "./fixtures/restart-test/compose-depends-on.yaml", "up", "-d")
111+
112+
// Recreate nginx (change label) → with-restart depends with restart:true
113+
// so it should be stopped then restarted (same container, not replaced)
114+
cli := NewCLI(t, WithEnv(
115+
"COMPOSE_PROJECT_NAME=e2e-dep-restart",
116+
"LABEL=recreate",
117+
))
118+
res := cli.RunDockerComposeCmd(t, "-f", "./fixtures/restart-test/compose-depends-on.yaml", "up", "-d")
119+
out := res.Combined()
120+
121+
// The dependent with restart:true should have been stopped then started
122+
assert.Assert(t, strings.Contains(out, "e2e-dep-restart-with-restart-1"),
123+
"expected dependent container in output: %s", out)
124+
125+
// All 3 containers should be running after convergence
126+
res = c.RunDockerComposeCmd(t, "ps", "--format", "{{.State}}")
127+
for _, line := range strings.Split(strings.TrimSpace(res.Stdout()), "\n") {
128+
assert.Equal(t, strings.TrimSpace(line), "running", "expected all containers running")
129+
}
130+
}
131+
101132
func TestRestartWithProfiles(t *testing.T) {
102133
c := NewParallelCLI(t, WithEnv(
103134
"COMPOSE_PROJECT_NAME=e2e-restart-profiles",

pkg/e2e/up_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,33 @@ func TestScaleDoesntRecreate(t *testing.T) {
153153
assert.Check(t, !strings.Contains(res.Combined(), "Recreated"))
154154
}
155155

156+
func TestUpIdempotent(t *testing.T) {
157+
c := NewCLI(t)
158+
const projectName = "compose-e2e-idempotent"
159+
t.Cleanup(func() {
160+
c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
161+
})
162+
163+
c.RunDockerComposeCmd(t, "-f", "fixtures/simple-composefile/compose.yaml",
164+
"--project-name", projectName, "up", "-d")
165+
166+
res := c.RunDockerCmd(t, "inspect", fmt.Sprintf("%s-simple-1", projectName), "-f", "{{ .Id }}")
167+
initialID := strings.TrimSpace(res.Stdout())
168+
169+
// Second up with no changes
170+
res = c.RunDockerComposeCmd(t, "-f", "fixtures/simple-composefile/compose.yaml",
171+
"--project-name", projectName, "up", "-d")
172+
173+
assert.Assert(t, strings.Contains(res.Stderr(), "Running"),
174+
"expected Running in output: %s", res.Stderr())
175+
assert.Assert(t, !strings.Contains(res.Stderr(), "Recreated"),
176+
"unexpected Recreated: %s", res.Stderr())
177+
178+
// Container ID unchanged
179+
res = c.RunDockerCmd(t, "inspect", fmt.Sprintf("%s-simple-1", projectName), "-f", "{{ .Id }}")
180+
assert.Equal(t, strings.TrimSpace(res.Stdout()), initialID)
181+
}
182+
156183
func TestUpWithDependencyNotRequired(t *testing.T) {
157184
c := NewCLI(t)
158185
const projectName = "compose-e2e-dependency-not-required"

pkg/e2e/volumes_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,31 @@ func TestUpRecreateVolumes(t *testing.T) {
159159
res.Assert(t, icmd.Expected{Out: "zot"})
160160
}
161161

162+
func TestUpRecreateVolumesAlsoRecreatesContainers(t *testing.T) {
163+
c := NewCLI(t)
164+
const projectName = "compose-e2e-recreate-vol-ctr"
165+
t.Cleanup(func() {
166+
c.cleanupWithDown(t, projectName)
167+
})
168+
169+
c.RunDockerComposeCmd(t, "-f", "./fixtures/recreate-volumes/compose.yaml",
170+
"--project-name", projectName, "up", "-d")
171+
172+
// Get initial container ID
173+
res := c.RunDockerCmd(t, "inspect", fmt.Sprintf("%s-app-1", projectName), "-f", "{{ .Id }}")
174+
initialID := strings.TrimSpace(res.Stdout())
175+
176+
// Change volume config + auto-confirm → volume and container recreated
177+
c.RunDockerComposeCmd(t, "-f", "./fixtures/recreate-volumes/compose2.yaml",
178+
"--project-name", projectName, "up", "-d", "-y")
179+
180+
// Container ID should have changed (container was removed and recreated
181+
// because Docker requires container removal to delete a volume)
182+
res = c.RunDockerCmd(t, "inspect", fmt.Sprintf("%s-app-1", projectName), "-f", "{{ .Id }}")
183+
newID := strings.TrimSpace(res.Stdout())
184+
assert.Assert(t, newID != initialID, "expected container to be recreated after volume change")
185+
}
186+
162187
func TestUpRecreateVolumes_IgnoreBinds(t *testing.T) {
163188
c := NewCLI(t)
164189
const projectName = "compose-e2e-recreate-volumes"

0 commit comments

Comments
 (0)