diff --git a/tests/_bash.py b/tests/_bash.py new file mode 100644 index 00000000..f480b330 --- /dev/null +++ b/tests/_bash.py @@ -0,0 +1,36 @@ +import functools +import os +import shutil +import subprocess + +# Warning: get_bash_executable and require_bash use @functools.lru_cache. +# Do not monkeypatch os.environ['PATH'] or shutil.which in tests and expect +# these functions to re-evaluate. If you must patch them, call .cache_clear() +# before and after the test. + +@functools.lru_cache(maxsize=1) +def get_bash_executable() -> str | None: + if os.name == 'nt': + for candidate in ( + r'C:\Program Files\Git\bin\bash.exe', + r'C:\Program Files\Git\usr\bin\bash.exe', + r'C:\Program Files (x86)\Git\bin\bash.exe', + os.path.expandvars(r'%LOCALAPPDATA%\Programs\Git\bin\bash.exe'), + ): + if os.path.exists(candidate): + return candidate + bash = shutil.which('bash') + if bash and 'WindowsApps' not in bash: + return bash + return None + +@functools.lru_cache(maxsize=1) +def require_bash() -> bool: + bash = get_bash_executable() + if not bash: + return False + try: + res = subprocess.run([bash, '-c', 'echo 1'], capture_output=True, text=True, timeout=2) + return res.returncode == 0 + except Exception: + return False diff --git a/tests/test_pre_push_hook_contract.py b/tests/test_pre_push_hook_contract.py index a9ef026a..a7e96eed 100644 --- a/tests/test_pre_push_hook_contract.py +++ b/tests/test_pre_push_hook_contract.py @@ -5,34 +5,7 @@ import subprocess import unittest from pathlib import Path - -@functools.lru_cache(maxsize=1) -def get_bash_executable() -> str | None: - if os.name == 'nt': - for candidate in ( - r'C:\Program Files\Git\bin\bash.exe', - r'C:\Program Files\Git\usr\bin\bash.exe', - r'C:\Program Files (x86)\Git\bin\bash.exe', - os.path.expandvars(r'%LOCALAPPDATA%\Programs\Git\bin\bash.exe'), - ): - if os.path.exists(candidate): - return candidate - bash = shutil.which('bash') - if bash and 'WindowsApps' not in bash: - return bash - return None - - -@functools.lru_cache(maxsize=1) -def require_bash() -> bool: - bash = get_bash_executable() - if not bash: - return False - try: - res = subprocess.run([bash, '-c', 'echo 1'], capture_output=True, text=True, timeout=2) - return res.returncode == 0 - except Exception: - return False +from tests._bash import get_bash_executable, require_bash REPO_ROOT = Path(__file__).resolve().parents[1] diff --git a/tests/test_roadmap_helpers.py b/tests/test_roadmap_helpers.py index f7307c71..8fe5e0e4 100644 --- a/tests/test_roadmap_helpers.py +++ b/tests/test_roadmap_helpers.py @@ -14,34 +14,7 @@ REPO_ROOT = Path(__file__).resolve().parents[1] NEXT_ID = REPO_ROOT / 'scripts' / 'roadmap-next-id.sh' DOGFOOD_PROBE = REPO_ROOT / 'scripts' / 'dogfood-probe.py' - -@functools.lru_cache(maxsize=1) -def get_bash_executable() -> str | None: - if os.name == 'nt': - for candidate in ( - r'C:\Program Files\Git\bin\bash.exe', - r'C:\Program Files\Git\usr\bin\bash.exe', - r'C:\Program Files (x86)\Git\bin\bash.exe', - os.path.expandvars(r'%LOCALAPPDATA%\Programs\Git\bin\bash.exe'), - ): - if os.path.exists(candidate): - return candidate - bash = shutil.which('bash') - if bash and 'WindowsApps' not in bash: - return bash - return None - - -@functools.lru_cache(maxsize=1) -def require_bash() -> bool: - bash = get_bash_executable() - if not bash: - return False - try: - res = subprocess.run([bash, '-c', 'echo 1'], capture_output=True, text=True, timeout=2) - return res.returncode == 0 - except Exception: - return False +from tests._bash import get_bash_executable, require_bash def run_next_id(roadmap: Path, script: Path = NEXT_ID) -> subprocess.CompletedProcess[str]: