#!/usr/bin/env python3 """0.31.7 offline update-retention regression without systemd or root privileges.""" from __future__ import annotations import os from pathlib import Path import subprocess import tempfile import time ROOT = Path(__file__).resolve().parents[2] UPDATER = ROOT / "update.sh" def mkdir_age(path: Path, days: int) -> None: path.mkdir(parents=True) stamp = time.time() - days * 86400 os.utime(path, (stamp, stamp)) def call(update_prefix: Path, cargo_target: Path, mockbin: Path, *, fail: bool = False, plan: bool = False) -> subprocess.CompletedProcess[str]: env = os.environ.copy() env["PATH"] = f"{mockbin}:{env.get('PATH', '')}" env["CARGO_TARGET_DIR"] = str(cargo_target) env["RP_TEST_UPDATE_FAIL"] = "1" if fail else "0" args = ["bash", str(UPDATER), "--prefix", str(update_prefix)] if plan: args.append("--plan") return subprocess.run(args, env=env, capture_output=True, text=True, timeout=15, check=False) def main() -> None: subprocess.run(["bash", "-n", str(UPDATER)], check=True) with tempfile.TemporaryDirectory(prefix="rp-release-0317-") as tmp: root = Path(tmp) prefix = root / "installed rustproxy" releases = prefix / "releases" backups = prefix / "backups" releases.mkdir(parents=True) backups.mkdir() for name, age in [("r-new", 0), ("r-second", 2), ("r-live", 8), ("r-ancient", 30), ("r-older extra", 40)]: mkdir_age(releases / name, age) (prefix / "current").symlink_to(releases / "r-live", target_is_directory=True) for days in range(1, 6): mkdir_age(backups / f"before-{days}", days) target = root / "target" for profile in ["release", "debug"]: (target / profile / "deps").mkdir(parents=True) (target / profile / ".fingerprint").mkdir() (target / profile / "build").mkdir() (target / profile / "rust-proxy").write_text("compiled binary") mkdir_age(target / profile / "incremental" / "old", 50) mkdir_age(target / profile / "incremental" / "new", 1) mockbin = root / "bin" mockbin.mkdir() stub = mockbin / "python3" stub.write_text("#!/bin/sh\n[ \"${RP_TEST_UPDATE_FAIL:-0}\" != 1 ] || exit 17\nexit 0\n") stub.chmod(0o755) result = call(prefix, target, mockbin, fail=True) assert result.returncode == 17, result.stderr assert (releases / "r-older extra").exists(), "failed update cleaned releases" assert (target / "release" / "incremental" / "old").exists(), "failed update cleaned target" result = call(prefix, target, mockbin, plan=True) assert result.returncode == 0, result.stderr assert (releases / "r-older extra").exists(), "planned update cleaned releases" result = call(prefix, target, mockbin) assert result.returncode == 0, (result.stdout, result.stderr) assert sorted(path.name for path in releases.iterdir()) == ["r-live", "r-new"], result.stdout assert sorted(path.name for path in backups.iterdir()) == ["before-1", "before-2", "before-3"], result.stdout assert (prefix / "current").resolve() == releases / "r-live", "current symlink changed" for profile in ["debug", "release"]: assert not (target / profile / "incremental" / "old").exists(), "old incremental retained" assert (target / profile / "incremental" / "new").exists(), "recent incremental removed" for artifact in ["deps", ".fingerprint", "build", "rust-proxy", ".cargo-lock"]: assert (target / profile / artifact).exists(), f"removed Cargo artifact: {profile}/{artifact}" # Safety: do not touch target if a parent directory is a symlink. target_symlink_root = root / "symlink" target_symlink_root.symlink_to(target, target_is_directory=True) mkdir_age(target / "release" / "incremental" / "symlink-test", 50) result = call(prefix, target_symlink_root, mockbin) assert result.returncode == 0, result.stderr assert (target / "release" / "incremental" / "symlink-test").exists(), "followed symlinked target" # Static source contracts for the managed-update cache handoff. deploy = (ROOT / "scripts/deploy/deploy_runtime.py").read_text() runtime = (ROOT / "src/main/admin_runtime.rs").read_text() demote = (ROOT / "src/cache/cold/demotion.rs").read_text() assert "stop_for_managed_update(data)" in deploy assert "run(['systemctl', 'stop', SERVICE])" in deploy assert "data_dir.join(\".managed-update\").is_file()" in runtime assert "state.cache.flush_hot_to_cold_for_update()" in runtime assert "self.flush_hot_to_cold_inner(true).await" in demote print("PASS: 0.31.7 failed/plan/successful update, two releases + three backups, Cargo safety/symlink guards, managed-update flush wiring.") if __name__ == "__main__": main()