diff --git a/pkg/e2e/checks.go b/pkg/e2e/checks.go index 12b6fbda6..68898b976 100644 --- a/pkg/e2e/checks.go +++ b/pkg/e2e/checks.go @@ -24,6 +24,7 @@ package e2e import ( "fmt" + "os" "runtime" "slices" "strings" @@ -376,6 +377,39 @@ func OneOffsRemoved(service string) Check { } } +// ImageExists expects an image with the given reference to be present in the +// local store. +func ImageExists(ref string) Check { + return Check{ + name: fmt.Sprintf("image %q exists", ref), + fn: func(ctx *CheckContext) error { + res := icmd.RunCmd(ctx.scenario.cli.NewDockerCmd(ctx.scenario.t, "image", "inspect", "--format", "{{.Id}}", ref)) + if res.ExitCode != 0 { + return fmt.Errorf("not found: %s", strings.TrimSpace(res.Combined())) + } + return nil + }, + } +} + +// FileExists expects a non-empty file at the given host path, e.g. the output +// of an export command. +func FileExists(path string) Check { + return Check{ + name: fmt.Sprintf("file %q exists", path), + fn: func(ctx *CheckContext) error { + info, err := os.Stat(path) + if err != nil { + return err + } + if info.Size() == 0 { + return fmt.Errorf("file is empty") + } + return nil + }, + } +} + // LabelSet expects every container of the service to carry a non-empty label. func LabelSet(service, key string) Check { return Check{ diff --git a/pkg/e2e/commit_test.go b/pkg/e2e/commit_test.go index 0daf13054..7298b6e79 100644 --- a/pkg/e2e/commit_test.go +++ b/pkg/e2e/commit_test.go @@ -21,73 +21,34 @@ import ( ) func TestCommit(t *testing.T) { - const projectName = "e2e-commit-service" - 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/commit/compose.yaml", "--project-name", projectName, "up", "-d", "service") - - c.RunDockerComposeCmd( - t, - "--project-name", - projectName, - "commit", - "-a", - "John Hannibal Smith ", - "-c", - "ENV DEBUG=true", - "-m", - "sample commit", - "service", - "service:latest", - ) + s := NewScenario(t, "commit must produce an image from the service's container") + image := s.Project() + ":latest" + s.Defer(DockerCmd("image", "rm", "-f", image).MayFail()). + Step("up starts the service", + ComposeCmd("up", "-d", "service"), + ServiceState("service", "running")). + Step("commit turns the container into a tagged image", + ComposeCmd("commit", + "-a", "John Hannibal Smith ", + "-c", "ENV DEBUG=true", + "-m", "sample commit", + "service", image), + ImageExists(image)) } func TestCommitWithReplicas(t *testing.T) { - const projectName = "e2e-commit-service-with-replicas" - 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/commit/compose.yaml", "--project-name", projectName, "up", "-d", "service-with-replicas") - - c.RunDockerComposeCmd( - t, - "--project-name", - projectName, - "commit", - "-a", - "John Hannibal Smith ", - "-c", - "ENV DEBUG=true", - "-m", - "sample commit", - "--index=1", - "service-with-replicas", - "service-with-replicas:1", - ) - c.RunDockerComposeCmd( - t, - "--project-name", - projectName, - "commit", - "-a", - "John Hannibal Smith ", - "-c", - "ENV DEBUG=true", - "-m", - "sample commit", - "--index=2", - "service-with-replicas", - "service-with-replicas:2", - ) + s := NewScenario(t, "commit --index must pick the requested replica of a scaled service") + image1, image2 := s.Project()+":1", s.Project()+":2" + s.Defer( + DockerCmd("image", "rm", "-f", image1).MayFail(), + DockerCmd("image", "rm", "-f", image2).MayFail()). + Step("up starts the replicas", + ComposeCmd("up", "-d", "service-with-replicas"), + ServiceScale("service-with-replicas", 3)). + Step("commit --index=1 images the first replica", + ComposeCmd("commit", "-m", "sample commit", "--index=1", "service-with-replicas", image1), + ImageExists(image1)). + Step("commit --index=2 images the second replica", + ComposeCmd("commit", "-m", "sample commit", "--index=2", "service-with-replicas", image2), + ImageExists(image2)) } diff --git a/pkg/e2e/configs_test.go b/pkg/e2e/configs_test.go index c7d86f1c7..899c5168f 100644 --- a/pkg/e2e/configs_test.go +++ b/pkg/e2e/configs_test.go @@ -18,40 +18,21 @@ package e2e import ( "testing" - - "gotest.tools/v3/icmd" ) func TestConfigFromEnv(t *testing.T) { - c := NewParallelCLI(t) - defer c.cleanupWithDown(t, "configs") - - t.Run("config from file", func(t *testing.T) { - res := icmd.RunCmd(c.NewDockerComposeCmd(t, "-f", "./fixtures/configs/compose.yaml", "run", "from_file")) - res.Assert(t, icmd.Expected{Out: "This is my config file"}) - }) - - t.Run("config from env", func(t *testing.T) { - res := icmd.RunCmd(c.NewDockerComposeCmd(t, "-f", "./fixtures/configs/compose.yaml", "run", "from_env"), - func(cmd *icmd.Cmd) { - cmd.Env = append(cmd.Env, "CONFIG=config") - }) - res.Assert(t, icmd.Expected{Out: "config"}) - }) - - t.Run("config inlined", func(t *testing.T) { - res := icmd.RunCmd(c.NewDockerComposeCmd(t, "-f", "./fixtures/configs/compose.yaml", "run", "inlined"), - func(cmd *icmd.Cmd) { - cmd.Env = append(cmd.Env, "CONFIG=config") - }) - res.Assert(t, icmd.Expected{Out: "This is my config"}) - }) - - t.Run("custom target", func(t *testing.T) { - res := icmd.RunCmd(c.NewDockerComposeCmd(t, "-f", "./fixtures/configs/compose.yaml", "run", "target"), - func(cmd *icmd.Cmd) { - cmd.Env = append(cmd.Env, "CONFIG=config") - }) - res.Assert(t, icmd.Expected{Out: "This is my config"}) - }) + NewScenario(t, "each config source — file, environment, inline content — must mount with the right content"). + Env("CONFIG=config"). + Step("a file-sourced config mounts its file's content", + ComposeCmd("run", "from_file"), + OutputContains("This is my config file")). + Step("an environment-sourced config mounts the variable's value", + ComposeCmd("run", "from_env"), + OutputContains("config")). + Step("an inline config mounts its interpolated content", + ComposeCmd("run", "inlined"), + OutputContains("This is my config")). + Step("a custom target mounts the config at the requested path", + ComposeCmd("run", "target"), + OutputContains("This is my config")) } diff --git a/pkg/e2e/export_test.go b/pkg/e2e/export_test.go index baa0dc5b9..5cceb693a 100644 --- a/pkg/e2e/export_test.go +++ b/pkg/e2e/export_test.go @@ -17,34 +17,32 @@ package e2e import ( + "path/filepath" "testing" ) func TestExport(t *testing.T) { - const projectName = "e2e-export-service" - 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/export/compose.yaml", "--project-name", projectName, "up", "-d", "service") - c.RunDockerComposeCmd(t, "--project-name", projectName, "export", "-o", "service.tar", "service") + out := filepath.Join(t.TempDir(), "service.tar") + NewScenario(t, "export must write the service container's filesystem as a tar archive"). + Step("up starts the service", + ComposeCmd("up", "-d", "service"), + ServiceState("service", "running")). + Step("export writes the container filesystem to the output file", + ComposeCmd("export", "-o", out, "service"), + FileExists(out)) } func TestExportWithReplicas(t *testing.T) { - const projectName = "e2e-export-service-with-replicas" - 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/export/compose.yaml", "--project-name", projectName, "up", "-d", "service-with-replicas") - c.RunDockerComposeCmd(t, "--project-name", projectName, "export", "-o", "r1.tar", "--index=1", "service-with-replicas") - c.RunDockerComposeCmd(t, "--project-name", projectName, "export", "-o", "r2.tar", "--index=2", "service-with-replicas") + dir := t.TempDir() + r1, r2 := filepath.Join(dir, "r1.tar"), filepath.Join(dir, "r2.tar") + NewScenario(t, "export --index must pick the requested replica of a scaled service"). + Step("up starts the replicas", + ComposeCmd("up", "-d", "service-with-replicas"), + ServiceScale("service-with-replicas", 3)). + Step("export --index=1 writes the first replica's filesystem", + ComposeCmd("export", "-o", r1, "--index=1", "service-with-replicas"), + FileExists(r1)). + Step("export --index=2 writes the second replica's filesystem", + ComposeCmd("export", "-o", r2, "--index=2", "service-with-replicas"), + FileExists(r2)) } diff --git a/pkg/e2e/logs_test.go b/pkg/e2e/logs_test.go index 3706f1364..afc2e0890 100644 --- a/pkg/e2e/logs_test.go +++ b/pkg/e2e/logs_test.go @@ -31,46 +31,26 @@ import ( ) func TestLocalComposeLogs(t *testing.T) { - c := NewParallelCLI(t) - - const projectName = "compose-e2e-logs" - - t.Run("up", func(t *testing.T) { - c.RunDockerComposeCmd(t, "-f", "./fixtures/logs-test/compose.yaml", "--project-name", projectName, "up", "-d") - }) - - t.Run("logs", func(t *testing.T) { - res := c.RunDockerComposeCmd(t, "--project-name", projectName, "logs") - res.Assert(t, icmd.Expected{Out: `PING localhost`}) - res.Assert(t, icmd.Expected{Out: `hello`}) - }) - - t.Run("logs ping", func(t *testing.T) { - res := c.RunDockerComposeCmd(t, "--project-name", projectName, "logs", "ping") - res.Assert(t, icmd.Expected{Out: `PING localhost`}) - assert.Assert(t, !strings.Contains(res.Stdout(), "hello")) - }) - - t.Run("logs hello", func(t *testing.T) { - res := c.RunDockerComposeCmd(t, "--project-name", projectName, "logs", "hello", "ping") - res.Assert(t, icmd.Expected{Out: `PING localhost`}) - res.Assert(t, icmd.Expected{Out: `hello`}) - }) - - t.Run("logs hello index", func(t *testing.T) { - res := c.RunDockerComposeCmd(t, "--project-name", projectName, "logs", "--index", "2", "hello") - - // docker-compose logs hello - // logs-test-hello-2 | hello - // logs-test-hello-1 | hello - t.Log(res.Stdout()) - assert.Assert(t, !strings.Contains(res.Stdout(), "hello-1")) - assert.Assert(t, strings.Contains(res.Stdout(), "hello-2")) - }) - - t.Run("down", func(t *testing.T) { - _ = c.RunDockerComposeCmd(t, "--project-name", projectName, "down") - }) + NewScenario(t, "logs must aggregate the requested services' output, and --index must select one replica"). + Step("up starts the services", + ComposeCmd("up", "-d"), + Eventually(ServiceState("hello", "exited"), 10*time.Second)). + Step("logs aggregates every service's output", + ComposeCmd("logs"), + OutputContains("PING localhost"), + OutputContains("hello")). + Step("logs on one service filters the others out", + ComposeCmd("logs", "ping"), + OutputContains("PING localhost"), + OutputNotContains("hello")). + Step("logs accepts several services", + ComposeCmd("logs", "hello", "ping"), + OutputContains("PING localhost"), + OutputContains("hello")). + Step("logs --index selects a single replica", + ComposeCmd("logs", "--index", "2", "hello"), + OutputContains("hello-2"), + OutputNotContains("hello-1")) } func TestLocalComposeLogsFollow(t *testing.T) { diff --git a/pkg/e2e/fixtures/commit/compose.yaml b/pkg/e2e/testdata/TestCommit/compose.yaml similarity index 89% rename from pkg/e2e/fixtures/commit/compose.yaml rename to pkg/e2e/testdata/TestCommit/compose.yaml index 28e4b15bd..6ab3d9093 100644 --- a/pkg/e2e/fixtures/commit/compose.yaml +++ b/pkg/e2e/testdata/TestCommit/compose.yaml @@ -6,4 +6,4 @@ services: image: alpine command: sleep infinity deploy: - replicas: 3 \ No newline at end of file + replicas: 3 diff --git a/pkg/e2e/fixtures/export/compose.yaml b/pkg/e2e/testdata/TestCommitWithReplicas/compose.yaml similarity index 89% rename from pkg/e2e/fixtures/export/compose.yaml rename to pkg/e2e/testdata/TestCommitWithReplicas/compose.yaml index 28e4b15bd..6ab3d9093 100644 --- a/pkg/e2e/fixtures/export/compose.yaml +++ b/pkg/e2e/testdata/TestCommitWithReplicas/compose.yaml @@ -6,4 +6,4 @@ services: image: alpine command: sleep infinity deploy: - replicas: 3 \ No newline at end of file + replicas: 3 diff --git a/pkg/e2e/fixtures/configs/compose.yaml b/pkg/e2e/testdata/TestConfigFromEnv/compose.yaml similarity index 100% rename from pkg/e2e/fixtures/configs/compose.yaml rename to pkg/e2e/testdata/TestConfigFromEnv/compose.yaml diff --git a/pkg/e2e/fixtures/configs/config.txt b/pkg/e2e/testdata/TestConfigFromEnv/config.txt similarity index 95% rename from pkg/e2e/fixtures/configs/config.txt rename to pkg/e2e/testdata/TestConfigFromEnv/config.txt index 58b9a4061..4e8fc5908 100644 --- a/pkg/e2e/fixtures/configs/config.txt +++ b/pkg/e2e/testdata/TestConfigFromEnv/config.txt @@ -1 +1,2 @@ This is my config file + diff --git a/pkg/e2e/testdata/TestExport/compose.yaml b/pkg/e2e/testdata/TestExport/compose.yaml new file mode 100644 index 000000000..6ab3d9093 --- /dev/null +++ b/pkg/e2e/testdata/TestExport/compose.yaml @@ -0,0 +1,9 @@ +services: + service: + image: alpine + command: sleep infinity + service-with-replicas: + image: alpine + command: sleep infinity + deploy: + replicas: 3 diff --git a/pkg/e2e/testdata/TestExportWithReplicas/compose.yaml b/pkg/e2e/testdata/TestExportWithReplicas/compose.yaml new file mode 100644 index 000000000..6ab3d9093 --- /dev/null +++ b/pkg/e2e/testdata/TestExportWithReplicas/compose.yaml @@ -0,0 +1,9 @@ +services: + service: + image: alpine + command: sleep infinity + service-with-replicas: + image: alpine + command: sleep infinity + deploy: + replicas: 3 diff --git a/pkg/e2e/testdata/TestLocalComposeLogs/compose.yaml b/pkg/e2e/testdata/TestLocalComposeLogs/compose.yaml new file mode 100644 index 000000000..0375a7446 --- /dev/null +++ b/pkg/e2e/testdata/TestLocalComposeLogs/compose.yaml @@ -0,0 +1,10 @@ +services: + ping: + image: alpine + init: true + command: ping localhost -c 1 + hello: + image: alpine + command: echo hello + deploy: + replicas: 2