# Test for modules/server-deploy-finalize.nix. # # Covers the decision and scheduling logic with fabricated profile directories, # since spawning a second booted NixOS toplevel to diff kernels is too heavy for # a runNixOSTest. We rely on the shellcheck pass baked into writeShellApplication # to catch syntax regressions in the script itself. { lib, pkgs, inputs, ... }: pkgs.testers.runNixOSTest { name = "deploy-finalize"; node.specialArgs = { inherit inputs lib; username = "testuser"; }; nodes.machine = { ... }: { imports = [ ../modules/server-deploy-finalize.nix ]; services.deployFinalize = { enable = true; # Shorter default in the test to make expected-substring assertions # stable and reinforce that the option is wired through. delay = 15; }; }; testScript = '' start_all() machine.wait_for_unit("multi-user.target") # Test fixtures: fabricated profile trees whose kernel/initrd/kernel-modules # symlinks are under test control. `readlink -e` requires the targets to # exist, so we point at real files in /tmp rather than non-existent paths. machine.succeed( "mkdir -p /tmp/profile-same /tmp/profile-changed-kernel " "/tmp/profile-changed-initrd /tmp/profile-changed-modules " "/tmp/profile-missing /tmp/fake-targets" ) machine.succeed( "touch /tmp/fake-targets/alt-kernel /tmp/fake-targets/alt-initrd " "/tmp/fake-targets/alt-modules" ) booted_kernel = machine.succeed("readlink -e /run/booted-system/kernel").strip() booted_initrd = machine.succeed("readlink -e /run/booted-system/initrd").strip() booted_modules = machine.succeed("readlink -e /run/booted-system/kernel-modules").strip() def link_profile(path, kernel, initrd, modules): machine.succeed(f"ln -sf {kernel} {path}/kernel") machine.succeed(f"ln -sf {initrd} {path}/initrd") machine.succeed(f"ln -sf {modules} {path}/kernel-modules") # profile-same: matches booted exactly → should choose `switch`. link_profile("/tmp/profile-same", booted_kernel, booted_initrd, booted_modules) machine.succeed("mkdir -p /tmp/profile-same/bin") machine.succeed( "ln -sf /run/current-system/bin/switch-to-configuration " "/tmp/profile-same/bin/switch-to-configuration" ) # profile-changed-kernel: kernel differs only → should choose `reboot`. link_profile( "/tmp/profile-changed-kernel", "/tmp/fake-targets/alt-kernel", booted_initrd, booted_modules, ) # profile-changed-initrd: initrd differs only → should choose `reboot`. link_profile( "/tmp/profile-changed-initrd", booted_kernel, "/tmp/fake-targets/alt-initrd", booted_modules, ) # profile-changed-modules: kernel-modules differs only → should choose `reboot`. # Catches the obelisk PR / nixpkgs auto-upgrade case where modules rebuild # against the same kernel but ABI-incompatible. link_profile( "/tmp/profile-changed-modules", booted_kernel, booted_initrd, "/tmp/fake-targets/alt-modules", ) # profile-missing: no kernel/initrd/kernel-modules → should fail closed. with subtest("dry-run against identical profile selects switch"): rc, out = machine.execute( "deploy-finalize --dry-run --profile /tmp/profile-same 2>&1" ) assert rc == 0, f"rc={rc}\n{out}" assert "action=switch" in out, out assert "services only" in out, out assert "dry-run — not scheduling" in out, out assert "would run: /tmp/profile-same/bin/switch-to-configuration switch" in out, out assert "would schedule: systemd-run" in out, out with subtest("dry-run against changed-kernel profile selects reboot"): rc, out = machine.execute( "deploy-finalize --dry-run --profile /tmp/profile-changed-kernel 2>&1" ) assert rc == 0, f"rc={rc}\n{out}" assert "action=reboot" in out, out assert "reason=kernel changed" in out, out assert "systemctl reboot" in out, out with subtest("dry-run against changed-initrd profile selects reboot"): rc, out = machine.execute( "deploy-finalize --dry-run --profile /tmp/profile-changed-initrd 2>&1" ) assert rc == 0, f"rc={rc}\n{out}" assert "action=reboot" in out, out assert "reason=initrd changed" in out, out with subtest("dry-run against changed-modules profile selects reboot"): rc, out = machine.execute( "deploy-finalize --dry-run --profile /tmp/profile-changed-modules 2>&1" ) assert rc == 0, f"rc={rc}\n{out}" assert "action=reboot" in out, out assert "reason=kernel-modules changed" in out, out with subtest("dry-run against empty profile fails closed with rc=1"): rc, out = machine.execute( "deploy-finalize --dry-run --profile /tmp/profile-missing 2>&1" ) assert rc == 1, f"rc={rc}\n{out}" assert "missing kernel, initrd, or kernel-modules" in out, out with subtest("--delay override is reflected in output"): rc, out = machine.execute( "deploy-finalize --dry-run --delay 7 --profile /tmp/profile-same 2>&1" ) assert rc == 0, f"rc={rc}\n{out}" assert "delay=7s" in out, out with subtest("configured default delay from module option is used"): rc, out = machine.execute( "deploy-finalize --dry-run --profile /tmp/profile-same 2>&1" ) assert rc == 0, f"rc={rc}\n{out}" # module option delay=15 in nodes.machine above. assert "delay=15s" in out, out with subtest("unknown option rejected with rc=2"): rc, out = machine.execute("deploy-finalize --bogus 2>&1") assert rc == 2, f"rc={rc}\n{out}" assert "unknown option --bogus" in out, out with subtest("non-dry run arms a transient systemd timer"): # Long delay so the timer doesn't fire during the test. We stop it # explicitly afterwards. rc, out = machine.execute( "deploy-finalize --delay 3600 --profile /tmp/profile-same 2>&1" ) assert rc == 0, f"scheduling rc={rc}\n{out}" # Confirm exactly one transient timer is active. timers = machine.succeed( "systemctl list-units --type=timer --no-legend 'deploy-finalize-*.timer' " "--state=waiting | awk 'NF{print $1}'" ).strip().splitlines() assert len(timers) == 1, f"expected exactly one pending timer, got {timers}" assert timers[0].startswith("deploy-finalize-"), timers with subtest("back-to-back scheduling cancels the previous timer"): # The previous subtest left one timer armed. Schedule again; the old # one should be stopped before the new unit name is created. machine.succeed("sleep 1") # ensure a distinct unit-name timestamp rc, out = machine.execute( "deploy-finalize --delay 3600 --profile /tmp/profile-same 2>&1" ) assert rc == 0, f"second-schedule rc={rc}\n{out}" timers = machine.succeed( "systemctl list-units --type=timer --no-legend 'deploy-finalize-*.timer' " "--state=waiting | awk 'NF{print $1}'" ).strip().splitlines() assert len(timers) == 1, f"expected only the new timer, got {timers}" # Clean up so the test's shutdown path is quiet. machine.succeed( "systemctl stop 'deploy-finalize-*.timer' 'deploy-finalize-*.service' " "2>/dev/null || true" ) ''; }