Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 26 additions & 29 deletions pkg/e2e/compose_run_build_once_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,54 +20,45 @@ import (
"crypto/rand"
"encoding/hex"
"fmt"
"regexp"
"strings"
"testing"

"gotest.tools/v3/assert"
"gotest.tools/v3/icmd"
)

// TestRunBuildOnce tests that services with pull_policy: build are only built once
// when using 'docker compose run', even when they are dependencies.
// This addresses a bug where dependencies were built twice: once in startDependencies
// and once in ensureImagesExists.
func TestRunBuildOnce(t *testing.T) {
c := NewCLI(t)
c := NewParallelCLI(t)

t.Run("dependency with pull_policy build is built only once", func(t *testing.T) {
projectName := randomProjectName("build-once")
res := c.RunDockerComposeCmd(t, "-p", projectName, "-f", "./fixtures/run-test/build-once.yaml", "down", "--rmi", "local", "--remove-orphans")
res.Assert(t, icmd.Success)
_ = c.RunDockerComposeCmd(t, "-p", projectName, "-f", "./fixtures/run-test/build-once.yaml", "down", "--rmi", "local", "--remove-orphans", "-v")
res := c.RunDockerComposeCmd(t, "-p", projectName, "-f", "./fixtures/run-test/build-once.yaml", "--verbose", "run", "--build", "--rm", "curl")

res = c.RunDockerComposeCmd(t, "-p", projectName, "-f", "./fixtures/run-test/build-once.yaml", "run", "--build", "--rm", "curl")
res.Assert(t, icmd.Success)
output := res.Stdout()

// Count how many times nginx was built by looking for its unique RUN command output
nginxBuilds := strings.Count(res.Combined(), "Building nginx at")
nginxBuilds := countServiceBuilds(output, projectName, "nginx")

// nginx should build exactly once, not twice
assert.Equal(t, nginxBuilds, 1, "nginx dependency should build once, but built %d times", nginxBuilds)
assert.Assert(t, strings.Contains(res.Combined(), "curl service"))
assert.Equal(t, nginxBuilds, 1, "nginx should build once, built %d times\nOutput:\n%s", nginxBuilds, output)
assert.Assert(t, strings.Contains(res.Stdout(), "curl service"))

c.RunDockerComposeCmd(t, "-p", projectName, "-f", "./fixtures/run-test/build-once.yaml", "down", "--remove-orphans")
})

t.Run("nested dependencies build only once each", func(t *testing.T) {
projectName := randomProjectName("build-nested")
_ = c.RunDockerComposeCmd(t, "-p", projectName, "-f", "./fixtures/run-test/build-once-nested.yaml", "down", "--rmi", "local", "--remove-orphans", "-v")

res := c.RunDockerComposeCmd(t, "-p", projectName, "-f", "./fixtures/run-test/build-once-nested.yaml", "--verbose", "run", "--build", "--rm", "app")
res.Assert(t, icmd.Success)

output := res.Combined()

dbBuildMarker := fmt.Sprintf("naming to docker.io/library/%s-db", projectName)
apiBuildMarker := fmt.Sprintf("naming to docker.io/library/%s-api", projectName)
appBuildMarker := fmt.Sprintf("naming to docker.io/library/%s-app", projectName)
output := res.Stdout()

dbBuilds := strings.Count(output, dbBuildMarker)
apiBuilds := strings.Count(output, apiBuildMarker)
appBuilds := strings.Count(output, appBuildMarker)
dbBuilds := countServiceBuilds(output, projectName, "db")
apiBuilds := countServiceBuilds(output, projectName, "api")
appBuilds := countServiceBuilds(output, projectName, "app")

assert.Equal(t, dbBuilds, 1, "db should build once, built %d times\nOutput:\n%s", dbBuilds, output)
assert.Equal(t, apiBuilds, 1, "api should build once, built %d times\nOutput:\n%s", apiBuilds, output)
Expand All @@ -79,21 +70,27 @@ func TestRunBuildOnce(t *testing.T) {

t.Run("service with no dependencies builds once", func(t *testing.T) {
projectName := randomProjectName("build-simple")
res := c.RunDockerComposeCmd(t, "-p", projectName, "-f", "./fixtures/run-test/build-once-no-deps.yaml", "down", "--rmi", "local", "--remove-orphans")
res.Assert(t, icmd.Success)
_ = c.RunDockerComposeCmd(t, "-p", projectName, "-f", "./fixtures/run-test/build-once-no-deps.yaml", "down", "--rmi", "local", "--remove-orphans")
res := c.RunDockerComposeCmd(t, "-p", projectName, "-f", "./fixtures/run-test/build-once-no-deps.yaml", "run", "--build", "--rm", "simple")

output := res.Stdout()

res = c.RunDockerComposeCmd(t, "-p", projectName, "-f", "./fixtures/run-test/build-once-no-deps.yaml", "run", "--build", "--rm", "simple")
res.Assert(t, icmd.Success)
simpleBuilds := countServiceBuilds(output, projectName, "simple")

// Should build exactly once
simpleBuilds := strings.Count(res.Combined(), "Simple service built at")
assert.Equal(t, simpleBuilds, 1, "simple should build once, built %d times", simpleBuilds)
assert.Assert(t, strings.Contains(res.Combined(), "Simple service"))
assert.Equal(t, simpleBuilds, 1, "simple should build once, built %d times\nOutput:\n%s", simpleBuilds, output)
assert.Assert(t, strings.Contains(res.Stdout(), "Simple service"))

c.RunDockerComposeCmd(t, "-p", projectName, "-f", "./fixtures/run-test/build-once-no-deps.yaml", "down", "--remove-orphans")
})
}

// countServiceBuilds counts how many times a service was built by matching
// the "naming to *{projectName}-{serviceName}* done" pattern in the output
func countServiceBuilds(output, projectName, serviceName string) int {
pattern := regexp.MustCompile(`naming to .*` + regexp.QuoteMeta(projectName) + `-` + regexp.QuoteMeta(serviceName) + `.* done`)
return len(pattern.FindAllString(output, -1))
}

// randomProjectName generates a unique project name for parallel test execution
// Format: prefix-<8 random hex chars> (e.g., "build-once-3f4a9b2c")
func randomProjectName(prefix string) string {
Expand Down
2 changes: 1 addition & 1 deletion pkg/e2e/fixtures/run-test/build-once-no-deps.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ services:
build:
dockerfile_inline: |
FROM alpine
RUN echo "Simple service built at $(date)" > /build.txt
RUN echo "Simple built at $(date)" > /build.txt
CMD echo "Simple service"

2 changes: 1 addition & 1 deletion pkg/e2e/fixtures/run-test/build-once.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ services:
build:
dockerfile_inline: |
FROM alpine
RUN echo "Building nginx at $(date)" > /build-time.txt
RUN echo "Nginx built at $(date)" > /build-time.txt
CMD sleep 3600

# Service that depends on nginx
Expand Down