From 5983eadae237f1c7f554a4d89554f08e90bb0dbf Mon Sep 17 00:00:00 2001 From: Nils Stinnesbeck Date: Thu, 29 Oct 2026 13:34:21 +0100 Subject: [PATCH] Add fast iteration tooling nix run .#fast, .#test and .#webui-dev; see DEVELOPING.md. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F --- DEVELOPING.md | 52 ++++++++++++++++++++++++++++++ pkgs/router-webui/dev_test.go | 39 +++++++++++++++++++++++ scripts/fast.sh | 59 +++++++++++++++++++++++++++++++++++ scripts/test.sh | 45 ++++++++++++++++++++++++++ scripts/webui-dev.sh | 28 +++++++++++++++++ 5 files changed, 223 insertions(+) create mode 100644 DEVELOPING.md create mode 100644 pkgs/router-webui/dev_test.go create mode 100644 scripts/fast.sh create mode 100644 scripts/test.sh create mode 100644 scripts/webui-dev.sh diff --git a/DEVELOPING.md b/DEVELOPING.md new file mode 100644 index 0000000..e912614 --- /dev/null +++ b/DEVELOPING.md @@ -0,0 +1,52 @@ +# Developing + +Three tools, from instant to slow. Run them from the repository root; the +flake only sees files git knows about, so `git add` new files first. + +## `nix run .#webui-dev`: the web UI on your machine + +Sample settings, sample DHCP leases and your real interface counters, on + (password `devpassword1`). Templates and CSS are +re-read on every request: edit, reload the browser. A template with a typo +keeps the last good version and logs the error. Pings need root and show "no +answer"; Apply does nothing useful here. + +## `nix run .#fast`: no VMs + +Go formatting, vet and tests, then builds the installer, a router built from +settings (through the release) and the home gateway. That runs shellcheck over +the scripts and evaluates the modules: most failures of the VM tests +(typos, type errors, a script that does not build) show up here, in about a +minute (half of that when little changed). + + nix run .#fast -- --drivers also builds every VM test's driver: type-checks + the test scripts, builds the VMs' systems + nix run .#fast -- --all also the checks that need no VM and `nix flake check` + +## `nix run .#test -- NAME...`: VM tests + +One at a time (each needs gigabytes of memory), a line per test with its time, +and for a failure the step that failed and what it said; the full log is in +`${TMPDIR:-/tmp}/router-test-logs/NAME.log`. + + nix run .#test -- update setup + nix run .#test -- --all + nix run .#test -- --list + +`nix run .#test -- -i update` builds the test's interactive driver and gives a +Python prompt: `start_all()`, `target.succeed("...")`, `test_script()`. Boot +the VMs once and try a step again instead of running the whole test. + +### Where the time goes + +A VM test spends most of its time in the system under test, not in building: +`router-install` (about a minute), booting (20 s each), and everything that +runs `router-apply` on the router (building a NixOS system there takes about a +minute) or waits for a rollback timer. A source change rebuilds the systems +again on the host. So: + +- Try a change with `webui-dev` or `fast` first; run a VM test only to confirm. +- Run only the test for what you changed: `update` for `router-update` and + the Update page, `setup` for `router-setup`, `apply` for the settings and + the web UI, `basic` for the firewall and DHCP. +- Use `-i` to iterate on a test script. diff --git a/pkgs/router-webui/dev_test.go b/pkgs/router-webui/dev_test.go new file mode 100644 index 0000000..57f6d75 --- /dev/null +++ b/pkgs/router-webui/dev_test.go @@ -0,0 +1,39 @@ +package main + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestDevAssetsAreReloaded(t *testing.T) { + dir := t.TempDir() + if err := os.CopyFS(dir, os.DirFS(".")); err != nil { + t.Fatal(err) + } + old := assets + assets = os.DirFS(dir) + defer func() { assets = old }() + + sys := System{ConfigDir: filepath.Join(dir, "config"), StateDir: dir} + a := newApp(sys, newCollector(sys, ""), true) + h := a.routes() + if strings.Contains(do(h, "GET", "/login", nil, nil).Body.String(), "reloaded-marker") { + t.Fatal("marker already there") + } + + login := filepath.Join(dir, "templates", "login.html") + data, _ := os.ReadFile(login) + i := strings.LastIndex(string(data), "{{end}}") + os.WriteFile(login, []byte(string(data)[:i]+"

reloaded-marker

