diff --git a/BUILD.bazel b/BUILD.bazel index dc5a819d..e04c8156 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -26,13 +26,4 @@ load("@gazelle//:def.bzl", "gazelle") # native.java_proto_library). # gazelle:proto disable_global -# Export marker files for test data dependencies (used by FindRepoRoot in tests) -exports_files( - [ - "MODULE.bazel", - "go.mod", - ], - visibility = ["//visibility:public"], -) - gazelle(name = "gazelle") diff --git a/Makefile b/Makefile index a0178520..a55fbef1 100644 --- a/Makefile +++ b/Makefile @@ -38,8 +38,8 @@ GOIMPORTS_VERSION ?= v0.33.0 # contracts); all generated stubs land in the same protopb/ dir. PROTO_PACKAGES = api/base/change api/base/mergestrategy api/base/messagequeue api/runway/messagequeue api/runway api/submitqueue/gateway api/submitqueue/orchestrator api/stovepipe stovepipe/core/messagequeue -# Set REPO_ROOT for docker-compose -export REPO_ROOT := $(shell pwd) +# Local Compose builds use the repository checkout as their build context. +export SQ_DOCKER_BUILD_CONTEXT := $(shell pwd) # Fails if git working tree is dirty. Usage: $(call assert_clean,fix command) define assert_clean @@ -132,7 +132,7 @@ clean-proto: ## Clean generated proto files deps: tidy-go ## Download and tidy Go dependencies @echo "Dependencies installed!" -e2e-test: build-all-linux ## Run end-to-end tests (hermetic, auto-builds binaries; runs in parallel) +e2e-test: ## Run end-to-end tests (Bazel builds declared Docker inputs; runs in parallel) @echo "Running end-to-end tests (parallel)..." @$(BAZEL) test //test/e2e/... --test_output=errors @@ -147,7 +147,7 @@ gazelle: ## Update BUILD.bazel files @echo "Running Gazelle to update BUILD files..." @$(BAZEL) run //:gazelle -integration-test: build-all-linux ## Run all integration tests (auto-builds binaries; runs in parallel) +integration-test: ## Run all integration tests (Bazel builds declared Docker inputs; runs in parallel) @echo "Running all integration tests (parallel)..." @$(BAZEL) test //test/integration/... --test_output=errors @@ -159,11 +159,11 @@ integration-test-extensions: ## Run extension integration tests (runs in paralle @echo "Running extension integration tests (parallel)..." @$(BAZEL) test //test/integration/submitqueue/extension/... //test/integration/extension/... --test_output=errors -integration-test-submitqueue-gateway: build-submitqueue-gateway-linux ## Run Gateway integration tests (auto-builds binary) +integration-test-submitqueue-gateway: ## Run Gateway integration tests (Bazel builds declared Docker inputs) @echo "Running Gateway integration tests..." @$(BAZEL) test //test/integration/submitqueue/gateway:go_default_test --test_output=streamed -integration-test-submitqueue-orchestrator: build-submitqueue-orchestrator-linux ## Run Orchestrator integration tests (auto-builds binary) +integration-test-submitqueue-orchestrator: ## Run Orchestrator integration tests (Bazel builds declared Docker inputs) @echo "Running Orchestrator integration tests..." @$(BAZEL) test //test/integration/submitqueue/orchestrator:go_default_test --test_output=streamed diff --git a/doc/howto/TESTING.md b/doc/howto/TESTING.md index d2839740..a57073b4 100644 --- a/doc/howto/TESTING.md +++ b/doc/howto/TESTING.md @@ -62,7 +62,7 @@ make e2e-test # Build make build # Build all targets -make build-all-linux # Build Linux binaries for Docker +make build-all-linux # Build Linux binaries for manual Compose workflows ``` ### Testing Levels @@ -88,10 +88,13 @@ make build-all-linux # Build Linux binaries for Docker Tests use **docker-compose** via `ComposeStack` to spin up containers automatically: -1. `NewComposeStack()` registers cleanup (stop log tailing, tear down containers) -2. `Up()` starts containers, waits for healthchecks (`--wait`), and auto-tails container logs to stderr -3. Tests run against those containers with **real-time log output** -4. On cleanup, containers are torn down automatically (set `SKIP_CLEANUP=true` to keep them for inspection) +1. Each `go_test` declares its Compose file, schemas, Dockerfiles, configuration, and cross-compiled Linux service binaries in Bazel `data`. +2. `NewComposeStack()` resolves only those Bazel runfiles and stages the declared Docker build-context files in a temporary directory. +3. `Up()` starts containers, waits for healthchecks (`--wait`), and auto-tails container logs to stderr. +4. Tests run against those containers with **real-time log output**. +5. On cleanup, containers are torn down automatically (set `SKIP_CLEANUP=true` to keep them for inspection). + +Automated Docker tests do not discover or mount the repository checkout. A test that adds a build-context input must add it to its Bazel `data` dependencies and its `ComposeConfig.DockerBuildContext` mapping. --- diff --git a/service/runway/server/BUILD.bazel b/service/runway/server/BUILD.bazel index 49067b0f..4294b191 100644 --- a/service/runway/server/BUILD.bazel +++ b/service/runway/server/BUILD.bazel @@ -1,4 +1,14 @@ -load("@rules_go//go:def.bzl", "go_binary", "go_library") +load("@rules_go//go:def.bzl", "go_binary", "go_cross_binary", "go_library") + +filegroup( + name = "docker_test_context", + testonly = True, + srcs = [ + "Dockerfile", + ":runway_linux_amd64", + ], + visibility = ["//test:__subpackages__"], +) go_library( name = "go_default_library", @@ -32,3 +42,9 @@ go_binary( embed = [":go_default_library"], visibility = ["//visibility:public"], ) + +go_cross_binary( + name = "runway_linux_amd64", + platform = "@rules_go//go/toolchain:linux_amd64", + target = ":runway", +) diff --git a/service/runway/server/docker-compose.yml b/service/runway/server/docker-compose.yml index 25bb7f8d..d8cc682b 100644 --- a/service/runway/server/docker-compose.yml +++ b/service/runway/server/docker-compose.yml @@ -37,7 +37,7 @@ services: runway-service: image: ${SQ_DOCKER_IMAGE_PREFIX:-submitqueue}-runway-service:latest build: - context: ${REPO_ROOT} + context: ${SQ_DOCKER_BUILD_CONTEXT} dockerfile: service/runway/server/Dockerfile ports: - "8080" # Random ephemeral port to avoid conflicts diff --git a/service/stovepipe/docker-compose.yml b/service/stovepipe/docker-compose.yml index 3c816c21..6aa9e82f 100644 --- a/service/stovepipe/docker-compose.yml +++ b/service/stovepipe/docker-compose.yml @@ -59,7 +59,7 @@ services: stovepipe-service: image: ${SQ_DOCKER_IMAGE_PREFIX:-submitqueue}-stovepipe-service:latest build: - context: ${REPO_ROOT} + context: ${SQ_DOCKER_BUILD_CONTEXT} dockerfile: service/stovepipe/server/Dockerfile ports: - "8080" # Random ephemeral port to avoid conflicts diff --git a/service/stovepipe/server/BUILD.bazel b/service/stovepipe/server/BUILD.bazel index faf7b91e..d0272db2 100644 --- a/service/stovepipe/server/BUILD.bazel +++ b/service/stovepipe/server/BUILD.bazel @@ -1,4 +1,14 @@ -load("@rules_go//go:def.bzl", "go_binary", "go_library") +load("@rules_go//go:def.bzl", "go_binary", "go_cross_binary", "go_library") + +filegroup( + name = "docker_test_context", + testonly = True, + srcs = [ + "Dockerfile", + ":stovepipe_linux_amd64", + ], + visibility = ["//test:__subpackages__"], +) go_library( name = "go_default_library", @@ -40,3 +50,9 @@ go_binary( embed = [":go_default_library"], visibility = ["//visibility:public"], ) + +go_cross_binary( + name = "stovepipe_linux_amd64", + platform = "@rules_go//go/toolchain:linux_amd64", + target = ":stovepipe", +) diff --git a/service/submitqueue/docker-compose.yml b/service/submitqueue/docker-compose.yml index 2b11dc2f..13481450 100644 --- a/service/submitqueue/docker-compose.yml +++ b/service/submitqueue/docker-compose.yml @@ -52,7 +52,7 @@ services: gateway-service: image: ${SQ_DOCKER_IMAGE_PREFIX:-submitqueue}-gateway-service:latest build: - context: ${REPO_ROOT} + context: ${SQ_DOCKER_BUILD_CONTEXT} dockerfile: service/submitqueue/gateway/server/Dockerfile ports: - "8080" # Random ephemeral port to avoid conflicts @@ -75,7 +75,7 @@ services: orchestrator-service: image: ${SQ_DOCKER_IMAGE_PREFIX:-submitqueue}-orchestrator-service:latest build: - context: ${REPO_ROOT} + context: ${SQ_DOCKER_BUILD_CONTEXT} dockerfile: service/submitqueue/orchestrator/server/Dockerfile ports: - "8080" # Random ephemeral port to avoid conflicts @@ -100,7 +100,7 @@ services: runway-service: image: ${SQ_DOCKER_IMAGE_PREFIX:-submitqueue}-runway-service:latest build: - context: ${REPO_ROOT} + context: ${SQ_DOCKER_BUILD_CONTEXT} dockerfile: service/runway/server/Dockerfile ports: - "8080" # Random ephemeral port to avoid conflicts diff --git a/service/submitqueue/gateway/server/BUILD.bazel b/service/submitqueue/gateway/server/BUILD.bazel index 39241b0a..eebb6ab4 100644 --- a/service/submitqueue/gateway/server/BUILD.bazel +++ b/service/submitqueue/gateway/server/BUILD.bazel @@ -1,10 +1,21 @@ -load("@rules_go//go:def.bzl", "go_binary", "go_library", "go_test") +load("@rules_go//go:def.bzl", "go_binary", "go_cross_binary", "go_library", "go_test") exports_files( ["docker-compose.yml"], visibility = ["//visibility:public"], ) +filegroup( + name = "docker_test_context", + testonly = True, + srcs = [ + "Dockerfile", + "queues.yaml", + ":gateway_linux_amd64", + ], + visibility = ["//test:__subpackages__"], +) + go_library( name = "gateway_lib", srcs = ["main.go"], @@ -41,6 +52,12 @@ go_binary( visibility = ["//visibility:public"], ) +go_cross_binary( + name = "gateway_linux_amd64", + platform = "@rules_go//go/toolchain:linux_amd64", + target = ":gateway", +) + go_test( name = "go_default_test", srcs = ["main_test.go"], diff --git a/service/submitqueue/gateway/server/docker-compose.yml b/service/submitqueue/gateway/server/docker-compose.yml index 7d439b60..e312adc2 100644 --- a/service/submitqueue/gateway/server/docker-compose.yml +++ b/service/submitqueue/gateway/server/docker-compose.yml @@ -52,7 +52,7 @@ services: gateway-service: image: ${SQ_DOCKER_IMAGE_PREFIX:-submitqueue}-gateway-service:latest build: - context: ${REPO_ROOT} + context: ${SQ_DOCKER_BUILD_CONTEXT} dockerfile: service/submitqueue/gateway/server/Dockerfile ports: - "8080" # Random ephemeral port to avoid conflicts diff --git a/service/submitqueue/orchestrator/server/BUILD.bazel b/service/submitqueue/orchestrator/server/BUILD.bazel index 72005d10..77115199 100644 --- a/service/submitqueue/orchestrator/server/BUILD.bazel +++ b/service/submitqueue/orchestrator/server/BUILD.bazel @@ -1,10 +1,20 @@ -load("@rules_go//go:def.bzl", "go_binary", "go_library") +load("@rules_go//go:def.bzl", "go_binary", "go_cross_binary", "go_library") exports_files( ["docker-compose.yml"], visibility = ["//visibility:public"], ) +filegroup( + name = "docker_test_context", + testonly = True, + srcs = [ + "Dockerfile", + ":orchestrator_linux_amd64", + ], + visibility = ["//test:__subpackages__"], +) + go_library( name = "orchestrator_lib", srcs = ["main.go"], @@ -71,3 +81,9 @@ go_binary( embed = [":orchestrator_lib"], visibility = ["//visibility:public"], ) + +go_cross_binary( + name = "orchestrator_linux_amd64", + platform = "@rules_go//go/toolchain:linux_amd64", + target = ":orchestrator", +) diff --git a/service/submitqueue/orchestrator/server/docker-compose.yml b/service/submitqueue/orchestrator/server/docker-compose.yml index b9e20150..d0ca6a06 100644 --- a/service/submitqueue/orchestrator/server/docker-compose.yml +++ b/service/submitqueue/orchestrator/server/docker-compose.yml @@ -52,7 +52,7 @@ services: orchestrator-service: image: ${SQ_DOCKER_IMAGE_PREFIX:-submitqueue}-orchestrator-service:latest build: - context: ${REPO_ROOT} + context: ${SQ_DOCKER_BUILD_CONTEXT} dockerfile: service/submitqueue/orchestrator/server/Dockerfile ports: - "8080" # Random ephemeral port to avoid conflicts diff --git a/test/e2e/stovepipe/BUILD.bazel b/test/e2e/stovepipe/BUILD.bazel index b68903aa..365a1e3a 100644 --- a/test/e2e/stovepipe/BUILD.bazel +++ b/test/e2e/stovepipe/BUILD.bazel @@ -7,16 +7,16 @@ go_test( "suite_test.go", ], data = [ - "//:MODULE.bazel", - "//:go.mod", "//platform/extension/messagequeue/mysql/schema", "//service/stovepipe:docker-compose.yml", + "//service/stovepipe/server:docker_test_context", "//stovepipe/extension/storage/mysql/schema", ], tags = [ "e2e", "external", "integration", + "requires-network", ], deps = [ "//api/stovepipe/protopb:go_default_library", diff --git a/test/e2e/stovepipe/suite_test.go b/test/e2e/stovepipe/suite_test.go index 04c75d38..8c1480e0 100644 --- a/test/e2e/stovepipe/suite_test.go +++ b/test/e2e/stovepipe/suite_test.go @@ -16,13 +16,12 @@ package e2e_test // Stovepipe end-to-end tests. // -// These tests use docker-compose from service/stovepipe/docker-compose.yml, -// which requires a pre-built Linux binary. Run with the make target (builds -// binaries + runs the test): +// These tests use docker-compose from service/stovepipe/docker-compose.yml. +// Bazel cross-compiles and declares every Docker build-context input. Run with: // // make e2e-test // -// or only this package (after building the binary): +// or only this package: // // bazel test //test/e2e/stovepipe:stovepipe_test // @@ -35,7 +34,6 @@ package e2e_test import ( "context" "database/sql" - "path/filepath" "testing" "time" @@ -80,12 +78,13 @@ func (s *StovepipeE2ESuite) SetupSuite() { s.log.Logf("Starting Stovepipe e2e test suite using docker-compose") - // Set REPO_ROOT for the docker-compose build context. - repoRoot := testutil.FindRepoRoot(t) - t.Setenv("REPO_ROOT", repoRoot) - - composeFile := filepath.Join(repoRoot, "service/stovepipe/docker-compose.yml") - s.stack = testutil.NewComposeStack(t, s.log, s.ctx, composeFile, "e2e-stovepipe") + s.stack = testutil.NewComposeStack(t, s.log, s.ctx, testutil.ComposeConfig{ + ComposeFile: "service/stovepipe/docker-compose.yml", + DockerBuildContext: []testutil.DockerBuildContextFile{ + {Runfile: "service/stovepipe/server/stovepipe_linux_amd64", Path: ".docker-bin/stovepipe"}, + {Runfile: "service/stovepipe/server/Dockerfile", Path: "service/stovepipe/server/Dockerfile"}, + }, + }, "e2e-stovepipe") err := s.stack.Up() require.NoError(t, err, "failed to start compose stack") diff --git a/test/e2e/submitqueue/BUILD.bazel b/test/e2e/submitqueue/BUILD.bazel index eda42174..d9308e39 100644 --- a/test/e2e/submitqueue/BUILD.bazel +++ b/test/e2e/submitqueue/BUILD.bazel @@ -7,17 +7,19 @@ go_test( "suite_test.go", ], data = [ - "//:MODULE.bazel", - "//:go.mod", "//platform/extension/counter/mysql/schema", "//platform/extension/messagequeue/mysql/schema", + "//service/runway/server:docker_test_context", "//service/submitqueue:docker-compose.yml", + "//service/submitqueue/gateway/server:docker_test_context", + "//service/submitqueue/orchestrator/server:docker_test_context", "//submitqueue/extension/storage/mysql/schema", ], tags = [ "e2e", "external", "integration", + "requires-network", ], deps = [ "//api/base/change/protopb:go_default_library", diff --git a/test/e2e/submitqueue/suite_test.go b/test/e2e/submitqueue/suite_test.go index 84052b0e..c0a5cbef 100644 --- a/test/e2e/submitqueue/suite_test.go +++ b/test/e2e/submitqueue/suite_test.go @@ -16,16 +16,15 @@ package e2e_test // E2E Integration Tests // -// These tests use docker-compose from service/submitqueue/docker-compose.yml -// which requires pre-built Linux binaries. +// These tests use docker-compose from service/submitqueue/docker-compose.yml. +// Bazel cross-compiles and declares every Docker build-context input. // -// Run with make target (builds binaries + runs test): +// Run with: // make e2e-test import ( "context" "database/sql" - "path/filepath" "testing" "time" @@ -76,14 +75,18 @@ func (s *E2EIntegrationSuite) SetupSuite() { s.log.Logf("Starting E2E integration test suite using docker-compose") - // Set REPO_ROOT for docker-compose volume mounts and build context - repoRoot := testutil.FindRepoRoot(t) - t.Setenv("REPO_ROOT", repoRoot) - - // Use docker-compose from service/submitqueue (full stack) - // NOTE: Assumes Linux binaries are pre-built via make target - composeFile := filepath.Join(repoRoot, "service/submitqueue/docker-compose.yml") - s.stack = testutil.NewComposeStack(t, s.log, s.ctx, composeFile, "e2e-submitqueue") + s.stack = testutil.NewComposeStack(t, s.log, s.ctx, testutil.ComposeConfig{ + ComposeFile: "service/submitqueue/docker-compose.yml", + DockerBuildContext: []testutil.DockerBuildContextFile{ + {Runfile: "service/submitqueue/gateway/server/gateway_linux_amd64", Path: ".docker-bin/gateway"}, + {Runfile: "service/submitqueue/gateway/server/Dockerfile", Path: "service/submitqueue/gateway/server/Dockerfile"}, + {Runfile: "service/submitqueue/gateway/server/queues.yaml", Path: "service/submitqueue/gateway/server/queues.yaml"}, + {Runfile: "service/submitqueue/orchestrator/server/orchestrator_linux_amd64", Path: ".docker-bin/orchestrator"}, + {Runfile: "service/submitqueue/orchestrator/server/Dockerfile", Path: "service/submitqueue/orchestrator/server/Dockerfile"}, + {Runfile: "service/runway/server/runway_linux_amd64", Path: ".docker-bin/runway"}, + {Runfile: "service/runway/server/Dockerfile", Path: "service/runway/server/Dockerfile"}, + }, + }, "e2e-submitqueue") // Start the compose stack (Gateway + Orchestrator + 2 MySQL DBs) err := s.stack.Up() diff --git a/test/integration/extension/counter/mysql/BUILD.bazel b/test/integration/extension/counter/mysql/BUILD.bazel index d5686dfd..6e952177 100644 --- a/test/integration/extension/counter/mysql/BUILD.bazel +++ b/test/integration/extension/counter/mysql/BUILD.bazel @@ -5,13 +5,12 @@ go_test( srcs = ["counter_test.go"], data = [ "docker-compose.yml", - "//:MODULE.bazel", - "//:go.mod", "//platform/extension/counter/mysql/schema", ], tags = [ "external", "integration", + "requires-network", ], deps = [ "//platform/extension/counter/mysql:go_default_library", diff --git a/test/integration/extension/counter/mysql/counter_test.go b/test/integration/extension/counter/mysql/counter_test.go index 6787b4c5..49d23e07 100644 --- a/test/integration/extension/counter/mysql/counter_test.go +++ b/test/integration/extension/counter/mysql/counter_test.go @@ -53,7 +53,7 @@ func (s *MySQLCounterIntegrationSuite) SetupSuite() { t, s.log, ctx, - "docker-compose.yml", + testutil.ComposeConfig{ComposeFile: "test/integration/extension/counter/mysql/docker-compose.yml"}, "ext-counter-mysql", // Test context for meaningful container names ) diff --git a/test/integration/extension/messagequeue/mysql/BUILD.bazel b/test/integration/extension/messagequeue/mysql/BUILD.bazel index a74d8997..c958520d 100644 --- a/test/integration/extension/messagequeue/mysql/BUILD.bazel +++ b/test/integration/extension/messagequeue/mysql/BUILD.bazel @@ -5,13 +5,12 @@ go_test( srcs = ["queue_test.go"], data = [ "docker-compose.yml", - "//:MODULE.bazel", - "//:go.mod", "//platform/extension/messagequeue/mysql/schema", ], tags = [ "external", "integration", + "requires-network", ], deps = [ "//platform/base/messagequeue:go_default_library", diff --git a/test/integration/extension/messagequeue/mysql/queue_test.go b/test/integration/extension/messagequeue/mysql/queue_test.go index ed006a6e..d39c8e2c 100644 --- a/test/integration/extension/messagequeue/mysql/queue_test.go +++ b/test/integration/extension/messagequeue/mysql/queue_test.go @@ -61,7 +61,7 @@ func (s *SQLQueueIntegrationSuite) SetupSuite() { t, s.log, s.ctx, - "docker-compose.yml", + testutil.ComposeConfig{ComposeFile: "test/integration/extension/messagequeue/mysql/docker-compose.yml"}, "ext-messagequeue-sql", // Test context for meaningful container names ) diff --git a/test/integration/stovepipe/BUILD.bazel b/test/integration/stovepipe/BUILD.bazel index 3ca8251d..2a7c54c8 100644 --- a/test/integration/stovepipe/BUILD.bazel +++ b/test/integration/stovepipe/BUILD.bazel @@ -4,15 +4,15 @@ go_test( name = "go_default_test", srcs = ["suite_test.go"], data = [ - "//:MODULE.bazel", - "//:go.mod", "//platform/extension/messagequeue/mysql/schema", "//service/stovepipe:docker-compose.yml", + "//service/stovepipe/server:docker_test_context", "//stovepipe/extension/storage/mysql/schema", ], tags = [ "external", "integration", + "requires-network", ], deps = [ "//api/stovepipe/protopb:go_default_library", diff --git a/test/integration/stovepipe/extension/storage/mysql/BUILD.bazel b/test/integration/stovepipe/extension/storage/mysql/BUILD.bazel index 5beb0fee..16d529d6 100644 --- a/test/integration/stovepipe/extension/storage/mysql/BUILD.bazel +++ b/test/integration/stovepipe/extension/storage/mysql/BUILD.bazel @@ -5,13 +5,12 @@ go_test( srcs = ["storage_test.go"], data = [ "docker-compose.yml", - "//:MODULE.bazel", - "//:go.mod", "//stovepipe/extension/storage/mysql/schema", ], tags = [ "external", "integration", + "requires-network", ], deps = [ "//stovepipe/entity:go_default_library", diff --git a/test/integration/stovepipe/extension/storage/mysql/storage_test.go b/test/integration/stovepipe/extension/storage/mysql/storage_test.go index ee55fe5a..1c77351f 100644 --- a/test/integration/stovepipe/extension/storage/mysql/storage_test.go +++ b/test/integration/stovepipe/extension/storage/mysql/storage_test.go @@ -37,7 +37,7 @@ func TestMySQLStorage(t *testing.T) { t, log, ctx, - "docker-compose.yml", + testutil.ComposeConfig{ComposeFile: "test/integration/stovepipe/extension/storage/mysql/docker-compose.yml"}, "ext-stovepipe-storage-mysql", ) diff --git a/test/integration/stovepipe/suite_test.go b/test/integration/stovepipe/suite_test.go index 3f7495eb..00ad3620 100644 --- a/test/integration/stovepipe/suite_test.go +++ b/test/integration/stovepipe/suite_test.go @@ -16,11 +16,10 @@ package stovepipe // Stovepipe integration tests // -// These tests use compose from service/stovepipe/docker-compose.yml and require -// a pre-built Linux binary (make integration-test runs //test/integration/... -// and builds all Linux binaries via build-all-linux). The stack runs the -// Stovepipe gRPC service plus a storage MySQL (request, request_uri) and a queue -// MySQL (process stage). +// These tests use compose from service/stovepipe/docker-compose.yml. Bazel +// cross-compiles and declares every Docker build-context input. The stack runs +// the Stovepipe gRPC service plus a storage MySQL (request, request_uri) and a +// queue MySQL (process stage). // // Run with: // make integration-test @@ -30,7 +29,6 @@ package stovepipe import ( "context" "database/sql" - "path/filepath" "testing" _ "github.com/go-sql-driver/mysql" @@ -63,11 +61,13 @@ func (s *StovepipeIntegrationSuite) SetupSuite() { s.log.Logf("Starting Stovepipe integration test suite using compose") - repoRoot := testutil.FindRepoRoot(t) - t.Setenv("REPO_ROOT", repoRoot) - - composeFile := filepath.Join(repoRoot, "service/stovepipe/docker-compose.yml") - s.stack = testutil.NewComposeStack(t, s.log, s.ctx, composeFile, "svc-stovepipe") + s.stack = testutil.NewComposeStack(t, s.log, s.ctx, testutil.ComposeConfig{ + ComposeFile: "service/stovepipe/docker-compose.yml", + DockerBuildContext: []testutil.DockerBuildContextFile{ + {Runfile: "service/stovepipe/server/stovepipe_linux_amd64", Path: ".docker-bin/stovepipe"}, + {Runfile: "service/stovepipe/server/Dockerfile", Path: "service/stovepipe/server/Dockerfile"}, + }, + }, "svc-stovepipe") err := s.stack.Up() require.NoError(t, err, "failed to start compose stack") diff --git a/test/integration/submitqueue/core/consumer/BUILD.bazel b/test/integration/submitqueue/core/consumer/BUILD.bazel index ce6b2203..148b3222 100644 --- a/test/integration/submitqueue/core/consumer/BUILD.bazel +++ b/test/integration/submitqueue/core/consumer/BUILD.bazel @@ -5,13 +5,12 @@ go_test( srcs = ["consumer_test.go"], data = [ "docker-compose.yml", - "//:MODULE.bazel", - "//:go.mod", "//platform/extension/messagequeue/mysql/schema", ], tags = [ "external", "integration", + "requires-network", ], deps = [ "//platform/base/messagequeue:go_default_library", diff --git a/test/integration/submitqueue/core/consumer/consumer_test.go b/test/integration/submitqueue/core/consumer/consumer_test.go index 98709155..0509d6e3 100644 --- a/test/integration/submitqueue/core/consumer/consumer_test.go +++ b/test/integration/submitqueue/core/consumer/consumer_test.go @@ -76,7 +76,7 @@ func (s *ConsumerIntegrationSuite) SetupSuite() { t, s.log, s.ctx, - "docker-compose.yml", + testutil.ComposeConfig{ComposeFile: "test/integration/submitqueue/core/consumer/docker-compose.yml"}, "core-submitqueue-consumer", ) diff --git a/test/integration/submitqueue/extension/storage/mysql/BUILD.bazel b/test/integration/submitqueue/extension/storage/mysql/BUILD.bazel index d96ad07f..433b80d9 100644 --- a/test/integration/submitqueue/extension/storage/mysql/BUILD.bazel +++ b/test/integration/submitqueue/extension/storage/mysql/BUILD.bazel @@ -5,13 +5,12 @@ go_test( srcs = ["storage_test.go"], data = [ "docker-compose.yml", - "//:MODULE.bazel", - "//:go.mod", "//submitqueue/extension/storage/mysql/schema", ], tags = [ "external", "integration", + "requires-network", ], deps = [ "//submitqueue/extension/storage/mysql:go_default_library", diff --git a/test/integration/submitqueue/extension/storage/mysql/storage_test.go b/test/integration/submitqueue/extension/storage/mysql/storage_test.go index 03e3ee4a..bdbab96d 100644 --- a/test/integration/submitqueue/extension/storage/mysql/storage_test.go +++ b/test/integration/submitqueue/extension/storage/mysql/storage_test.go @@ -53,7 +53,7 @@ func (s *MySQLStorageIntegrationSuite) SetupSuite() { t, s.log, ctx, - "docker-compose.yml", + testutil.ComposeConfig{ComposeFile: "test/integration/submitqueue/extension/storage/mysql/docker-compose.yml"}, "ext-submitqueue-storage-mysql", // Test context for meaningful container names ) diff --git a/test/integration/submitqueue/gateway/BUILD.bazel b/test/integration/submitqueue/gateway/BUILD.bazel index 34fa0ad0..cf1991d8 100644 --- a/test/integration/submitqueue/gateway/BUILD.bazel +++ b/test/integration/submitqueue/gateway/BUILD.bazel @@ -4,16 +4,16 @@ go_test( name = "go_default_test", srcs = ["suite_test.go"], data = [ - "//:MODULE.bazel", - "//:go.mod", "//platform/extension/counter/mysql/schema", "//platform/extension/messagequeue/mysql/schema", "//service/submitqueue/gateway/server:docker-compose.yml", + "//service/submitqueue/gateway/server:docker_test_context", "//submitqueue/extension/storage/mysql/schema", ], tags = [ "external", "integration", + "requires-network", ], deps = [ "//api/base/change/protopb:go_default_library", diff --git a/test/integration/submitqueue/gateway/suite_test.go b/test/integration/submitqueue/gateway/suite_test.go index c3882c5d..4f885b7f 100644 --- a/test/integration/submitqueue/gateway/suite_test.go +++ b/test/integration/submitqueue/gateway/suite_test.go @@ -16,10 +16,10 @@ package gateway // Gateway Integration Tests // -// These tests use docker-compose from service/submitqueue/gateway/server/docker-compose.yml -// which requires pre-built Linux binaries. +// These tests use docker-compose from service/submitqueue/gateway/server/docker-compose.yml. +// Bazel cross-compiles and declares every Docker build-context input. // -// Run with make target (builds binary + runs test): +// Run with: // make integration-test-gateway // // For manual testing with docker-compose: @@ -29,7 +29,6 @@ import ( "context" "database/sql" "fmt" - "path/filepath" "testing" "time" @@ -85,14 +84,14 @@ func (s *GatewayIntegrationSuite) SetupSuite() { s.log.Logf("Starting Gateway integration test suite using docker-compose") - // Set REPO_ROOT for docker-compose volume mounts and build context - repoRoot := testutil.FindRepoRoot(t) - t.Setenv("REPO_ROOT", repoRoot) - - // Use docker-compose from service/submitqueue/gateway/server - // NOTE: Assumes Linux binary is pre-built via make target - composeFile := filepath.Join(repoRoot, "service/submitqueue/gateway/server/docker-compose.yml") - s.stack = testutil.NewComposeStack(t, s.log, s.ctx, composeFile, "svc-submitqueue-gateway") + s.stack = testutil.NewComposeStack(t, s.log, s.ctx, testutil.ComposeConfig{ + ComposeFile: "service/submitqueue/gateway/server/docker-compose.yml", + DockerBuildContext: []testutil.DockerBuildContextFile{ + {Runfile: "service/submitqueue/gateway/server/gateway_linux_amd64", Path: ".docker-bin/gateway"}, + {Runfile: "service/submitqueue/gateway/server/Dockerfile", Path: "service/submitqueue/gateway/server/Dockerfile"}, + {Runfile: "service/submitqueue/gateway/server/queues.yaml", Path: "service/submitqueue/gateway/server/queues.yaml"}, + }, + }, "svc-submitqueue-gateway") // Start the compose stack (Gateway + 2 MySQL DBs) err := s.stack.Up() diff --git a/test/integration/submitqueue/orchestrator/BUILD.bazel b/test/integration/submitqueue/orchestrator/BUILD.bazel index fa2d2743..1ee2fcb7 100644 --- a/test/integration/submitqueue/orchestrator/BUILD.bazel +++ b/test/integration/submitqueue/orchestrator/BUILD.bazel @@ -4,16 +4,16 @@ go_test( name = "go_default_test", srcs = ["suite_test.go"], data = [ - "//:MODULE.bazel", - "//:go.mod", "//platform/extension/counter/mysql/schema", "//platform/extension/messagequeue/mysql/schema", "//service/submitqueue/orchestrator/server:docker-compose.yml", + "//service/submitqueue/orchestrator/server:docker_test_context", "//submitqueue/extension/storage/mysql/schema", ], tags = [ "external", "integration", + "requires-network", ], deps = [ "//api/submitqueue/orchestrator/protopb:go_default_library", diff --git a/test/integration/submitqueue/orchestrator/suite_test.go b/test/integration/submitqueue/orchestrator/suite_test.go index bcdba170..a1b8eb38 100644 --- a/test/integration/submitqueue/orchestrator/suite_test.go +++ b/test/integration/submitqueue/orchestrator/suite_test.go @@ -16,10 +16,10 @@ package orchestrator // Orchestrator Integration Tests // -// These tests use docker-compose from service/submitqueue/orchestrator/server/docker-compose.yml -// which requires pre-built Linux binaries. +// These tests use docker-compose from service/submitqueue/orchestrator/server/docker-compose.yml. +// Bazel cross-compiles and declares every Docker build-context input. // -// Run with make target (builds binary + runs test): +// Run with: // make integration-test-orchestrator // // For manual testing with docker-compose: @@ -28,7 +28,6 @@ package orchestrator import ( "context" "database/sql" - "path/filepath" "testing" "github.com/stretchr/testify/assert" @@ -60,14 +59,13 @@ func (s *OrchestratorIntegrationSuite) SetupSuite() { s.log.Logf("Starting Orchestrator integration test suite using docker-compose") - // Set REPO_ROOT for docker-compose volume mounts and build context - repoRoot := testutil.FindRepoRoot(t) - t.Setenv("REPO_ROOT", repoRoot) - - // Use docker-compose from service/submitqueue/orchestrator/server - // NOTE: Assumes Linux binary is pre-built via make target - composeFile := filepath.Join(repoRoot, "service/submitqueue/orchestrator/server/docker-compose.yml") - s.stack = testutil.NewComposeStack(t, s.log, s.ctx, composeFile, "svc-submitqueue-orchestrator") + s.stack = testutil.NewComposeStack(t, s.log, s.ctx, testutil.ComposeConfig{ + ComposeFile: "service/submitqueue/orchestrator/server/docker-compose.yml", + DockerBuildContext: []testutil.DockerBuildContextFile{ + {Runfile: "service/submitqueue/orchestrator/server/orchestrator_linux_amd64", Path: ".docker-bin/orchestrator"}, + {Runfile: "service/submitqueue/orchestrator/server/Dockerfile", Path: "service/submitqueue/orchestrator/server/Dockerfile"}, + }, + }, "svc-submitqueue-orchestrator") // Start the compose stack (Orchestrator + 2 MySQL DBs) err := s.stack.Up() diff --git a/test/testutil/compose.go b/test/testutil/compose.go index ab1ab5b1..040c4d36 100644 --- a/test/testutil/compose.go +++ b/test/testutil/compose.go @@ -19,9 +19,13 @@ import ( "crypto/sha256" "database/sql" "fmt" + "hash" + "io" "os" "os/exec" + "os/user" "path/filepath" + "sort" "strings" "testing" "time" @@ -32,6 +36,22 @@ import ( "google.golang.org/grpc/credentials/insecure" ) +// DockerBuildContextFile maps a declared Bazel runfile into a Docker build context. +type DockerBuildContextFile struct { + // Runfile is the workspace-relative path of a file declared in the test's data. + Runfile string + // Path is the relative destination path inside the staged Docker build context. + Path string +} + +// ComposeConfig declares every file a Docker Compose test needs from Bazel runfiles. +type ComposeConfig struct { + // ComposeFile is the workspace-relative path of the Compose file. + ComposeFile string + // DockerBuildContext contains the files needed by services with build directives. + DockerBuildContext []DockerBuildContextFile +} + // ComposeStack manages a docker-compose stack for testing. type ComposeStack struct { composeFile string @@ -44,30 +64,32 @@ type ComposeStack struct { logCmd *exec.Cmd // background "docker compose logs -f" process } -// getDockerComposeCommand returns the docker-compose command to use. -// Tries "docker-compose" first (V1), falls back to "docker compose" (V2). +// getDockerComposeCommand returns the Docker Compose command to use. +// Compose V2 supports the --wait lifecycle used by the test harness. func getDockerComposeCommand() []string { - // Try docker-compose (V1) + if _, err := exec.LookPath("docker"); err == nil { + return []string{"docker", "compose"} + } + if _, err := exec.LookPath("docker-compose"); err == nil { return []string{"docker-compose"} } - // Fall back to docker compose (V2) return []string{"docker", "compose"} } -// NewComposeStack creates a new compose stack from the given docker-compose file. +// NewComposeStack creates a new compose stack from declared Bazel runfiles. // Automatically registers cleanup to tear down the stack. // testContext should describe what's being tested (e.g., "gateway", "storage", "e2e-submitqueue"). -func NewComposeStack(t *testing.T, log *TestLogger, ctx context.Context, composeFile, testContext string) *ComposeStack { +func NewComposeStack(t *testing.T, log *TestLogger, ctx context.Context, config ComposeConfig, testContext string) *ComposeStack { t.Helper() - // Setup Docker environment setupDockerEnv(t) - // Get absolute path to compose file - absPath, err := filepath.Abs(composeFile) - require.NoError(t, err, "failed to get absolute path to compose file") + composeFile := resolveRunfile(t, config.ComposeFile) + inputHash := sha256.New() + hashFile(t, inputHash, config.ComposeFile, composeFile) + buildContext := stageDockerBuildContext(t, inputHash, config.DockerBuildContext) // Generate meaningful project name: sq-test-{context}-{short-timestamp} // Results in container names like: sq-test-gateway-a1b2c3d-mysql-app-1 @@ -75,13 +97,13 @@ func NewComposeStack(t *testing.T, log *TestLogger, ctx context.Context, compose projectName := fmt.Sprintf("sq-test-%s-%s", testContext, timestamp) stack := &ComposeStack{ - composeFile: absPath, + composeFile: composeFile, projectName: projectName, t: t, log: log, ctx: ctx, composeCmd: getDockerComposeCommand(), - composeEnv: composeEnvironment(t, absPath), + composeEnv: composeEnvironment(inputHash.Sum(nil), buildContext), } // Register cleanup @@ -92,7 +114,7 @@ func NewComposeStack(t *testing.T, log *TestLogger, ctx context.Context, compose log.Logf("SKIP_CLEANUP=true - keeping containers for inspection") log.Logf("Container prefix: %s", projectName) composeCmd := strings.Join(stack.composeCmd, " ") - log.Logf("Clean up manually: %s -f %s -p %s down -v", composeCmd, absPath, projectName) + log.Logf("Clean up manually: %s -f %s -p %s down -v", composeCmd, composeFile, projectName) return } @@ -103,6 +125,83 @@ func NewComposeStack(t *testing.T, log *TestLogger, ctx context.Context, compose return stack } +func resolveRunfile(t *testing.T, runfile string) string { + t.Helper() + + if filepath.IsAbs(runfile) { + require.FileExists(t, runfile, "runfile does not exist") + return runfile + } + + if testSrcDir := os.Getenv("TEST_SRCDIR"); testSrcDir != "" { + workspace := os.Getenv("TEST_WORKSPACE") + if workspace == "" { + workspace = "_main" + } + resolved := filepath.Join(testSrcDir, workspace, filepath.FromSlash(runfile)) + require.FileExists(t, resolved, "Bazel runfile does not exist") + return resolved + } + + resolved, err := filepath.Abs(runfile) + require.NoError(t, err, "failed to resolve file %s", runfile) + require.FileExists(t, resolved, "file does not exist") + return resolved +} + +func stageDockerBuildContext(t *testing.T, inputHash hash.Hash, files []DockerBuildContextFile) string { + t.Helper() + + if len(files) == 0 { + return "" + } + + files = append([]DockerBuildContextFile(nil), files...) + sort.Slice(files, func(i, j int) bool { + return files[i].Path < files[j].Path + }) + + contextDir := t.TempDir() + for _, file := range files { + require.True(t, filepath.IsLocal(file.Path), "Docker build context path must be relative: %s", file.Path) + + source := resolveRunfile(t, file.Runfile) + destination := filepath.Join(contextDir, filepath.FromSlash(file.Path)) + require.NoError(t, os.MkdirAll(filepath.Dir(destination), 0o755), "failed to create Docker build context directory") + + sourceFile, err := os.Open(source) + require.NoError(t, err, "failed to open Docker build context runfile %s", file.Runfile) + + info, err := sourceFile.Stat() + require.NoError(t, err, "failed to stat Docker build context runfile %s", file.Runfile) + + destinationFile, err := os.OpenFile(destination, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, info.Mode().Perm()) + require.NoError(t, err, "failed to create Docker build context file %s", file.Path) + + _, _ = io.WriteString(inputHash, file.Path) + _, _ = inputHash.Write([]byte{0}) + _, err = io.Copy(io.MultiWriter(destinationFile, inputHash), sourceFile) + require.NoError(t, err, "failed to stage Docker build context file %s", file.Path) + require.NoError(t, destinationFile.Close(), "failed to close Docker build context file %s", file.Path) + require.NoError(t, sourceFile.Close(), "failed to close Docker build context runfile %s", file.Runfile) + } + + return contextDir +} + +func hashFile(t *testing.T, inputHash hash.Hash, logicalPath, file string) { + t.Helper() + + _, _ = io.WriteString(inputHash, logicalPath) + _, _ = inputHash.Write([]byte{0}) + + f, err := os.Open(file) + require.NoError(t, err, "failed to open declared input %s", logicalPath) + _, err = io.Copy(inputHash, f) + require.NoError(t, err, "failed to hash declared input %s", logicalPath) + require.NoError(t, f.Close(), "failed to close declared input %s", logicalPath) +} + // Up starts all services in the compose stack. // Uses --wait to block until all services with healthcheck directives are healthy. func (s *ComposeStack) Up() error { @@ -125,8 +224,8 @@ func (s *ComposeStack) Up() error { } // down stops and removes all services and volumes in the compose stack. -// Locally built images use stable per-worktree names and remain cached for -// subsequent test runs. +// Locally built images use declared-input hashes and remain cached for +// subsequent runs with the same build context. func (s *ComposeStack) down() { s.log.Logf("Stopping compose stack") @@ -359,90 +458,42 @@ func setupDockerEnv(t *testing.T) { if os.Getenv("HOME") == "" { t.Setenv("HOME", t.TempDir()) } -} - -// composeEnvironment returns the environment used by every compose command. -// The image prefix is stable within a worktree so --build can reuse images -// between test runs, but differs across worktrees to avoid stale cross-checkout -// images when multiple changes are tested on the same Docker daemon. -func composeEnvironment(t *testing.T, composeFile string) []string { - t.Helper() - - repoRoot := composeRepoRoot(t, composeFile) - sum := sha256.Sum256([]byte(repoRoot)) - imagePrefix := fmt.Sprintf("sq-test-%x", sum[:6]) - - env := os.Environ() - env = append(env, "SQ_DOCKER_IMAGE_PREFIX="+imagePrefix) - env = append(env, "SQ_MYSQL_DATA_MOUNT_TYPE=tmpfs") - env = append(env, "SQ_MYSQL_INITDB_SKIP_TZINFO=1") - return env -} - -// composeRepoRoot resolves the repository containing composeFile. Bazel exposes -// repository markers as runfile symlinks, so resolving a marker identifies the -// source checkout without invoking a host-provided Git executable. -func composeRepoRoot(t *testing.T, composeFile string) string { - t.Helper() - - if repoRoot := os.Getenv("REPO_ROOT"); repoRoot != "" { - return repoRoot - } - dir := filepath.Dir(composeFile) - for { - for _, marker := range []string{"MODULE.bazel", "go.mod"} { - markerPath := filepath.Join(dir, marker) - if realMarker, err := filepath.EvalSymlinks(markerPath); err == nil { - return filepath.Dir(realMarker) + // Bazel may replace HOME with a sandbox directory. Preserve access to the + // host Docker CLI configuration and user-installed Compose plugin without + // exposing the source checkout. + if os.Getenv("DOCKER_CONFIG") == "" { + currentUser, err := user.Current() + if err == nil { + dockerConfig := filepath.Join(currentUser.HomeDir, ".docker") + if info, statErr := os.Stat(dockerConfig); statErr == nil && info.IsDir() { + t.Setenv("DOCKER_CONFIG", dockerConfig) } } - - parent := filepath.Dir(dir) - if parent == dir { - t.Fatalf("repository root not found") - } - dir = parent } } -// FindRepoRoot finds the repository root. -// Checks REPO_ROOT env var, then git, then walks up to find marker files. -func FindRepoRoot(t *testing.T) string { - t.Helper() - - // Check if REPO_ROOT is set (from .envrc or test environment) - if repoRoot := os.Getenv("REPO_ROOT"); repoRoot != "" { - return repoRoot - } - - // Try git (works outside Bazel sandbox) - cmd := exec.Command("git", "rev-parse", "--show-toplevel") - if output, err := cmd.Output(); err == nil { - if repoRoot := strings.TrimSpace(string(output)); repoRoot != "" { - return repoRoot - } +// composeEnvironment returns the environment used by every compose command. +// The image prefix is derived from declared inputs so identical contexts reuse +// images without sharing stale images across different source revisions. +func composeEnvironment(inputHash []byte, buildContext string) []string { + env := os.Environ() + env = setEnvironment(env, "SQ_DOCKER_IMAGE_PREFIX", fmt.Sprintf("sq-test-%x", inputHash[:6])) + env = setEnvironment(env, "SQ_MYSQL_DATA_MOUNT_TYPE", "tmpfs") + env = setEnvironment(env, "SQ_MYSQL_INITDB_SKIP_TZINFO", "1") + if buildContext != "" { + env = setEnvironment(env, "SQ_DOCKER_BUILD_CONTEXT", buildContext) } + return env +} - // Walk up from current directory to find marker files - // In Bazel sandbox, marker files are symlinks - resolve them to get source location - dir, err := os.Getwd() - require.NoError(t, err, "failed to get working directory") - - for { - // Try to find and resolve marker file symlinks - for _, marker := range []string{"MODULE.bazel", "go.mod"} { - markerPath := filepath.Join(dir, marker) - if realMarker, err := filepath.EvalSymlinks(markerPath); err == nil { - return filepath.Dir(realMarker) - } - } - - // Move up one directory - parent := filepath.Dir(dir) - if parent == dir { - t.Fatalf("repository root not found") +func setEnvironment(env []string, key, value string) []string { + prefix := key + "=" + filtered := env[:0] + for _, entry := range env { + if !strings.HasPrefix(entry, prefix) { + filtered = append(filtered, entry) } - dir = parent } + return append(filtered, prefix+value) }