diff --git a/scripts/check_deliberate_break.py b/scripts/check_deliberate_break.py index 7144e04e0..1950403df 100644 --- a/scripts/check_deliberate_break.py +++ b/scripts/check_deliberate_break.py @@ -251,6 +251,17 @@ def _uses_pytest_runtime(command: tuple[str, ...]) -> bool: """Return whether a command runs pytest in the active Python environment.""" if not command: return False + if Path(command[0]).name == "pytest": + pytest_path = shutil.which(command[0]) + if not pytest_path: + return False + launcher = _python_shebang_launcher(Path(pytest_path), shutil.which) + if launcher is None: + return False + probe = (*launcher, "-c", PYYAML_PROBE_CODE) + return Path(launcher[0]).resolve() == Path( + sys.executable + ).resolve() and not _python_probe_changes_import_context(probe) executable = shutil.which(command[0]) or command[0] probe = _python_module_pytest_probe(command, 0) return ( @@ -780,7 +791,7 @@ def verify_spec( except subprocess.TimeoutExpired as exc: return _json_result( VERDICT_BROKEN, - reason="command-timeout", + reason="tamper-check-timeout", command=list(exc.cmd) if isinstance(exc.cmd, (tuple, list)) else str(exc.cmd), timeout=exc.timeout, ) diff --git a/templates/consumer-repo/scripts/check_deliberate_break.py b/templates/consumer-repo/scripts/check_deliberate_break.py index 7144e04e0..1950403df 100644 --- a/templates/consumer-repo/scripts/check_deliberate_break.py +++ b/templates/consumer-repo/scripts/check_deliberate_break.py @@ -251,6 +251,17 @@ def _uses_pytest_runtime(command: tuple[str, ...]) -> bool: """Return whether a command runs pytest in the active Python environment.""" if not command: return False + if Path(command[0]).name == "pytest": + pytest_path = shutil.which(command[0]) + if not pytest_path: + return False + launcher = _python_shebang_launcher(Path(pytest_path), shutil.which) + if launcher is None: + return False + probe = (*launcher, "-c", PYYAML_PROBE_CODE) + return Path(launcher[0]).resolve() == Path( + sys.executable + ).resolve() and not _python_probe_changes_import_context(probe) executable = shutil.which(command[0]) or command[0] probe = _python_module_pytest_probe(command, 0) return ( @@ -780,7 +791,7 @@ def verify_spec( except subprocess.TimeoutExpired as exc: return _json_result( VERDICT_BROKEN, - reason="command-timeout", + reason="tamper-check-timeout", command=list(exc.cmd) if isinstance(exc.cmd, (tuple, list)) else str(exc.cmd), timeout=exc.timeout, ) diff --git a/tests/scripts/test_check_deliberate_break.py b/tests/scripts/test_check_deliberate_break.py index bb5b7bca8..58a004f97 100644 --- a/tests/scripts/test_check_deliberate_break.py +++ b/tests/scripts/test_check_deliberate_break.py @@ -202,6 +202,28 @@ def test_tamper_os_error_is_broken(tmp_path, monkeypatch) -> None: } +def test_tamper_timeout_has_distinct_reason(tmp_path, monkeypatch) -> None: + repo = tmp_path / "repo" + repo.mkdir() + _init_repo(repo) + base, spec = _sound_spec(repo) + command = ["git", "diff", f"{base}...HEAD"] + monkeypatch.setattr( + deliberate_break, + "_changed_assertions", + lambda *_args: (_ for _ in ()).throw(subprocess.TimeoutExpired(command, 17)), + ) + + result = verify_spec(spec, base=base, cwd=repo) + + assert result == { + "verdict": VERDICT_BROKEN, + "reason": "tamper-check-timeout", + "command": command, + "timeout": 17, + } + + def test_new_assertion_in_existing_test_file_is_not_tamper(tmp_path, monkeypatch) -> None: repo = tmp_path / "repo" repo.mkdir() @@ -998,6 +1020,30 @@ def test_active_python_with_flags_is_managed_pytest_runtime() -> None: ) +def test_plain_pytest_with_active_python_shebang_is_managed(tmp_path, monkeypatch) -> None: + pytest_launcher = tmp_path / "pytest" + pytest_launcher.write_text(f"#!{sys.executable}\n", encoding="utf-8") + monkeypatch.setattr( + deliberate_break.shutil, + "which", + lambda name: str(pytest_launcher) if name == "pytest" else None, + ) + + assert deliberate_break._uses_pytest_runtime(("pytest", "-q")) + + +def test_plain_pytest_with_changed_import_context_is_not_managed(tmp_path, monkeypatch) -> None: + pytest_launcher = tmp_path / "pytest" + pytest_launcher.write_text(f"#!{sys.executable} -I\n", encoding="utf-8") + monkeypatch.setattr( + deliberate_break.shutil, + "which", + lambda name: str(pytest_launcher) if name == "pytest" else None, + ) + + assert not deliberate_break._uses_pytest_runtime(("pytest", "-q")) + + def test_active_python_with_hash_policy_is_managed_pytest_runtime() -> None: assert deliberate_break._uses_pytest_runtime( (