diff --git a/acceptance/acceptance_test.go b/acceptance/acceptance_test.go index d05035652e..e4534ede0e 100644 --- a/acceptance/acceptance_test.go +++ b/acceptance/acceptance_test.go @@ -191,17 +191,22 @@ func hasRunFilter() bool { // requirePrerequisites verifies external tool prerequisites before doing any // work, so a stale toolchain fails fast with an actionable message instead of // producing confusing diffs deep into the run. -func requirePrerequisites(t *testing.T) { - // Scripts use jq 1.7 features (the pick/1 builtin and the `.foo.[]` iteration syntax). - internal.RequireJQ(t, "1.7") - // uv builds the databricks-bundles wheel and provides the test interpreter - // via `uv python find`, which landed in the 0.3 line. - internal.RequireUV(t, "0.4") - // ruff 0.9.1 is pinned across the repo (python/pyproject.toml, Taskfile.yml); - // the check-formatting test's golden output assumes its formatter behavior. - internal.RequireRuff(t, "0.9.1") - // Acceptance scripts import the stdlib tomllib module, added in Python 3.11. - internal.EnsurePython(t, "3.11") +// +// It reports whether all checks passed; a failure surfaces as +// TestAccept/prerequisites rather than a bare TestAccept. +func requirePrerequisites(t *testing.T) bool { + return t.Run("prerequisites", func(t *testing.T) { + // Scripts use jq 1.7 features (the pick/1 builtin and the `.foo.[]` iteration syntax). + internal.RequireJQ(t, "1.7") + // uv builds the databricks-bundles wheel and provides the test interpreter + // via `uv python find`, which landed in the 0.3 line. + internal.RequireUV(t, "0.4") + // ruff 0.9.1 is pinned across the repo (python/pyproject.toml, Taskfile.yml); + // the check-formatting test's golden output assumes its formatter behavior. + internal.RequireRuff(t, "0.9.1") + // Acceptance scripts import the stdlib tomllib module, added in Python 3.11. + internal.RequirePython(t, "3.11") + }) } func testAccept(t *testing.T, inprocessMode bool, singleTest string) int { @@ -245,7 +250,14 @@ func testAccept(t *testing.T, inprocessMode bool, singleTest string) int { os.Unsetenv(v) //nolint:usetesting // t.Setenv cannot unset } - requirePrerequisites(t) + if !requirePrerequisites(t) { + // Don't run the suite against a stale toolchain; the failed subtest + // has already marked the parent test as failed. + return 0 + } + // Run after the version check passed; must use the top-level t so the PATH + // change survives for the rest of the run. + internal.ConfigurePython(t, "3.11") buildDir := getBuildDir(t, cwd, runtime.GOOS, runtime.GOARCH) diff --git a/acceptance/internal/python.go b/acceptance/internal/python.go index 13f1554421..6a3af08cc6 100644 --- a/acceptance/internal/python.go +++ b/acceptance/internal/python.go @@ -11,14 +11,12 @@ import ( "golang.org/x/mod/semver" ) -// EnsurePython makes `python3` on PATH resolve to a Python >= minVersion (e.g. -// "3.11"). Acceptance scripts invoke `python3` directly and some import stdlib -// modules added in newer versions, but a host's default python3 may be older. -// On non-Windows hosts uv (see RequireUV) selects a compatible interpreter, -// which we symlink as python3/python into a temp dir prepended to PATH. On -// Windows os.Symlink needs extra privileges, so we instead require that the -// python3 already on PATH satisfies the floor. -func EnsurePython(t *testing.T, minVersion string) { +// RequirePython fails the test if no Python >= minVersion (e.g. "3.11") is +// available for the acceptance suite. It only verifies; ConfigurePython performs +// the PATH setup. On Windows os.Symlink needs extra privileges, so we require the +// python3 already on PATH to satisfy the floor; elsewhere uv must be able to find +// a compatible interpreter. +func RequirePython(t *testing.T, minVersion string) { if runtime.GOOS == "windows" { out, err := exec.Command("python3", "--version").Output() if err != nil { @@ -31,6 +29,27 @@ func EnsurePython(t *testing.T, minVersion string) { return } + if _, err := exec.Command("uv", "python", "find", ">="+minVersion).Output(); err != nil { + t.Fatalf("uv could not find python >= %s: %v", minVersion, err) + } +} + +// ConfigurePython makes `python3` on PATH resolve to a Python >= minVersion for +// the rest of the run. Acceptance scripts invoke `python3` directly and some +// import stdlib modules added in newer versions, but a host's default python3 +// may be older. On non-Windows hosts uv (see RequireUV) selects a compatible +// interpreter, which we symlink as python3/python into a temp dir prepended to +// PATH. On Windows we rely on the python3 already on PATH, which RequirePython +// has verified satisfies the floor. +// +// It must run on the top-level test's t: the t.Setenv and t.TempDir below are +// undone when that test returns, so calling it from a subtest would revert the +// PATH change before the rest of the suite runs. +func ConfigurePython(t *testing.T, minVersion string) { + if runtime.GOOS == "windows" { + return + } + out, err := exec.Command("uv", "python", "find", ">="+minVersion).Output() if err != nil { t.Fatalf("uv could not find python >= %s: %v", minVersion, err) diff --git a/acceptance/internal/python_test.go b/acceptance/internal/python_test.go index c7ba0a41ae..4a0ebcd800 100644 --- a/acceptance/internal/python_test.go +++ b/acceptance/internal/python_test.go @@ -16,12 +16,13 @@ func TestPythonVersionOK(t *testing.T) { assert.False(t, pythonVersionOK("garbage", "3.11")) } -func TestEnsurePython(t *testing.T) { +func TestConfigurePython(t *testing.T) { if _, err := exec.LookPath("uv"); err != nil { t.Skip("uv not installed") } - EnsurePython(t, "3.11") + RequirePython(t, "3.11") + ConfigurePython(t, "3.11") // After setup, the python3 resolved from PATH must satisfy the floor. out, err := exec.Command("python3", "-c", "import sys; print(sys.version_info >= (3, 11))").Output()