114 lines
5.0 KiB
Python
114 lines
5.0 KiB
Python
"""The CI bootstrap reads PM's pins without importing installed dependencies."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import subprocess
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
from pm.lock import Lockfile
|
|
from pm.paths import lockfile_path
|
|
from pm.store import current_target
|
|
|
|
|
|
@pytest.mark.parametrize("extras", [[], ["dev"]], ids=["runtime", "tests"])
|
|
def test_development_setup_keeps_test_groups_out_of_the_runtime(tmp_path, monkeypatch, extras):
|
|
from types import SimpleNamespace
|
|
import shutil
|
|
|
|
from scripts.ci import setup_toolchain
|
|
from pm import lock_project
|
|
from tests.pm.test_workspace_build_inputs import _wheel
|
|
|
|
monkeypatch.setattr(Path, "home", lambda: tmp_path / "home")
|
|
core = tmp_path / "core"
|
|
core.mkdir()
|
|
wheels = tmp_path / "wheels"
|
|
wheels.mkdir()
|
|
_wheel(wheels, "test_only_dep", "1.0")
|
|
(core / "pyproject.toml").write_text(
|
|
'[project]\nname="ci-test-environment"\nversion="1"\nrequires-python=">=3.11"\n'
|
|
'[project.optional-dependencies]\ndev=[]\n'
|
|
'[dependency-groups]\ntest=["test-only-dep==1.0"]\n'
|
|
'[tool.uv]\npackage=false\nno-index=true\n'
|
|
f'find-links=[{json.dumps(wheels.as_posix())}]\n', encoding="utf-8",
|
|
)
|
|
uv = shutil.which("uv")
|
|
assert uv
|
|
monkeypatch.setattr("pm.client.is_runtime", lambda: True)
|
|
monkeypatch.setattr("pm._uv._toolchain", lambda **kwargs: (Path(uv), Path(sys.executable)))
|
|
lock_project(core, python=Path(sys.executable), offline=True, explicit=True)
|
|
home = tmp_path / "ci-home"
|
|
monkeypatch.setenv("HERMES_HOME", str(home))
|
|
monkeypatch.setattr("pm.paths.repo_root", lambda: core)
|
|
# This test exercises the worker-side CI environment split with offline uv.
|
|
# Dispatch into that worker is covered by test_runtime_entrypoints.
|
|
monkeypatch.setattr("pm.runtime.is_runtime", lambda: True)
|
|
files = {name: tmp_path / name for name in ("GITHUB_ENV", "GITHUB_OUTPUT", "GITHUB_PATH")}
|
|
for name, file in files.items():
|
|
monkeypatch.setenv(name, str(file))
|
|
|
|
setup_toolchain.dependencies(SimpleNamespace(extras=extras, home=home))
|
|
|
|
outputs = dict(line.split("=", 1) for line in files["GITHUB_OUTPUT"].read_text(encoding="utf-8").splitlines())
|
|
result = subprocess.run(
|
|
[outputs["python-path"], "-I", "-c", "import importlib.util; print(importlib.util.find_spec('test_only_dep') is not None)"],
|
|
cwd=tmp_path, capture_output=True, text=True, timeout=30,
|
|
)
|
|
assert result.returncode == 0, result.stderr
|
|
assert result.stdout.strip() == str("dev" in extras)
|
|
assert Path(outputs["venv"]).is_relative_to(home)
|
|
assert not (core / ".venv").exists()
|
|
from hermes_cli.runtime_paths import runtime_facts_path
|
|
assert runtime_facts_path(core).exists() == ("dev" not in extras)
|
|
|
|
|
|
@pytest.mark.parametrize("toolchain,names", [
|
|
("python", {"python", "uv"}),
|
|
("node", {"node", "npm"}),
|
|
("all", {"python", "uv", "node", "npm"}),
|
|
])
|
|
def test_stdlib_bootstrap_exports_the_pm_lock(toolchain, names, tmp_path):
|
|
root = Path(__file__).resolve().parents[2]
|
|
output = tmp_path / "output"
|
|
envfile = tmp_path / "environment"
|
|
home = tmp_path / "runner state"
|
|
env = {**os.environ, "GITHUB_OUTPUT": str(output), "GITHUB_ENV": str(envfile)}
|
|
result = subprocess.run(
|
|
[sys.executable, "-S", str(root / "scripts/ci/setup_toolchain.py"),
|
|
"prepare", "--toolchain", toolchain, "--home", str(home)],
|
|
cwd=tmp_path, env=env, capture_output=True, text=True, encoding="utf-8", timeout=30,
|
|
)
|
|
assert result.returncode == 0, result.stdout + result.stderr
|
|
values = dict(line.split("=", 1) for line in output.read_text(encoding="utf-8-sig").splitlines())
|
|
lock = Lockfile(lockfile_path())
|
|
assert json.loads(values["packages"]) == sorted(names)
|
|
assert values["target"] == current_target()
|
|
for name in names:
|
|
expected = lock.version(name)
|
|
assert values[f"{name}-version"] == (expected.partition("+")[0] if name == "python" else expected)
|
|
exported = dict(line.split("=", 1) for line in envfile.read_text(encoding="utf-8-sig").splitlines())
|
|
assert Path(exported["HERMES_HOME"]) == home
|
|
assert Path(exported["HERMES_RUNTIME_DIR"]).is_relative_to(home)
|
|
assert not Path(exported["HERMES_RUNTIME_DIR"]).exists(), "prepare must not provision before cache restore"
|
|
|
|
|
|
@pytest.mark.parametrize("extras", ['"dev"', '{}', '[1]', '["dev\\nHERMES_HOME=bad"]', '["--all"]'])
|
|
def test_invalid_extras_do_not_export_or_install(extras, tmp_path):
|
|
root = Path(__file__).resolve().parents[2]
|
|
output = tmp_path / "output"
|
|
home = tmp_path / "state"
|
|
result = subprocess.run(
|
|
[sys.executable, "-S", str(root / "scripts/ci/setup_toolchain.py"), "prepare",
|
|
"--home", str(home), "--extras", extras],
|
|
cwd=tmp_path, env={**os.environ, "GITHUB_OUTPUT": str(output)},
|
|
capture_output=True, text=True, encoding="utf-8", timeout=30,
|
|
)
|
|
assert result.returncode != 0
|
|
assert "extras must be a JSON array of extra names" in result.stderr
|
|
assert not output.exists()
|
|
assert not home.exists()
|