From: Jakub Czajka Date: Wed, 15 Jul 2026 22:49:55 +0000 (+0000) Subject: [tests] Add VPS test harness infrastructure. X-Git-Url: https://git.ekhem.eu.org/?a=commitdiff_plain;h=764e4e1f6b5f0839dce2017105367432c0c521c0;p=guix.git [tests] Add VPS test harness infrastructure. Boot a marionette VM that inherits from vps-operating-system with static networking and a throwaway SSH key. No individual test cases yet — the harness validates that the VM boots and the marionette REPL is reachable. Add test-vps / test-vps-fresh Makefile targets with cache. Co-Authored-By: Claude --- diff --git a/.claude/hooks/block-builds b/.claude/hooks/block-builds index 924c138..ce3922d 100755 --- a/.claude/hooks/block-builds +++ b/.claude/hooks/block-builds @@ -33,7 +33,7 @@ case "$command" in '}}' exit 0 ;; - *'make build-'*) + *'make build-'*|*'make test-'*) $GUIX_BIN/echo \ '{"hookSpecificOutput": {' \ ' "hookEventName": "PreToolUse",' \ diff --git a/Makefile b/Makefile index a6ab116..896a046 100644 --- a/Makefile +++ b/Makefile @@ -60,6 +60,29 @@ build-vps-home: build-vps: build-vps-system build-vps-home +# VPS test suite — validates configurations then boots a VM and +# runs all test cases. Uses cache by default; tests are +# deterministic for given inputs. +test-vps: build-vps-system build-vps-home + @out=$$(GUIX_PACKAGE_PATH=$(CURDIR) guix build -L $(CURDIR) \ + -e '(@ (tests vps-base) %test-vps)' 2>/dev/null); \ + if [ -n "$$out" ] && [ -e "$$out" ]; then \ + echo "📋 Cached: $$out"; \ + echo " Run 'make test-vps-fresh' to re-execute."; \ + else \ + GUIX_PACKAGE_PATH=$(CURDIR) guix build -L $(CURDIR) \ + -e '(@ (tests vps-base) %test-vps)'; \ + fi + +# Force fresh tests — delete cached result and rebuild. +test-vps-fresh: build-vps-system build-vps-home + @out=$$(GUIX_PACKAGE_PATH=$(CURDIR) guix build -L $(CURDIR) \ + -e '(@ (tests vps-base) %test-vps)' 2>/dev/null); \ + [ -n "$$out" ] && [ -e "$$out" ] \ + && guix gc --delete "$$out" 2>/dev/null; \ + GUIX_PACKAGE_PATH=$(CURDIR) guix build -L $(CURDIR) \ + -e '(@ (tests vps-base) %test-vps)' + tags: find $(guix-store) -wholename "*guix-$(guix-version)*.scm" -or \ -wholename "*guix-module-union*.scm" -or \ diff --git a/tests/vps-base.scm b/tests/vps-base.scm new file mode 100644 index 0000000..4d5a50a --- /dev/null +++ b/tests/vps-base.scm @@ -0,0 +1,178 @@ +;;; Copyright (c) 2026 Jakub Czajka +;;; License: GPL-3.0 or later. +;;; +;;; VPS test harness — one VM for all test sections. +;;; Inherits from the production operating system so any change to +;;; vps-system.scm or vps-home.scm is automatically tested. + +(define-module (tests vps-base) + #:use-module (gnu home) + #:use-module (gnu tests) + #:use-module (gnu system) + #:use-module (gnu system vm) + #:use-module (gnu services) + #:use-module (gnu services base) + #:use-module (gnu services ssh) + #:use-module (gnu services networking) + #:use-module (gnu services shepherd) + #:use-module (gnu packages admin) + #:use-module (gnu packages base) + #:use-module (gnu packages bash) + #:use-module (gnu packages ssh) + #:use-module (vps-system) + #:use-module (vps-home) + #:use-module (guix gexp) + #:export (%test-vps %vps-test-os run-vps-test)) + +;;; +;;; Test SSH keypair — generated at build time. +;;; + +(define %test-ssh-keypair + (computed-file "test-ssh-keypair" + (with-imported-modules '((guix build utils)) + #~(begin + (use-modules (guix build utils)) + (mkdir #$output) + (invoke #$(file-append openssh + "/bin/ssh-keygen") + "-t" + "ed25519" + "-f" + (string-append #$output + "/id_ed25519") + "-N" + "" + "-C" + "guix-test@vps") + (copy-file (string-append #$output + "/id_ed25519.pub") + (string-append #$output + "/authorized_key")))) + #:local-build? #f)) + +;;; +;;; Static networking — QEMU user-mode DHCP is unreliable during +;;; early boot, causing dhcpcd to hang and block the marionette REPL. +;;; + +(define %static-network-config + (let ((addr (network-address (device "eth0") + (value "10.0.2.15/24"))) + (route (network-route (destination "default") + (gateway "10.0.2.2")))) + (static-networking (addresses (list addr)) + (routes (list route))))) + +(define %static-networking + (service static-networking-service-type + (list %static-network-config))) + +;;; +;;; Test operating system — inherits from production with only two +;;; overrides: static networking and a throwaway SSH key for dak. +;;; + +(define (test-os-services base-os) + "Return the service list for the test VM. Removes dhcpcd (QEMU +DHCP is unreliable), prepends static networking, and adds a throwaway +ed25519 key to the production SSH config." + (define (add-test-key config) + (let ((key (file-append %test-ssh-keypair "/authorized_key"))) + (openssh-configuration (inherit config) + (authorized-keys `(("dak" ,key)))))) + (cons %static-networking + (modify-services (operating-system-user-services base-os) + (delete dhcpcd-service-type) + (openssh-service-type config => + (add-test-key config))))) + +(define %vps-test-os + (marionette-operating-system (operating-system + (inherit vps-operating-system) + (packages (append (home-environment-packages + vps-home-environment) + (operating-system-packages + vps-operating-system))) + (services + (test-os-services vps-operating-system))) + #:imported-modules '((gnu services herd) + (guix combinators)))) + +;;; +;;; SSH session helper — key-only auth as user dak via the +;;; forwarded port (host:2222 -> guest:72). +;;; + +(define (make-ssh-connect marionette-gexp) + "Return a gexp that connects over SSH using the system ssh +command, runs a shell command, and verifies the witness file." + #~(lambda () + (let* ((ssh #$(file-append openssh "/bin/ssh")) + (key (string-append #$%test-ssh-keypair "/id_ed25519")) + (exit (system* ssh + "-i" + key + "-o" + "StrictHostKeyChecking=no" + "-o" + "UserKnownHostsFile=/dev/null" + "-o" + "BatchMode=yes" + "-o" + "ConnectTimeout=10" + "-p" + "2222" + "dak@localhost" + "echo hello > /tmp/witness"))) + (and (zero? exit) + (wait-for-file "/tmp/witness" + #$marionette-gexp))))) + +;;; +;;; Test harness. +;;; + +(define (run-vps-test test-cases) + "Return a derivation that boots a VM with %vps-test-os and runs +every function in TEST-CASES. Each element is a function +(marionette-gexp) -> gexp that returns test assertions." + (define os + %vps-test-os) + (define vm + (virtual-machine (operating-system + os) + (port-forwardings '((2222 . 72))))) + + (define test + (with-imported-modules '((gnu build marionette)) + #~(begin + (use-modules (gnu build marionette) + (srfi srfi-64) + (ice-9 match)) + + (define marionette + (make-marionette (list #$vm))) + + (define runner + (system-test-runner #$output)) + + (test-runner-current runner) + + (test-begin "vps") + + #$@(map (lambda (tc) + (tc #~marionette)) test-cases) + + (test-end "vps")))) + (gexp->derivation "vps-test" test)) + +;;; +;;; System test record — test cases are added incrementally in +;;; subsequent commits. +;;; + +(define %test-vps + (system-test (name "vps") + (description "VPS test suite.") + (value (run-vps-test '()))))