Skip to content

Commit 149eac3

Browse files
committed
acc: make acceptance tests work in Databricks development environments
The acceptance harness blocks external network access during tests. In some Databricks-internal development environments, local tooling issues network calls that trip the sandbox and fail the suite even though CI passes. Ignore the local probes that are not test traffic and disable the background beacons so tests behave the same as on CI. Also assert the external toolchain (jq, uv, ruff) and provision python via uv up front, so a missing or stale tool fails fast with a clear message. Co-authored-by: Isaac
1 parent 179c38c commit 149eac3

13 files changed

Lines changed: 309 additions & 2 deletions

File tree

Taskfile.yml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,19 @@ vars:
66
EXE_EXT: '{{if eq OS "windows"}}.exe{{end}}'
77
TEST_PACKAGES: ./acceptance/internal ./libs/... ./internal/... ./cmd/... ./bundle/... ./experimental/ssh/... .
88
ACCEPTANCE_TEST_FILTER: ""
9+
# Python >= 3.11 (the version this repo's scripts target) resolved via uv, so
10+
# tasks fail clearly when uv or a suitable interpreter is missing instead of
11+
# running a helper script under an old default python3.
12+
PYTHON:
13+
sh: 'uv python find ">=3.11"'
914
# Single brace-expansion glob covering every //go:embed target in the repo,
1015
# computed by grepping `//go:embed` directives. Evaluated lazily by Task so
1116
# tasks that don't reference it pay nothing. testdata/ dirs are covered by
1217
# a separate static `**/testdata/**` glob, not this script.
1318
# Limitation: git grep only scans tracked files; new //go:embed directives in
1419
# untracked files are missed until the file is staged or committed.
1520
EMBED_SOURCES:
16-
sh: 'python3 tools/list_embeds.py'
21+
sh: '{{.PYTHON}} tools/list_embeds.py'
1722

1823
# pydabs-* tasks live in python/Taskfile.yml so `task pydabs-foo` works when
1924
# run from python/. Flattened so they keep their `pydabs-` names at the root.
@@ -948,7 +953,7 @@ tasks:
948953
generates:
949954
- bundle/direct/dresources/apitypes.generated.yml
950955
cmds:
951-
- "sh -c 'python3 bundle/direct/tools/generate_apitypes.py .codegen/cli.json acceptance/bundle/refschema/out.fields.txt > bundle/direct/dresources/apitypes.generated.yml'"
956+
- "sh -c '{{.PYTHON}} bundle/direct/tools/generate_apitypes.py .codegen/cli.json acceptance/bundle/refschema/out.fields.txt > bundle/direct/dresources/apitypes.generated.yml'"
952957

953958
generate-direct-resources:
954959
desc: Generate direct engine resources YAML

acceptance/acceptance_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,14 @@ func testAccept(t *testing.T, inprocessMode bool, singleTest string) int {
229229
os.Unsetenv(v) //nolint:usetesting // t.Setenv cannot unset
230230
}
231231

232+
// Verify external tool prerequisites before doing any work, so a stale
233+
// toolchain fails fast with an actionable message instead of producing
234+
// confusing diffs deep into the run.
235+
internal.RequireModernJq(t)
236+
internal.RequireModernUv(t)
237+
internal.RequireModernRuff(t)
238+
internal.EnsureModernPython(t)
239+
232240
buildDir := getBuildDir(t, cwd, runtime.GOOS, runtime.GOARCH)
233241

