feat(tools): commit the VM test harness + one-command installer regression
Promotes the throwaway harness that verified the installer into tools/:
- tools/vm/qmp.py QMP keystroke injection / typing / quit
- tools/vm/vncshot.py GL-safe screenshots via VNC readback (QMP
screendump shows "no surface" with virtio-vga-gl)
- tools/vm/gap-analysis.py drv-graph diff that converged the offline
pin set; run it when offline installs build
from source
- tools/test-install.sh the full offline regression: build ISO, boot
offline, unattended LUKS+swap install via a
config disk (typed long commands drop keys),
wait for poweroff, boot the installed disk,
screenshot the first boot for visual verdict
docs/TESTING.md §4 now points at the script.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
151
tools/test-install.sh
Executable file
151
tools/test-install.sh
Executable file
@@ -0,0 +1,151 @@
|
||||
#!/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=""
|
||||
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"
|
||||
Reference in New Issue
Block a user