mirror of
https://github.com/docker/compose.git
synced 2026-10-12 02:13:08 +00:00
test(e2e): Files() declares multi-file projects as a txtar archive
A scenario could only declare a lone compose.yaml; anything needing a Dockerfile, an env file or a config file fell back to fixture directories, breaking the "self-contained in the test source" rule. Files() takes the project files as a txtar archive (golang.org/x/tools/txtar, already in the module graph): each file introduced by a `-- name --` line, extracted into the project directory. txtar is the format Go's own cmd/go tests are written in — diff-friendly, and one every human and coding agent already knows by heart, which is the point: e2e scenarios are the main reading surface between the two. TestUpBuildUnchangedContext demonstrates the need — compose.yaml + Dockerfile + a copied file — and locks a real invariant: an unchanged `up --build` hits the build cache and must not recreate the service. Project() is exposed so a scenario can Defer removal of the image it built. Co-Authored-By: Claude Fable 5 <[email protected]> Signed-off-by: Nicolas De Loof <[email protected]>
This commit is contained in:
co-authored by
Claude Fable 5
parent
a9c0af4bab
commit
0ac224d9d9
@@ -55,6 +55,7 @@ require (
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6
|
||||
golang.org/x/sync v0.22.0
|
||||
golang.org/x/sys v0.47.0
|
||||
golang.org/x/tools v0.47.0
|
||||
google.golang.org/grpc v1.83.0
|
||||
gotest.tools/v3 v3.5.2
|
||||
tags.cncf.io/container-device-interface v1.1.0
|
||||
|
||||
+17
-1
@@ -42,7 +42,23 @@ Rules:
|
||||
create is a no-op", not "run create again". The transcript of step names
|
||||
should read as the specification.
|
||||
- **The compose model is inline.** A scenario is self-contained in the test
|
||||
source; no fixture directories. Interpolate runtime values via `Env`.
|
||||
source; no fixture directories. Interpolate runtime values via `Env`. When
|
||||
the project needs more than a `compose.yaml` (a Dockerfile, an env file, a
|
||||
config file), declare all files with `Files` as a [txtar](https://pkg.go.dev/golang.org/x/tools/txtar)
|
||||
archive — the format Go's own `cmd/go` tests are written in: diff-friendly,
|
||||
and one every human and coding agent already knows by heart:
|
||||
|
||||
```go
|
||||
s.Files(`
|
||||
-- compose.yaml --
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
-- Dockerfile --
|
||||
FROM alpine
|
||||
CMD ["sleep", "infinity"]
|
||||
`)
|
||||
```
|
||||
- **Regression tests link the issue** in a comment above the test, with a
|
||||
sentence on the failure mode being locked.
|
||||
|
||||
|
||||
@@ -673,3 +673,30 @@ func TestBuildEscaped(t *testing.T) {
|
||||
res = c.RunDockerComposeCmd(t, "--project-directory", "./fixtures/build-test/escaped", "build", "--no-cache", "arg")
|
||||
res.Assert(t, icmd.Success)
|
||||
}
|
||||
|
||||
// TestUpBuildUnchangedContext locks the invariant that rebuilding an
|
||||
// unchanged build context hits the build cache and leaves the running
|
||||
// service alone: same image, same config hash, same container.
|
||||
func TestUpBuildUnchangedContext(t *testing.T) {
|
||||
s := NewScenario(t, "an unchanged up --build must hit the build cache and not recreate the service")
|
||||
s.Files(`
|
||||
-- compose.yaml --
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
-- Dockerfile --
|
||||
FROM alpine
|
||||
COPY marker /marker
|
||||
CMD ["sleep", "infinity"]
|
||||
-- marker --
|
||||
v1
|
||||
`).
|
||||
Defer(DockerCmd("image", "rm", "-f", s.Project()+"-app")).
|
||||
Step("up builds the image and starts the service",
|
||||
ComposeCmd("up", "-d", "--build"),
|
||||
ServiceState("app", "running")).
|
||||
Step("an unchanged up --build reuses the cached image and the container",
|
||||
ComposeCmd("up", "-d", "--build"),
|
||||
NotRecreated("app"),
|
||||
LabelUnchanged("app", "com.docker.compose.config-hash"))
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"golang.org/x/tools/txtar"
|
||||
"gotest.tools/v3/icmd"
|
||||
)
|
||||
|
||||
@@ -147,6 +148,10 @@ func projectNameFor(testName string) string {
|
||||
// declarative layer doesn't cover.
|
||||
func (s *Scenario) CLI() *CLI { return s.cli }
|
||||
|
||||
// Project returns the compose project name the scenario runs under, e.g. to
|
||||
// Defer the removal of an image the project built.
|
||||
func (s *Scenario) Project() string { return s.project }
|
||||
|
||||
// Compose declares the project's compose model, written to a temporary
|
||||
// directory so the whole scenario is self-contained in the test source.
|
||||
func (s *Scenario) Compose(yaml string) *Scenario {
|
||||
@@ -159,6 +164,33 @@ func (s *Scenario) Compose(yaml string) *Scenario {
|
||||
return s
|
||||
}
|
||||
|
||||
// Files declares the project's files — compose.yaml plus whatever it needs
|
||||
// (Dockerfile, .env, config files) — as a txtar archive: each file introduced
|
||||
// by a `-- name --` line, extracted into the project directory. The archive
|
||||
// must contain a compose.yaml, which becomes the scenario's compose file.
|
||||
// txtar is the format Go's own cmd/go tests are written in: diff-friendly,
|
||||
// and trivial to read and write for humans and coding agents alike.
|
||||
func (s *Scenario) Files(archive string) *Scenario {
|
||||
s.t.Helper()
|
||||
dir := s.t.TempDir()
|
||||
for _, f := range txtar.Parse([]byte(archive)).Files {
|
||||
path := filepath.Join(dir, f.Name)
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
s.t.Fatalf("failed to create directory for %s: %v", f.Name, err)
|
||||
}
|
||||
if err := os.WriteFile(path, f.Data, 0o644); err != nil {
|
||||
s.t.Fatalf("failed to write %s: %v", f.Name, err)
|
||||
}
|
||||
if f.Name == "compose.yaml" {
|
||||
s.file = path
|
||||
}
|
||||
}
|
||||
if s.file == "" {
|
||||
s.t.Fatal("Files archive must contain a compose.yaml")
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Env sets environment variables applied to every subsequent step command
|
||||
// (and interpolated in the compose model).
|
||||
func (s *Scenario) Env(kv ...string) *Scenario {
|
||||
|
||||
Reference in New Issue
Block a user