diff --git a/package.nix b/package.nix index b0fad51..08b6bbc 100644 --- a/package.nix +++ b/package.nix @@ -10,6 +10,7 @@ rustHooks, rustPackages, versionCheckHook, + writeScript, }: lib.callWith' rustPackages ({ rustPlatform, cargo, @@ -37,12 +38,29 @@ in { cp -r --reflink=auto "$dynixCommand/"* "$out/" mkdir -p "$modules" cp -r --reflink=auto "$dynixModules/"* "$modules/" + install -Dm a=rx "$dynixTestingClient" "$out/libexec/dynix-testing-client.py" ''; # # SUB-DERIVATONS # + dynixTestingClient = writeScript "dynix-testing-client.py" '' + #!/usr/bin/env python3 + import socket, sys, os, json + try: + sockpath = sys.argv[1] + except IndexError: + sockpath = f"{os.environ['XDG_RUNTIME_DIR']}/dynix.sock" + sock = socket.socket(family=socket.AF_UNIX) + sock.connect(sockpath) + sock.sendall(sys.stdin.buffer.read()) + sock.settimeout(20) + reply = json.loads(sock.recv(256).decode("utf-8")) + print(json.dumps(reply, indent=2)) + sys.exit(reply["status"]) + ''; + dynixCommand = stdenv.mkDerivation { pname = "${self.pname}-command"; inherit (self) version; diff --git a/src/daemon.rs b/src/daemon.rs index 1a874ac..5f1b88c 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -195,6 +195,7 @@ impl Daemon { token } + #[expect(dead_code)] fn register_with_name(&mut self, fd: RawFd, kind: FdKind, name: Box) -> Token { let token = next_token(); diff --git a/src/lib.rs b/src/lib.rs index ab4d946..cfa921f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -231,6 +231,7 @@ pub fn get_next_prio_line( let penultimate = penultimate.unwrap(); let new_generation = 0 - new_prio; + info!("setting '{option_name}' to '{new_value}' for generation '{new_generation}'"); let new_line = SourceLine { line: penultimate.line, diff --git a/tests/default.nix b/tests/default.nix index 57b1c94..0541f6f 100644 --- a/tests/default.nix +++ b/tests/default.nix @@ -50,16 +50,11 @@ requiredBy = [ "multi-user.target" ]; after = [ "default.target" ]; script = '' - if [[ -e /etc/nixos/hardware-configuration.nix ]]; then - echo "install-dynix: configuration already copied; nothing to do" - exit 0 - fi - nix profile install -vv "${dynix.drvPath}^*" # " mkdir -vp /etc/nixos nixos-generate-config - cp -rv --dereference /run/current-system/sw/share/nixos/*.nix /etc/nixos/ + cp -rvf --dereference /run/current-system/sw/share/nixos/*.nix /etc/nixos/ if ! [[ -e /etc/nixos/dynix-vm-configuration.nix ]]; then echo "FAILURE" echo "FAILURE" >&2 diff --git a/tests/dynix-vm-configuration.nix b/tests/dynix-vm-configuration.nix index ee1c19b..677ef70 100644 --- a/tests/dynix-vm-configuration.nix +++ b/tests/dynix-vm-configuration.nix @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: EUPL-1.1 -{ pkgs, lib, modulesPath, ... }: +{ pkgs, lib, modulesPath, config, ... }: let moduleList = import (modulesPath + "/module-list.nix"); @@ -64,9 +64,20 @@ in "NIXOS_CONFIG" = "/etc/nixos/configuration.nix"; }; - #systemd.services.dynix-daemon = { - # - #}; + systemd.services.dynix-daemon = { + enable = true; + path = [ config.nix.package ]; + serviceConfig = { + Environment = [ + "RUST_LOG=trace" + ]; + ExecSearchPath = [ "/run/current-system/sw/bin" ]; + SuccessExitStatus = [ "0" "2" ]; + # `bash -l` so XDG_RUNTIME_DIR is set correctly. lol. + ExecStart = "bash -l -c 'exec /root/.nix-profile/bin/dynix daemon --color=always'"; + SyslogIdentifier = "dynix-daemon"; + }; + }; environment.shellAliases = { ls = "eza --long --header --group --group-directories-first --classify --binary"; @@ -78,5 +89,6 @@ in ripgrep netcat.nc socat + python3 ]; } diff --git a/tests/gotosocial/test-script.py b/tests/gotosocial/test-script.py index eca3044..0f3131e 100644 --- a/tests/gotosocial/test-script.py +++ b/tests/gotosocial/test-script.py @@ -5,7 +5,7 @@ from pathlib import Path import shlex import textwrap -from typing import cast, TYPE_CHECKING +from typing import Any, cast, TYPE_CHECKING from beartype import beartype @@ -18,6 +18,13 @@ if TYPE_CHECKING: assert machine.shell is not None ls = "eza -lah --color=always --group-directories-first" +testing_client = "/root/.nix-profile/libexec/dynix-testing-client.py" + +ANSI_RESET = "\x1b[0m" +ANSI_BOLD = "\x1b[1m" +ANSI_NOBOLD = "\x1b[22m" +ANSI_RED = "\x1b[31m" +ANSI_GREEN = "\x1b[32m" @beartype def run_log(machine: Machine, *commands: str, timeout: int | None = 60) -> str: @@ -54,71 +61,108 @@ def get_config_file() -> str: config_file_path.unlink() + machine.logger.info(f"{ANSI_GREEN}INFO{ANSI_RESET}: got config file:") + machine.logger.info(textwrap.indent(data, " ")) + return data @beartype -def dynix_append(option: str, value: str): +def dynix_append_cli(option: str, value: Any): + value = f'"{value}"' if isinstance(value, str) else value machine.succeed(f''' dynix append {shlex.quote(option)} {shlex.quote(value)} '''.strip()) -@beartype -def do_apply(): expr = textwrap.dedent(""" (import { }).config.dynamicism.applyDynamicConfiguration { } """).strip() - - machine.succeed(rf""" + machine.succeed(textwrap.dedent(rf""" nix run --show-trace --log-format raw-with-logs --impure -E {shlex.quote(expr)} - """.strip()) + """).strip()) +@beartype +def dynix_append_daemon(option: str, value: Any): + import json + payload = json.dumps(dict( + action="append", + args=dict( + name=option, + value=value, + ), + )) + + machine.succeed(f"echo '{payload}' | {testing_client} /run/user/0/dynix.sock") + +@beartype +def run_all_tests(machine: Machine, *, use_daemon: bool): + dynix_append = dynix_append_daemon if use_daemon else dynix_append_cli + + dynix_out = machine.succeed("dynix --version") + assert "dynix" in dynix_out, f"dynix not in {dynix_out=}" + + machine.succeed("systemctl start user@0.service") + machine.wait_for_unit("user@0.service") + machine.succeed("systemctl start dynix-daemon.service") + machine.wait_for_unit("dynix-daemon.service") + + machine.log("REBUILDING configuration inside VM") + machine.succeed("env PAGER= nixos-rebuild switch --log-format raw-with-logs --no-reexec --fallback") + machine.wait_for_unit("gotosocial.service") + + # Make sure the config before any dynamic changes is what we expect. + config_text = get_config_file() + lines = config_text.splitlines() + try: + application_name = next(line for line in lines if line.startswith("application-name:")) + except StopIteration: + raise AssertionError(f"no 'application-name:' found in config file: {textwrap.indent(config_text, " ")}") + assert "gotosocial-for-machine" in application_name, f"'gotosocial-for-machine' should be in {application_name=}" + + try: + host = next(line for line in lines if line.startswith("host:")) + except StopIteration: + raise AssertionError(f"no 'host:' found in config file: {textwrap.indent(config_text, " ")}") + assert "gotosocial-machine" in host, f"'gotosocial-machine' should be in {host=}" + + new_app_name = "yay!" + dynix_append("services.gotosocial.settings.application-name", f'{new_app_name}') + + config_text = get_config_file() + lines = config_text.splitlines() + try: + application_name = next(line for line in lines if line.startswith("application-name:")) + except StopIteration: + raise AssertionError(f"no 'application-name:' found in config file: {textwrap.indent(config_text, " ")}") + assert new_app_name in application_name, f"'{new_app_name}' should be in {application_name=}" + + machine.log("REBUILDING configuration inside VM") + machine.succeed("env PAGER= nixos-rebuild switch --log-format raw-with-logs --no-reexec --fallback") + + machine.wait_for_unit("gotosocial.service") + + config_text = get_config_file() + lines = config_text.splitlines() + try: + application_name = next(line for line in lines if line.startswith("application-name:")) + except StopIteration: + raise AssertionError(f"no 'application-name:' found in config file: {textwrap.indent(config_text, " ")}") + assert "gotosocial-for-machine" in application_name, f"'gotosocial-for-machine' should be in {application_name=}" + +machine.start(allow_reboot=True) machine.wait_for_unit("default.target") machine.wait_for_unit("install-dynix.service") - -dynix_out = machine.succeed("dynix --version") -assert "dynix" in dynix_out, f"dynix not in {dynix_out=}" - -machine.log("REBUILDING configuration inside VM") -machine.succeed("env PAGER= nixos-rebuild switch --log-format raw-with-logs --no-reexec --fallback") -machine.wait_for_unit("gotosocial.service") - -# Make sure the config before any dynamic changes is what we expect. -config_text = get_config_file() -lines = config_text.splitlines() try: - application_name = next(line for line in lines if line.startswith("application-name:")) -except StopIteration: - raise AssertionError(f"no 'application-name:' found in config file: {textwrap.indent(config_text, " ")}") -assert "gotosocial-for-machine" in application_name, f"'gotosocial-for-machine' should be in {application_name=}" + run_all_tests(machine, use_daemon=False) +except Exception as e: + machine.logger.error(f"{ANSI_RED}ERROR{ANSI_RESET} during {ANSI_BOLD}CLI{ANSI_RESET} tests: {e}") + raise +machine.reboot() + +machine.wait_for_unit("install-dynix.service") try: - host = next(line for line in lines if line.startswith("host:")) -except StopIteration: - raise AssertionError(f"no 'host:' found in config file: {textwrap.indent(config_text, " ")}") -assert "gotosocial-machine" in host, f"'gotosocial-machine' should be in {host=}" - -new_app_name = "yay!" -dynix_append("services.gotosocial.settings.application-name", f'"{new_app_name}"') -do_apply() - -config_text = get_config_file() -lines = config_text.splitlines() -try: - application_name = next(line for line in lines if line.startswith("application-name:")) -except StopIteration: - raise AssertionError(f"no 'application-name:' found in config file: {textwrap.indent(config_text, " ")}") -assert new_app_name in application_name, f"'{new_app_name}' should be in {application_name=}" - -machine.log("REBUILDING configuration inside VM") -machine.succeed("env PAGER= nixos-rebuild switch --log-format raw-with-logs --no-reexec --fallback") - -machine.wait_for_unit("gotosocial.service") - -config_text = get_config_file() -lines = config_text.splitlines() -try: - application_name = next(line for line in lines if line.startswith("application-name:")) -except StopIteration: - raise AssertionError(f"no 'application-name:' found in config file: {textwrap.indent(config_text, " ")}") -assert "gotosocial-for-machine" in application_name, f"'gotosocial-for-machine' should be in {application_name=}" + run_all_tests(machine, use_daemon=True) +except Exception as e: + machine.logger.error(f"{ANSI_RED}ERROR{ANSI_RESET} during {ANSI_BOLD}daemon{ANSI_RESET} tests: {e}") + raise diff --git a/tests/harmonia/test-script.py b/tests/harmonia/test-script.py index e27b9ce..83ca0db 100644 --- a/tests/harmonia/test-script.py +++ b/tests/harmonia/test-script.py @@ -21,6 +21,7 @@ if TYPE_CHECKING: assert machine.shell is not None ls = "eza -lah --color=always --group-directories-first" +testing_client = "/root/.nix-profile/libexec/dynix-testing-client.py" indent = functools.partial(textwrap.indent, prefix=' ') @@ -65,9 +66,6 @@ def get_config_file() -> dict[str, Any]: @beartype def dynix_append_daemon(option: str, value: Any): - #machine.succeed(f''' - # dynix append {shlex.quote(option)} {shlex.quote(str(value))} - #'''.strip()) payload = json.dumps(dict( action="append", args=dict( @@ -76,12 +74,10 @@ def dynix_append_daemon(option: str, value: Any): ), )) - machine.succeed(f''' - echo '{payload}' | socat -T10 -,ignoreeof /run/user/0/dynix.sock - ''') + machine.succeed(f"echo '{payload}' | {testing_client} /run/user/0/dynix.sock") @beartype -def dynix_append_traditional(option: str, value: Any): +def dynix_append_cli(option: str, value: Any): machine.succeed(f''' dynix append {shlex.quote(option)} {shlex.quote(str(value))} '''.strip()) @@ -94,76 +90,81 @@ def dynix_append_traditional(option: str, value: Any): """.strip()) @beartype -def dynix_append(option: str, value: Any): - use_daemon = True - #use_daemon = False - if use_daemon: - dynix_append_daemon(option, value) - else: - dynix_append_traditional(option, value) +def run_all_tests(machine: Machine, *, use_daemon: bool): + dynix_append = dynix_append_daemon if use_daemon else dynix_append_cli -machine.log("Doing test initialization and checks") + # + # Setup. + # + dynix_out = machine.succeed("dynix --version") + assert "dynix" in dynix_out, f"dynix not in {dynix_out=}" -machine.wait_for_unit("default.target") -machine.wait_for_unit("install-dynix.service") + machine.succeed("systemctl start user@0.service") + machine.wait_for_unit("user@0.service") -dynix_out = machine.succeed("dynix --version") -assert "dynix" in dynix_out, f"dynix not in {dynix_out=}" + run_log(machine, "systemctl start dynix-daemon.service") + machine.wait_for_unit("dynix-daemon.service") -machine.succeed("systemctl start user@0.service") -machine.wait_for_unit("user@0.service") + machine.log("Checking initial harmonia.service conditions") -machine.succeed(textwrap.dedent(r''' - systemd-run --collect --unit=dynix-daemon.service \ - -E "RUST_LOG=trace" \ - -E "PATH=$PATH" \ - -E "NIX_PATH=$NIX_PATH" \ - -E "NIXOS_CONFIG=$NIXOS_CONFIG" \ - -p "SuccessExitStatus=0 2" \ - dynix daemon --color=always -''')) -machine.wait_for_unit("dynix-daemon.service") + # Config should have our initial values. + config_toml = get_config_file() + 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" -machine.log("Checking initial harmonia.service conditions") + with machine.nested("must succeed: initial nixos-rebuild switch"): + machine.succeed("env PAGER= nixos-rebuild switch --log-format raw-with-logs --no-reexec -v --fallback") -# Config should have our initial values. -config_toml = get_config_file() -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" + # Config should not have changed. + config_toml = get_config_file() + 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.log("Testing dynamic workers=20") + new_workers = 20 + dynix_append("services.harmonia.settings.workers", new_workers) + + machine.log("Testing that workers, but not max_connection_rate, changed") + # Workers, but not max connection rate, should have changed. + config_toml = get_config_file() + from pprint import pformat + machine.log(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" + + machine.log("Testing dynamic max_connection_rate=100") + new_max_connection_rate = 100 + dynix_append("services.harmonia.settings.max_connection_rate", new_max_connection_rate) + + # Max connection rate should have changed, and workers should be the same as before. + config_toml = get_config_file() + print(f"checking connection rate, {use_daemon=}") + assert int(config_toml['max_connection_rate']) == new_max_connection_rate, f"{config_toml['max_connection_rate']=} != {new_max_connection_rate}" + print(f"checking workers, {use_daemon=}") + assert int(config_toml['workers']) == new_workers, f"{config_toml['workers']=} != {new_workers}" + + machine.log("Done with tests; stopping dynix-daemon") + machine.succeed("systemctl stop dynix-daemon.service") + + # And this should set everything back. machine.succeed("env PAGER= nixos-rebuild switch --log-format raw-with-logs --no-reexec -v --fallback") + machine.wait_for_unit("harmonia.service") + config_toml = get_config_file() + 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' -# Config should not have changed. -config_toml = get_config_file() -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" +machine.start(allow_reboot=True) +machine.wait_for_unit("install-dynix.service") +try: + run_all_tests(machine, use_daemon=False) +except Exception as e: + machine.log(f"ERROR running CLI tests: {e}") -machine.log("Testing dynamic workers=20") -new_workers = 20 -dynix_append("services.harmonia.settings.workers", new_workers) +machine.reboot() -machine.log("Testing that workers, but not max_connectin_rate, changed") -# Workers, but not max connection rate, should have changed. -config_toml = get_config_file() -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" - -machine.log("Testing dynamic max_connection_rate=100") -new_max_connection_rate = 100 -dynix_append("services.harmonia.settings.max_connection_rate", new_max_connection_rate) - -# Max connection rate should have changed, and workers should be the same as before. -config_toml = get_config_file() -assert int(config_toml['max_connection_rate']) == new_max_connection_rate, f"{config_toml['max_connection_rate']=} != {new_max_connection_rate}" -assert int(config_toml['workers']) == new_workers, f"{config_toml['workers']=} != {new_workers}" - -machine.log("Done with tests; stopping dynix-daemon") -machine.succeed("systemctl stop dynix-daemon.service") - -# And this should set everything back. -machine.succeed("env PAGER= nixos-rebuild switch --log-format raw-with-logs --no-reexec -v --fallback") -machine.wait_for_unit("harmonia.service") -config_toml = get_config_file() -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' +machine.wait_for_unit("install-dynix.service") +try: + run_all_tests(machine, use_daemon=True) +except Exception as e: + machine.log(f"ERROR running DAEMON tests: {e}") + raise