Add three more compose-file sanity checks

- depends_on cycles (not just direct self-reference): a three-color DFS
  over the dependency graph reports the actual cycle path.
- Duplicate host-port/protocol across (or within) services: two services
  both publishing the same (host_port, protocol) would only ever leave one
  reachable, even though both DNAT rules would get added later.
- container_name colliding with another service's own implicit name, not
  just two explicit container_names matching each other.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz
This commit is contained in:
2026-09-07 09:31:10 +00:00
parent a6130aa69d
commit 410faff79c
2 changed files with 244 additions and 4 deletions
+97
View File
@@ -110,6 +110,19 @@ TEST_CASE("compose file: container_name -- optional, and duplicates across servi
" image: busybox:latest\n"
" container_name: shared\n");
CHECK_FALSE(load_compose_file(colliding).has_value());
// A service's own explicit container_name colliding with a *different*
// service's implicit identity (its own name, since it has no
// container_name of its own) -- also an error, not just two explicit
// container_names matching each other.
auto colliding_with_name = write_compose(scratch.path(),
"services:\n"
" web:\n"
" image: busybox:latest\n"
" worker:\n"
" image: busybox:latest\n"
" container_name: web\n");
CHECK_FALSE(load_compose_file(colliding_with_name).has_value());
}
TEST_CASE("compose file: command -- list form used as-is, scalar form wrapped in sh -c", "[unit]") {
@@ -243,6 +256,53 @@ TEST_CASE("compose file: depends_on -- long and short forms, condition handling,
CHECK_FALSE(load_compose_file(undeclared).has_value());
}
TEST_CASE("compose file: depends_on cycles are rejected, acyclic graphs are not", "[unit]") {
ScratchXdgDirs scratch;
// A 2-service direct cycle (a -> b -> a) -- distinct from the
// already-tested direct self-reference (a -> a).
auto two_cycle = write_compose(scratch.path(),
"services:\n"
" a:\n"
" image: busybox:latest\n"
" depends_on: [\"b\"]\n"
" b:\n"
" image: busybox:latest\n"
" depends_on: [\"a\"]\n");
CHECK_FALSE(load_compose_file(two_cycle).has_value());
// A longer, 3-service cycle (a -> b -> c -> a).
auto three_cycle = write_compose(scratch.path(),
"services:\n"
" a:\n"
" image: busybox:latest\n"
" depends_on: [\"b\"]\n"
" b:\n"
" image: busybox:latest\n"
" depends_on: [\"c\"]\n"
" c:\n"
" image: busybox:latest\n"
" depends_on: [\"a\"]\n");
CHECK_FALSE(load_compose_file(three_cycle).has_value());
// A valid, acyclic multi-service graph (a diamond: d depends on both b
// and c, both of which depend on a) must still load successfully.
auto acyclic = write_compose(scratch.path(),
"services:\n"
" a:\n"
" image: busybox:latest\n"
" b:\n"
" image: busybox:latest\n"
" depends_on: [\"a\"]\n"
" c:\n"
" image: busybox:latest\n"
" depends_on: [\"a\"]\n"
" d:\n"
" image: busybox:latest\n"
" depends_on: [\"b\", \"c\"]\n");
CHECK(load_compose_file(acyclic).has_value());
}
TEST_CASE("compose file: stop_grace_period parses durations, rejects malformed ones", "[unit]") {
ScratchXdgDirs scratch;
@@ -386,6 +446,43 @@ TEST_CASE("compose file: ports -- list and scalar forms reuse parse_port_forward
CHECK_FALSE(load_compose_file(bad).has_value());
}
TEST_CASE("compose file: duplicate host-port/protocol across (or within) services is an error", "[unit]") {
ScratchXdgDirs scratch;
auto across_services = write_compose(scratch.path(),
"services:\n"
" web:\n"
" image: busybox:latest\n"
" ports:\n"
" - \"8080:80\"\n"
" web2:\n"
" image: busybox:latest\n"
" ports:\n"
" - \"8080:81\"\n");
CHECK_FALSE(load_compose_file(across_services).has_value());
auto within_one_service = write_compose(scratch.path(),
"services:\n"
" web:\n"
" image: busybox:latest\n"
" ports:\n"
" - \"8080:80\"\n"
" - \"8080:81\"\n");
CHECK_FALSE(load_compose_file(within_one_service).has_value());
// Same host port, different protocols -- must still succeed, matching
// port_forward.h's own existing "same port pair once per protocol"
// precedent (e.g. a DNS-like service forwarding both tcp and udp).
auto different_protocols = write_compose(scratch.path(),
"services:\n"
" web:\n"
" image: busybox:latest\n"
" ports:\n"
" - \"53:53/tcp\"\n"
" - \"53:53/udp\"\n");
CHECK(load_compose_file(different_protocols).has_value());
}
TEST_CASE("compose file: service volumes -- bind mounts (absolute-resolved, ro), named volume references",
"[unit]") {
ScratchXdgDirs scratch;