#!/usr/bin/env bash # End-to-end regression test for nomarchy-install: build the live ISO, # run a fully OFFLINE unattended install into a fresh VM disk, then boot # the installed system and capture what the first boot looks like. # # This automates the verification protocol from docs/TESTING.md §4 that # debugged the installer in the first place. It asserts the machine- # checkable parts (installer reaches poweroff; installed disk boots); # the visual part — a fully themed desktop, no autogenerated-config # banner — lands as screenshots for a human (or agent) to inspect: # # $NOMARCHY_VM_DIR/install-typed.png the typed command, pre-enter # $NOMARCHY_VM_DIR/luks-prompt.png initrd passphrase prompt # $NOMARCHY_VM_DIR/first-boot.png THE result: themed desktop # # Requirements: KVM, qemu, OVMF, dosfstools (mkfs.vfat), mtools (mcopy), # python3. Tunables (env): # NOMARCHY_VM_DIR scratch + screenshots (default /tmp/nomarchy-vm) # NOMARCHY_TARGET_DIR where the 25G target.img lives — must NOT be # tmpfs (default ~/.cache/nomarchy-vm) # NOMARCHY_BOOT_WAIT seconds to wait for the live desktop (default 480) # NOMARCHY_INSTALL_TIMEOUT seconds before declaring a hang (default 2700) set -euo pipefail cd "$(dirname "$0")/.." VM_DIR="${NOMARCHY_VM_DIR:-/tmp/nomarchy-vm}" TARGET_DIR="${NOMARCHY_TARGET_DIR:-$HOME/.cache/nomarchy-vm}" BOOT_WAIT="${NOMARCHY_BOOT_WAIT:-480}" INSTALL_TIMEOUT="${NOMARCHY_INSTALL_TIMEOUT:-2700}" export NOMARCHY_QMP_SOCK="$VM_DIR/qmp.sock" QMP=tools/vm/qmp.py SHOT=tools/vm/vncshot.py VNC_DISPLAY=17 # port 5917 for tool in qemu-system-x86_64 mkfs.vfat mcopy python3; do command -v "$tool" >/dev/null || { echo "missing $tool — try: nix shell nixpkgs#qemu nixpkgs#dosfstools nixpkgs#mtools" >&2 exit 1 } done [ -r /dev/kvm ] || { echo "no KVM — this test is too slow without it" >&2; exit 1; } mkdir -p "$VM_DIR" "$TARGET_DIR" if df --output=fstype "$TARGET_DIR" | tail -1 | grep -q tmpfs; then echo "NOMARCHY_TARGET_DIR=$TARGET_DIR is tmpfs — the install writes ~10G; pick a real disk" >&2 exit 1 fi OVMF="${NOMARCHY_OVMF:-}" [ -n "$OVMF" ] || for c in /run/current-system/sw/share/qemu/edk2-x86_64-code.fd \ /run/current-system/sw/share/OVMF/OVMF_CODE.fd \ /usr/share/OVMF/OVMF_CODE.fd; do [ -f "$c" ] && { OVMF="$c"; break; } done [ -n "$OVMF" ] || { echo "no OVMF firmware found" >&2; exit 1; } echo "==> Building the live ISO..." nix build .#nixosConfigurations.nomarchy-live.config.system.build.isoImage ISO=$(ls -1 result/iso/*.iso | head -n1) echo "==> Preparing disks..." # Unattended config rides in on a tiny vfat disk: the installer test # can't reliably TYPE a 200-char env line into the guest (keystrokes # get dropped), but one short mount-and-run command works. cat > "$VM_DIR/go.sh" <<'EOF' #!/usr/bin/env bash export NOMARCHY_UNATTENDED=1 export NOMARCHY_DISK=/dev/vda export NOMARCHY_USERNAME=ada export NOMARCHY_PASSWORD=nomarchy export NOMARCHY_HOSTNAME=testbox export NOMARCHY_LUKS_PASSPHRASE=testtest1 export NOMARCHY_SWAP_GB=2 export NOMARCHY_FINISH=poweroff nomarchy-install 2>&1 | tee /tmp/install.log EOF rm -f "$VM_DIR/config.img" truncate -s 8M "$VM_DIR/config.img" mkfs.vfat "$VM_DIR/config.img" >/dev/null mcopy -i "$VM_DIR/config.img" "$VM_DIR/go.sh" ::go.sh rm -f "$TARGET_DIR/target.img" "$NOMARCHY_QMP_SOCK" qemu-img create -f raw "$TARGET_DIR/target.img" 25G >/dev/null qemu_base=( qemu-system-x86_64 -enable-kvm -cpu host -m 6144 -smp 2 -drive "if=pflash,format=raw,readonly=on,file=$OVMF" -device virtio-vga-gl -display egl-headless -vnc "127.0.0.1:$VNC_DISPLAY" -device virtio-net-pci,netdev=n0 -netdev user,id=n0,restrict=on # OFFLINE -qmp "unix:$NOMARCHY_QMP_SOCK,server,nowait" ) echo "==> Phase 1: installing (offline) — this takes ~20-30 min..." "${qemu_base[@]}" \ -drive "file=$TARGET_DIR/target.img,if=virtio,format=raw" \ -drive "file=$VM_DIR/config.img,if=virtio,format=raw,readonly=on" \ -cdrom "$ISO" -boot d \ >"$VM_DIR/qemu-install.log" 2>&1 & QPID=$! trap 'kill $QPID 2>/dev/null || true' EXIT until [ -S "$NOMARCHY_QMP_SOCK" ]; do sleep 2; done echo " waiting ${BOOT_WAIT}s for the live desktop..." sleep "$BOOT_WAIT" python3 "$QMP" key meta_l-ret sleep 8 python3 "$QMP" type 'sudo bash -c "mkdir -p /m; mount /dev/vdb /m; bash /m/go.sh"' sleep 2 python3 "$SHOT" "$VM_DIR/install-typed.png" 127.0.0.1 "59$VNC_DISPLAY" python3 "$QMP" key ret echo " install running (typed command captured in install-typed.png)..." waited=0 while kill -0 $QPID 2>/dev/null; do if (( waited >= INSTALL_TIMEOUT )); then python3 "$SHOT" "$VM_DIR/install-hung.png" 127.0.0.1 "59$VNC_DISPLAY" || true echo "FAIL: installer did not power off within ${INSTALL_TIMEOUT}s (see install-hung.png)" >&2 exit 1 fi sleep 20; waited=$((waited + 20)) done trap - EXIT echo " VM powered off after ~${waited}s — install completed." echo "==> Phase 2: first boot of the installed system..." rm -f "$NOMARCHY_QMP_SOCK" "${qemu_base[@]}" \ -drive "file=$TARGET_DIR/target.img,if=virtio,format=raw" \ >"$VM_DIR/qemu-boot.log" 2>&1 & QPID=$! trap 'kill $QPID 2>/dev/null || true' EXIT until [ -S "$NOMARCHY_QMP_SOCK" ]; do sleep 2; done sleep 40 python3 "$SHOT" "$VM_DIR/luks-prompt.png" 127.0.0.1 "59$VNC_DISPLAY" python3 "$QMP" type 'testtest1' python3 "$QMP" key ret sleep 150 python3 "$SHOT" "$VM_DIR/first-boot.png" 127.0.0.1 "59$VNC_DISPLAY" python3 "$QMP" quit || true trap - EXIT echo echo "PASS (machine-checkable part): install powered off, installed disk boots." echo "Now INSPECT $VM_DIR/first-boot.png:" echo " expected: themed desktop (wallpaper + waybar), NO red banner, no login prompt" echo " if it shows the Hyprland water-drop or an 'autogenerated config' banner," echo " log in on tty2 (ada/nomarchy) and read /var/log/nomarchy-hm-preactivate.log"