From 2196496cac5c3bde6d057973bea431985cfcdbaf Mon Sep 17 00:00:00 2001 From: danielmeppiel Date: Tue, 9 Jun 2026 17:47:53 +0200 Subject: [PATCH] fix(tests): skip install.sh safety tests when POSIX bash is unavailable On windows-latest GitHub runners the ambient bash on PATH is the WSL stub (C:\Windows\System32\bash.exe), which exits 1 for every invocation when no distribution is installed. That made all 30 bash-dependent install-safety tests fail with a uniform exit code of 1 (assert 1 == 0/11/12/14) even though install.sh is the Unix installer (Windows uses install.ps1). Probe for a usable POSIX bash and skip the bash-dependent test classes when none exists, keeping the pure-Python sentinel tests running. Also set MSYS_NO_PATHCONV/MSYS2_ARG_CONV_EXCL so a real Git Bash does not mangle POSIX path arguments. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/unit/install/test_install_safety.py | 46 ++++++++++++++++++++++- 1 file changed, 44 insertions(+), 2 deletions(-) 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."""