diff --git a/tests/unit/install/test_install_safety.py b/tests/unit/install/test_install_safety.py index c84fba579..8a586a5e9 100644 --- a/tests/unit/install/test_install_safety.py +++ b/tests/unit/install/test_install_safety.py @@ -34,6 +34,42 @@ SENTINEL_END = re.compile(r"^# INSTALL_SAFETY_END", re.MULTILINE) +def _usable_bash() -> bool: + """Return True only when a real POSIX bash is invocable. + + install.sh is the Unix installer (Windows uses install.ps1), and these + tests shell out to ``bash`` to exercise its safety validator. On + ``windows-latest`` GitHub runners the ambient ``bash`` on PATH is the WSL + stub (``C:\\Windows\\System32\\bash.exe``); with no distribution installed + it exits non-zero for every invocation, which would otherwise make every + bash-dependent test here fail with a uniform exit code rather than skip. + Probe with a trivial POSIX command and treat anything unexpected as "no + usable bash" so the suite skips instead of failing on such platforms. + """ + try: + proc = subprocess.run( + ["bash", "-c", "printf ok"], + capture_output=True, + text=True, + timeout=10, + ) + except (OSError, subprocess.SubprocessError): + return False + return proc.returncode == 0 and proc.stdout.strip() == "ok" + + +_BASH_USABLE = _usable_bash() + +requires_bash = pytest.mark.skipif( + not _BASH_USABLE, + reason="POSIX bash unavailable (e.g. Windows WSL stub); install.sh is the Unix installer", +) + +# Prevent Git Bash (MSYS2) from rewriting POSIX-looking arguments such as +# /usr/local/lib/apm into Windows paths when a real bash is present. +_BASH_ENV_EXTRA = {"MSYS_NO_PATHCONV": "1", "MSYS2_ARG_CONV_EXCL": "*"} + + def _read_install_sh() -> str: """Read install.sh in a shell-safe form across platforms. @@ -81,7 +117,7 @@ def _run_validator(lib_dir: str, home: str | None = None) -> int: input="", capture_output=True, text=True, - env={**os.environ, "HOME": home}, + env={**os.environ, "HOME": home, **_BASH_ENV_EXTRA}, cwd=tmp, timeout=10, ) @@ -102,7 +138,7 @@ def _run_prepare_parent(lib_dir: str, home: str | None = None) -> int: input="", capture_output=True, text=True, - env={**os.environ, "HOME": home}, + env={**os.environ, "HOME": home, **_BASH_ENV_EXTRA}, cwd=tmp, timeout=10, ) @@ -114,6 +150,7 @@ def _run_prepare_parent(lib_dir: str, home: str | None = None) -> int: # --------------------------------------------------------------------------- +@requires_bash class TestAbsolutePathGuard: def test_accepts_unix_absolute(self): assert _run_validator("/usr/local/lib/apm") == 0 @@ -133,6 +170,7 @@ def test_rejects_dot_relative(self): # --------------------------------------------------------------------------- +@requires_bash class TestSuffixGuard: def test_accepts_apm_suffix(self): assert _run_validator("/opt/apm") == 0 @@ -172,6 +210,7 @@ def test_rejects_partial_match(self): # --------------------------------------------------------------------------- +@requires_bash class TestBlocklistGuard: @pytest.mark.parametrize( "broad_path", @@ -244,6 +283,7 @@ def test_rejects_local_share(self): # --------------------------------------------------------------------------- +@requires_bash class TestMarkerFileGuard: """Guard 4 only fires when the directory exists and is non-empty. The Python harness stages directories under a tempdir and calls the validator @@ -306,6 +346,7 @@ def test_accepts_nonexistent_dir(self): # --------------------------------------------------------------------------- +@requires_bash class TestUserLocalInstall: def test_prepare_parent_creates_missing_user_local_lib_without_sudo(self): with tempfile.TemporaryDirectory() as tmp: @@ -328,6 +369,7 @@ def test_prepare_parent_falls_back_when_parent_unwritable(self): protected.chmod(0o755) +@requires_bash class TestReportedIncident: """The exact command from issue #1690's reproduction must be blocked."""