Skip to content

test(e2e): migration to the Scenario DSL - #14054

Closed
ndeloof wants to merge 21 commits into
e2e-scenario-dslfrom
e2e-scenario-migration
Closed

test(e2e): migration to the Scenario DSL#14054
ndeloof wants to merge 21 commits into
e2e-scenario-dslfrom
e2e-scenario-migration

Conversation

@ndeloof

@ndeloofndeloof commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Continues the migration started in #14047 (stacked on it): the bulk of the e2e suite moves to the declarative Scenario DSL — pause, no-deps, start/stop, orphans, expose, container_name, ipc, wait, cascade, profiles, healthcheck, recreate, scale, secrets, env_file, configs, commit, export, logs, pull, networks, exec, hooks, cp, environment, config, publish checks, run, up, volumes, build-once, providers, the 'Built' build family, image identity/images and model. Compose models are inlined (txtar for multi-file projects) and ~45 fixture directories/files are removed.

The check vocabulary grows accordingly (all in checks.go): ExitCode, ServiceNotCreated, one-off awareness (OneOffState, OneOffsUntouched, OneOffsRemoved — service checks now ignore run containers), Recreated, ServiceHealthy, ServiceScale, ReplicaNumbers, ImageExists, FileExists, FileContains, StdoutContains, StderrContains, OutputMatches, OutputMatchesCount. Action modifiers: Within (timeout), WithStdin (prompt answers); Scenario.Dir() exposes the project directory for host-file interactions.

Migration surfaced a few things reviewers should know:

  • TestPauseServiceDoesNotExist now locks the current behavior (unknown service rejected), resolving the legacy TODO.
  • TestRemoveOrphans states its real intent: the removal came from the fixture's .env declaring COMPOSE_REMOVE_ORPHANS=true, invisible in the legacy test.
  • TestScaleDownNoRecreate's final assertion now actually runs (the legacy test built the command without executing it).
  • TestNetworkConfigChanged locks that a subnet change replaces the network but reconnects the same container.
  • commit/export now verify the produced image/archive exists instead of only checking exit 0; several fixed image/container/volume names become project-scoped, removing parallel-run collisions.

Kept legacy by design: tests driving long-running/background processes (watch, logs -f, cancel/SIGINT, attach-restart, wait infinity), format-verification tests (ps, config --hash cross-check), invocation-without-f tests (COMPOSE_FILE-from-.env family), and infra-heavy tests (registry publish, buildx builders, dind TLS, sentences fixture, bridge tree diffs).

🤖 Generated with Claude Code

@ndeloofndeloof changed the title test(e2e): migrate pause, no-deps, start/stop and orphans tests to the Scenario DSLtest(e2e): migration to the Scenario DSLAug 17, 2026
@ndeloof
ndeloofforce-pushed the e2e-scenario-migration branch 2 times, most recently from d909437 to 9e24b58CompareAugust 17, 2026 07:01
@glours

Copy link
Copy Markdown
Contributor

Why not using embed directives and keep Compose and Dockerfile fixtures in their own files?

@ndeloof
ndeloof marked this pull request as ready for review August 18, 2026 09:34
@ndeloof
ndeloof requested review from a team as code ownersAugust 18, 2026 09:34
@ndeloof
ndeloof requested a review from gloursAugust 18, 2026 09:34
@ndeloof

Copy link
Copy Markdown
ContributorAuthor

Why not using embed directives and keep Compose and Dockerfile fixtures in their own files?

The intent is to get the whole test scenario in a single file, so a human get all the input visible at once.
Also, using fixtures for our e2e tests, we used to share them between tests making them harder to express the intended scenario

