PR #122161 stopped boot from activating the in-tree venv or .venv when PM has committed no environment. Six Linux tests and four Windows tests still used that tree to supply their probe modules. - Launcher tests put the probe in a committed generation. A custom HERMES_HOME is its own dependency root, so it gets its own commit. - The legacy row of test_pre_pm_base_dependencies_activate_only_at_boot asserted the removed behavior. test_boot_never_activates_the_pre_pm_venv now covers the inverse. The payload row stays. - The mint payload fixture writes manifest.json as real payloads do, so boot selects the payload venv. - The PowerShell activate test expects PYTHONPATH to be the checkout alone. The bootstrap .venv packages do not leak in.
166 lines
7.1 KiB
Python
166 lines
7.1 KiB
Python
"""Live tests for the win32 launcher mint (scripts/build/mint_launchers.py).
|
|
|
|
These RUN the real mint on the current interpreter and then execute the
|
|
minted launcher — the strongest proof the mechanism is intact (the research
|
|
experiments proved it once; these tests keep proving it on every run):
|
|
|
|
* the minted exe is a real PE with a `#!<launcher_dir>\\..` shebang,
|
|
* the <launcher_dir> placeholder resolves against the launcher's OWN
|
|
directory (proved by moving the tree before running),
|
|
* the wrapper puts the payload repo + venv site-packages on sys.path and
|
|
dispatches to the entry module with argv forwarded and the exit code
|
|
returned.
|
|
|
|
Runs on Windows with the locked ``test`` dependency group. POSIX payloads
|
|
use $0-relative bash trampolines instead.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
_REPO = Path(__file__).resolve().parents[2]
|
|
_MINT = _REPO / "scripts" / "build" / "mint_launchers.py"
|
|
|
|
pytestmark = [
|
|
pytest.mark.platforms("windows"),
|
|
]
|
|
|
|
|
|
@pytest.fixture()
|
|
def payload_tree(tmp_path: Path):
|
|
"""A miniature payload: bin/, repo snapshot with a stub hermes_cli,
|
|
venv site-packages, and a REAL interpreter at the fake store path."""
|
|
bin_dir = tmp_path / "stage" / "bin"
|
|
py_dir = tmp_path / "stage" / "tools" / "py-1"
|
|
repo = tmp_path / "stage" / "repo"
|
|
site = tmp_path / "stage" / "venv" / "Lib" / "site-packages"
|
|
for d in (bin_dir, py_dir, repo / "hermes_cli", repo / "pm", site):
|
|
d.mkdir(parents=True, exist_ok=True)
|
|
|
|
# A real interpreter at the shebang's target: the launcher will create
|
|
# the process from <launcher_dir>\..\tools\py-1\python.exe, so the exe
|
|
# must actually be able to boot there. Copy from the BASE interpreter —
|
|
# a venv python.exe is a launcher stub whose DLLs/Lib live beside the
|
|
# real one.
|
|
base = Path(sys.base_prefix)
|
|
for name in os.listdir(base):
|
|
if name in ("Lib", "Scripts", "share", "include"):
|
|
continue
|
|
src = base / name
|
|
if src.is_dir():
|
|
shutil.copytree(src, py_dir / name)
|
|
else:
|
|
shutil.copy2(src, py_dir / name)
|
|
shutil.copytree(
|
|
base / "Lib",
|
|
py_dir / "Lib",
|
|
ignore=shutil.ignore_patterns("__pycache__", "site-packages", "test", "idlelib", "tkinter", "turtledemo", "config-*"),
|
|
)
|
|
|
|
# Exercise the real bootstrap before the fixture entry point.
|
|
for relative in ("hermes_bootstrap.py", "hermes_constants.py", "hermes_cli/__init__.py",
|
|
"pm/environments.py", "pm/filesystem.py", "hermes_cli/runtime_state.py",
|
|
"hermes_cli/_early_recovery.py", "hermes_cli/_parser.py",
|
|
# prepare_launch returns early for a fixture repo (no .git), but the bootstrap
|
|
# imports these two before it can tell.
|
|
"hermes_cli/venv_sync.py", "hermes_cli/steward.py"):
|
|
shutil.copy2(_REPO / relative, repo / relative)
|
|
(repo / "hermes_cli" / "main.py").write_text(
|
|
"import os, sys\n"
|
|
"def main():\n"
|
|
" import hermes_cli, stubdep\n"
|
|
" assert sys.argv[0].lower().endswith('hermes.exe'), sys.argv[0]\n"
|
|
" assert sys.argv[1:] == ['--version'], sys.argv\n"
|
|
" print('OK', os.environ.get('PYTHONHOME'))\n"
|
|
" return 7\n",
|
|
encoding="utf-8",
|
|
)
|
|
(site / "stubdep.py").write_text("X = 1\n", encoding="utf-8")
|
|
# Real payloads ship this beside repo/ (scripts/build/agent.py); boot selects its venv from it.
|
|
(tmp_path / "stage" / "manifest.json").write_text(
|
|
json.dumps({"schema": 1, "repo": "repo", "venv": "venv", "store": "tools"}), encoding="utf-8")
|
|
|
|
return {"bin": bin_dir, "root": tmp_path / "stage", "tmp": tmp_path, "site": site, "repo": repo}
|
|
|
|
|
|
def _mint(bin_dir: Path, wrapper: Path, specs) -> list[str]:
|
|
env = dict(
|
|
os.environ,
|
|
HERMES_MINT_BIN_DIR=str(bin_dir),
|
|
HERMES_MINT_SPECS=json.dumps(specs),
|
|
HERMES_MINT_WRAPPER=str(wrapper),
|
|
HERMES_MINT_PYTHON=r"<launcher_dir>\..\tools\py-1\python.exe",
|
|
)
|
|
proc = subprocess.run([sys.executable, str(_MINT)], capture_output=True, text=True, env=env)
|
|
assert proc.returncode == 0, proc.stderr
|
|
return proc.stdout.split()
|
|
|
|
|
|
def _render_wrapper(tmp_path: Path) -> Path:
|
|
from scripts.build.launchers import render_wrapper
|
|
text = render_wrapper("hermes_cli.main:main", "../repo", "../venv/Lib/site-packages")
|
|
out = tmp_path / "wrapper.py"
|
|
out.write_text(text, encoding="utf-8")
|
|
return out
|
|
|
|
|
|
def test_mint_writes_exactly_one_named_exe_per_spec(payload_tree, tmp_path):
|
|
wrapper = _render_wrapper(tmp_path)
|
|
out = _mint(payload_tree["bin"], wrapper, [{"name": "hermes", "module": "hermes_cli.main", "func": "main"}])
|
|
assert out == ["hermes.exe"] or out == [str(payload_tree["bin"] / "hermes.exe")]
|
|
assert sorted(p.name for p in payload_tree["bin"].iterdir()) == ["hermes.exe"], "no versioned twin may remain"
|
|
|
|
|
|
def test_minted_launcher_shebang_carries_the_launcher_dir_placeholder(payload_tree, tmp_path):
|
|
wrapper = _render_wrapper(tmp_path)
|
|
_mint(payload_tree["bin"], wrapper, [{"name": "hermes", "module": "hermes_cli.main", "func": "main"}])
|
|
exe = payload_tree["bin"] / "hermes.exe"
|
|
blob = exe.read_bytes()
|
|
shebang = blob[blob.rfind(b"#!"):]
|
|
assert shebang.startswith(b"#!<launcher_dir>\\..\\tools\\py-1\\python.exe\n"), shebang[:60]
|
|
|
|
|
|
def test_minted_launcher_runs_relocated_and_forwards_exit_code(payload_tree, tmp_path):
|
|
wrapper = _render_wrapper(tmp_path)
|
|
_mint(payload_tree["bin"], wrapper, [{"name": "hermes", "module": "hermes_cli.main", "func": "main"}])
|
|
|
|
# RELOCATE the whole tree before running: the launcher must resolve the
|
|
# interpreter and the payload paths relative to its own dir, wherever
|
|
# the install lands.
|
|
moved = payload_tree["tmp"] / "moved-install"
|
|
shutil.move(str(payload_tree["root"]), str(moved))
|
|
exe = moved / "bin" / "hermes.exe"
|
|
|
|
env = {k: v for k, v in os.environ.items() if k not in ("PYTHONPATH", "PYTHONHOME", "PYTHONPYCACHEPREFIX")}
|
|
env["LOCALAPPDATA"] = str(payload_tree["tmp"] / "lad")
|
|
proc = subprocess.run([str(exe), "--version"], capture_output=True, text=True, cwd=tmp_path, env=env)
|
|
assert proc.returncode == 7, proc.stderr[-800:]
|
|
assert proc.stdout.strip() == "OK None" # PYTHONHOME was dropped
|
|
|
|
# The default pycache prefix kept bytecode out of the (sealed) repo.
|
|
assert (payload_tree["tmp"] / "lad" / "hermes" / "pycache").exists()
|
|
assert not (moved / "repo" / "hermes_cli" / "__pycache__").exists()
|
|
|
|
|
|
def test_mint_rejects_a_non_launcher_dir_shebang(tmp_path):
|
|
env = dict(
|
|
os.environ,
|
|
HERMES_MINT_BIN_DIR=str(tmp_path),
|
|
HERMES_MINT_SPECS=json.dumps([{"name": "hermes", "module": "m", "func": "main"}]),
|
|
HERMES_MINT_PYTHON=r"C:\abs\python.exe",
|
|
)
|
|
wrapper = tmp_path / "w.py"
|
|
wrapper.write_text("x = 1\n", encoding="utf-8")
|
|
env["HERMES_MINT_WRAPPER"] = str(wrapper)
|
|
proc = subprocess.run([sys.executable, str(_MINT)], capture_output=True, text=True, env=env)
|
|
assert proc.returncode != 0
|
|
assert "launcher_dir" in proc.stderr
|