From b84c9c7382dc05d1ff5f3bdaf2bac7ce3c332007 Mon Sep 17 00:00:00 2001 From: Benedikt Peetz Date: Sat, 18 Jul 2026 22:35:46 +0200 Subject: tests: Refactor to unify tests and avoid duplicated code --- tests/default.nix | 242 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 241 insertions(+), 1 deletion(-) (limited to 'tests/default.nix') 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