…cks error paths
Migrate the four pause tests to the Scenario DSL: the invariant is the
container state (paused/running), not an HTTP timeout, so the scenarios
observe state and drop the port-mapping helpers and the pause fixture.
ExitCode joins the check vocabulary for error-path steps, paired with
MayFail. TestPauseServiceDoesNotExist now locks the current behavior —
pause of an unknown service is rejected with 'no such service' — which
resolves the TODO the legacy test carried: it asserted exit 0 while
wishing for an error, and the CLI has since started validating selected
services.
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
…bservable
Migrate the volumes_from and network_mode:service donor tests to the
Scenario DSL, inlining the compose model and dropping the no-deps
fixtures. Each scenario removes the donor container behind compose's
back and expects up --no-deps to be rejected with the 'container
missing' error — the CLI's reported decision being the observable.
ServiceNotCreated joins the check vocabulary: it asserts a service has
no container at all, here proving the donor really is gone before the
failing step.
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
Migrate the state-friendly start/stop tests to the Scenario DSL:
- TestStartStop: stop halts containers in place, start brings the same
ones back (NotRecreated); the ls checks match the project name exactly
in the JSON output, immune to the prefix collisions the other
start-stop project names would cause under parallel runs.
- TestStartStopWithDependencies: dependency start/stop propagation read
from container state instead of 'Container X Started' messages.
- TestUpNoDeps replaces the 'Up no-deps links' subtest as its own
scenario: up --no-deps must not create the linked dependency.
- TestStartSingleServiceAndDependency: create/start walk the dependency
chain and nothing else — now asserting the unrelated services have no
container at all, stronger than the legacy message matching.
The one-off and multi-file tests stay legacy: the vocabulary has no
one-off awareness yet and a scenario holds a single compose file.
The links and start-stop-deps fixtures become unused and are removed.
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
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>
…ORPHANS from .env
The legacy test read as 'up removes leftover one-offs', but plain up
only warns about them: the removal it asserted came from the fixture's
.env declaring COMPOSE_REMOVE_ORPHANS=true (added by 'restore
setEnvWithDotEnv', d3d378b) — an invariant the fixture kept invisible.
The scenario declares both files inline as a txtar archive, so the
mechanism under test is in plain sight, and observes the one-off's
removal from container state rather than ps output.
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
- 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 <nicolas.deloof@gmail.com>
…eate
Vocabulary grows with Recreated (the mirror of NotRecreated),
ServiceHealthy (healthcheck status observed from inspect) and a Within
action modifier bounding blocking commands like up --wait, replacing
the hand-rolled goroutine+timer of the legacy TestStartInterval.
- cascade: abort-on-container-exit/failure scenarios inline the model;
exit-code propagation (111) is locked with ExitCode. The one-off
variant stays legacy — it drives a long-running up in the background.
- profiles: the four scenarios observe which services materialize
instead of grepping ps output; the dotenv variant declares its .env
inline, making the activation mechanism visible.
- TestRecreateWithNoDeps now asserts both sides: the service is
replaced (Recreated) and the healthy dependency is left alone.
- Fixtures start_interval/, profiles/compose.yaml+test-profile.env and
recreate-no-deps.yaml are removed; cascade/ stays for the legacy
one-off test.
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
ServiceScale and ReplicaNumbers join the vocabulary: replica counts and
numbering read from container labels replace counting 'Started' lines
in the CLI output.
- TestScaleDownNoRecreate's final assertion now actually runs: the
legacy test built the scale-down command without executing it, so
'scale down removes obsolete replicas' was never exercised; the
scenario locks the real behavior (stale replicas dropped, up-to-date
ones kept, whatever their numbers).
- secrets scenarios inline the include-based project (compose.yaml,
child project and env_file as one txtar archive).
- TestRawEnvFile and TestRunEnvFile inline their env files;
TestUnusedMissingEnvFile stays legacy for its completion assertions,
which drive the plugin/standalone binaries directly.
- Fixtures scale/, env-secret/ and dotenv/raw are removed.
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
ImageExists and FileExists join the vocabulary, giving commit and
export a real observable: the legacy tests only asserted the commands
exited 0, the scenarios verify the committed image and the exported
archive actually exist. Both also drop their fixed image tags and
cwd-relative tar outputs for project-scoped tags and temp files, so
parallel runs cannot collide and nothing is left behind in the repo.
TestLocalComposeLogs waits for the echo service to exit before reading
logs (Eventually), closing the race the legacy test tolerated. The
follow and large-logs tests stay legacy: both drive a long-running
command in the background. ps_test.go stays legacy as well — it
verifies ps table/JSON formatting, which is inherently output-shaped.
Fixtures configs/, commit/ and export/ are removed.
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
- pull: the CLI's Pulled/Skipped report is the observable, so the five
scenarios (always-pull, present-locally, build-only, failure,
--ignore-pull-failures) legitimately match output; models are inline
and the whole compose-pull fixture tree — including the orphaned
duplicate-images directory no test referenced — is removed.
- networks: aliases, links, ipam, network_mode: service, subnet change,
mac_address and interface_name become scenarios. The subnet-change
scenario locks what actually happens: the network is replaced but the
container is reconnected, not recreated (NotRecreated), which the
legacy test never distinguished. TestNetworks (fixed host ports,
HTTP fixture services) and TestNetworkRecreate (OR-shaped output
assertion) stay legacy.
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
- exec: exit-code propagation, -e env forwarding and one-off targeting
become two scenarios with an inline model.
- hooks: the thirteen post_start/pre_stop/pre_start tests migrate. The
volume probe is a named action (probeVolume); wc -l counts are locked
by matching the exact '1 /mnt/tokens.txt' line instead of parsing.
The spec-change scenario replaces its two fixture files with one
model interpolating ${HOOK_VERSION}, so the changed spec — and the
Recreated expectation it implies — is visible in the test source.
pre_stop scenarios drop the fixture's fixed 'sample-data' volume
name, which parallel projects used to share.
Fixtures hooks/ and pre_start/ are removed.
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
- cp: the twelve copy directions become one scenario; Dir() joins the
Scenario API for actions that exchange files with the host, and
FileContains observes what landed there. The files copied in and out
live in the txtar archive instead of writing into the fixture tree.
- environment: TestEnvPriority splits into three scenarios, one per
compose model (environment section, bare, env_file section), each
declaring its .env variants inline; StdoutContains joins the
vocabulary so container output cannot collide with progress noise
(the unused framework predicate of the same name is removed). The
fixed 'env-compose-priority' image name becomes project-scoped.
- config: rendering assertions move to scenario steps; the config
output is the observable here by definition.
TestConfigHashMatchesContainerLabel stays legacy: it cross-references
hashes captured at runtime, which checks cannot hold.
- Fixtures cp-test/, environment/ and config/ are removed.
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
The dry-run publish checks split into nine scenarios, one per model
and refusal reason; the prompt text, refusal exit code (130) and
publication report are the observables. WithStdin joins the Action
modifiers to feed prompt answers. TestPublish stays legacy: it drives
a real registry container and loads oci:// projects with multiple -f
files. Only the oci/ fixture subtree it needs survives.
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
- compose run splits into seven scenarios (one-off basics, ports, deps,
optional deps, quiet pull, --pull always, chained build contexts).
The orphan-warning cases fold into the basic scenario: an exited
one-off IS the orphan the next run warns about, no second compose
file needed. Piped-stdin and stop-signal tests stay legacy.
- up/compose_up: unhealthy service, dependency exit (both now also
locking that dependents stay in created state), build dependencies
(project-scoped image name instead of the fixture's global one),
optional dependency, --all-resources, profile targeting (dropping the
fixture's global container_names), image-by-ID, exit-code-from (42
and 143), port ranges, stdout/stderr separation (StderrContains joins
the vocabulary) and logging-driver reconfiguration (now locking the
Recreated expectation). The Ctrl-C dependency test stays legacy.
- Fixtures start-fail/, resources/, stop/, port-range/, stdout-stderr/,
logging-driver/, profiles/ and most of dependencies/ and run-test/
are removed.
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
- volumes: external-volume switching, volume recreation on definition
change and image volumes become scenarios; the two-compose-file
fixtures collapse into one model with an interpolated volume name or
label, so the changed definition is visible in the test. The rebuild
scenario locks the Recreated expectation and scopes the source image
to the project. TestLocalComposeVolume (fixed host port, HTTP) and
TestProjectVolumeBind (host file written mid-test) stay legacy.
- build-once: the three run --build regression cases each get a
scenario; OutputMatchesCount joins the vocabulary to count the build
reports, replacing the hand-rolled regex counter, and image names are
project-scoped instead of random suffixes.
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
- TestExec locks the container-selection order (service container over
one-off) with one-off state observed at each step.
- providers: a shared providerScenario helper wires the
example-provider binary into PATH; the env assertions anchor on the
'test-1 | ' log prefix, replacing the hand-rolled env parser, and
the stop-hook sentinel file is observed with FileExists.
- Fixtures exec/ and providers/ are removed.
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
…han scoping
- TestRm, TestRemoveOrphaned and TestUnnecessaryResources observe which
containers exist instead of grepping docker ps.
- TestCompatibility drops the heavyweight sentences fixture: underscore
naming shows on any model.
- TestConfig/TestConfigInterpolate build their expected rendering from
the scenario's own project name and directory; OutputMatches joins
the vocabulary for TestInitContainer's ordering assertion.
- TestStopWithDependenciesAttached inlines its model.
- Kept legacy: TestLocalComposeUp (fixed port, HTTP, shared sentences
fixture), TestDownComposefileInParentFolder (project name inferred
from a fixture-relative directory), TestAttachRestart (long-running
attached up), the COMPOSE_FILE-from-.env family and TestNestedDotEnv
(they exercise invocation without -f, which ComposeCmd always sets).
- Fixtures init-container/, external/ and dependencies/ are removed.
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
TestBuildDependsOn, Subset, DependentImage, SubDependencies,
LongOutputLine, DependentImageWithProfile, Escaped, Tags and Secrets
become scenarios: models inline (the long-line Dockerfile generated
with strings.Repeat instead of 34 copy-pasted lines), image names
project-scoped with Defer cleanup, and the secrets build asserts
ImageExists on top of the Dockerfile's own diff checks.
Kept legacy: TestLocalComposeBuild (DOCKER_BUILDKIT=0/1 matrix over a
shared fixture with fixed names and host port), the buildx-builder
family (SSH, platforms, privileged, entitlements, builder, TLS) which
provisions builders and dind daemons, and TestBuildImageDependencies
(classic/bake matrix). Their fixtures under build-test/ stay; the
single-purpose subdirectories are removed.
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
- TestUpIdempotentContainerdStore joins the scenario style of its
corner-case siblings: Serial + ContainerdImageStore requirement, the
regression story in a comment, NotRecreated as the invariant. The
requireContainerdStore helper and the last identity fixture go away.
- TestImagesAfterImageRemoved scopes its throwaway tag to the project
and observes container state before pulling the image record away.
- TestComposeModel inlines its model; verified in CI where the
docker-model plugin is installed (the local runner cannot pull the
model, as with the legacy test).
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
The plugin/stable/containerd CI job caught a race the DSL had: hook
helper containers carry the project and service labels but no replica
number, and can be destroyed between the snapshot's ps listing and its
batch inspect — which then failed and blanked the entire snapshot,
making ServiceState report 'service has no container' while the
container was right there. Containers are now inspected one by one,
tolerating disappearances, and helper containers (neither numbered as
a replica nor one-off) are excluded from snapshots altogether so they
can't transiently pollute service checks either.
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
…it 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 <nicolas.deloof@gmail.com>
@ndeloof

Copy link
Copy Markdown
ContributorAuthor

Superseded by #14087, which includes this migration converted to the testdata// layout.

@ndeloofndeloof closed this Aug 18, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@ndeloof@glours