234242
// Set up terraform for tests. Skip on DBR - tests with RunsOnDbr only use direct deployment.
@@ -759,6 +767,21 @@ func runTest(t *testing.T,
759767
// into compared output. Tests can override this via [Env] in test.toml.
760768
cmd.Env = append(cmd.Env, "DATABRICKS_CLI_DISABLE_UPDATE_CHECK=true")
761769

770+
// Neutralize Databricks-internal development-environment interference so
771+
// acceptance tests behave the same as on CI (which has none of this). Two
772+
// sources both reach the blocking proxy on every git invocation:
773+
//
774+
// 1. A command-timing shim that wraps git (ahead of the real binary on
775+
// PATH) and POSTs per-command metrics over the network.
776+
// COMMAND_TIMER_DISABLE=1 makes it pass through without the beacon.
777+
// 2. A managed global git config installs a core.hooksPath whose hooks
778+
// (secret scanning, etc.) also beacon metrics. Ignoring the global and
779+
// system git config disables those hooks and keeps tests hermetic; tests
780+
// configure the repos they create via git-repo-init locally.
781+
cmd.Env = append(cmd.Env, "COMMAND_TIMER_DISABLE=1")
782+
cmd.Env = append(cmd.Env, "GIT_CONFIG_GLOBAL="+os.DevNull)
783+
cmd.Env = append(cmd.Env, "GIT_CONFIG_SYSTEM="+os.DevNull)
784+
762785
for _, kv := range testEnv {
763786
key, value, _ := strings.Cut(kv, "=")
764787
// Only add replacement by default if value is part of EnvMatrix with more than 1 option and length is 4 or more chars

acceptance/internal/jq.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package internal
2+
3+
import (
4+
"fmt"
5+
"os/exec"
6+
"strings"
7+
"testing"
8+
)
9+
10+
// RequireModernJq fails the run if jq is missing or older than 1.7. Acceptance
11+
// scripts use jq 1.7 features (the pick/1 builtin and the `.foo.[]` iteration
12+
// syntax); an older jq compiles them as errors and produces spurious diffs
13+
// across many tests rather than one clear failure.
14+
func RequireModernJq(t *testing.T) {
15+
out, err := exec.Command("jq", "--version").Output()
16+
if err != nil {
17+
t.Fatalf("jq not found on PATH (acceptance tests require jq >= 1.7): %v", err)
18+
}
19+
version := strings.TrimSpace(string(out))
20+
if !jqVersionOK(version) {
21+
t.Fatalf("acceptance tests require jq >= 1.7 (found %q); install a newer jq", version)
22+
}
23+
}
24+
25+
// jqVersionOK reports whether `jq --version` output (e.g. "jq-1.7.1") is >= 1.7.
26+
func jqVersionOK(version string) bool {
27+
var major, minor int
28+
if _, err := fmt.Sscanf(version, "jq-%d.%d", &major, &minor); err != nil {
29+
return false
30+
}
31+
return major > 1 || (major == 1 && minor >= 7)
32+
}

acceptance/internal/jq_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package internal
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestJqVersionOK(t *testing.T) {
10+
assert.True(t, jqVersionOK("jq-1.7"))
11+
assert.True(t, jqVersionOK("jq-1.7.1"))
12+
assert.True(t, jqVersionOK("jq-1.8.1"))
13+
assert.True(t, jqVersionOK("jq-2.0"))
14+
assert.False(t, jqVersionOK("jq-1.6"))
15+
assert.False(t, jqVersionOK("jq version 1.7"))
16+
assert.False(t, jqVersionOK(""))
17+
}

acceptance/internal/python.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package internal
2+
3+
import (
4+
"os"
5+
"os/exec"
6+
"path/filepath"
7+
"strings"
8+
"testing"
9+
)
10+
11+
// EnsureModernPython makes `python3` on PATH resolve to a Python >= 3.11, the
12+
// version this repo's scripts target. Acceptance scripts invoke `python3`
13+
// directly and some import stdlib modules added in 3.11 (e.g. tomllib in
14+
// acceptance/bundle/resources/permissions/analyze_requests.py), but a host's
15+
// default python3 may be older. uv (already required for building the
16+
// databricks-bundles wheel) discovers or provisions a suitable interpreter; we
17+
// symlink it as python3/python into a temp dir prepended to PATH so every
18+
// script and build step resolves it. Fails hard if uv is missing or has no
19+
// suitable Python.
20+
func EnsureModernPython(t *testing.T) {
21+
out, err := exec.Command("uv", "python", "find", ">=3.11").Output()
22+
if err != nil {
23+
t.Fatalf("uv could not find python >= 3.11: %v", err)
24+
}
25+
python := strings.TrimSpace(string(out))
26+
27+
binDir := t.TempDir()
28+
for _, link := range []string{"python3", "python"} {
29+
if err := os.Symlink(python, filepath.Join(binDir, link)); err != nil {
30+
t.Fatalf("failed to symlink %s as %s: %v", python, link, err)
31+
}
32+
}
33+
t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))
34+
t.Logf("acceptance tests: using %s (via uv) as python3", python)
35+
}

acceptance/internal/python_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package internal
2+
3+
import (
4+
"os/exec"
5+
"strings"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestEnsureModernPython(t *testing.T) {
13+
if _, err := exec.LookPath("uv"); err != nil {
14+
t.Skip("uv not installed")
15+
}
16+
17+
EnsureModernPython(t)
18+
19+
// After setup, the python3 resolved from PATH must satisfy the floor.
20+
out, err := exec.Command("python3", "-c", "import sys; print(sys.version_info >= (3, 11))").Output()
21+
require.NoError(t, err)
22+
assert.Equal(t, "True", strings.TrimSpace(string(out)))
23+
}

acceptance/internal/rejecting_proxy.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"net/http"
88
"strings"
99
"testing"
10+
11+
"github.com/databricks/cli/libs/testserver"
1012
)
1113

