Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions src/roam/commands/cmd_verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -3716,14 +3716,44 @@ def _gather_and_rank_tests(conn, sym_ids, src_paths, changed_paths, root):
return _existing_ranked_paths(ranked, root), None


def _resolve_test_interpreter(root: Path, first_target: str) -> str:
"""Find the interpreter that owns the target project's test dependencies.

roam is commonly installed isolated from any one project (``uv tool
install``, pipx), so ``sys.executable`` is roam's own interpreter, not
the project under test — it has neither pytest nor the project's
dependencies, so collection fails before any test runs. Walk up from the
first impacted test file (not just ``root``, so a venv nested under a
subdirectory of a monorepo is found before a repo-root one) looking for
a ``.venv``/``venv``. Falls back to ``sys.executable``, preserving
today's behaviour when no project venv is found (e.g. roam is already
running inside the project's own venv).
"""
import sys

bin_dir = "Scripts" if os.name == "nt" else "bin"
exe_name = "python.exe" if os.name == "nt" else "python"

start = (root / first_target).parent
for directory in (start, *start.parents):
for venv_name in (".venv", "venv"):
candidate = directory / venv_name / bin_dir / exe_name
if candidate.exists():
return str(candidate)
if directory == root:
break

return sys.executable


def _run_impacted_pytest(ordered: list[str], root: Path, timeout: int) -> dict:
"""Run pytest over the impacted (capped) test files and report failures."""
import subprocess
import sys

capped = len(ordered) > _MAX_TEST_FILES
targets = ordered[:_MAX_TEST_FILES]
cmd = [sys.executable, "-B", "-m", "pytest", *targets, "--tb=line", "-q", "-p", "no:cacheprovider"]
interpreter = _resolve_test_interpreter(root, targets[0])
cmd = [interpreter, "-B", "-m", "pytest", *targets, "--tb=line", "-q", "-p", "no:cacheprovider"]
try:
proc = subprocess.run(cmd, cwd=str(root), capture_output=True, text=True, timeout=timeout)
except subprocess.TimeoutExpired:
Expand Down
79 changes: 79 additions & 0 deletions tests/test_verify_autofire_hardening.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,85 @@ class Result:
assert result["violations"][0]["hard_block"] is True


def test_impacted_pytest_prefers_the_target_projects_own_venv(monkeypatch, tmp_path):
"""roam commonly runs isolated from any one project (uv tool install,
pipx) — sys.executable is roam's own interpreter, which has neither
pytest nor the target project's dependencies. It must prefer a venv
that actually belongs to the project under test."""
from roam.commands import cmd_verify

bin_dir = "Scripts" if os.name == "nt" else "bin"
exe_name = "python.exe" if os.name == "nt" else "python"
venv_python = tmp_path / ".venv" / bin_dir / exe_name
venv_python.parent.mkdir(parents=True)
venv_python.touch()

class Result:
returncode = 0
stdout = "1 passed"
stderr = ""

captured = {}
monkeypatch.setattr(
cmd_verify.subprocess,
"run",
lambda cmd, **kwargs: captured.setdefault("cmd", cmd) and Result(),
)
cmd_verify._run_impacted_pytest(["tests/test_app.py"], tmp_path, timeout=1)

assert captured["cmd"][0] == str(venv_python)


def test_impacted_pytest_prefers_a_venv_nested_under_the_target_file(monkeypatch, tmp_path):
"""A monorepo subproject's own venv (e.g. a nested Python service in an
otherwise non-Python repo) must win over a repo-root venv, since that is
the interpreter that actually has the subproject's dependencies."""
from roam.commands import cmd_verify

bin_dir = "Scripts" if os.name == "nt" else "bin"
exe_name = "python.exe" if os.name == "nt" else "python"
root_venv = tmp_path / ".venv" / bin_dir / exe_name
root_venv.parent.mkdir(parents=True)
root_venv.touch()
nested_venv = tmp_path / "service" / ".venv" / bin_dir / exe_name
nested_venv.parent.mkdir(parents=True)
nested_venv.touch()

class Result:
returncode = 0
stdout = "1 passed"
stderr = ""

captured = {}
monkeypatch.setattr(
cmd_verify.subprocess,
"run",
lambda cmd, **kwargs: captured.setdefault("cmd", cmd) and Result(),
)
cmd_verify._run_impacted_pytest(["service/tests/test_app.py"], tmp_path, timeout=1)

assert captured["cmd"][0] == str(nested_venv)


def test_impacted_pytest_falls_back_to_roams_own_interpreter_with_no_project_venv(monkeypatch, tmp_path):
from roam.commands import cmd_verify

class Result:
returncode = 0
stdout = "1 passed"
stderr = ""

captured = {}
monkeypatch.setattr(
cmd_verify.subprocess,
"run",
lambda cmd, **kwargs: captured.setdefault("cmd", cmd) and Result(),
)
cmd_verify._run_impacted_pytest(["tests/test_app.py"], tmp_path, timeout=1)

assert captured["cmd"][0] == sys.executable


def test_capped_impacted_tests_are_disclosed_as_partial(monkeypatch, tmp_path):
from roam.commands import cmd_verify

Expand Down