diff --git a/modules-package.nix b/modules-package.nix index b36b809..d29908a 100644 --- a/modules-package.nix +++ b/modules-package.nix @@ -19,17 +19,14 @@ in stdenv.mkDerivation (self: { ]; }; - phases = [ "unpackPhase" "patchPhase" "installPhase "]; + phases = [ "unpackPhase" "patchPhase" "installPhase" ]; installPhase = lib.dedent '' - mkdir -vp "$out" - cp -rv * "$out/" + mkdir -p "$out" + cp -r * "$out/" - #mkdir -vp "$modules/share/nix/modules/dynix" - #cp --reflink=auto -rv * "$modules/share/nix/modules/dynix/" - - mkdir -vp "$modules/share/nixos/modules/dynix" - cp --reflink=auto -rv * "$modules/share/nixos/modules/dynix/" + mkdir -p "$modules/share/nixos/modules/dynix" + cp --reflink=auto -r "$out/"* "$modules/share/nixos/modules/dynix/" ''; passthru.mkDevShell = { @@ -51,6 +48,8 @@ in stdenv.mkDerivation (self: { ] |> lib.concatStringsSep ":"; }; + passthru.modulesPath = self.finalPackage.modules + "/share/nixos/modules"; + passthru.tests = lib.fix (callPackage ./tests { dynix = self.finalPackage; }).packages; diff --git a/modules/dynamicism/default.nix b/modules/dynamicism/default.nix index dae5bd6..5e2b4e7 100644 --- a/modules/dynamicism/default.nix +++ b/modules/dynamicism/default.nix @@ -4,6 +4,9 @@ let mkOption showOption ; + inherit (lib.asserts) + checkAssertWarn + ; t = lib.types; inherit (import ./lib.nix { inherit lib; }) @@ -19,7 +22,6 @@ let opts = options.dynamicism; subOpts = lib.mapAttrs (_: metaAttr: metaAttr.configuration.options) options.dynamicism.for.valueMeta.attrs; - settingsFormat = pkgs.formats.yaml { }; finalSettingsFor = { ... }@submod: recUpdateFoldl (optPath: lib.setAttrByPath optPath (lib.getAttrFromPath optPath config) @@ -82,45 +84,62 @@ in # # Generic implementation. # - config.dynamicism.doChange = { - option, - value, - configuration ? builtins.getEnv "NIXOS_CONFIG", - }: let - loc = opts.doChange.loc ++ [ "(function argument)" "value" ]; - option' = typeCheck loc convenientAttrPath option; - nixosAfter = evalNixos { - configuration = { config, ... }: { - imports = [ - configuration - (lib.setAttrByPath option' (lib.mkOverride (-999) value)) - ]; + config.system.activationScripts."dynamicism-reset" = { + deps = [ "etc" "stdio" "specialfs" ]; + text = '' + echo "DYNIX: removing existing systemd dropins" + # FIXME: do for each enabled submodule + if [[ -d /run/systemd/system ]]; then + rm -v /run/systemd/system/*/dynix-*.conf || true + fi + ''; + }; + config.dynamicism = { + doChange = { + option, + value, + configuration ? builtins.getEnv "NIXOS_CONFIG", + }: let + loc = opts.doChange.loc ++ [ "(function argument)" "value" ]; + option' = typeCheck loc convenientAttrPath option; + nixosAfter = evalNixos { + configuration = { config, ... }: { + imports = [ + configuration + (lib.setAttrByPath option' (lib.mkOverride (-999) value)) + ]; - environment.systemPackages = [ - config.dynamicism.for.gotosocial.activate - ]; + environment.systemPackages = [ + config.dynamicism.for.gotosocial.activate + ]; + }; }; - }; - allActivations = lib.mapAttrsToList (name: submod: submod.activate) config.dynamicism.for; - allActivationScripts = pkgs.writeShellApplication { - name = "dynamicism-activate"; - runtimeInputs = allActivations; - text = nixosAfter.config.dynamicism.for - |> lib.mapAttrsToList (name: submod: '' - echo "Activating dynamicism for ${name}" - ${lib.getExe submod.activate} - '') - |> lib.concatStringsSep "\n"; - }; - in allActivationScripts; + allActivations = config.dynamicism.for + |> lib.filterAttrs (name: submod: submod.enable) + |> lib.mapAttrsToList (name: submod: submod.activate); + allActivationScripts = pkgs.writeShellApplication { + name = "dynamicism-activate"; + runtimeInputs = allActivations; + text = nixosAfter.config.dynamicism.for + |> lib.filterAttrs (name: submod: submod.enable) + |> lib.mapAttrsToList (name: submod: '' + echo "Activating dynamicism for ${name}" + ${lib.getExe submod.activate} + '') + |> lib.concatStringsSep "\n"; + }; + in allActivationScripts; - config.dynamicism.finalSettings = lib.asserts.checkAssertWarn ourAssertions [ ] ( - recUpdateFoldlAttrs (name: { ... }@submod: finalSettingsFor submod) config.dynamicism.for - ); + finalSettings = config.dynamicism.for + |> recUpdateFoldlAttrs (name: { ... }@submod: finalSettingsFor submod) + |> checkAssertWarn ourAssertions [ ]; + }; # Implementations. imports = [ ./gotosocial.nix + ./harmonia.nix + #./tzupdate.nix ]; } diff --git a/modules/dynamicism/gotosocial.nix b/modules/dynamicism/gotosocial.nix index d52e68c..cbcff2b 100644 --- a/modules/dynamicism/gotosocial.nix +++ b/modules/dynamicism/gotosocial.nix @@ -3,17 +3,20 @@ let cfg = config.dynamicism.for.gotosocial; settingsFormat = pkgs.formats.yaml { }; + configFile = settingsFormat.generate "gotosocial-override.yml" config.services.gotosocial.settings; in { dynamicism.for.gotosocial = { source-options = [ "services.gotosocial.settings" ]; - configFile = settingsFormat.generate "gotosocial-overrde.yml" config.services.gotosocial.settings; - - unitDropins."gotosocial.service" = pkgs.writeText "gotosocial-override.conf" '' - [Service] - ExecStart= - ExecStart=${lib.getExe' pkgs.gotosocial "gotosocial"} --config-path ${cfg.configFile} start - ''; + unitDropins."gotosocial.service" = pkgs.writeTextFile { + name = "gotosocial-override.conf"; + text = '' + [Service] + ExecStart= + ExecStart=${lib.getExe' pkgs.gotosocial "gotosocial"} --config-path ${configFile} start + ''; + passthru = { inherit configFile; }; + }; }; } diff --git a/modules/dynamicism/harmonia.nix b/modules/dynamicism/harmonia.nix new file mode 100644 index 0000000..e4284d0 --- /dev/null +++ b/modules/dynamicism/harmonia.nix @@ -0,0 +1,26 @@ +{ pkgs, lib, config, ... }: +let + cfg = config.dynamicism.for.harmonia; + settingsFormat = pkgs.formats.toml { }; + + # FIXME: referring to config.dynamicism.finalSettings.* in here + # makes lib.checkAssertWarn in the generic module cause infinite recursion. + finalSettings = config.services.harmonia.settings; + + configFile = settingsFormat.generate "harmonia-override.toml" finalSettings; +in +{ + dynamicism.for.harmonia = { + source-options = [ "services.harmonia.settings" ]; + + unitDropins."harmonia.service" = pkgs.writeTextFile { + name = "harmonia-override.conf"; + text = '' + [Service] + Environment=CONFIG_FILE=${configFile} + ''; + + passthru = { inherit configFile; }; + }; + }; +} diff --git a/modules/dynamicism/submodule.nix b/modules/dynamicism/submodule.nix index 1b9dac9..4fe269c 100644 --- a/modules/dynamicism/submodule.nix +++ b/modules/dynamicism/submodule.nix @@ -65,7 +65,7 @@ in }; unitDropins = mkOption { - type = t.attrsOf t.pathInStore; + type = t.attrsOf t.package; internal = true; }; @@ -82,7 +82,7 @@ in text = let doEdits = config.unitDropins |> lib.mapAttrsToList (service: dropin: '' - cat "${dropin}" | systemctl edit "${service}" --runtime --stdin + cat "${dropin}" | systemctl edit "${service}" --runtime --drop=dynix-${dropin.name} --stdin ''); doReloads = config.unitDropins |> lib.mapAttrsToList (service: _: '' diff --git a/modules/dynamicism/tzupdate.nix b/modules/dynamicism/tzupdate.nix new file mode 100644 index 0000000..73f634d --- /dev/null +++ b/modules/dynamicism/tzupdate.nix @@ -0,0 +1,20 @@ +{ pkgs, lib, config, ... }: let + cfg = config.dynamicism.for.tzupdate; + + + # FIXME: referring to config.dynamicism.finalSettings.* in here + # makes lib.checkAssertWarn in the generic module cause infinite recursion. + #finalSettings = config.dynamicism.finalSettings.tzupdate; + finalSettings = config.services.tzupdate.timer; +in +{ + dynamicism.for.tzupdate = { + source-options = [ "services.tzupdate.timer" ]; + + unitDropins."tzupdate.timer" = pkgs.writeText "tzupdate-timer-override.conf" '' + [Timer] + OnCalendar= + OnCalendar=${finalSettings.interval} + ''; + }; +} diff --git a/tests/default.nix b/tests/default.nix index a6aea2d..40369b9 100644 --- a/tests/default.nix +++ b/tests/default.nix @@ -10,22 +10,40 @@ runDynixTest = testModule: pkgs.testers.runNixOSTest { imports = [ testModule ]; - # Note: these are arguments to the *test* modules. - # Not the NixOS modules for the NixOS configuration the test is testing. - # Wew. - _module.args = { inherit dynix; }; - # Why is this argument called "extraBaseModule**s**" but take a single module argument... - extraBaseModules = { name, ... }: { - #imports = [ dynixInjectionModule ]; - config.environment.systemPackages = [ dynix ]; + # Also note this is an extra base module for each node of the test, + # not an extra test module. + extraBaseModules = { name, config, options, modulesPath, ... }: { + imports = (import "${modulesPath}/module-list.nix") ++ [ + ./module-allow-rebuild-in-vm.nix + "${modulesPath}/testing/test-instrumentation.nix" + (toString dynix) + ]; + environment.systemPackages = [ dynix ]; + + systemd.services."install-dynix" = { + enable = true; + serviceConfig.Type = "oneshot"; + serviceConfig.RemainAfterExit = true; + path = [ config.system.path ]; + wantedBy = [ "multi-user.target" ]; + after = [ "default.target" ]; + script = '' + nix profile install -vv "$(realpath /run/current-system/sw/share/nixos/modules/dynix/)" + ''; + }; + + passthru = { inherit options; }; + # Just making something in this strict in `name`, # which is only present as an argument for nodes and I don't want to # confuse that with the test modules. - config.warnings = builtins.seq name [ ]; + warnings = builtins.seq name [ ]; }; }; in lib.makeScope lib.callPackageWith (self: { gotosocial = runDynixTest ./gotosocial/test.nix; + harmonia = runDynixTest ./harmonia/test.nix; + #tzupdate = runDynixTest ./tzupdate/test.nix; }) diff --git a/tests/gotosocial/configuration-package.nix b/tests/gotosocial/configuration-package.nix index e6bb010..96a89d8 100644 --- a/tests/gotosocial/configuration-package.nix +++ b/tests/gotosocial/configuration-package.nix @@ -1,9 +1,7 @@ { runCommand, -}: runCommand "tests-basic-configuration-dot-nix" { +}: runCommand "tests-gotosocial-configuration-dot-nix" { } '' set -euo pipefail - mkdir -vp "$out/share/nixos" - cp -rv ${./configuration.nix} "$out/share/nixos/configuration.nix" - #cp -rv ${../../modules/dynamicism} "$out/share/nixos/dynamicism" + install -Dm a=r ${./configuration.nix} "$out/share/nixos/configuration.nix" '' diff --git a/tests/gotosocial/configuration.nix b/tests/gotosocial/configuration.nix index bc751ed..deef056 100644 --- a/tests/gotosocial/configuration.nix +++ b/tests/gotosocial/configuration.nix @@ -18,7 +18,7 @@ in system.switch.enable = true; documentation.enable = false; - networking.hostName = "machine"; + networking.hostName = "gotosocial-machine"; boot.loader.grub = { enable = true; @@ -47,7 +47,7 @@ in enable = true; setupPostgresqlDB = true; settings = { - application-name = "gotosocial-for-${name}"; + application-name = "gotosocial-for-machine"; host = "${name}.local"; }; }; diff --git a/tests/gotosocial/test-script.py b/tests/gotosocial/test-script.py index 03974fc..d8b2af8 100644 --- a/tests/gotosocial/test-script.py +++ b/tests/gotosocial/test-script.py @@ -34,7 +34,6 @@ def run_log(machine: Machine, *commands: str, timeout: int | None = 60) -> str: def get_config_file() -> str: machine.wait_for_unit("gotosocial.service") gotosocial_pid = int(machine.get_unit_property("gotosocial.service", "MainPID")) - print(f"{gotosocial_pid=}") cmdline = machine.succeed(f"cat /proc/{gotosocial_pid}/cmdline") cmdline_args = cmdline.split("\0") @@ -53,8 +52,7 @@ def get_config_file() -> str: machine.wait_for_unit("default.target") assert "lix" in machine.succeed("nix --version").lower() machine.log("INIT") - -machine.succeed("nix profile install -vv $(realpath /run/current-system/sw/share/nixos/modules/dynix)") +run_log(machine, "journalctl --no-pager -eu install-dynix.service") machine.succeed("nixos-generate-config") machine.succeed("mkdir -vp /etc/nixos") diff --git a/tests/gotosocial/test.nix b/tests/gotosocial/test.nix index 4356685..9628b93 100644 --- a/tests/gotosocial/test.nix +++ b/tests/gotosocial/test.nix @@ -1,7 +1,7 @@ -{ dynix, ... }: +{ ... }: { - name = "nixos-test-dynamicism-main"; + name = "nixos-test-dynamicism-gotosocial"; defaults = { ... }: { }; @@ -9,27 +9,12 @@ p.beartype ]; - nodes.machine = { pkgs, config, ... }: { + nodes.machine = { pkgs, ... }: { # NOTE: Anything in this `nodes.machine = ` module will not be included # in the VM's NixOS configuration once it does `nixos-rebuild switch`, # except for `./configuration.nix` which will be copied to `/etc/nixos/`. # dynix will also be statefully installed to root's user profile. - imports = [ - ./configuration.nix - (toString dynix) - ]; - - system.includeBuildDependencies = true; - system.switch.enable = true; - - virtualisation.additionalPaths = [ config.system.build.toplevel ]; - virtualisation = { - memorySize = 4096; - cores = 4; - writableStore = true; - mountHostNixStore = true; - installBootLoader = true; - }; + imports = [ ./configuration.nix ]; environment.systemPackages = let configFileTree = pkgs.callPackage ./configuration-package.nix { }; @@ -38,7 +23,5 @@ ]; }; - # What's a little IFD between friends? - testScript = ./test-script.py - |> builtins.readFile; + testScript = builtins.readFile ./test-script.py; } diff --git a/tests/harmonia/configuration.nix b/tests/harmonia/configuration.nix new file mode 100644 index 0000000..7bf19bb --- /dev/null +++ b/tests/harmonia/configuration.nix @@ -0,0 +1,72 @@ +{ pkgs, lib, config, modulesPath, ... }: +let + moduleList = import "${modulesPath}/module-list.nix"; + + dynixFromSearchPath = let + res = builtins.tryEval ; + in lib.optional res.success res.value; +in +{ + imports = [ + "${modulesPath}/testing/test-instrumentation.nix" + ./hardware-configuration.nix + ] ++ lib.concatLists [ + dynixFromSearchPath + moduleList + ]; + + dynamicism.for.harmonia.enable = true; + services.harmonia = { + enable = true; + settings = { + # Default. + workers = 4; + # Default. + max_connection_rate = 256; + }; + }; + + system.switch.enable = true; + documentation.enable = false; + + networking.hostName = "harmonia-machine"; + + boot.loader.grub = { + enable = true; + device = "/dev/vda"; + forceInstall = true; + }; + + nix = { + package = pkgs.lixPackageSets.latest.lix; + nixPath = [ + "nixpkgs=${pkgs.path}" + "/nix/var/nix/profiles/per-user/root/profile/share/nixos/modules" + ]; + + settings = { + experimental-features = [ "nix-command" "pipe-operator" ]; + substituters = lib.mkForce [ ]; + hashed-mirrors = null; + connect-timeout = 1; + # For my debugging purposes. + show-trace = true; + }; + }; + + environment.pathsToLink = [ "/share" ]; + environment.extraOutputsToInstall = [ "modules" ]; + environment.variables = { + "NIXOS_CONFIG" = "/etc/nixos/configuration.nix"; + }; + + environment.shellAliases = { + ls = "eza --long --header --group --group-directories-first --classify --binary"; + }; + + environment.systemPackages = with pkgs; [ + eza + fd + ripgrep + ]; +} diff --git a/tests/harmonia/hardware-configuration.nix b/tests/harmonia/hardware-configuration.nix new file mode 100644 index 0000000..facb35d --- /dev/null +++ b/tests/harmonia/hardware-configuration.nix @@ -0,0 +1,4 @@ +{ ... }: +{ + +} diff --git a/tests/harmonia/test-script.py b/tests/harmonia/test-script.py new file mode 100644 index 0000000..a91bf78 --- /dev/null +++ b/tests/harmonia/test-script.py @@ -0,0 +1,125 @@ +import functools +from pathlib import Path +from pprint import pformat +import shlex +import textwrap +import tomllib +from typing import Any, cast, TYPE_CHECKING + +from beartype import beartype + +from test_driver.machine import Machine +from test_driver.errors import RequestedAssertionFailed + +if TYPE_CHECKING: + global machine + machine = cast(Machine, ...) + assert machine.shell is not None + +ls = "eza -lah --color=always --group-directories-first" + +indent = functools.partial(textwrap.indent, prefix=' ') + +@beartype +def run_log(machine: Machine, *commands: str, timeout: int | None = 60) -> str: + output = "" + for command in commands: + with machine.nested(f"must succeed: {command}"): + (status, out) = machine.execute(f"{command} | tee /dev/stderr", timeout=timeout) + if status != 0: + machine.log(f"output: {out}") + raise RequestedAssertionFailed( + f"command `{command}` failed (exit code {status})", + ) + output += out + + return output + +@beartype +def get_config_file() -> dict[str, Any]: + machine.wait_for_unit("harmonia.service") + pid = int(machine.get_unit_property("harmonia.service", "MainPID")) + env_lines: list[str] = machine.succeed(f"cat /proc/{pid}/environ").replace("\0", "\n").splitlines() + pairs: list[list[str]] = [line.split("=", maxsplit=1) for line in env_lines] + env = dict(pairs) + + config_file = Path(env["CONFIG_FILE"]) + + machine.log(f"copying from VM: {config_file=}") + machine.copy_from_vm(config_file.as_posix()) + + config_file_path = machine.out_dir / config_file.name + with open(config_file_path, "rb") as f: + config_data = tomllib.load(f) + + config_file_path.unlink() + return config_data + +machine.wait_for_unit("default.target") +assert "lix" in machine.succeed("nix --version").lower() +machine.log("INIT") + +# Config should have our initial values. +config_toml = get_config_file() +machine.log(f"config.toml BEFORE first rebuild (initial): {indent(pformat(config_toml))}") +assert int(config_toml['workers']) == 4, f"{config_toml['workers']=} != 4" +assert int(config_toml['max_connection_rate']) == 256, f"{config_toml['max_connection_rate']=} != 256" + +with machine.nested("must succeed: initial nixos-rebuild switch"): + machine.succeed("nixos-generate-config") + machine.succeed("mkdir -vp /etc/nixos") + # Dereference is required since that configuration.nix is probably a symlink to the store. + machine.succeed("cp -rv --dereference /run/current-system/sw/share/nixos/configuration.nix /etc/nixos/") + machine.succeed("env PAGER= nixos-rebuild switch --log-format raw-with-logs --fallback") + +# Config should not have changed. +config_toml = get_config_file() +machine.log(f"config.toml after first rebuild: {indent(pformat(config_toml))}") +assert int(config_toml['workers']) == 4, f"{config_toml['workers']=} != 4" +assert int(config_toml['max_connection_rate']) == 256, f"{config_toml['max_connection_rate']=} != 256" + +new_workers = 20 +expr = textwrap.dedent(f""" + let + nixos = import {{ }}; + in nixos.config.dynamicism.doChange {{ + option = "services.harmonia.settings.workers"; + value = {new_workers}; + }} +""").strip() +machine.succeed(rf""" + nix run --show-trace --log-format raw-with-logs --impure -E {shlex.quote(expr)} +""".strip()) + +# Workers, but not max connection rate, should have changed. +config_toml = get_config_file() +machine.log(f"config.toml after DYNAMIC ACTIVATION: {indent(pformat(config_toml))}") +assert int(config_toml['workers']) == new_workers, f"{config_toml['workers']=} != {new_workers}" +assert int(config_toml['max_connection_rate']) == 256, f"{config_toml['max_connection_rate']=} != 256" + +new_max_connection_rate = 100 +expr = textwrap.dedent(f""" + let + nixos = import {{ }}; + in nixos.config.dynamicism.doChange {{ + option = [ "services" "harmonia" "settings" "max_connection_rate" ]; + value = {new_max_connection_rate}; + }} +""").strip() +machine.succeed(rf""" + nix run --show-trace --log-format raw-with-logs --impure -E {shlex.quote(expr)} +""".strip()) + +# Max connection rate should have changed, but workers should have reverted. +config_toml = get_config_file() +machine.log(f"config_toml after SECOND dynamic activation: {indent(pformat(config_toml))}") +assert int(config_toml['max_connection_rate']) == new_max_connection_rate, f"{config_toml['max_connection_rate']=} != {new_max_connection_rate}" + +# And this should set everything back. +machine.succeed("env PAGER= nixos-rebuild switch --log-format raw-with-logs --fallback") +machine.systemctl("restart harmonia.service") +machine.wait_for_unit("harmonia.service") +config_toml = get_config_file() +machine.log(f"config_toml after NORMAL activation: {indent(pformat(config_toml))}") +assert int(config_toml['max_connection_rate']) == 256, f'{config_toml["max_connection_rate"]=} != 256' +assert int(config_toml['workers']) == 4, f'{config_toml["workers"]=} != 4' diff --git a/tests/harmonia/test.nix b/tests/harmonia/test.nix new file mode 100644 index 0000000..0aea418 --- /dev/null +++ b/tests/harmonia/test.nix @@ -0,0 +1,23 @@ +{ ... }: +{ + name = "nixos-test-dynamicism-harmonia"; + + defaults = { ... }: { }; + + extraPythonPackages = p: [ p.beartype ]; + + nodes.machine = { name, pkgs, ... }: { + imports = [ ./configuration.nix ]; + + environment.systemPackages = let + configFileTree = pkgs.runCommand "${name}-configuration-dot-nix" { } '' + set -euo pipefail + install -Dm a=r ${./configuration.nix} "$out/share/nixos/configuration.nix" + ''; + in [ + configFileTree + ]; + }; + + testScript = builtins.readFile ./test-script.py; +} diff --git a/tests/module-allow-rebuild-in-vm.nix b/tests/module-allow-rebuild-in-vm.nix new file mode 100644 index 0000000..3ec14d5 --- /dev/null +++ b/tests/module-allow-rebuild-in-vm.nix @@ -0,0 +1,31 @@ +{ name, config, ... }: + +{ + system.includeBuildDependencies = true; + system.switch.enable = true; + + documentation.enable = false; + + virtualisation = { + additionalPaths = [ config.system.build.toplevel ]; + writableStore = true; + mountHostNixStore = true; + installBootLoader = true; + + # With how much memory Nix eval uses, this is essentially required. + memorySize = 4096; + cores = 4; + + }; + + boot.loader.grub = { + enable = true; + device = "/dev/vda"; + forceInstall = true; + }; + + # Just making something in this strict in `name`, + # which is only present as an argument for nodes and I don't want to + # confuse that with the test modules. + warnings = builtins.seq name [ ]; +} diff --git a/tests/tzupdate/configuration.nix b/tests/tzupdate/configuration.nix new file mode 100644 index 0000000..176975c --- /dev/null +++ b/tests/tzupdate/configuration.nix @@ -0,0 +1,67 @@ +{ pkgs, lib, config, modulesPath, ... }: +let + name = config.networking.hostName; + moduleList = import "${modulesPath}/module-list.nix"; + + dynixFromSearchPath = let + res = builtins.tryEval ; + in lib.optional res.success res.value; +in +{ + imports = [ + "${modulesPath}/testing/test-instrumentation.nix" + ./hardware-configuration.nix + ] ++ lib.concatLists [ + moduleList + dynixFromSearchPath + ]; + + dynamicism.for.tzupdate.enable = true; + services.tzupdate = { + enable = true; + }; + + system.switch.enable = true; + documentation.enable = false; + + networking.hostName = "tzupdate-machine"; + + boot.loader.grub = { + enable = true; + device = "/dev/vda"; + forceInstall = true; + }; + + nix = { + package = pkgs.lixPackageSets.latest.lix; + nixPath = [ + "nixpkgs=${pkgs.path}" + "/nix/var/nix/profiles/per-user/root/profile/share/nixos/modules" + ]; + + settings = { + experimental-features = [ "nix-command" "pipe-operator" ]; + substituters = lib.mkForce [ ]; + hashed-mirrors = null; + connect-timeout = 1; + # For my debugging purposes. + show-trace = true; + }; + }; + + environment.pathsToLink = [ "/share" ]; + environment.extraOutputsToInstall = [ "modules" ]; + environment.variables = { + "NIXOS_CONFIG" = "/etc/nixos/configuration.nix"; + }; + + environment.shellAliases = { + ls = "eza --long --header --group --group-directories-first --classify --binary"; + }; + + environment.systemPackages = with pkgs; [ + eza + fd + ripgrep + ]; +} diff --git a/tests/tzupdate/hardware-configuration.nix b/tests/tzupdate/hardware-configuration.nix new file mode 100644 index 0000000..facb35d --- /dev/null +++ b/tests/tzupdate/hardware-configuration.nix @@ -0,0 +1,4 @@ +{ ... }: +{ + +} diff --git a/tests/tzupdate/test-script.py b/tests/tzupdate/test-script.py new file mode 100644 index 0000000..53762a0 --- /dev/null +++ b/tests/tzupdate/test-script.py @@ -0,0 +1,52 @@ +#from pathlib import Path +#import shlex +#import textwrap +from typing import cast, TYPE_CHECKING + +from beartype import beartype + +from test_driver.machine import Machine +from test_driver.errors import RequestedAssertionFailed + +if TYPE_CHECKING: + global machine + machine = cast(Machine, ...) + assert machine.shell is not None + +ls = "eza -lah --color=always --group-directories-first" + +@beartype +def run_log(machine: Machine, *commands: str, timeout: int | None = 60) -> str: + output = "" + for command in commands: + with machine.nested(f"must succeed: {command}"): + (status, out) = machine.execute(f"{command} | tee /dev/stderr", timeout=timeout) + if status != 0: + machine.log(f"output: {out}") + raise RequestedAssertionFailed( + f"command `{command}` failed (exit code {status})", + ) + output += out + + return output + +machine.wait_for_unit("default.target") +assert "lix" in machine.succeed("nix --version").lower() +machine.log("INIT") + + +machine.succeed("nixos-generate-config") +machine.succeed("mkdir -vp /etc/nixos") +# Dereference is required since that configuration.nix is probably a symlink to the store. +machine.succeed("cp -rv --dereference /run/current-system/sw/share/nixos/configuration.nix /etc/nixos/") + +machine.succeed("env PAGER= nixos-rebuild switch --log-format raw-with-logs --fallback") + +@beartype +def get_interval() -> str: + prop = machine.get_unit_property("tzupdate.timer", "OnCalendar") + print(f"{prop=}") + + return prop + +get_interval() diff --git a/tests/tzupdate/test.nix b/tests/tzupdate/test.nix new file mode 100644 index 0000000..2d42c10 --- /dev/null +++ b/tests/tzupdate/test.nix @@ -0,0 +1,24 @@ +{ ... }: + +{ + name = "nixos-test-dynamicism-tzupdate"; + + extraPythonPackages = p: [ + p.beartype + ]; + + nodes.machine = { name, pkgs, ... }: { + imports = [ ./configuration.nix ]; + + environment.systemPackages = let + configFileTree = pkgs.runCommand "${name}-configuration-dot-nix" { } '' + set -euo pipefail + install -Dm a=r ${./configuration.nix} "$out/share/nixos/configuration.nix" + ''; + in [ + configFileTree + ]; + }; + + testScript = builtins.readFile ./test-script.py; +}