Skip to content

Commit 420b9d2

Browse files
committed
Simplify/clarify bash.exe check for hook tests; do it only once
1 parent 5d11394 commit 420b9d2

File tree

1 file changed

+38
-35
lines changed

1 file changed

+38
-35
lines changed

test/test_index.py

+38-35
Original file line numberDiff line numberDiff line change
@@ -39,32 +39,11 @@
3939
log = logging.getLogger(__name__)
4040

4141

42-
class _WinBashMeta(enum.EnumMeta):
43-
"""Metaclass allowing :class:`_WinBash` custom behavior when called."""
44-
45-
def __call__(cls):
46-
return cls._check()
47-
48-
4942
@enum.unique
50-
class _WinBash(enum.Enum, metaclass=_WinBashMeta):
43+
class _WinBashStatus(enum.Enum):
5144
"""Status of bash.exe for native Windows. Affects which commit hook tests can pass.
5245
53-
Call ``_WinBash()`` to check the status.
54-
55-
This can't be reliably discovered using :func:`shutil.which`, as that approximates
56-
how a shell is expected to search for an executable. On Windows, there are major
57-
differences between how executables are found by a shell and otherwise. (This is the
58-
cmd.exe Windows shell and should not be confused with bash.exe.) Our run_commit_hook
59-
function in GitPython uses subprocess.Popen, including to run bash.exe on Windows.
60-
It does not pass shell=True (and should not). Popen calls CreateProcessW, which
61-
searches several locations prior to using the PATH environment variable. It is
62-
expected to search the System32 directory, even if another directory containing the
63-
executable precedes it in PATH. (There are other differences, less relevant here.)
64-
When WSL is installed, even if no WSL *systems* are installed, bash.exe exists in
65-
System32, and Popen finds it even if another bash.exe precedes it in PATH, as
66-
happens on CI. If WSL is absent, System32 may still have bash.exe, as Windows users
67-
and administrators occasionally copy executables there in lieu of extending PATH.
46+
Call :meth:`check` to check the status.
6847
"""
6948

7049
INAPPLICABLE = enum.auto()
@@ -74,13 +53,13 @@ class _WinBash(enum.Enum, metaclass=_WinBashMeta):
7453
"""No command for ``bash.exe`` is found on the system."""
7554

7655
NATIVE = enum.auto()
77-
"""Running ``bash.exe`` operates outside any WSL environment (as with Git Bash)."""
56+
"""Running ``bash.exe`` operates outside any WSL distribution (as with Git Bash)."""
7857

7958
WSL = enum.auto()
80-
"""Running ``bash.exe`` runs bash on a WSL system."""
59+
"""Running ``bash.exe`` calls ``bash`` in a WSL distribution."""
8160

8261
WSL_NO_DISTRO = enum.auto()
83-
"""Running ``bash.exe` tries to run bash on a WSL system, but none exists."""
62+
"""Running ``bash.exe` tries to run bash on a WSL distribution, but none exists."""
8463

8564
ERROR_WHILE_CHECKING = enum.auto()
8665
"""Could not determine the status.
@@ -92,13 +71,34 @@ class _WinBash(enum.Enum, metaclass=_WinBashMeta):
9271
"""
9372

9473
@classmethod
95-
def _check(cls):
74+
def check(cls):
75+
"""Check the status of the ``bash.exe`` :func:`index.fun.run_commit_hook` uses.
76+
77+
This uses EAFP, attempting to run a command via ``bash.exe``. Which ``bash.exe``
78+
is used can't be reliably discovered by :func:`shutil.which`, which approximates
79+
how a shell is expected to search for an executable. On Windows, there are major
80+
differences between how executables are found by a shell and otherwise. (This is
81+
the cmd.exe Windows shell, and shouldn't be confused with bash.exe itself. That
82+
the command being looked up also happens to be an interpreter is not relevant.)
83+
84+
:func:`index.fun.run_commit_hook` uses :class:`subprocess.Popen`, including when
85+
it runs ``bash.exe`` on Windows. It doesn't pass ``shell=True`` (and shouldn't).
86+
On Windows, `Popen` calls ``CreateProcessW``, which searches several locations
87+
prior to using the ``PATH`` environment variable. It is expected to search the
88+
``System32`` directory, even if another directory containing the executable
89+
precedes it in ``PATH``. (Other differences are less relevant here.) When WSL is
90+
installed, even with no distributions, ``bash.exe`` exists in ``System32``, and
91+
`Popen` finds it even if another ``bash.exe`` precedes it in ``PATH``, as on CI.
92+
If WSL is absent, ``System32`` may still have ``bash.exe``, as Windows users and
93+
administrators occasionally put executables there in lieu of extending ``PATH``.
94+
"""
9695
if os.name != "nt":
9796
return cls.INAPPLICABLE
9897

