Make the setup tests pass on Windows

The tests assumed a POSIX host, but pytest also runs on windows-latest.
Both path flavours are now built with PurePosixPath and PureWindowsPath,
which work on either host, the bin/Scripts layout is asserted against the
host, and the executable bit is only checked where it exists.

Also drops the trailing separator from PATH when PATH is unset, since an
empty entry makes Unix search the working directory for executables.
This commit is contained in:
Jesse Hills
2026-09-01 08:58:51 +12:00
parent f8ea4873b6
commit 7b2e2850f2
2 changed files with 46 additions and 28 deletions
+6 -1
View File
@@ -88,7 +88,12 @@ def venv_environment(venv: Path) -> dict[str, str]:
env = dict(os.environ)
env["VIRTUAL_ENV"] = str(venv)
env.pop("PYTHONHOME", None)
env["PATH"] = os.pathsep.join([str(bin_dir(venv)), env.get("PATH", "")])
path = str(bin_dir(venv))
# An empty entry would be appended if PATH is unset, and on Unix that means
# the working directory is searched for executables.
if existing := env.get("PATH"):
path = os.pathsep.join([path, existing])
env["PATH"] = path
return env
+40 -27
View File
@@ -2,11 +2,11 @@
import importlib.util
import os
from pathlib import Path
from pathlib import Path, PurePosixPath, PureWindowsPath
import runpy
import sys
from types import ModuleType
from unittest.mock import MagicMock, Mock, call, patch
from unittest.mock import Mock, call, patch
import pytest
@@ -29,52 +29,62 @@ def script_setup() -> ModuleType:
# --- bin_dir / venv_python / activate_hint -----------------------------------
def test_bin_dir_posix_layout(script_setup: ModuleType, tmp_path: Path) -> None:
"""The venv scheme resolves executables under bin/ on this host."""
assert script_setup.bin_dir(tmp_path) == tmp_path / "bin"
def test_bin_dir_matches_host_layout(script_setup: ModuleType, tmp_path: Path) -> None:
"""The venv scheme resolves to Scripts on Windows and bin everywhere else."""
expected = "Scripts" if os.name == "nt" else "bin"
assert script_setup.bin_dir(tmp_path) == tmp_path / expected
# Both flavours are exercised on every host. Pure paths are used because a real
# Path refuses to change flavour: PosixPath cannot be built on Windows, and
# WindowsPath cannot be built on Unix.
def test_venv_python_posix(script_setup: ModuleType, tmp_path: Path) -> None:
with patch.object(script_setup.os, "name", "posix"):
with (
patch.object(
script_setup, "bin_dir", return_value=PurePosixPath("/x/venv/bin")
),
patch.object(script_setup.os, "name", "posix"),
):
result = script_setup.venv_python(tmp_path)
assert result == tmp_path / "bin" / "python"
assert result == PurePosixPath("/x/venv/bin/python")
def test_venv_python_nt(script_setup: ModuleType, tmp_path: Path) -> None:
# A real Path cannot flip OS flavour mid-test (Python refuses to build a
# WindowsPath on a POSIX host), so bin_dir is stubbed with a MagicMock:
# only the "python.exe" vs "python" branch is under test here.
fake_bin = MagicMock()
with (
patch.object(script_setup, "bin_dir", return_value=fake_bin),
patch.object(
script_setup, "bin_dir", return_value=PureWindowsPath(r"C:\x\venv\Scripts")
),
patch.object(script_setup.os, "name", "nt"),
):
script_setup.venv_python(tmp_path)
fake_bin.__truediv__.assert_called_once_with("python.exe")
result = script_setup.venv_python(tmp_path)
assert result == PureWindowsPath(r"C:\x\venv\Scripts\python.exe")
def test_activate_hint_posix(script_setup: ModuleType) -> None:
with patch.object(script_setup.os, "name", "posix"):
with (
patch.object(script_setup, "ROOT", PurePosixPath("/x")),
patch.object(
script_setup, "bin_dir", return_value=PurePosixPath("/x/venv/bin")
),
patch.object(script_setup.os, "name", "posix"),
):
hint = script_setup.activate_hint()
assert hint == "source venv/bin/activate"
def test_activate_hint_nt(script_setup: ModuleType) -> None:
# Same reasoning as test_venv_python_nt: stub bin_dir with a MagicMock so
# no real Path is built while os.name is patched to "nt".
fake_bin = MagicMock()
fake_bin.relative_to.return_value.__truediv__.return_value = (
"venv\\Scripts\\activate"
)
with (
patch.object(script_setup, "bin_dir", return_value=fake_bin),
patch.object(script_setup, "ROOT", PureWindowsPath(r"C:\x")),
patch.object(
script_setup, "bin_dir", return_value=PureWindowsPath(r"C:\x\venv\Scripts")
),
patch.object(script_setup.os, "name", "nt"),
):
hint = script_setup.activate_hint()
# The nt branch returns str(activate) as-is, skipping the "source " prefix.
assert hint == "venv\\Scripts\\activate"
fake_bin.relative_to.assert_called_once_with(script_setup.ROOT)
fake_bin.relative_to.return_value.__truediv__.assert_called_once_with("activate")
assert hint == r"venv\Scripts\activate"
# --- run -----------------------------------------------------------------
@@ -199,7 +209,8 @@ def test_venv_environment_path_fallback_when_unset(
venv = tmp_path / "venv"
monkeypatch.delenv("PATH", raising=False)
env = script_setup.venv_environment(venv)
assert env["PATH"] == str(script_setup.bin_dir(venv)) + os.pathsep
# No trailing separator: an empty PATH entry means "search the cwd".
assert env["PATH"] == str(script_setup.bin_dir(venv))
# --- find_uv -----------------------------------------------------------------
@@ -395,7 +406,9 @@ def test_install_git_hooks_happy_path_installs_hook(
)
installed = hooks_dir / "post-checkout"
assert installed.read_text() == source_hook.read_text()
assert (installed.stat().st_mode & 0o777) == 0o755
if os.name != "nt":
# Windows has no POSIX permission bits for chmod to set.
assert (installed.stat().st_mode & 0o777) == 0o755
def test_install_git_hooks_skips_copy_when_hooks_dir_missing(