Root-level tests/ is for root-level modules; 19 files testing scripts/, 3 testing pm/ and 5 testing hermes_cli/ move to the mirrored directory (workflow file lists and cross-imports updated; path arithmetic bumped one level). tests/pm gains a conftest with the isolated_machine_home fixture that seven modules had copy-pasted verbatim.
172 lines
7.8 KiB
Python
172 lines
7.8 KiB
Python
"""Real bootstrap downloads preserve the pin across an upstream outage."""
|
|
import hashlib
|
|
import io
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
import tarfile
|
|
import threading
|
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
|
|
|
|
@pytest.fixture
|
|
def server():
|
|
class Handler(BaseHTTPRequestHandler):
|
|
def do_GET(self):
|
|
self.server.requests.append(self.path)
|
|
body = self.server.files.get(self.path)
|
|
self.send_response(200 if body is not None else 404)
|
|
self.send_header("Content-Length", str(len(body or b"")))
|
|
self.end_headers()
|
|
self.wfile.write(body or b"")
|
|
|
|
def log_message(self, *_args):
|
|
pass
|
|
|
|
http = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
|
|
http.files, http.requests = {}, []
|
|
thread = threading.Thread(target=http.serve_forever, daemon=True)
|
|
thread.start()
|
|
try:
|
|
yield http, f"http://127.0.0.1:{http.server_port}"
|
|
finally:
|
|
http.shutdown()
|
|
http.server_close()
|
|
thread.join(timeout=5)
|
|
|
|
|
|
def fixture_bytes(server, mode, body):
|
|
http, base = server
|
|
if mode != "missing":
|
|
http.files["/primary"] = b"bad bytes" if mode == "corrupt" else body
|
|
http.files["/mirror"] = body
|
|
return base + "/primary", base + "/mirror", hashlib.sha256(body).hexdigest()
|
|
|
|
|
|
@pytest.mark.platforms("windows")
|
|
@pytest.mark.parametrize("mode", ["primary", "missing", "corrupt", "both-missing"])
|
|
def test_windows_download_uses_only_verified_candidates(tmp_path, server, mode):
|
|
body = b"exact archive bytes"
|
|
primary, mirror, digest = fixture_bytes(server, mode, body)
|
|
if mode == "both-missing":
|
|
server[0].files.clear()
|
|
destination = tmp_path / "out.zip"
|
|
script = tmp_path / "driver.ps1"
|
|
script.write_text(
|
|
f". '{ROOT / 'scripts/install.ps1'}' -HermesHome '{tmp_path / 'home'}' -InstallDir '{tmp_path / 'repo'}'\n"
|
|
f"Invoke-VerifiedDownload -Url '{primary}' -MirrorUrl '{mirror}' -Sha256 '{digest}' -OutFile '{destination}'\n",
|
|
encoding="utf-8",
|
|
)
|
|
result = subprocess.run(["powershell", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", str(script)],
|
|
capture_output=True, text=True, timeout=60)
|
|
if mode in ("primary", "missing"):
|
|
assert result.returncode == 0, result.stdout + result.stderr
|
|
assert destination.read_bytes() == body
|
|
else:
|
|
assert result.returncode != 0
|
|
assert primary in result.stdout + result.stderr
|
|
if mode == "both-missing":
|
|
assert mirror in result.stdout + result.stderr
|
|
assert server[0].requests == (["/primary", "/mirror"] if mode in ("missing", "both-missing") else ["/primary"])
|
|
|
|
|
|
@pytest.mark.platforms("windows")
|
|
@pytest.mark.parametrize("name", ["uv", "git"])
|
|
def test_windows_bootstrap_rejects_corrupt_bytes_before_extract(tmp_path, server, name):
|
|
primary, mirror, digest = fixture_bytes(server, "corrupt", b"expected archive")
|
|
script = tmp_path / "caller.ps1"
|
|
script.write_text(
|
|
f". '{ROOT / 'scripts/install.ps1'}' -HermesHome '{tmp_path / 'home'}'\n"
|
|
f"$env:HERMES_RUNTIME_DIR = '{tmp_path / 'tools'}'\n"
|
|
"function Get-Command { param($Name) return $null }\n"
|
|
"$target = 'win32-' + (Get-WindowsArch)\n"
|
|
f"$pin = @{{Url='{primary}'; MirrorUrl='{mirror}'; Sha256='{digest}'}}\n"
|
|
+ ("$script:UvPinFiles[$target] = $pin\nGet-Uv\n" if name == "uv" else "$script:GitPinFiles[$target] = $pin\nGet-PinnedGit\n"),
|
|
encoding="utf-8",
|
|
)
|
|
result = subprocess.run(["powershell", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", str(script)],
|
|
capture_output=True, text=True, timeout=60)
|
|
assert result.returncode != 0
|
|
assert "digest mismatch" in result.stdout + result.stderr
|
|
assert server[0].requests == ["/primary"]
|
|
assert not (tmp_path / "tools").exists()
|
|
|
|
|
|
def uv_archive():
|
|
data = b"#!/bin/sh\n[ \"$1\" = --version ] && { printf 'fixture uv\\n'; exit 0; }; exit 73\n"
|
|
buffer = io.BytesIO()
|
|
with tarfile.open(fileobj=buffer, mode="w:gz") as archive:
|
|
member = tarfile.TarInfo("uv/uv")
|
|
member.mode, member.size = 0o755, len(data)
|
|
archive.addfile(member, io.BytesIO(data))
|
|
return buffer.getvalue()
|
|
|
|
|
|
@pytest.mark.platforms("posix")
|
|
@pytest.mark.parametrize("mode", ["primary", "missing", "corrupt", "both-missing"])
|
|
def test_posix_download_keeps_the_pinned_hash(tmp_path, server, mode):
|
|
primary, mirror, digest = fixture_bytes(server, mode, uv_archive())
|
|
if mode == "both-missing":
|
|
server[0].files.clear()
|
|
script = f"""
|
|
source '{ROOT / 'scripts/install.sh'}'
|
|
command() {{ if [ "$*" = '-v uv' ]; then return 1; fi; builtin command "$@"; }}
|
|
uv_bootstrap_pin() {{ UV_PIN_VERSION=fixture; UV_PIN_URL='{primary}'; UV_PIN_MIRROR='{mirror}'; UV_PIN_SHA256='{digest}'; }}
|
|
ensure_uv
|
|
"""
|
|
env = {**os.environ, "HERMES_HOME": str(tmp_path / "home"), "HOME": str(tmp_path / "home"), "HERMES_RUNTIME_DIR": str(tmp_path / "tools")}
|
|
result = subprocess.run(["bash", "-c", script], cwd=tmp_path, env=env, capture_output=True, text=True, timeout=60)
|
|
assert (result.returncode == 0) == (mode in ("primary", "missing")), result.stdout + result.stderr
|
|
if mode == "corrupt":
|
|
assert "digest mismatch" in result.stderr
|
|
if mode == "both-missing":
|
|
assert primary in result.stderr and mirror in result.stderr
|
|
assert server[0].requests == (["/primary", "/mirror"] if mode in ("missing", "both-missing") else ["/primary"])
|
|
|
|
|
|
@pytest.mark.platforms("windows")
|
|
def test_generated_windows_pins_match_the_shared_authority(tmp_path):
|
|
from pm.artifact_mirror import mirror_url
|
|
script = tmp_path / "pins.ps1"
|
|
script.write_text(
|
|
f". '{ROOT / 'scripts/install.ps1'}' -HermesHome '{tmp_path / 'home'}'\n"
|
|
"@{uv=$script:UvPinFiles; git=$script:GitPinFiles} | ConvertTo-Json -Depth 5 -Compress\n",
|
|
encoding="utf-8",
|
|
)
|
|
result = subprocess.run(["powershell", "-NoProfile", "-NonInteractive", "-File", str(script)], capture_output=True, text=True, timeout=30)
|
|
assert result.returncode == 0, result.stderr
|
|
emitted = json.loads(result.stdout)
|
|
lock = json.loads((ROOT / "pm/lock.json").read_text(encoding="utf-8"))["packages"]
|
|
for name, targets in emitted.items():
|
|
assert targets
|
|
for target, pin in targets.items():
|
|
authority = lock[name]["artifacts"][target]
|
|
assert (pin["Url"], pin["Sha256"], pin["MirrorUrl"]) == (authority["url"], authority["sha256"], mirror_url(authority["sha256"]))
|
|
|
|
|
|
@pytest.mark.platforms("posix")
|
|
def test_dev_setup_reaches_the_same_mirror_without_python(tmp_path, server):
|
|
from pm.store import current_target
|
|
body = uv_archive()
|
|
digest = hashlib.sha256(body).hexdigest()
|
|
http, base = server
|
|
http.files["/archive/" + digest] = body
|
|
repo = tmp_path / "repo"
|
|
(repo / "pm").mkdir(parents=True)
|
|
shutil.copyfile(ROOT / "setup-hermes.sh", repo / "setup-hermes.sh")
|
|
(repo / "pm/artifact-mirror.json").write_text(json.dumps({"origin": base, "prefix": "archive/"}, indent=2), encoding="utf-8")
|
|
(repo / "pm/lock.json").write_text(json.dumps({"packages": {
|
|
"uv": {"version": "fixture", "artifacts": {current_target(): {"url": base + "/missing.tar.gz", "sha256": digest}}},
|
|
"python": {"version": "3.14.7"},
|
|
}}, indent=2, sort_keys=True), encoding="utf-8")
|
|
env = {**os.environ, "HERMES_HOME": str(tmp_path / "home"), "HOME": str(tmp_path / "home"), "HERMES_RUNTIME_DIR": str(tmp_path / "tools")}
|
|
result = subprocess.run(["bash", str(repo / "setup-hermes.sh")], cwd=repo, env=env, text=True, capture_output=True, timeout=60)
|
|
assert result.returncode == 73, result.stdout + result.stderr # stop at the bootstrap interpreter boundary
|
|
assert http.requests == ["/missing.tar.gz", "/archive/" + digest]
|