9998
try:
10099
# Print rather than returning the test command's exit status so that if a
101-
# failure occurs before we even get to this point, we will detect it.
100+
# failure occurs before we even get to this point, we will detect it. See
101+
# https://superuser.com/a/1749811 for information on ways to check for WSL.
102102
script = 'test -e /proc/sys/fs/binfmt_misc/WSLInterop; echo "$?"'
103103
command = ["bash.exe", "-c", script]
104104
proc = subprocess.run(command, capture_output=True, check=True, text=True)
@@ -129,6 +129,9 @@ def _error(cls, error_or_process):
129129
return cls.ERROR_WHILE_CHECKING
130130

131131

132+
_win_bash_status = _WinBashStatus.check()
133+
134+
132135
def _make_hook(git_dir, name, content, make_exec=True):
133136
"""A helper to create a hook"""
134137
hp = hook_path(name, git_dir)
@@ -998,7 +1001,7 @@ class Mocked:
9981001
self.assertEqual(rel, os.path.relpath(path, root))
9991002

10001003
@pytest.mark.xfail(
1001-
_WinBash() is _WinBash.WSL_NO_DISTRO,
1004+
_win_bash_status is _WinBashStatus.WSL_NO_DISTRO,
10021005
reason="Currently uses the bash.exe for WSL even with no WSL distro installed",
10031006
raises=HookExecutionError,
10041007
)
@@ -1009,7 +1012,7 @@ def test_pre_commit_hook_success(self, rw_repo):
10091012
index.commit("This should not fail")
10101013

10111014
@pytest.mark.xfail(
1012-
_WinBash() is _WinBash.WSL_NO_DISTRO,
1015+
_win_bash_status is _WinBashStatus.WSL_NO_DISTRO,
10131016
reason="Currently uses the bash.exe for WSL even with no WSL distro installed",
10141017
raises=AssertionError,
10151018
)
@@ -1020,7 +1023,7 @@ def test_pre_commit_hook_fail(self, rw_repo):
10201023
try:
10211024
index.commit("This should fail")
10221025
except HookExecutionError as err:
1023-
if _WinBash() is _WinBash.ABSENT:
1026+
if _win_bash_status is _WinBashStatus.ABSENT:
10241027
self.assertIsInstance(err.status, OSError)
10251028
self.assertEqual(err.command, [hp])
10261029
self.assertEqual(err.stdout, "")
@@ -1036,12 +1039,12 @@ def test_pre_commit_hook_fail(self, rw_repo):
10361039
raise AssertionError("Should have caught a HookExecutionError")
10371040

10381041
@pytest.mark.xfail(
1039-
_WinBash() in {_WinBash.ABSENT, _WinBash.WSL},
1042+
_win_bash_status in {_WinBashStatus.ABSENT, _WinBashStatus.WSL},
10401043
reason="Specifically seems to fail on WSL bash (in spite of #1399)",
10411044
raises=AssertionError,
10421045
)
10431046
@pytest.mark.xfail(
1044-
_WinBash() is _WinBash.WSL_NO_DISTRO,
1047+
_win_bash_status is _WinBashStatus.WSL_NO_DISTRO,
10451048
reason="Currently uses the bash.exe for WSL even with no WSL distro installed",
10461049
raises=HookExecutionError,
10471050
)
@@ -1059,7 +1062,7 @@ def test_commit_msg_hook_success(self, rw_repo):
10591062
self.assertEqual(new_commit.message, "{} {}".format(commit_message, from_hook_message))
10601063

10611064
@pytest.mark.xfail(
1062-
_WinBash() is _WinBash.WSL_NO_DISTRO,
1065+
_win_bash_status is _WinBashStatus.WSL_NO_DISTRO,
10631066
reason="Currently uses the bash.exe for WSL even with no WSL distro installed",
10641067
raises=AssertionError,
10651068
)
@@ -1070,7 +1073,7 @@ def test_commit_msg_hook_fail(self, rw_repo):
10701073
try:
10711074
index.commit("This should fail")
10721075
except HookExecutionError as err:
1073-
if _WinBash() is _WinBash.ABSENT:
1076+
if _win_bash_status is _WinBashStatus.ABSENT:
10741077
self.assertIsInstance(err.status, OSError)
10751078
self.assertEqual(err.command, [hp])
10761079
self.assertEqual(err.stdout, "")

0 commit comments

Comments
 (0)