fd18b547e5
Adds the `user.rust` UPM to workshop-engine for managing Rust toolchains, components, cross-compilation targets, and cargo operations: - `rustup default <toolchain>` / `rustup component add` / `rustup target add` - `cargo fetch` for dependency pre-fetch with optional `--locked` - `cargo install` for global crate tools with registry support Includes 275-line test suite (`tests/upm/test_rust.py`) covering the full lifecycle with optional network tests.
276 lines
9.2 KiB
Python
276 lines
9.2 KiB
Python
"""Rust UPM system tests driven by bare workshop-baker prebake.
|
|
|
|
These tests exercise the ``user.rust`` UPM through its full lifecycle:
|
|
1. sysdep injection (rustup via pacman, repology_endpoint: none)
|
|
2. rustup toolchain / component / target installation
|
|
3. cargo fetch (project dependency pre-fetch)
|
|
4. cargo install (global crate tools)
|
|
|
|
Observability
|
|
* ``RUST_LOG=info`` is injected into every baker invocation.
|
|
* On failure the *full* stdout + stderr is printed (no truncation).
|
|
* An LLM judge (``tests/llm/judge.py``) diagnoses failures with a
|
|
human-readable reason, cached per-log-hash to save tokens.
|
|
|
|
Network-dependent tests (toolchain download) are skipped by default.
|
|
Set ``RUST_UPM_NETWORK=1`` to run them.
|
|
"""
|
|
|
|
import os
|
|
import subprocess
|
|
import pytest
|
|
from probes import binary_exists
|
|
|
|
# ── helpers ─────────────────────────────────────────────────────────
|
|
|
|
RUST_LOG = os.environ.get("RUST_LOG", "info")
|
|
|
|
|
|
def _yml(user_section):
|
|
"""Build a minimal prebake.yml focused on the rust UPM."""
|
|
return """\
|
|
version: "1.0"
|
|
environment:
|
|
builder: "baremetal"
|
|
bootstrap:
|
|
user: "vulcan"
|
|
security:
|
|
drop_after: never
|
|
envvars:
|
|
prebake:
|
|
RUSTUP_DIST_SERVER: "https://mirrors.tuna.tsinghua.edu.cn/rustup"
|
|
|
|
dependencies:
|
|
config:
|
|
repology_endpoint: none
|
|
system:
|
|
pacman:
|
|
packages:
|
|
user:
|
|
rust:
|
|
""" + _indent(user_section, 6)
|
|
|
|
|
|
def _indent(text, spaces):
|
|
pad = " " * spaces
|
|
return "\n".join(pad + line if line.strip() else line
|
|
for line in text.splitlines())
|
|
|
|
|
|
def _cargo_toml(dir_path):
|
|
path = os.path.join(dir_path, "Cargo.toml")
|
|
with open(path, "w") as f:
|
|
f.write('[package]\nname = "test"\nversion = "0.1.0"\nedition = "2021"\n')
|
|
return path
|
|
|
|
|
|
def _query(cmd, timeout=60):
|
|
try:
|
|
r = subprocess.run(cmd, capture_output=True, text=True,
|
|
timeout=timeout, env={**os.environ, "HOME": "/root"})
|
|
return {"success": r.returncode == 0, "stdout": r.stdout,
|
|
"stderr": r.stderr}
|
|
except Exception as e:
|
|
return {"success": False, "stdout": "", "stderr": str(e)}
|
|
|
|
|
|
def _run_prebake(run_baker, config_path, timeout, operation, expected):
|
|
"""Run prebake with RUST_LOG, return (result, full_log)."""
|
|
env_vars = {"RUST_LOG": RUST_LOG}
|
|
r = run_baker("prebake", config_path, timeout=timeout, env=env_vars)
|
|
full_log = "[stdout]\n{}\n[stderr]\n{}".format(r["stdout"], r["stderr"])
|
|
return r, full_log
|
|
|
|
|
|
def _check(r, full_log, operation, expected):
|
|
"""Assert success; on failure call LLM judge and show full output."""
|
|
if r["success"]:
|
|
return
|
|
|
|
diagnosis = ""
|
|
try:
|
|
from llm import judge_log
|
|
j = judge_log(operation, expected, full_log)
|
|
diagnosis = "\n LLM diagnosis: [{}] {}".format(j.verdict, j.reason)
|
|
except Exception:
|
|
pass
|
|
|
|
pytest.fail(
|
|
"{} failed ({} chars):\n{}\n{}".format(
|
|
operation, len(full_log), full_log, diagnosis)
|
|
)
|
|
|
|
|
|
# ── system dependency resolution ────────────────────────────────────
|
|
|
|
|
|
def test_repology_none_config(run_baker, work_dir):
|
|
cfg = os.path.join(work_dir, "rust.yml")
|
|
with open(cfg, "w") as f:
|
|
f.write(_yml("""\
|
|
cargo: {}
|
|
"""))
|
|
|
|
r, log = _run_prebake(run_baker, cfg, timeout=120,
|
|
operation="UPM sysdep: repology_endpoint=none",
|
|
expected="rustup gets installed via pacman")
|
|
|
|
_check(r, log, "repology-none sysdep resolution",
|
|
"pacman installs rustup, empty cargo config")
|
|
|
|
assert binary_exists("/usr/bin/rustup"), "rustup not installed via sysdep"
|
|
|
|
|
|
# ── rustup: toolchain & components ──────────────────────────────────
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
"RUST_UPM_NETWORK" not in os.environ,
|
|
reason="set RUST_UPM_NETWORK=1 for tests that download toolchain/crates"
|
|
)
|
|
def test_toolchain_and_component(run_baker, work_dir):
|
|
cfg = os.path.join(work_dir, "rust.yml")
|
|
with open(cfg, "w") as f:
|
|
f.write(_yml("""\
|
|
toolchain: "stable"
|
|
components: ["rustfmt"]
|
|
"""))
|
|
|
|
r, log = _run_prebake(run_baker, cfg, timeout=600,
|
|
operation="rustup default stable + component add rustfmt",
|
|
expected="stable toolchain active, rustfmt installed")
|
|
|
|
_check(r, log, "toolchain + rustfmt installation",
|
|
"rustup default stable succeeds; rustfmt available")
|
|
|
|
assert binary_exists("/usr/bin/rustup"), "rustup not installed"
|
|
r2 = _query(["rustup", "which", "rustc"])
|
|
assert r2["success"], "rustc not found via rustup which"
|
|
assert os.path.isfile(r2["stdout"].strip()), "rustc binary missing"
|
|
|
|
|
|
# ── cross-compilation targets ───────────────────────────────────────
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
"RUST_UPM_NETWORK" not in os.environ,
|
|
reason="set RUST_UPM_NETWORK=1 for tests that download toolchain/crates"
|
|
)
|
|
def test_cross_compile_target(run_baker, work_dir):
|
|
cfg = os.path.join(work_dir, "rust.yml")
|
|
with open(cfg, "w") as f:
|
|
f.write(_yml("""\
|
|
toolchain: "stable"
|
|
targets: ["wasm32-unknown-unknown"]
|
|
"""))
|
|
|
|
r, log = _run_prebake(run_baker, cfg, timeout=600,
|
|
operation="rustup target add wasm32-unknown-unknown",
|
|
expected="wasm32 target appears in installed list")
|
|
|
|
_check(r, log, "cross-compilation target installation",
|
|
"wasm32-unknown-unknown is installed")
|
|
|
|
r2 = _query(["rustup", "target", "list", "--installed"])
|
|
assert r2["success"], "rustup target list failed"
|
|
assert "wasm32-unknown-unknown" in r2["stdout"], "wasm32 target missing"
|
|
|
|
|
|
# ── cargo fetch ─────────────────────────────────────────────────────
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
"RUST_UPM_NETWORK" not in os.environ,
|
|
reason="set RUST_UPM_NETWORK=1 for tests that download toolchain/crates"
|
|
)
|
|
def test_cargo_fetch(run_baker, work_dir):
|
|
manifest = _cargo_toml(work_dir)
|
|
cfg = os.path.join(work_dir, "rust.yml")
|
|
with open(cfg, "w") as f:
|
|
f.write(_yml("""\
|
|
toolchain: "stable"
|
|
cargo:
|
|
fetch: true
|
|
locked: true
|
|
manifests: ["{manifest}"]
|
|
""".format(manifest=manifest)))
|
|
|
|
r, log = _run_prebake(run_baker, cfg, timeout=600,
|
|
operation="cargo fetch on minimal Cargo.toml",
|
|
expected="~/.cargo/registry/cache populated")
|
|
|
|
_check(r, log, "cargo fetch dependency pre-fetch",
|
|
"cargo fetch succeeds, registry cache non-empty")
|
|
|
|
cache = os.path.expanduser("~/.cargo/registry/cache")
|
|
assert os.path.isdir(cache), "registry/cache missing"
|
|
assert len(os.listdir(cache)) > 0, "registry/cache empty after fetch"
|
|
|
|
|
|
# ── cargo install ───────────────────────────────────────────────────
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
"RUST_UPM_NETWORK" not in os.environ,
|
|
reason="set RUST_UPM_NETWORK=1 for tests that download toolchain/crates"
|
|
)
|
|
def test_cargo_install(run_baker, work_dir):
|
|
cfg = os.path.join(work_dir, "rust.yml")
|
|
with open(cfg, "w") as f:
|
|
f.write(_yml("""\
|
|
toolchain: "stable"
|
|
cargo:
|
|
packages: ["cargo-audit"]
|
|
locked: true
|
|
"""))
|
|
|
|
r, log = _run_prebake(run_baker, cfg, timeout=1200,
|
|
operation="cargo install cargo-audit",
|
|
expected="cargo-audit binary in ~/.cargo/bin/")
|
|
|
|
_check(r, log, "cargo install crate",
|
|
"cargo-audit compiled and installed")
|
|
|
|
assert binary_exists(
|
|
os.path.expanduser("~/.cargo/bin/cargo-audit")
|
|
), "cargo-audit not installed"
|
|
|
|
|
|
# ── full flow ───────────────────────────────────────────────────────
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
"RUST_UPM_NETWORK" not in os.environ,
|
|
reason="set RUST_UPM_NETWORK=1 for tests that download toolchain/crates"
|
|
)
|
|
def test_full_flow(run_baker, work_dir):
|
|
manifest = _cargo_toml(work_dir)
|
|
cfg = os.path.join(work_dir, "rust.yml")
|
|
with open(cfg, "w") as f:
|
|
f.write(_yml("""\
|
|
toolchain: "stable"
|
|
components: ["rustfmt"]
|
|
targets: ["wasm32-unknown-unknown"]
|
|
cargo:
|
|
fetch: true
|
|
locked: true
|
|
manifests: ["{manifest}"]
|
|
packages: ["cargo-watch"]
|
|
""".format(manifest=manifest)))
|
|
|
|
r, log = _run_prebake(run_baker, cfg, timeout=1200,
|
|
operation="full Rust UPM flow",
|
|
expected="all stages succeed")
|
|
|
|
_check(r, log, "full Rust UPM end-to-end",
|
|
"toolchain + cross target + cargo fetch + cargo install all pass")
|
|
|
|
assert binary_exists("/usr/bin/rustup")
|
|
r2 = _query(["rustup", "which", "rustc"])
|
|
assert r2["success"]
|
|
r3 = _query(["rustup", "target", "list", "--installed"])
|
|
assert "wasm32-unknown-unknown" in r3["stdout"]
|
|
cache = os.path.expanduser("~/.cargo/registry/cache")
|
|
assert os.path.isdir(cache) and len(os.listdir(cache)) > 0
|
|
assert binary_exists(os.path.expanduser("~/.cargo/bin/cargo-watch"))
|