From 4edbc73b640d254967db356a8fc3ec72ee693002 Mon Sep 17 00:00:00 2001 From: afjcjsbx Date: Wed, 13 May 2026 19:54:32 +0200 Subject: [PATCH] fix(integration): execute suite commands directly in docker runner --- .../run_integration_tests_script_test.go | 153 ++++++++++++++++++ scripts/run-integration-tests.sh | 10 +- 2 files changed, 160 insertions(+), 3 deletions(-) create mode 100644 integration/run_integration_tests_script_test.go diff --git a/integration/run_integration_tests_script_test.go b/integration/run_integration_tests_script_test.go new file mode 100644 index 000000000..33b77c620 --- /dev/null +++ b/integration/run_integration_tests_script_test.go @@ -0,0 +1,153 @@ +package integration + +import ( + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +func TestRunIntegrationTestsScriptExecutesSuiteCommand(t *testing.T) { + bashPath, err := exec.LookPath("bash") + if err != nil { + t.Skip("bash not available") + } + + repoRoot := repoRootFromTestFile(t) + suitesRoot := filepath.Join(repoRoot, "integration", "suites") + suiteDir, err := os.MkdirTemp(suitesRoot, "runner-script-") + if err != nil { + t.Fatalf("MkdirTemp() error = %v", err) + } + t.Cleanup(func() { + _ = os.RemoveAll(suiteDir) + }) + + suiteName := filepath.Base(suiteDir) + if err := os.WriteFile( + filepath.Join(suiteDir, "suite.env"), + []byte("TEST_COMMAND='printf runner-ok'\n"), + 0o644, + ); err != nil { + t.Fatalf("WriteFile(suite.env) error = %v", err) + } + if err := os.WriteFile( + filepath.Join(suiteDir, "docker-compose.yml"), + []byte("services:\n fake-dependency:\n image: busybox\n"), + 0o644, + ); err != nil { + t.Fatalf("WriteFile(docker-compose.yml) error = %v", err) + } + + stubDir := t.TempDir() + logPath := filepath.Join(t.TempDir(), "docker.log") + if err := os.WriteFile(filepath.Join(stubDir, "docker"), []byte(`#!/bin/sh +set -eu + +log_file="${DOCKER_LOG:?}" +{ + printf '%s\n' '---' + for arg in "$@"; do + printf '%s\n' "$arg" + done +} >>"$log_file" + +subcommand="" +for arg in "$@"; do + case "$arg" in + config|up|run|down) + subcommand="$arg" + ;; + esac +done + +case "$subcommand" in + config) + printf '%s\n' integration-runner fake-dependency + ;; + up) + ;; + run) + printf '%s\n' runner-ok + ;; + down) + ;; + *) + printf 'unexpected docker invocation: %s\n' "$*" >&2 + exit 1 + ;; +esac +`), 0o755); err != nil { + t.Fatalf("WriteFile(docker stub) error = %v", err) + } + + cmd := exec.Command(bashPath, filepath.Join(repoRoot, "scripts", "run-integration-tests.sh"), suiteName) + cmd.Dir = repoRoot + cmd.Env = append(os.Environ(), + "PATH="+stubDir+string(os.PathListSeparator)+os.Getenv("PATH"), + "DOCKER_LOG="+logPath, + ) + + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("run-integration-tests.sh error = %v\noutput:\n%s", err, output) + } + if !strings.Contains(string(output), "runner-ok") { + t.Fatalf("script output did not include runner output:\n%s", output) + } + + logData, err := os.ReadFile(logPath) + if err != nil { + t.Fatalf("ReadFile(logPath) error = %v", err) + } + + runArgs := findLoggedDockerInvocation(t, string(logData), "run") + if strings.Contains(strings.Join(runArgs, "\n"), "\nsh\n-c\n") { + t.Fatalf("docker compose run unexpectedly wrapped TEST_COMMAND with sh -c:\n%v", runArgs) + } + + if !containsArg(runArgs, "integration-runner") { + t.Fatalf("docker compose run args missing runner service:\n%v", runArgs) + } + if !containsArg(runArgs, "printf runner-ok") { + t.Fatalf("docker compose run args missing suite command as a single argument:\n%v", runArgs) + } +} + +func repoRootFromTestFile(t *testing.T) string { + t.Helper() + + wd, err := os.Getwd() + if err != nil { + t.Fatalf("Getwd() error = %v", err) + } + return filepath.Dir(wd) +} + +func findLoggedDockerInvocation(t *testing.T, logData, subcommand string) []string { + t.Helper() + + for _, block := range strings.Split(logData, "---\n") { + block = strings.TrimSpace(block) + if block == "" { + continue + } + args := strings.Split(block, "\n") + if containsArg(args, subcommand) { + return args + } + } + + t.Fatalf("did not find docker %q invocation in log:\n%s", subcommand, logData) + return nil +} + +func containsArg(args []string, want string) bool { + for _, arg := range args { + if arg == want { + return true + } + } + return false +} diff --git a/scripts/run-integration-tests.sh b/scripts/run-integration-tests.sh index b66388b65..ee092223e 100644 --- a/scripts/run-integration-tests.sh +++ b/scripts/run-integration-tests.sh @@ -73,10 +73,12 @@ run_suite() { trap cleanup EXIT echo "==> [$suite_name] resolving services" - mapfile -t services < <(docker compose "${compose_args[@]}" config --services) + local services=() + while IFS= read -r service; do + services+=("$service") + done < <(docker compose "${compose_args[@]}" config --services) local dependency_services=() - local service for service in "${services[@]}"; do if [[ "$service" != "$runner_service" ]]; then dependency_services+=("$service") @@ -89,7 +91,9 @@ run_suite() { fi echo "==> [$suite_name] running: $TEST_COMMAND" - docker compose "${compose_args[@]}" run --rm "$runner_service" sh -c "$TEST_COMMAND" + # integration-runner already uses `bash -lc` as its entrypoint, so pass the + # suite command as a single argument for Bash to execute directly. + docker compose "${compose_args[@]}" run --rm "$runner_service" "$TEST_COMMAND" ) }