fix: preserve native model switches and bound project searches
This commit is contained in:
1 parent
db5e876f40
commit
e7d3080e0a
14 files changed
+371
-35
No files matched your search
@@ -20,6 +20,7 @@ import subprocess
|
||||
import pytest
|
||||
|
||||
from tools.file_operations import ShellFileOperations
|
||||
from tools.environments.local import LocalEnvironment
|
||||
|
||||
# Big enough to prove containment, small enough to keep the test fast.
|
||||
GIANT = 5 * 1024 * 1024 # 5MB single line
|
||||
@@ -27,22 +28,20 @@ GIANT = 5 * 1024 * 1024 # 5MB single line
|
||||
STDOUT_CEILING = 1 * 1024 * 1024
|
||||
|
||||
|
||||
class RecordingEnv:
|
||||
class RecordingEnv(LocalEnvironment):
|
||||
"""Local bash executor that records the largest stdout it returned."""
|
||||
|
||||
def __init__(self, cwd):
|
||||
self.cwd = cwd
|
||||
super().__init__(cwd=cwd)
|
||||
self.max_stdout = 0
|
||||
|
||||
def execute(self, command, timeout=60, **kwargs):
|
||||
proc = subprocess.run(
|
||||
["bash", "-c", command],
|
||||
capture_output=True, text=True, errors="replace",
|
||||
timeout=timeout + 30,
|
||||
)
|
||||
out = proc.stdout + (proc.stderr or "")
|
||||
# Use the application's shell selection/path handling. On Windows a
|
||||
# bare 'bash' may select WSL, whose filesystem is not the native one.
|
||||
result = super().execute(command, timeout=timeout + 30, **kwargs)
|
||||
out = result.get("output", "")
|
||||
self.max_stdout = max(self.max_stdout, len(out))
|
||||
return {"output": out, "returncode": proc.returncode}
|
||||
return result
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
@@ -58,6 +57,8 @@ def _ops(giant_dir, engine):
|
||||
env = RecordingEnv(str(giant_dir))
|
||||
ops = ShellFileOperations(env)
|
||||
ops._has_command = lambda cmd: cmd == engine
|
||||
resolve = ops._resolve_command
|
||||
ops._resolve_command = lambda cmd: None if cmd == "rg" and engine != "rg" else resolve(cmd)
|
||||
return ops, env
|
||||
|
||||
|
||||
@@ -96,5 +97,5 @@ def test_line_cap_skipped_for_path_and_count_modes(giant_dir, engine, output_mod
|
||||
if output_mode == "files_only":
|
||||
assert {os.path.basename(f) for f in result.files} == {"trace.json", "small.py"}
|
||||
else:
|
||||
assert {os.path.basename(k) for k in result.counts} == {"trace.json", "small.py"}
|
||||
assert {os.path.basename(k) for k, count in result.counts.items() if count} == {"trace.json", "small.py"}
|
||||
assert env.max_stdout < STDOUT_CEILING
|
||||
@@ -0,0 +1,112 @@
|
||||
"""Project scope and resource limits: real local paths, no broad subprocesses."""
|
||||
from unittest.mock import Mock
|
||||
|
||||
from tools.file_operations import ShellFileOperations, SearchResult, ExecuteResult
|
||||
from tools.environments.local import LocalEnvironment
|
||||
|
||||
|
||||
def projects(tmp_path):
|
||||
for name in ('one', 'two'):
|
||||
project = tmp_path / name
|
||||
project.mkdir()
|
||||
(project / '.git').mkdir()
|
||||
(project / 'source.py').write_text('needle\n')
|
||||
return tmp_path / 'one'
|
||||
|
||||
|
||||
def test_ambiguous_scope_starts_no_process(tmp_path):
|
||||
projects(tmp_path)
|
||||
ops = ShellFileOperations(LocalEnvironment(str(tmp_path)))
|
||||
ops._exec = Mock(side_effect=AssertionError('must not start process'))
|
||||
result = ops.search('needle', str(tmp_path))
|
||||
assert 'scope' in result.error.lower()
|
||||
assert 'one' in result.error and 'two' in result.error
|
||||
ops._exec.assert_not_called()
|
||||
|
||||
|
||||
def test_umbrella_uses_cwd_project_and_reports_it(tmp_path):
|
||||
project = projects(tmp_path)
|
||||
ops = ShellFileOperations(LocalEnvironment(str(project)))
|
||||
ops._exec = Mock(return_value=ExecuteResult(stdout='exists', exit_code=0))
|
||||
ops._search_content = Mock(return_value=SearchResult())
|
||||
result = ops.search('needle', str(tmp_path))
|
||||
assert ops._search_content.call_args.args[1] == str(project)
|
||||
assert str(project) in result.warning
|
||||
|
||||
|
||||
def test_broad_opt_in_and_explicit_file(tmp_path):
|
||||
project = projects(tmp_path)
|
||||
ops = ShellFileOperations(LocalEnvironment(str(project)))
|
||||
ops._exec = Mock(return_value=ExecuteResult(stdout='exists', exit_code=0))
|
||||
ops._search_content = Mock(return_value=SearchResult())
|
||||
for root, broad in ((tmp_path, True), (project / 'source.py', False)):
|
||||
assert not ops.search('needle', str(root), allow_broad=broad).error
|
||||
assert ops._search_content.call_args.args[1] == str(root)
|
||||
|
||||
|
||||
def test_rg_commands_bound_threads_and_prune(tmp_path):
|
||||
project = projects(tmp_path)
|
||||
ops = ShellFileOperations(LocalEnvironment(str(project)))
|
||||
ops._exec = Mock(return_value=ExecuteResult(stdout='', exit_code=1))
|
||||
ops._search_with_rg('needle', str(project), '*.py', 50, 0, 'content', 0, rg_executable='rg')
|
||||
command = ops._exec.call_args.args[0]
|
||||
assert '--threads 2' in command
|
||||
assert 'node_modules' in command and '.git' in command
|
||||
assert '*.py' in command
|
||||
ops._search_files_rg('*.py', str(project), 50, 0, rg_executable='rg')
|
||||
command = ops._exec.call_args.args[0]
|
||||
assert '--threads 2' in command and 'node_modules' in command
|
||||
|
||||
|
||||
def test_concurrent_searches_have_two_slots_and_release_after_failure(tmp_path):
|
||||
import threading
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
project = projects(tmp_path)
|
||||
lock = threading.Lock()
|
||||
release = threading.Event()
|
||||
entered = threading.Event()
|
||||
active = peak = 0
|
||||
|
||||
def run_search(*args):
|
||||
nonlocal active, peak
|
||||
with lock:
|
||||
active += 1
|
||||
peak = max(peak, active)
|
||||
if active == 2:
|
||||
entered.set()
|
||||
try:
|
||||
assert release.wait(5)
|
||||
return SearchResult()
|
||||
finally:
|
||||
with lock:
|
||||
active -= 1
|
||||
|
||||
with ThreadPoolExecutor(max_workers=6) as pool:
|
||||
pending = []
|
||||
for _ in range(6):
|
||||
ops = ShellFileOperations(LocalEnvironment(str(project)))
|
||||
ops._search_scoped = run_search
|
||||
pending.append(pool.submit(ops.search, 'needle'))
|
||||
try:
|
||||
assert entered.wait(3)
|
||||
assert peak == 2
|
||||
finally:
|
||||
release.set()
|
||||
assert all(not task.result().error for task in pending)
|
||||
assert peak == 2
|
||||
|
||||
|
||||
def test_real_rg_project_content_and_nested_glob(tmp_path):
|
||||
project = projects(tmp_path)
|
||||
(project / 'src').mkdir()
|
||||
(project / 'src' / 'nested.py').write_text('needle\n')
|
||||
(project / 'node_modules').mkdir()
|
||||
(project / 'node_modules' / 'noise.py').write_text('needle\n')
|
||||
ops = ShellFileOperations(LocalEnvironment(str(project)))
|
||||
result = ops.search('needle', str(tmp_path), file_glob='*.py')
|
||||
assert not result.error
|
||||
assert len(result.matches) == 2
|
||||
assert all('node_modules' not in item.path for item in result.matches)
|
||||
result = ops.search('src/*.py', str(project), target='files')
|
||||
assert not result.error
|
||||
assert len(result.files) == 1 and 'nested.py' in result.files[0]
|
||||
Reference in new issue
Block a user