From 4be9dc64223828d783ec49406efe6b03cd76a256 Mon Sep 17 00:00:00 2001 From: Pieter Noordhuis Date: Wed, 24 Jun 2026 22:57:22 +0200 Subject: [PATCH 1/2] acc: surface tool-version checks as a TestAccept/prerequisites subtest The acceptance prerequisite checks (jq, uv, ruff, python) ran inline in TestAccept, so a stale or missing tool failed as a bare TestAccept, which says nothing about the cause. Run them in a TestAccept/prerequisites subtest so the failure names the actual problem. Split EnsurePython into RequirePython (pure version check, safe in the subtest) and ConfigurePython (the PATH side effect). ConfigurePython runs on the parent TestAccept t after the subtest passes: it prepends to PATH via t.Setenv and uses t.TempDir, both of which are torn down when their test returns, so running it from a subtest would revert the PATH change before the rest of the suite runs. Co-authored-by: Isaac --- acceptance/acceptance_test.go | 38 ++++++++++++++++++++---------- acceptance/internal/python.go | 35 ++++++++++++++++++++------- acceptance/internal/python_test.go | 5 ++-- 3 files changed, 56 insertions(+), 22 deletions(-) diff --git a/acceptance/acceptance_test.go b/acceptance/acceptance_test.go index d05035652e..ea85225df7 100644 --- a/acceptance/acceptance_test.go +++ b/acceptance/acceptance_test.go @@ -191,17 +191,24 @@ 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. ConfigurePython is +// intentionally not run here: it mutates PATH for the rest of the run via +// t.Setenv, which a subtest would tear down on return. +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 +252,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() From 27237fb32e32d97f1749b2dc4b320856e8b2cdc0 Mon Sep 17 00:00:00 2001 From: Pieter Noordhuis Date: Wed, 24 Jun 2026 23:03:03 +0200 Subject: [PATCH 2/2] acc: trim requirePrerequisites comment ConfigurePython's PATH side effect is documented on ConfigurePython itself; it is not a concern of requirePrerequisites. Co-authored-by: Isaac --- acceptance/acceptance_test.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/acceptance/acceptance_test.go b/acceptance/acceptance_test.go index ea85225df7..e4534ede0e 100644 --- a/acceptance/acceptance_test.go +++ b/acceptance/acceptance_test.go @@ -193,9 +193,7 @@ func hasRunFilter() bool { // producing confusing diffs deep into the run. // // It reports whether all checks passed; a failure surfaces as -// TestAccept/prerequisites rather than a bare TestAccept. ConfigurePython is -// intentionally not run here: it mutates PATH for the rest of the run via -// t.Setenv, which a subtest would tear down on return. +// 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).