"""Local POSIX content/file search runs rg as a direct argv subprocess. The shell path pays two bash spawns per search (``test -e`` probe + ``set -o pipefail; rg ... | head``). On a ``LocalEnvironment`` the same rg argv can run natively with a bounded stdout read; the parser and every argument builder are shared, so the two transports must agree on results. """ import json import sys import pytest from tools.environments.local import LocalEnvironment from tools.file_operations import ShellFileOperations pytestmark = pytest.mark.skipif(sys.platform == "win32", reason="native rg lane is POSIX-only") @pytest.fixture def tree(tmp_path): (tmp_path / "a.py").write_text("needle one\nplain\nneedle two\n") (tmp_path / "b.txt").write_text("needle three\n") (tmp_path / "sub").mkdir() (tmp_path / "sub" / "c.py").write_text("needle four\n") return tmp_path @pytest.fixture(scope="module") def _local_env(tmp_path_factory): """One real LocalEnvironment per module (constructing one costs ~0.8 s).""" return LocalEnvironment(cwd=str(tmp_path_factory.mktemp("native-rg"))) @pytest.fixture def ops_factory(_local_env): """``make(tree, spy)`` → ShellFileOperations over the shared env, every execute recorded in ``spy``.""" real = type(_local_env).execute.__get__(_local_env, type(_local_env)) def make(tree, spy): _local_env.cwd = str(tree) def recording(command, *a, **kw): spy.append(command) return real(command, *a, **kw) _local_env.execute = recording return ShellFileOperations(_local_env, cwd=str(tree)) yield make _local_env.__dict__.pop("execute", None) def _normalized(result): d = result.to_dict() for key in ("matches", "files"): if key in d: d[key] = sorted(json.dumps(item, sort_keys=True) for item in d[key]) return d def test_native_search_never_touches_the_shell_and_matches_shell_results(tree, ops_factory, monkeypatch): cases = [ dict(pattern="needle", path=str(tree)), # (no offset/limit slicing here: rg's parallel walk orders files # nondeterministically, so a page differs run-to-run on either transport) dict(pattern="needle", path=str(tree), output_mode="count"), dict(pattern="needle", path=str(tree), output_mode="files_only", file_glob="*.py"), dict(pattern="NEEDLE_NOPE", path=str(tree)), # zero-match probes dict(pattern="*.py", path=str(tree), target="files"), dict(pattern="needle", path=str(tree / "missing")), ] for case in cases: monkeypatch.setenv("HERMES_NATIVE_FILE_READ", "0") shell = _normalized(ops_factory(tree, []).search(**case)) monkeypatch.setenv("HERMES_NATIVE_FILE_READ", "1") calls = [] native = _normalized(ops_factory(tree, calls).search(**case)) assert native == shell, case # rg resolution (``command -v rg``) still goes through the shell once; the # existence probe and the rg pipeline itself must not. assert not [c for c in calls if "pipefail" in c or c.startswith("test -e")], case def test_native_runner_honours_deadline_and_interrupt_while_rg_is_silent(tree, ops_factory, monkeypatch): """A search producing no output must still stop at the deadline / on /stop (the shell path gets this from ``_wait_for_process``).""" import threading import time from tools import interrupt ops = ops_factory(tree, []) started = time.monotonic() result = ops._run_rg_native(["sh", "-c", "'sleep 30'"], 5, timeout=1) assert result.exit_code == 124 and time.monotonic() - started < 5 tid = threading.get_ident() threading.Timer(0.3, lambda: interrupt.set_interrupt(True, tid)).start() try: started = time.monotonic() result = ops._run_rg_native(["sh", "-c", "'sleep 30'"], 5, timeout=30) finally: interrupt.set_interrupt(False, tid) assert result.exit_code == 130 and time.monotonic() - started < 5 def test_kill_switch_routes_search_back_to_the_shell(tree, ops_factory, monkeypatch): monkeypatch.setenv("HERMES_NATIVE_FILE_READ", "0") calls = [] result = ops_factory(tree, calls).search(pattern="needle", path=str(tree)) assert result.total_count == 4 assert any(c.startswith("test -e") for c in calls) assert any("pipefail" in c and "rg" in c for c in calls)