uv, npm ci, icon generation and the desktop build printed hundreds of lines on every interactive install, update and `hermes desktop`. pm/progress.py holds one policy. CI (CI / GITHUB_ACTIONS) or HERMES_VERBOSE=1 streams child output unchanged. Otherwise each child is one "→ label…" line, rewritten in place with its latest output on a terminal, or just the start and finish lines when piped. A failure always prints the last 80 lines. HERMES_VERBOSE=0 forces containment. Applied to PM's uv runs (no uv --verbose outside CI; quick steps stay silent unless they fail), the source build scripts (node-deps, TUI, icons, web; npm warn lines stay out of the live line), the desktop build and package steps, and the Windows ARM64 vcpkg/OpenSSL provider. The policy reads the parent's environment, so the CI=1 the builders force on their node children does not switch it to verbose.
87 lines
3.5 KiB
Python
87 lines
3.5 KiB
Python
"""Mirrored and air-gapped networks configure indexes through pip or uv; PM forwards
|
|
exactly that into uv while still refusing every other ambient uv setting."""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import subprocess
|
|
|
|
import pytest
|
|
|
|
from pm.environment import PythonEnvironment, _base_environment
|
|
from pm.package import InstallError
|
|
|
|
|
|
@pytest.fixture
|
|
def clean_index_env(monkeypatch, tmp_path):
|
|
for key in list(os.environ):
|
|
if key.startswith(("UV_", "PIP_")):
|
|
monkeypatch.delenv(key)
|
|
monkeypatch.setenv("PIP_CONFIG_FILE", os.devnull)
|
|
return tmp_path
|
|
|
|
|
|
def test_pip_index_reaches_uv_but_ambient_uv_selection_does_not(clean_index_env, monkeypatch):
|
|
monkeypatch.setenv("PIP_INDEX_URL", "https://mirror.example/simple")
|
|
monkeypatch.setenv("PIP_TRUSTED_HOST", "mirror.example")
|
|
monkeypatch.setenv("UV_HTTP_TIMEOUT", "300")
|
|
monkeypatch.setenv("UV_INDEX_CORP_PASSWORD", "s3cret")
|
|
monkeypatch.setenv("UV_PYTHON", "/poison/python")
|
|
monkeypatch.setenv("UV_CACHE_DIR", "/poison/cache")
|
|
monkeypatch.setenv("UV_PROJECT_ENVIRONMENT", "/poison/venv")
|
|
|
|
env = _base_environment()
|
|
|
|
assert env["UV_INDEX_URL"] == "https://mirror.example/simple"
|
|
assert env["UV_INSECURE_HOST"] == "mirror.example"
|
|
assert env["UV_HTTP_TIMEOUT"] == "300"
|
|
assert env["UV_INDEX_CORP_PASSWORD"] == "s3cret"
|
|
assert not {"UV_PYTHON", "UV_CACHE_DIR", "UV_PROJECT_ENVIRONMENT"} & env.keys()
|
|
|
|
|
|
def test_pip_conf_is_bridged_only_when_uv_has_no_index(clean_index_env, monkeypatch):
|
|
pip_conf = clean_index_env / "pip.conf"
|
|
# Percent-encoded credentials: pip reads its config raw, so must the bridge.
|
|
pip_conf.write_text("[global]\nindex-url = https://user:p%40ss@mirror.example/simple\n", encoding="utf-8")
|
|
monkeypatch.setenv("PIP_CONFIG_FILE", str(pip_conf))
|
|
|
|
assert _base_environment()["UV_INDEX_URL"] == "https://user:p%40ss@mirror.example/simple"
|
|
|
|
monkeypatch.setenv("UV_DEFAULT_INDEX", "https://explicit.example/simple")
|
|
env = _base_environment()
|
|
assert env["UV_DEFAULT_INDEX"] == "https://explicit.example/simple"
|
|
assert "UV_INDEX_URL" not in env
|
|
|
|
|
|
def test_streamed_runs_do_not_request_uv_debug_output(tmp_path, monkeypatch):
|
|
import io
|
|
from pm import environment
|
|
|
|
monkeypatch.setenv("HERMES_VERBOSE", "1")
|
|
|
|
seen: list[list[str]] = []
|
|
kwargs_seen: list[dict] = []
|
|
|
|
def record(command, **kwargs):
|
|
seen.append(command)
|
|
kwargs_seen.append(kwargs)
|
|
return subprocess.CompletedProcess(command, 0, "", "")
|
|
|
|
monkeypatch.setattr(environment, "_run_streaming", record)
|
|
PythonEnvironment(uv=tmp_path / "uv", python=tmp_path / "python", destination=tmp_path / "venv",
|
|
cache=tmp_path / "cache", env={}, output=io.StringIO())._run(["sync"], cwd=tmp_path, timeout=5)
|
|
(command,), (kwargs,) = seen, kwargs_seen
|
|
# Verbose only where the build backend speaks; uv's own DEBUG stays silent.
|
|
assert "--verbose" in command and kwargs["env"]["RUST_LOG"] == "uv_build_frontend=debug"
|
|
|
|
|
|
def test_uv_timeout_names_the_mirror_knobs(tmp_path, monkeypatch):
|
|
def stall(*args, **kwargs):
|
|
raise subprocess.TimeoutExpired(args[0], kwargs["timeout"])
|
|
|
|
monkeypatch.setattr(subprocess, "run", stall)
|
|
environment = PythonEnvironment(uv=tmp_path / "uv", python=tmp_path / "python",
|
|
destination=tmp_path / "venv", cache=tmp_path / "cache", env={})
|
|
with pytest.raises(InstallError, match="UV_INDEX_URL") as info:
|
|
environment._run(["sync"], cwd=tmp_path, timeout=7)
|
|
assert "timed out after 7s" in str(info.value)
|