The cold-runtime e2e and the uv dependency-leg test ship or digest the host interpreter as if it were a standalone Python. The earlier skips matched one file (sitecustomize.py) and one path (/usr), so /usr/local, Homebrew or a distro stdlib with other absolute links still failed. Skip instead when the shipped stdlib holds a link tarfile's data filter refuses, or when the prefix has unreadable files or is far larger than a single interpreter.
221 lines
10 KiB
Python
221 lines
10 KiB
Python
"""Optional dependency refreshes run in PM's repository with its installed tools."""
|
|
from argparse import Namespace
|
|
import importlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import sys
|
|
import tempfile
|
|
|
|
import pytest
|
|
|
|
from pm import cli, paths, registry
|
|
from pm.lock import Facts, Lockfile
|
|
from pm.package import Package
|
|
from pm.packages import Nodejs, Npm, Python, Uv
|
|
from pm.store import current_target, tree_digest
|
|
|
|
|
|
class ManualFixture(Package):
|
|
name = 'manual-fixture'
|
|
|
|
|
|
def project(tmp_path, monkeypatch):
|
|
repo = tmp_path / 'repo'
|
|
repo.mkdir()
|
|
(repo / 'pyproject.toml').write_text(
|
|
'[project]\nname="update-proof"\nversion="1"\nrequires-python=">=3.14"\n'
|
|
'[tool.uv]\npackage=false\n', encoding='utf-8')
|
|
caller = tmp_path / 'unrelated-project'
|
|
caller.mkdir()
|
|
(caller / 'pyproject.toml').write_text('not a project', encoding='utf-8')
|
|
lock = Lockfile(repo / 'pm/lock.json')
|
|
lock.set_pin('manual-fixture', '1', {})
|
|
lock.save()
|
|
monkeypatch.setitem(registry._packages, 'manual-fixture', ManualFixture())
|
|
monkeypatch.setattr(paths, 'repo_root', lambda: repo)
|
|
monkeypatch.setattr(cli, 'repo_root', lambda: repo)
|
|
monkeypatch.setattr(paths, 'lockfile_path', lambda: lock.path)
|
|
monkeypatch.setattr(Path, 'home', lambda: tmp_path / 'home')
|
|
monkeypatch.setenv('HERMES_HOME', str(tmp_path / 'home'))
|
|
monkeypatch.setenv('HERMES_RUNTIME_DIR', str(tmp_path / 'store'))
|
|
monkeypatch.chdir(caller)
|
|
|
|
run_live = cli._run_live
|
|
|
|
def run_owned(cmd, *, cwd, env, **kwargs):
|
|
assert Path(cwd).resolve() == repo.resolve(), 'refuse a dependency command outside the temporary project'
|
|
assert Path(cmd[0]).resolve().is_relative_to(tmp_path.resolve()), 'refuse a tool outside the temporary store'
|
|
return run_live(cmd, cwd=cwd, env=env, **kwargs)
|
|
|
|
monkeypatch.setattr(cli, '_run_live', run_owned)
|
|
return repo, caller, lock
|
|
|
|
|
|
def _shared_prefix(root: Path, *, limit: int = 50_000) -> str:
|
|
"""Why ``root`` cannot be digested as a standalone Python package entry, or ''.
|
|
|
|
The fixture records the host interpreter's prefix as PM's Python entry,
|
|
which means reading every file under it. A distro prefix (/usr, /usr/local,
|
|
Homebrew) holds unrelated, sometimes unreadable, trees; a standalone or
|
|
self-contained build holds one interpreter.
|
|
"""
|
|
count = 0
|
|
for directory, _, filenames in os.walk(root):
|
|
for name in filenames:
|
|
path = Path(directory) / name
|
|
count += 1
|
|
if count > limit:
|
|
return f'more than {limit} files'
|
|
if not path.is_symlink() and not os.access(path, os.R_OK):
|
|
return f'{path} is unreadable'
|
|
return ''
|
|
|
|
|
|
@pytest.mark.platforms('windows', 'posix')
|
|
def test_uv_refresh_uses_real_installed_tool_and_only_the_owned_project(tmp_path, monkeypatch, capsys):
|
|
repo, caller, lock = project(tmp_path, monkeypatch)
|
|
uv = Path(shutil.which('uv') or pytest.fail('canonical test environment requires uv'))
|
|
runtime = tmp_path / 'store'
|
|
managed = runtime / 'uv-fixture' / uv.name
|
|
managed.parent.mkdir(parents=True)
|
|
shutil.copy2(uv, managed)
|
|
python = Path(sys._base_executable).resolve()
|
|
python_root = python.parent if os.name == 'nt' else python.parents[1]
|
|
shared = _shared_prefix(python_root)
|
|
if shared:
|
|
pytest.skip(f"host Python prefix {python_root} is not self-contained: {shared}")
|
|
target = current_target()
|
|
facts = Facts(runtime / 'facts.json')
|
|
for name, package, entry in [('uv', Uv(), managed.parent), ('python', Python(), python_root)]:
|
|
monkeypatch.setitem(registry._packages, name, package)
|
|
lock.set_pin(name, 'fixture', {target: {'url': f'https://example.invalid/{name}', 'sha256': '1' * 64}})
|
|
facts.record(name, 'fixture', str(entry), package.env(entry, target), runtime,
|
|
target=target, artifacts=['1' * 64], digest=tree_digest(entry))
|
|
lock.save()
|
|
before_lock = lock.path.read_bytes()
|
|
parent_env = dict(os.environ)
|
|
syncs = []
|
|
ensure = importlib.import_module('pm.install')
|
|
monkeypatch.setattr(ensure, 'sync_venv', lambda **kw: syncs.append(kw))
|
|
args = Namespace(names=['manual-fixture'], target=None, check=True, uv=True, npm=False, termux=False)
|
|
assert cli.cmd_update(args) == 0
|
|
assert not (repo / 'uv.lock').exists()
|
|
assert not syncs
|
|
check_output = capsys.readouterr().out
|
|
args.check = False
|
|
assert cli.cmd_update(args) == 0
|
|
assert 'uv lock --upgrade' in check_output
|
|
assert (repo / 'uv.lock').is_file()
|
|
assert not (caller / 'uv.lock').exists()
|
|
assert syncs == [{'explicit': True}]
|
|
assert lock.path.read_bytes() == before_lock
|
|
assert dict(os.environ) == parent_env
|
|
|
|
original = (repo / 'uv.lock').read_bytes()
|
|
syncs.clear()
|
|
(repo / 'pyproject.toml').write_text('invalid project [', encoding='utf-8')
|
|
assert cli.cmd_update(args) == 1
|
|
assert (repo / 'uv.lock').read_bytes() == original and not syncs
|
|
assert 'Python lock refresh failed' in capsys.readouterr().out
|
|
|
|
managed.unlink()
|
|
assert cli.cmd_update(args) == 1
|
|
assert (repo / 'uv.lock').read_bytes() == original and not syncs
|
|
assert 'Python lock refresh failed' in capsys.readouterr().out
|
|
|
|
|
|
@pytest.mark.platforms('windows', 'posix')
|
|
def test_npm_refresh_uses_its_installed_entry_and_owned_project(monkeypatch, capsys):
|
|
# Keep the real tool's argv clear of the harness's hermes-update guard.
|
|
temp_root = Path(os.environ['LOCALAPPDATA']) / 'Temp' if os.name == 'nt' else Path('/tmp')
|
|
with tempfile.TemporaryDirectory(prefix='pm-deps-', dir=temp_root) as temporary, monkeypatch.context() as scoped:
|
|
monkeypatch = scoped
|
|
root = Path(temporary)
|
|
repo, caller, lock = project(root, monkeypatch)
|
|
user = root / 'user'
|
|
user.mkdir()
|
|
for key in ('HOME', 'USERPROFILE', 'APPDATA', 'LOCALAPPDATA'):
|
|
monkeypatch.setenv(key, str(user))
|
|
target = current_target()
|
|
runtime = root / 'store'
|
|
node_entry = runtime / 'node-fixture'
|
|
node_binary = node_entry / ('node.exe' if os.name == 'nt' else 'bin/node')
|
|
node_binary.parent.mkdir(parents=True)
|
|
node_binary.write_bytes(b'MZ' if os.name == 'nt' else b'#!/bin/sh\nexit 99\n')
|
|
if os.name != 'nt':
|
|
node_binary.chmod(0o755)
|
|
facts = Facts(runtime / 'facts.json')
|
|
node, npm = Nodejs(), Npm()
|
|
monkeypatch.setitem(registry._packages, 'node', node)
|
|
monkeypatch.setitem(registry._packages, 'npm', npm)
|
|
lock.set_pin('node', 'fixture', {target: {'url': 'https://example.invalid/node', 'sha256': '1' * 64}})
|
|
facts.record('node', 'fixture', str(node_entry), node.env(node_entry, target), runtime,
|
|
target=target, artifacts=['1' * 64])
|
|
lock.save()
|
|
npm_entry = runtime / 'npm-fixture'
|
|
cli_script = npm_entry / 'lib/npm_cli.py'
|
|
cli_script.parent.mkdir(parents=True)
|
|
cli_script.write_text(
|
|
"import json\n"
|
|
"from pathlib import Path\n"
|
|
"try:\n"
|
|
" package = json.loads(Path('package.json').read_text(encoding='utf-8'))\n"
|
|
" Path('package-lock.json').write_text(json.dumps({\n"
|
|
" 'name': package['name'], 'lockfileVersion': 3,\n"
|
|
" }), encoding='utf-8')\n"
|
|
"except Exception as error:\n"
|
|
" raise SystemExit(str(error))\n",
|
|
encoding='utf-8',
|
|
)
|
|
if os.name == 'nt':
|
|
(npm_entry / 'npm.cmd').write_text(
|
|
f'@echo off\r\n"{sys.executable}" "%~dp0lib\\npm_cli.py" %*\r\n', encoding='utf-8')
|
|
else:
|
|
npm_binary = npm_entry / 'bin/npm'
|
|
npm_binary.parent.mkdir()
|
|
npm_binary.write_text(
|
|
f'#!/bin/sh\nexec "{sys.executable}" "{cli_script}" "$@"\n',
|
|
encoding='utf-8',
|
|
)
|
|
npm_binary.chmod(0o755)
|
|
version = 'fixture'
|
|
lock.set_pin('npm', version, {target: {'url': 'https://example.invalid/npm', 'sha256': '2' * 64}})
|
|
facts.record('npm', version, str(npm_entry), npm.env(npm_entry, target), runtime,
|
|
target=target, artifacts=['2' * 64])
|
|
lock.save()
|
|
(repo / 'package.json').write_text(json.dumps({'name': 'pm-leg-proof', 'version': '1.0.0', 'private': True}), encoding='utf-8')
|
|
(repo / '.npmrc').write_text('offline=true\naudit=false\nfund=false\nignore-scripts=true\n', encoding='utf-8')
|
|
monkeypatch.setenv('PATH', str(Path(os.environ.get('SYSTEMROOT', '/')) / 'System32') if os.name == 'nt' else '/usr/bin:/bin')
|
|
monkeypatch.setenv('NODE_OPTIONS', '--invalid-option-to-be-scrubbed')
|
|
monkeypatch.setenv('npm_config_cache', str(root / 'ambient-cache'))
|
|
before = dict(os.environ)
|
|
before_lock = lock.path.read_bytes()
|
|
args = Namespace(names=['manual-fixture'], target=None, check=True, uv=False, npm=True, termux=False)
|
|
assert cli.cmd_update(args) == 0
|
|
assert not (repo / 'package-lock.json').exists()
|
|
args.check = False
|
|
assert cli.cmd_update(args) == 0, capsys.readouterr().out
|
|
assert json.loads((repo / 'package-lock.json').read_text(encoding='utf-8'))['name'] == 'pm-leg-proof'
|
|
assert not (caller / 'package-lock.json').exists()
|
|
assert dict(os.environ) == before
|
|
assert lock.path.read_bytes() == before_lock
|
|
assert not (root / 'ambient-cache').exists()
|
|
lock_bytes = (repo / 'package-lock.json').read_bytes()
|
|
(repo / 'package.json').write_text('not json', encoding='utf-8')
|
|
assert cli.cmd_update(args) == 1
|
|
assert (repo / 'package-lock.json').read_bytes() == lock_bytes
|
|
assert 'npm update failed' in capsys.readouterr().out
|
|
|
|
node_binary.rename(node_binary.with_suffix('.held'))
|
|
assert cli.cmd_update(args) == 1
|
|
assert (repo / 'package-lock.json').read_bytes() == lock_bytes
|
|
assert 'not installed' in capsys.readouterr().out
|
|
node_binary.with_suffix('.held').rename(node_binary)
|
|
npm.binary(npm_entry, target).unlink()
|
|
assert cli.cmd_update(args) == 1
|
|
assert (repo / 'package-lock.json').read_bytes() == lock_bytes
|
|
assert 'not installed' in capsys.readouterr().out
|
|
assert dict(os.environ) == before and lock.path.read_bytes() == before_lock
|