1214
// StartRejectingProxy starts an HTTP proxy server bound to a loopback port and
@@ -93,6 +95,12 @@ func handleBlockedConnection(t *testing.T, conn net.Conn, hint string) {
9395
if isLoopback || isReserved {
9496
// Expected unreachable fixture or local test server — log only, don't fail.
9597
t.Logf("blocking proxy: blocked loopback/reserved host: %s", detail)
98+
} else if testserver.IsLocalhostProbe(req) {
99+
// Some Databricks-internal development environments run a port watcher
100+
// that auto-forwards every new localhost listener and probes it with
101+
// `HEAD / Host: localhost`. This is not the CLI-under-test reaching the
102+
// internet, so log it instead of failing the test.
103+
t.Logf("blocking proxy: ignored localhost port-classification probe: %s", detail)
96104
} else {
97105
t.Errorf("internet access blocked by proxy: %s%s", detail, hint)
98106
}

acceptance/internal/ruff.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package internal
2+
3+
import (
4+
"fmt"
5+
"os/exec"
6+
"strings"
7+
"testing"
8+
)
9+
10+
// RequireModernRuff fails the run if ruff is missing or older than 0.9.1, the
11+
// version pinned across the repo (python/pyproject.toml, Taskfile.yml). The
12+
// pydabs check-formatting acceptance test runs `ruff format` and its golden
13+
// output assumes that formatter behavior.
14+
func RequireModernRuff(t *testing.T) {
15+
out, err := exec.Command("ruff", "--version").Output()
16+
if err != nil {
17+
t.Fatalf("ruff not found on PATH (acceptance tests require ruff >= 0.9.1): %v", err)
18+
}
19+
version := strings.TrimSpace(string(out))
20+
if !ruffVersionOK(version) {
21+
t.Fatalf("acceptance tests require ruff >= 0.9.1 (found %q); install a newer ruff", version)
22+
}
23+
}
24+
25+
// ruffVersionOK reports whether `ruff --version` output (e.g. "ruff 0.9.1") is >= 0.9.1.
26+
func ruffVersionOK(version string) bool {
27+
var major, minor, patch int
28+
if _, err := fmt.Sscanf(version, "ruff %d.%d.%d", &major, &minor, &patch); err != nil {
29+
return false
30+
}
31+
if major != 0 {
32+
return major > 0
33+
}
34+
if minor != 9 {
35+
return minor > 9
36+
}
37+
return patch >= 1
38+
}

acceptance/internal/ruff_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package internal
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestRuffVersionOK(t *testing.T) {
10+
assert.True(t, ruffVersionOK("ruff 0.9.1"))
11+
assert.True(t, ruffVersionOK("ruff 0.9.2"))
12+
assert.True(t, ruffVersionOK("ruff 0.11.0"))
13+
assert.True(t, ruffVersionOK("ruff 1.0.0"))
14+
assert.False(t, ruffVersionOK("ruff 0.9.0"))
15+
assert.False(t, ruffVersionOK("ruff 0.8.5"))
16+
assert.False(t, ruffVersionOK(""))
17+
}

acceptance/internal/uv.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package internal
2+
3+
import (
4+
"fmt"
5+
"os/exec"
6+
"strings"
7+
"testing"
8+
)
9+
10+
// RequireModernUv fails the run if uv is missing or older than 0.4. uv builds
11+
// the databricks-bundles wheel and provides the test interpreter via
12+
// `uv python find` (see EnsureModernPython), which landed in the 0.3 line; 0.4
13+
// is a small margin above that.
14+
func RequireModernUv(t *testing.T) {
15+
out, err := exec.Command("uv", "--version").Output()
16+
if err != nil {
17+
t.Fatalf("uv not found on PATH (acceptance tests require uv >= 0.4): %v", err)
18+
}
19+
version := strings.TrimSpace(string(out))
20+
if !uvVersionOK(version) {
21+
t.Fatalf("acceptance tests require uv >= 0.4 (found %q); install a newer uv", version)
22+
}
23+
}
24+
25+
// uvVersionOK reports whether `uv --version` output (e.g. "uv 0.11.22 (abc)") is >= 0.4.
26+
func uvVersionOK(version string) bool {
27+
var major, minor int
28+
if _, err := fmt.Sscanf(version, "uv %d.%d", &major, &minor); err != nil {
29+
return false
30+
}
31+
return major > 0 || minor >= 4
32+
}

0 commit comments

Comments
 (0)