102 lines
4.9 KiB
Python
Executable File
102 lines
4.9 KiB
Python
Executable File
#!/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()
|