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.
83 lines
3.9 KiB
Python
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)
|