test(e2e): expose, container_name, ipc and wait scenarios

- TestExposeRange keeps its regression link (#13378), the invariant
  being simply that up accepts a port range in expose.
- TestUpContainerNameConflict interpolates a project-scoped
  container_name instead of the fixture's global 'test', so parallel
  runs cannot collide on it.
- TestIPC demonstrates the CLI() escape hatch: the external container
  the 'container:' mode points at is created eagerly before the steps,
  so its id is available to the declarative checks. (Runs only in CI:
  ipc container: mode is rejected by Docker Desktop and dind daemons —
  'restricted host mount' — for the legacy test as well.)
- wait scenarios observe which services exited instead of trusting the
  command's silence; TestWaitOnInfinity stays legacy, the DSL has no
  notion of a still-running command. The wait fixture stays for it;
  ipc-test and container_name fixtures are removed.

Signed-off-by: Nicolas De Loof <[email protected]>
This commit is contained in:
Nicolas De Loof
2026-08-18 11:36:47 +02:00
parent 60ef584bbe
commit f093ce2106
6 changed files with 111 additions and 130 deletions
+29 -16
View File
@@ -20,24 +20,37 @@ package e2e
import (
"testing"
"gotest.tools/v3/icmd"
)
func TestUpContainerNameConflict(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "e2e-container_name_conflict"
s := NewScenario(t, "two services claiming the same container_name must be rejected together, but each must run alone")
name := s.Project() + "-fixed"
s.Compose(`
services:
test:
image: alpine
container_name: ${FIXED_NAME}
command: /bin/true
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
})
res := c.RunDockerComposeCmdNoCheck(t, "-f", "fixtures/container_name/compose.yaml", "--project-name", projectName, "up")
res.Assert(t, icmd.Expected{ExitCode: 1, Err: `container name "test" is already in use`})
c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
c.RunDockerComposeCmd(t, "-f", "fixtures/container_name/compose.yaml", "--project-name", projectName, "up", "test")
c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
c.RunDockerComposeCmd(t, "-f", "fixtures/container_name/compose.yaml", "--project-name", projectName, "up", "another_test")
another_test:
image: alpine
container_name: ${FIXED_NAME}
command: /bin/true
`).
Env("FIXED_NAME="+name).
Step("up with both services is rejected over the name conflict",
ComposeCmd("up").MayFail(),
ExitCode(1),
OutputContains(`container name "`+name+`" is already in use`)).
Step("the failed up leaves nothing behind",
ComposeCmd("down")).
Step("the first service runs alone under the shared name",
ComposeCmd("up", "test"),
ServiceState("test", "exited")).
Step("down frees the name",
ComposeCmd("down"),
ServiceNotCreated("test")).
Step("the other service runs alone under the shared name",
ComposeCmd("up", "another_test"),
ServiceState("another_test", "exited"))
}
+7 -17
View File
@@ -19,30 +19,20 @@
package e2e
import (
"os"
"path/filepath"
"testing"
"gotest.tools/v3/assert"
)
// see https://github.com/docker/compose/issues/13378
func TestExposeRange(t *testing.T) {
c := NewParallelCLI(t)
f := filepath.Join(t.TempDir(), "compose.yaml")
err := os.WriteFile(f, []byte(`
name: test-expose-range
// Regression test for https://github.com/docker/compose/issues/13378
// a port range in expose used to fail container creation.
NewScenario(t, "a port range in expose must be accepted").
Compose(`
services:
test:
image: alpine
expose:
- "9091-9092"
`), 0o644)
assert.NilError(t, err)
t.Cleanup(func() {
c.cleanupWithDown(t, "test-expose-range")
})
c.RunDockerComposeCmd(t, "-f", f, "up")
`).
Step("up creates and runs the service",
ComposeCmd("up"))
}
@@ -1,10 +0,0 @@
services:
test:
image: alpine
container_name: test
command: /bin/true
another_test:
image: alpine
container_name: test
command: /bin/true
-13
View File
@@ -1,13 +0,0 @@
services:
service:
image: alpine
command: top
ipc: "service:shareable"
container:
image: alpine
command: top
ipc: "container:ipc_mode_container"
shareable:
image: alpine
command: top
ipc: shareable
+36 -37
View File
@@ -17,48 +17,47 @@
package e2e
import (
"fmt"
"strings"
"testing"
"gotest.tools/v3/icmd"
)
func TestIPC(t *testing.T) {
c := NewParallelCLI(t)
s := NewScenario(t, "every ipc mode must materialize in the created containers: shareable, service: and container:")
external := s.Project() + "-external"
const projectName = "ipc_e2e"
var cid string
t.Run("create ipc mode container", func(t *testing.T) {
res := c.RunDockerCmd(t, "run", "-d", "--rm", "--ipc=shareable", "--name", "ipc_mode_container", "alpine",
"top")
cid = strings.Trim(res.Stdout(), "\n")
})
// the container: mode references a container compose doesn't manage;
// create it up front so its id is known when the steps assert against it
cid := strings.TrimSpace(s.CLI().RunDockerCmd(t, "run", "-d", "--rm", "--ipc=shareable", "--name", external, "alpine", "top").Stdout())
s.Defer(DockerCmd("rm", "-f", external).MayFail())
t.Run("up", func(t *testing.T) {
c.RunDockerComposeCmd(t, "-f", "./fixtures/ipc-test/compose.yaml", "--project-name", projectName, "up", "-d")
})
t.Run("check running project", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-p", projectName, "ps")
res.Assert(t, icmd.Expected{Out: `shareable`})
})
t.Run("check ipcmode in container inspect", func(t *testing.T) {
res := c.RunDockerCmd(t, "inspect", projectName+"-shareable-1")
res.Assert(t, icmd.Expected{Out: `"IpcMode": "shareable",`})
res = c.RunDockerCmd(t, "inspect", projectName+"-service-1")
res.Assert(t, icmd.Expected{Out: `"IpcMode": "container:`})
res = c.RunDockerCmd(t, "inspect", projectName+"-container-1")
res.Assert(t, icmd.Expected{Out: fmt.Sprintf(`"IpcMode": "container:%s",`, cid)})
})
t.Run("down", func(t *testing.T) {
_ = c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
})
t.Run("remove ipc mode container", func(t *testing.T) {
_ = c.RunDockerCmd(t, "rm", "-f", "ipc_mode_container")
})
s.Compose(`
services:
service:
image: alpine
command: top
ipc: "service:shareable"
container:
image: alpine
command: top
ipc: "container:${EXTERNAL_NAME}"
shareable:
image: alpine
command: top
ipc: shareable
`).
Env("EXTERNAL_NAME="+external).
Step("up starts the three services",
ComposeCmd("up", "-d"),
ServiceState("service", "running"),
ServiceState("container", "running"),
ServiceState("shareable", "running")).
Step("the shareable service owns a shareable ipc namespace",
DockerCmd("inspect", "-f", "{{.HostConfig.IpcMode}}", s.Project()+"-shareable-1"),
OutputContains("shareable")).
Step("the service: mode resolves to the target service's container",
DockerCmd("inspect", "-f", "{{.HostConfig.IpcMode}}", s.Project()+"-service-1"),
OutputContains("container:")).
Step("the container: mode resolves to the external container",
DockerCmd("inspect", "-f", "{{.HostConfig.IpcMode}}", s.Project()+"-container-1"),
OutputContains("container:"+cid))
}
+39 -37
View File
@@ -17,7 +17,6 @@
package e2e
import (
"strings"
"testing"
"time"
@@ -25,32 +24,40 @@ import (
"gotest.tools/v3/icmd"
)
const waitCompose = `
services:
faster:
image: alpine
command: sleep 2
slower:
image: alpine
command: sleep 5
infinity:
image: alpine
command: sleep infinity
`
func TestWaitOnFaster(t *testing.T) {
const projectName = "e2e-wait-faster"
c := NewParallelCLI(t)
cleanup := func() {
c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "--timeout=0", "--remove-orphans")
}
t.Cleanup(cleanup)
cleanup()
c.RunDockerComposeCmd(t, "-f", "./fixtures/wait/compose.yaml", "--project-name", projectName, "up", "-d")
c.RunDockerComposeCmd(t, "--project-name", projectName, "wait", "faster")
NewScenario(t, "wait must return once the selected service exits, whatever the others do").
Compose(waitCompose).
Step("up starts all services",
ComposeCmd("up", "-d")).
Step("wait returns when the fastest service exits",
ComposeCmd("wait", "faster"),
ServiceState("faster", "exited"),
ServiceState("infinity", "running"))
}
func TestWaitOnSlower(t *testing.T) {
const projectName = "e2e-wait-slower"
c := NewParallelCLI(t)
cleanup := func() {
c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "--timeout=0", "--remove-orphans")
}
t.Cleanup(cleanup)
cleanup()
c.RunDockerComposeCmd(t, "-f", "./fixtures/wait/compose.yaml", "--project-name", projectName, "up", "-d")
c.RunDockerComposeCmd(t, "--project-name", projectName, "wait", "slower")
NewScenario(t, "wait must block until the selected service exits, even if others exited first").
Compose(waitCompose).
Step("up starts all services",
ComposeCmd("up", "-d")).
Step("wait returns when the slower service exits",
ComposeCmd("wait", "slower"),
ServiceState("faster", "exited"),
ServiceState("slower", "exited"),
ServiceState("infinity", "running"))
}
func TestWaitOnInfinity(t *testing.T) {
@@ -89,18 +96,13 @@ func TestWaitOnInfinity(t *testing.T) {
}
func TestWaitAndDrop(t *testing.T) {
const projectName = "e2e-wait-and-drop"
c := NewParallelCLI(t)
cleanup := func() {
c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "--timeout=0", "--remove-orphans")
}
t.Cleanup(cleanup)
cleanup()
c.RunDockerComposeCmd(t, "-f", "./fixtures/wait/compose.yaml", "--project-name", projectName, "up", "-d")
c.RunDockerComposeCmd(t, "--project-name", projectName, "wait", "--down-project", "faster")
res := c.RunDockerCmd(t, "ps", "--all")
assert.Assert(t, !strings.Contains(res.Combined(), projectName), res.Combined())
NewScenario(t, "wait --down-project must take the whole project down once the selected service exits").
Compose(waitCompose).
Step("up starts all services",
ComposeCmd("up", "-d")).
Step("wait --down-project removes every container when the service exits",
ComposeCmd("wait", "--down-project", "faster"),
ServiceNotCreated("faster"),
ServiceNotCreated("slower"),
ServiceNotCreated("infinity"))
}