"+string(data)[i:]), 0o600) + if !strings.Contains(do(h, "GET", "/login", nil, nil).Body.String(), "reloaded-marker") { + t.Fatal("an edited template is not picked up") + } + + // A typo keeps the last good version. + os.WriteFile(login, []byte("{{define \"content\"}}{{ broken"), 0o600) + if !strings.Contains(do(h, "GET", "/login", nil, nil).Body.String(), "reloaded-marker") { + t.Fatal("a broken template replaced the working one") + } +} diff --git a/scripts/fast.sh b/scripts/fast.sh new file mode 100644 index 0000000..8024171 --- /dev/null +++ b/scripts/fast.sh @@ -0,0 +1,59 @@ +# The quick checks: no VM boots. Catches what most failed runs of the VM tests +# were about (Go and Nix errors, shellcheck, type errors in the test scripts, +# assertions). Evaluating NixOS is the cost: about a minute for the default. +# router-fast Go checks; the installer, a settings-built router +# (via the release) and the home gateway build +# router-fast --drivers also build every VM test's driver (type-checks the +# test scripts, builds the VMs' systems): minutes +# router-fast --all also every flake check without VMs (assertions, +# lib) and `nix flake check --no-build`: many minutes +root=$(git rev-parse --show-toplevel) +cd "$root" +drivers=0 all=0 +case ${1:-} in + --drivers) drivers=1 ;; + --all) drivers=1 all=1 ;; + "") ;; + *) echo "usage: router-fast [--drivers | --all]" >&2; exit 2 ;; +esac +start=$SECONDS +last=$SECONDS +step() { + printf '\n\033[1m== %s\033[0m (previous step: %ds)\n' "$*" $((SECONDS - last)) + last=$SECONDS +} + +step "web UI: gofmt, vet, tests" +( + cd pkgs/router-webui + if [ -n "$(gofmt -l .)" ]; then echo "gofmt wants to change:"; gofmt -l .; exit 1; fi + go vet ./... + go test ./... +) + +# New files must be known to git for a flake to see them. +untracked=$(git ls-files --others --exclude-standard) +if [ -n "$untracked" ]; then + printf '\nwarning: not known to git, so the flake ignores them (git add):\n%s\n' "$untracked" +fi + +step "systems build (shellcheck of the scripts, units, firewall rules)" +nix build --no-link --max-jobs 1 \ + .#nixosConfigurations.installer.config.system.build.toplevel \ + .#nixosConfigurations.home.config.system.build.toplevel \ + .#release + +if [ "$drivers" = 1 ]; then + step "VM test drivers build (type check)" + for t in basic pppoe vlans onestick rollback dns installer apply setup update image wireguard; do + echo " $t" + nix build --no-link --max-jobs 1 ".#checks.x86_64-linux.$t.driver" + done +fi +if [ "$all" = 1 ]; then + step "the checks that need no VM" + nix build --no-link --max-jobs 1 .#checks.x86_64-linux.{lib,assertions,example,home} + step "everything evaluates" + nix flake check --no-build +fi +printf '\n\033[1mok\033[0m (%ds)\n' $((SECONDS - start)) diff --git a/scripts/test.sh b/scripts/test.sh new file mode 100644 index 0000000..6e71dfe --- /dev/null +++ b/scripts/test.sh @@ -0,0 +1,45 @@ +# Runs VM tests one at a time (they need a lot of memory), says what failed. +# router-test NAME... run these tests +# router-test --all run all of them +# router-test -i NAME build the test's interactive driver and run it: +# a Python prompt where start_all(), machine.succeed(...) +# and test_script() work; boot once, retry steps +# router-test --list +# Logs: ${TMPDIR:-/tmp}/router-test-logs/NAME.log +vm_tests="basic pppoe vlans onestick rollback dns installer apply setup update image wireguard" +flake=${FLAKE:-.} +logs=${TMPDIR:-/tmp}/router-test-logs +mkdir -p "$logs" + +case ${1:-} in + --list) echo "$vm_tests" | tr ' ' '\n'; exit 0 ;; + -i|--interactive) + [ $# -eq 2 ] || { echo "usage: router-test -i NAME" >&2; exit 2; } + driver=$(nix build --no-link --print-out-paths "$flake#checks.x86_64-linux.$2.driverInteractive") + exec "$driver/bin/nixos-test-driver" --interactive ;; + --all) set -- $vm_tests ;; + ""|-*) echo "usage: router-test NAME... | --all | -i NAME | --list" >&2; exit 2 ;; +esac + +failed=0 +total=$SECONDS +for name in "$@"; do + log=$logs/$name.log + start=$SECONDS + printf '%-12s ' "$name" + if nix build "$flake#checks.x86_64-linux.$name" --max-jobs 1 -L --no-link > "$log" 2>&1; then + printf 'ok %4ds\n' $((SECONDS - start)) + else + failed=1 + printf 'FAILED %4ds %s\n' $((SECONDS - start)) "$log" + # The failing step and what it said, or the build error before any VM ran. + if grep -aq '!!! Test "' "$log"; then + grep -a -m1 '!!! Test "' "$log" | sed -E 's/^[^>]*> //; s/^/ /' | cut -c1-300 + grep -a '!!! ' "$log" | sed -n '2,12p' | sed -E 's/^[^>]*> //; s/^/ /' | cut -c1-240 + else + grep -a -E '^error|error:' -A8 "$log" | head -24 | sed 's/^/ /' | cut -c1-240 + fi + fi +done +printf '\n%d test(s), %ds\n' $# $((SECONDS - total)) +exit $failed diff --git a/scripts/webui-dev.sh b/scripts/webui-dev.sh new file mode 100644 index 0000000..3aee4f7 --- /dev/null +++ b/scripts/webui-dev.sh @@ -0,0 +1,28 @@ +# The web UI on your own machine, with sample settings, leases and your real +# interface counters. Templates and CSS are re-read on every request: edit, +# reload the browser. (Pings need root and fail here; applying does nothing.) +root=$(git rev-parse --show-toplevel) +dir=$(mktemp -d) +trap 'rm -rf "$dir"' EXIT +mkdir -p "$dir/config" +wan=$(ip -j route show default 2>/dev/null | jq -r '.[0].dev // empty') +lan=$(ip -j link show 2>/dev/null | jq -r --arg wan "$wan" '[.[] | select(.link_type == "ether" and .ifname != $wan) | .ifname][0] // "eth1"') +cat > "$dir/config/settings.json" < "$dir/leases" < "$dir/router-release" +cd "$root/pkgs/router-webui" +echo devpassword1 | go run . passwd "$dir/webui-password" +echo +echo " https://127.0.0.1:8443 password: devpassword1 (the certificate is self-signed)" +echo +exec go run . -listen 127.0.0.1:8443 -config "$dir/config" -state "$dir" -leases "$dir/leases" -assets .