115 lines
4.9 KiB
Python
115 lines
4.9 KiB
Python
"""Test: _install_python_dependencies_with_optional_fallback with stale VIRTUAL_ENV.
|
|
|
|
Simulates the real crash: a pip/system-Python install where PROJECT_ROOT is
|
|
site-packages and VIRTUAL_ENV=PROJECT_ROOT/venv does not exist.
|
|
"""
|
|
import os
|
|
import sys
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest import mock
|
|
|
|
import hermes_cli.main as main_mod
|
|
|
|
|
|
class StaleVirtualEnvTest(unittest.TestCase):
|
|
def _call(self, uv_cmd, venv_path, fake_executable, is_windows=False):
|
|
"""Run the function with a mocked uv/env and capture the subprocess call."""
|
|
captured = []
|
|
|
|
def fake_quarantine(cmd, *, env=None, scripts_dir=None, strict_quarantine=False):
|
|
captured.append((list(cmd), dict(env or {}), scripts_dir))
|
|
return None
|
|
|
|
def fake_verify(prefix, *, env=None):
|
|
return None
|
|
|
|
with mock.patch.object(main_mod, "_run_quarantined_install", fake_quarantine), \
|
|
mock.patch.object(main_mod, "_verify_console_scripts_installed", fake_verify), \
|
|
mock.patch.object(main_mod, "_venv_scripts_dir", return_value=None), \
|
|
mock.patch.object(main_mod, "_is_windows", return_value=is_windows), \
|
|
mock.patch.object(main_mod.sys, "executable", fake_executable), \
|
|
mock.patch.object(main_mod, "PROJECT_ROOT", Path("/fake/project")):
|
|
main_mod._install_python_dependencies_with_optional_fallback(
|
|
list(uv_cmd),
|
|
env={"VIRTUAL_ENV": str(venv_path)},
|
|
group="all",
|
|
)
|
|
return captured
|
|
|
|
def test_stale_virtualenv_pins_python(self):
|
|
"""VIRTUAL_ENV points at a nonexistent venv -> --python sys.executable."""
|
|
captured = self._call(
|
|
uv_cmd=[Path("/fake/uv"), "pip"],
|
|
venv_path=Path("/fake/project/venv"), # does not exist
|
|
fake_executable="/fake/python311/python.exe",
|
|
)
|
|
self.assertTrue(captured, "no subprocess call captured")
|
|
cmd, env, _ = captured[0]
|
|
# --python must come after 'install': uv pip install --python <exe> ...
|
|
self.assertIn("install", cmd)
|
|
self.assertIn("--python", cmd)
|
|
self.assertEqual(cmd[cmd.index("--python") + 1], "/fake/python311/python.exe")
|
|
# VIRTUAL_ENV removed from env
|
|
self.assertNotIn("VIRTUAL_ENV", env)
|
|
|
|
def test_existing_virtualenv_keeps_env(self):
|
|
"""VIRTUAL_ENV points at an existing venv -> unchanged, no --python."""
|
|
real_venv = Path(sys.executable).resolve().parent.parent
|
|
if not real_venv.is_dir():
|
|
self.skipTest("no real venv available in this test run")
|
|
captured = self._call(
|
|
uv_cmd=[Path("/fake/uv"), "pip"],
|
|
venv_path=real_venv,
|
|
fake_executable=sys.executable,
|
|
)
|
|
cmd, env, _ = captured[0]
|
|
self.assertNotIn("--python", cmd)
|
|
self.assertEqual(env.get("VIRTUAL_ENV"), str(real_venv))
|
|
|
|
def test_python_dash_m_uv_is_detected(self):
|
|
"""python -m uv must also trigger the pin (naive basename check misses it)."""
|
|
captured = self._call(
|
|
uv_cmd=[Path("/fake/python"), "-m", "uv", "pip"],
|
|
venv_path=Path("/fake/project/venv"), # does not exist
|
|
fake_executable="/fake/python311/python.exe",
|
|
)
|
|
self.assertTrue(captured, "no subprocess call captured")
|
|
cmd, _, _ = captured[0]
|
|
self.assertIn("--python", cmd)
|
|
self.assertEqual(cmd[cmd.index("--python") + 1], "/fake/python311/python.exe")
|
|
|
|
def test_existing_python_flag_wins(self):
|
|
"""A caller-supplied --python is not duplicated by the pin."""
|
|
captured = self._call(
|
|
uv_cmd=[Path("/fake/uv"), "pip"],
|
|
venv_path=Path("/fake/project/venv"),
|
|
fake_executable="/fake/python311/python.exe",
|
|
)
|
|
# Force the caller path through a manual pin with a pre-existing flag.
|
|
args = ["install", "--python", "/caller/choice/python.exe", "hermes"]
|
|
pinned = main_mod._insert_python_pin(args)
|
|
self.assertEqual(pinned, args, "existing --python must win")
|
|
self.assertEqual(pinned.count("--python"), 1)
|
|
|
|
def test_windows_pins_quarantine_to_interpreter_scripts_dir(self):
|
|
"""On Windows with a missing project venv, quarantine must target the
|
|
interpreter's Scripts dir (where the shims actually live), not None."""
|
|
fake_scripts = Path("/fake/python311/Scripts")
|
|
with mock.patch.object(
|
|
main_mod, "_interpreter_scripts_dir", return_value=fake_scripts
|
|
):
|
|
captured = self._call(
|
|
uv_cmd=[Path("/fake/uv"), "pip"],
|
|
venv_path=Path("/fake/project/venv"),
|
|
fake_executable="/fake/python311/python.exe",
|
|
is_windows=True,
|
|
)
|
|
self.assertTrue(captured, "no subprocess call captured")
|
|
_, _, scripts_dir = captured[0]
|
|
self.assertEqual(scripts_dir, fake_scripts)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main(verbosity=2)
|