From c31c5e27ddbc16a5b0fb1b4c78f22c1a58a9bb50 Mon Sep 17 00:00:00 2001 From: Gracy769 <169199616+Gracy769@users.noreply.github.com> Date: Thu, 3 Sep 2026 08:56:45 +0530 Subject: [PATCH] test: extract bash detection to shared _bash helper Moves get_bash_executable and require_bash into a shared module so their lru_cache state is per-session rather than per-test-module. Also adds a comment regarding monkeypatching since lru_cache will silently drop changes to os.environ if it has already been populated. Closes follow-up items from PR review. --- tests/_bash.py | 36 ++++++++++++++++++++++++++++ tests/test_pre_push_hook_contract.py | 29 +--------------------- tests/test_roadmap_helpers.py | 29 +--------------------- 3 files changed, 38 insertions(+), 56 deletions(-) create mode 100644 tests/_bash.py 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]: