Files
nixconfig/switch-test.py
T
OpenAI Coding Assistant 4893fcfec0 refactor: share workstation setup and boot-staged updates
Use dev-owned checkouts and one update policy on both hosts. Keep only hardware and deployment identity in host modules, use the same SDDM/UWSM workstation module in the VM, and install a host-configured manual switch command with lock regression tests.
2026-09-05 23:52:51 -05:00

83 lines
3.9 KiB
Python

"""Test manual switching for both hosts without Nix builds or host activation."""
import json
import os
from pathlib import Path
import subprocess
import sys
import tempfile
script = Path(sys.argv[1]).resolve()
if os.geteuid() == 0:
raise SystemExit("Run this check as an unprivileged user (or through nix flake check).")
mock = r'''import fcntl, json, os, pathlib, sys
name = pathlib.Path(sys.argv[0]).name
args = sys.argv[1:]
with open(os.environ["CALLS"], "a") as f:
f.write(json.dumps([name, *args]) + "\n")
if name == "nix" or (name == "sudo" and args != ["-v"]):
# Model sudo's descriptor cleanup: the parent shell must retain the lock.
try: os.close(9)
except OSError: pass
with open(pathlib.Path(os.environ["CACHE_DIRECTORY"]) / "lock", "a") as lock:
try: fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError: pass
else: raise AssertionError("Updater lock was released before activation completed")
if name == "nix":
assert pathlib.Path.cwd() == pathlib.Path(os.environ["NIXOS_CONFIG_REPO"])
assert args == ["build", f'.#nixosConfigurations.{os.environ["NIXOS_UPDATE_HOST"]}.config.system.build.toplevel', "--no-update-lock-file", "--no-link", "--print-out-paths"]
if os.environ["SCENARIO"] == "build-failure": sys.exit(42)
print(os.environ["BUILT"])
elif name == "sudo":
if args == ["-v"]:
if os.environ["SCENARIO"] == "sudo-failure": sys.exit(43)
else:
assert args[0] == os.environ["BUILT"] + "/sw/bin/nixos-rebuild"
assert args[2:] == ["--no-reexec", "--store-path", os.environ["BUILT"]]
if os.environ["SCENARIO"] == "activation-failure": sys.exit(44)
else:
raise AssertionError(name)
'''
with tempfile.TemporaryDirectory(prefix="switch-test-") as directory:
root = Path(directory)
repo = root / "repo with spaces"
binaries = root / "bin"
calls = root / "calls"
repo.mkdir()
binaries.mkdir()
for name in ["nix", "sudo"]:
executable = binaries / name
executable.write_text("#!" + sys.executable + "\n" + mock)
executable.chmod(0o755)
env = dict(os.environ, PATH=str(binaries) + os.pathsep + os.environ["PATH"],
NIXOS_CONFIG_REPO=str(repo), CACHE_DIRECTORY=str(root / "cache"),
CALLS=str(calls), BUILT=str(root / "built system"))
cases = [([], "switch", "success", 0), (["dry-activate"], "dry-activate", "success", 0),
(["boot"], "boot", "success", 0), (["test"], "test", "success", 0),
(["--help"], None, "help", 0), (["invalid"], None, "invalid", 2),
(["switch", "extra"], None, "invalid", 2),
([], None, "build-failure", 42), ([], None, "sudo-failure", 43),
([], "switch", "activation-failure", 44), ([], None, "invalid-host", 2)]
for host in ["nixos", "dev"]:
for args, action, scenario, expected_code in cases:
calls.unlink(missing_ok=True)
result = subprocess.run(
["bash", str(script), *args], cwd=root,
env=env | {"NIXOS_UPDATE_HOST": "unknown" if scenario == "invalid-host" else host,
"SCENARIO": scenario}, text=True, capture_output=True,
)
assert result.returncode == expected_code, (host, scenario, result.stdout, result.stderr)
log = [json.loads(line) for line in calls.read_text().splitlines()] if calls.exists() else []
if action:
assert len(log) == 3, log
assert log[0] == ["sudo", "-v"], log
assert log[-1][2] == action, log
elif scenario == "build-failure":
assert [call[0] for call in log] == ["sudo", "nix"], log
elif scenario == "sudo-failure":
assert log == [["sudo", "-v"]], log
else:
assert not log, log
print("PASS", host, args or ["default"], scenario)