aboutsummaryrefslogtreecommitdiffstats
path: root/tests/default.nix
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-07-18 22:35:46 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-07-18 22:35:46 +0200
commitb84c9c7382dc05d1ff5f3bdaf2bac7ce3c332007 (patch)
tree42b64963341fc7e6251e16c68dfbdb4b3be54aa1 /tests/default.nix
parentscripts/update_hosts.sh: Use faster `nix copy` and ping hosts after update (diff)
downloadnixos-server-b84c9c7382dc05d1ff5f3bdaf2bac7ce3c332007.zip
tests: Refactor to unify tests and avoid duplicated code
Diffstat (limited to '')
-rw-r--r--tests/default.nix242
1 files changed, 241 insertions, 1 deletions
diff --git a/tests/default.nix b/tests/default.nix
index d9b354a..48fa9ec 100644
--- a/tests/default.nix
+++ b/tests/default.nix
@@ -3,11 +3,251 @@
nixLib,
pkgs,
}: let
+ splitAttr = attr: let
+ name = builtins.elemAt (builtins.attrNames attr) 0;
+ in {
+ inherit name;
+ value = attr.${name};
+ };
+
+ vhack.runTest = {
+ name,
+ nodes ? {},
+ serverDomains ? [],
+ testScript ? {_, ...}: "",
+ services ? [],
+ ignore ? null,
+ }: let
+ # Apparently the default's (e.g. `serivces ? []`), are not applied for the `@args`
+ # syntax.
+ args = {
+ inherit name nodes serverDomains testScript services ignore;
+ };
+ in
+ specialArgs.nixos-lib.runTest {
+ hostPkgs = pkgs;
+ inherit (args) name;
+
+ meta.broken = pkgs.lib.mkIf (args.ignore != null) true;
+
+ node = {
+ specialArgs = {
+ inherit
+ (specialArgs)
+ pkgsUnstable
+ vhackPackages
+ nixpkgs-unstable
+ ;
+ inherit nixLib;
+ };
+
+ # Use the nixpkgs as constructed by the `nixpkgs.*` options
+ pkgs = null;
+ };
+
+ nodes = pkgs.lib.mkMerge [
+ {
+ acme = {
+ imports = [
+ ./common/acme/server.nix
+ ./common/dns/client.nix
+ ];
+ };
+ name_server = {nodes, ...}: {
+ imports =
+ specialArgs.extraModules
+ ++ [
+ ./common/acme/client.nix
+ ./common/dns/server.nix
+ ];
+
+ vhack.dns.zones = let
+ mkDomain = attr: let
+ split = splitAttr attr;
+ in {
+ name = "${split.value}";
+ value = {
+ SOA = {
+ nameServer = "ns";
+ adminEmail = "admin@server.com";
+ serial = 2025012301;
+ };
+ useOrigin = false;
+
+ A = [
+ nodes.${split.name}.networking.primaryIPAddress
+ ];
+ AAAA = [
+ nodes.${split.name}.networking.primaryIPv6Address
+ ];
+ };
+ };
+ in
+ builtins.listToAttrs (builtins.map mkDomain args.serverDomains);
+ };
+ }
+ args.nodes
+ (builtins.mapAttrs (_: _: {
+ imports = [
+ ./common/acme/client.nix
+ ./common/dns/client.nix
+ ];
+ })
+ args.nodes)
+ ];
+
+ testScript = {nodes, ...} @ testScriptInput: let
+ acme = import ./common/acme {inherit pkgs;};
+
+ waitFor = builtins.concatStringsSep "\n" (
+ builtins.map (attr: let
+ split = splitAttr attr;
+ in " maybe_wait(${split.name},\"${split.value}\", ${
+ if (builtins.hasAttr "start" attr && attr.start)
+ then "True"
+ else "False"
+ })")
+ args.services
+ );
+
+ acmeOnline = builtins.concatStringsSep "\n" (builtins.map (attr: let
+ split = splitAttr attr;
+ in " status.append(acme_online(${split.name}, \"${split.value}\", renewRan.get(\"acme-order-renew-${split.value}.service\", False)))")
+ args.serverDomains);
+
+ allRunning =
+ builtins.concatStringsSep "\n" (builtins.map (name: " all_services_running(${name})")
+ (builtins.attrNames args.nodes));
+
+ optional = condition: value:
+ if condition
+ then value
+ else "";
+ in
+ acme.prepare (builtins.attrNames args.nodes)
+ # The acme code runs `start_all` before it hands execution back to us.
+ # So we don't need to run it.
+ (
+ optional (args.services != [] || args.serverDomains != [])
+ # Python
+ ''
+ def unit_exists(host, unitName: str) -> bool:
+ (status, _) = host.execute(f"test -f '/etc/systemd/system/{unitName}'")
+ return status == 0
+
+ wasAlive: bool
+ def run_and_wait(host, unit: str):
+ global wasAlive
+ wasAlive = False
+ def wait_state(_last_try: bool) -> bool:
+ global wasAlive
+
+ state = host.get_unit_property(unit, "SubState")
+ if state == "failed":
+ assert False, f'Unit "{unit}" reached state "{state}"'
+
+ if state == "start" or state == "active" or state == "running":
+ wasAlive = True
+
+ host.log(f"(waiting) {unit} -> {state}")
+ return wasAlive and state == "dead"
+
+ with host.nested(f"Waiting for unit {unit}"):
+ retry(wait_state)
+ ''
+ + optional (args.serverDomains != [])
+ # Python
+ ''
+ def acme_online(host, domain, renewRan):
+ from typing import assert_never
+
+ unitMain = f"acme-{domain}.service"
+ unitRenew = f"acme-order-renew-{domain}.service"
+
+ infoMain = host.get_unit_property(unitMain, "SubState")
+ infoRenew = host.get_unit_property(unitRenew, "SubState")
+
+ match (infoMain, infoRenew):
+ case ("exited", "dead") | ("dead", "dead") if not renewRan:
+ # The unit might not exist, because we use the DNS values for
+ # generating this code (there is no needed mapping between DNS -> acme
+ # host). So we check, if it is an actual unit.
+ if unit_exists(host, unitMain):
+ host.start_job(f"acme-{domain}")
+ return ("Wait", unitRenew, host)
+ else:
+ return ("Done", None, None)
+
+ case ("exited", "dead") | ("exited", "dead") if renewRan:
+ # The unit is done, so it should be good?
+ host.require_unit_state(unitMain, "active")
+ host.succeed(f"${pkgs.lib.getExe pkgs.openssl} s_client -connect {domain}:443 -verify_return_error </dev/null")
+ return ("Done", None, None)
+
+ case (_, "running") | (_, "start"):
+ # It's currently running, let's wait
+ return ("Wait", unitRenew, host)
+
+ case other:
+ assert False, f"Wrong unit sub-states for ({unitMain},{unitRenew}): {other}"
+ assert_never() # i.e. unreachable
+
+ with subtest("Ensure, that all acme certificates have been fetched"):
+ renewRan = {}
+ while True:
+ status = []
+ ${acmeOnline}
+ done = True
+ for (state, unit, host) in status:
+ if state == "Wait":
+ run_and_wait(host, unit)
+ renewRan[unit] = True
+ if state != "Done":
+ done = False
+ if done:
+ break
+ ''
+ + optional (args.services != [])
+ # Python
+ ''
+ def maybe_wait(host, unit: str, should_start: bool):
+ if unit_exists(host, unit):
+ if should_start:
+ host.start_job(unit.removesuffix(".service"))
+ run_and_wait(host, unit)
+ else:
+ host.wait_for_unit(unit)
+ else:
+ assert False, f"Unit '{unit}' does not exist, test misconfigured"
+
+ with subtest("Waiting for units"):
+ ${waitFor}
+ ''
+ +
+ # Python
+ ''
+ def all_services_running(host):
+ import json
+ (status, output) = host.systemctl("list-units --state=failed --plain --no-pager --output=json")
+ host_failed = json.loads(output)
+ assert len(host_failed) == 0, f"Expected zero failing services, but found: {json.dumps(host_failed, indent=4)}"
+
+ with subtest("All services running"):
+ ${allRunning}
+
+ ${(args.testScript testScriptInput)}
+
+ with subtest("All services running"):
+ ${allRunning}
+ ''
+ );
+ };
+
tests = nixLib.mkByName {
baseDirectory = ./by-name;
fileName = "test.nix";
finalizeFunction = name: value:
- import value (nixLib.warnMerge specialArgs {inherit pkgs;} "the test args set");
+ import value (nixLib.warnMerge specialArgs {inherit pkgs vhack;} "the test args set");
};
in
tests