test(webui): make Windows git-fallback tests platform-independent

The known-dirs-probe test built its expected path from os.environ['ProgramFiles']
+ os.path.join, but the production code uses os.path.expandvars('%ProgramFiles%..').
On the Linux CI runners expandvars does not expand %VAR% syntax and os.path.join
uses '/', so the expected path diverged from what the code produced and the test
failed (assert None == 'C:\Program Files/Git/cmd/git.exe').

Mock the OS-specific surfaces (winreg, os.path.exists/expandvars/join) so the
Windows code path is exercised deterministically on any host. Verified the two
previously-failing assertions now pass under posix os.path semantics, and a
mutation check (reverting the fix) still makes the three behavior tests fail.
This commit is contained in:
allenliang2022 committed 2026-07-04 15:50:05 +08:00
1 parent 861cc9da77
commit 2c03277a97
1 file changed
+46 -19
@@ -10,15 +10,26 @@ after the server restarted with fixed code.
The fix adds a Windows fallback that resolves git.exe from the Git-for-Windows
registry ``InstallPath`` (and common install dirs) without relying on PATH.
These tests mock the OS-specific surfaces (winreg, os.path.exists,
os.path.expandvars) so they exercise the Windows code path deterministically on
ANY host — the CI runners are Linux, where %VAR% expansion and native path
separators differ, so nothing here may depend on the running platform's real
environment or filesystem.
"""
import os
import sys
from unittest.mock import MagicMock, patch
import api.updates as updates
# Deterministic install root used by the winreg stub. Kept as a forward-slash
# string so the expected git path is identical regardless of the host's
# os.path.join separator (CI is Linux; dev is Windows).
FAKE_GIT_ROOT = 'C:/Program Files/Git'
FAKE_GIT_EXE = FAKE_GIT_ROOT + '/cmd/git.exe'
def _fake_winreg():
def _fake_winreg(install_path=FAKE_GIT_ROOT):
"""Minimal winreg stub whose OpenKey/QueryValueEx yield a Git InstallPath."""
mod = MagicMock()
mod.HKEY_LOCAL_MACHINE = 0
@@ -35,43 +46,58 @@ def _fake_winreg():
return False
mod.OpenKey.return_value = _Key()
mod.QueryValueEx.return_value = (r'C:\Program Files\Git', 1)
mod.QueryValueEx.return_value = (install_path, 1)
return mod
def _patch_join_forward_slash():
"""Make os.path.join deterministic (forward slash) regardless of host OS.
The production code builds candidates with os.path.join; on Linux CI that
uses '/', on Windows '\\'. Pin it so expected paths match on both.
"""
def fake_join(*parts):
return '/'.join(p.rstrip('/\\') for p in parts)
return patch.object(updates.os.path, 'join', side_effect=fake_join)
def test_resolve_git_executable_uses_registry_on_windows():
winreg = _fake_winreg()
expected = os.path.join(r'C:\Program Files\Git', 'cmd', 'git.exe')
def fake_exists(p):
return p == expected
with patch.object(updates.shutil, 'which', return_value=None), \
patch.object(sys, 'platform', 'win32'), \
patch.dict('sys.modules', {'winreg': winreg}), \
patch.object(updates.os.path, 'exists', side_effect=fake_exists):
_patch_join_forward_slash(), \
patch.object(updates.os.path, 'exists', side_effect=lambda p: p == FAKE_GIT_EXE):
resolved = updates._resolve_git_executable()
assert resolved == expected
assert resolved == FAKE_GIT_EXE
def test_resolve_git_executable_probes_known_dirs_when_registry_absent():
winreg = _fake_winreg()
# Registry has no value -> QueryValueEx raises -> fall through to dir probe.
winreg = _fake_winreg()
winreg.QueryValueEx.side_effect = OSError('no InstallPath')
program_files = os.environ.get('ProgramFiles', r'C:\Program Files')
expected = os.path.join(program_files, 'Git', 'cmd', 'git.exe')
def fake_exists(p):
return p == expected
# The code probes os.path.expandvars('%ProgramFiles%\\Git\\cmd\\git.exe');
# mock expandvars deterministically so the test does not depend on the
# host's %VAR% expansion (Linux CI does not expand %ProgramFiles%).
probed = 'C:/Program Files/Git/cmd/git.exe'
def fake_expandvars(s):
if 'ProgramFiles%' in s and '(x86)' not in s:
return probed
# Other candidates resolve to a sentinel that never "exists".
return '/nonexistent/' + s
with patch.object(updates.shutil, 'which', return_value=None), \
patch.object(sys, 'platform', 'win32'), \
patch.dict('sys.modules', {'winreg': winreg}), \
patch.object(updates.os.path, 'exists', side_effect=fake_exists):
patch.object(updates.os.path, 'expandvars', side_effect=fake_expandvars), \
patch.object(updates.os.path, 'exists', side_effect=lambda p: p == probed):
resolved = updates._resolve_git_executable()
assert resolved == expected
assert resolved == probed
def test_resolve_git_executable_returns_none_on_windows_when_git_truly_absent():
@@ -81,6 +107,7 @@ def test_resolve_git_executable_returns_none_on_windows_when_git_truly_absent():
with patch.object(updates.shutil, 'which', return_value=None), \
patch.object(sys, 'platform', 'win32'), \
patch.dict('sys.modules', {'winreg': winreg}), \
patch.object(updates.os.path, 'expandvars', side_effect=lambda s: s), \
patch.object(updates.os.path, 'exists', return_value=False):
resolved = updates._resolve_git_executable()
@@ -103,14 +130,13 @@ def test_non_windows_never_touches_registry_fallback():
def test_detect_webui_version_recovers_via_windows_registry_fallback(tmp_path):
winreg = _fake_winreg()
expected_git = os.path.join(r'C:\Program Files\Git', 'cmd', 'git.exe')
def fake_exists(p):
# git.exe resolves; no api/_version.py fallback file present.
return p == expected_git
return p == FAKE_GIT_EXE
def fake_run(cmd, **kwargs):
assert cmd[0] == expected_git
assert cmd[0] == FAKE_GIT_EXE
if cmd[1:] == ['describe', '--tags', '--always']:
return MagicMock(returncode=0, stdout='v0.51.999\n', stderr='')
if cmd[1:] == ['diff-index', '--quiet', 'HEAD', '--']:
@@ -120,6 +146,7 @@ def test_detect_webui_version_recovers_via_windows_registry_fallback(tmp_path):
with patch.object(updates.shutil, 'which', return_value=None), \
patch.object(sys, 'platform', 'win32'), \
patch.dict('sys.modules', {'winreg': winreg}), \
_patch_join_forward_slash(), \
patch.object(updates.os.path, 'exists', side_effect=fake_exists), \
patch.object(updates, 'REPO_ROOT', tmp_path), \
patch.object(updates.subprocess, 'run', side_effect=fake_run):