#!/usr/bin/env bash # # scripts/test-mouse.sh — automated PS/2 mouse round trip. # # Builds the kernel, boots QEMU headless, injects mouse movement and # button presses through the monitor's `mouse_move` / `mouse_button` # commands, and verifies the guest decoded them via IRQ12 → ring → # packet decoder. Covers both surfaces: # - `cat /dev/mouse` (VFS snapshot of pending events) # - `mouse` (live streaming command, stopped by ^C) # # Usage: # ./scripts/test-mouse.sh # # Environment: # OVMF firmware (default: /usr/share/edk2/x64/OVMF.4m.fd) # QEMU qemu binary (default: qemu-system-x86_64) # set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PROJECT_DIR="$(dirname "$SCRIPT_DIR")" cd "$PROJECT_DIR" OVMF="${OVMF:-/usr/share/edk2/x64/OVMF.4m.fd}" QEMU="${QEMU:-qemu-system-x86_64}" ESP_DIR="$PROJECT_DIR/esp" QPID=/tmp/rsdos-mouse-qpid SOCK=/tmp/rsdos-mouse-qmon.sock LOG=/tmp/rsdos-mouse-serial.log QLOG=/tmp/rsdos-mouse-qemu.log # Never leave a QEMU behind. trap 'kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true' EXIT fail() { echo "FAIL: $1" [ -f "$LOG" ] && tail -20 "$LOG" exit 1 } # wait_log FILE PATTERN TIMEOUT-SECONDS wait_log() { local i=0 while [ "$i" -lt $((${3:-60} * 10)) ]; do grep -q "$2" "$1" 2>/dev/null && return 0 sleep 0.1 i=$((i + 1)) done return 1 } # wait_qemu_exit TIMEOUT-SECONDS wait_qemu_exit() { local i=0 while [ "$i" -lt $((${1:-30} * 10)) ]; do kill -0 "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || return 0 sleep 0.1 i=$((i + 1)) done return 1 } # qemu_hmp — send one raw HMP command to the monitor socket. qemu_hmp() { python3 - "$SOCK" "$1" <<'EOF' import socket, sys, time s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) s.connect(sys.argv[1]) s.sendall((sys.argv[2] + "\n").encode()) time.sleep(0.4) EOF } echo "==> 1/5 build kernel (debug)" cargo build --package rsdos --target x86_64-unknown-uefi echo "==> 2/5 deploy to ESP" mkdir -p "$ESP_DIR/EFI/BOOT" cp target/x86_64-unknown-uefi/debug/rsdos.efi "$ESP_DIR/EFI/BOOT/BOOTX64.EFI" echo "==> 3/5 boot QEMU (headless)" kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true sleep 1 rm -f "$SOCK" "$LOG" "$QEMU" -bios "$OVMF" \ -drive file=fat:rw:"$ESP_DIR",format=raw \ -serial file:"$LOG" \ -monitor unix:"$SOCK",server,nowait \ -m 256M -machine q35 -cpu qemu64 -no-reboot -display none \ >"$QLOG" 2>&1 & echo $! >"$QPID" wait_log "$LOG" "boot: entering shell" 90 || fail "kernel never reached the shell" grep -aq "ps2: mouse ready" "$LOG" || fail "mouse driver did not come up at boot" # Seed a move *before* any consumer runs, so `cat /dev/mouse` has a # queued event to drain (exercises the VFS snapshot path). qemu_hmp "mouse_move 8 4" echo "==> 4/5 drive the shell (cat /dev/mouse, mouse stream, poweroff)" python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" \ "cat /dev/mouse" \ "mouse" # While the `mouse` stream is live: a move, a left-press, a move with the # button held, then release — the button state must ride along on the # movement packet. qemu_hmp "mouse_move 20 7" qemu_hmp "mouse_button 1" qemu_hmp "mouse_move -5 -3" qemu_hmp "mouse_button 0" qemu_hmp "sendkey ctrl-c" python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" \ "poweroff" echo "==> 5/5 wait for shutdown + verify" wait_qemu_exit 30 || fail "QEMU did not exit after poweroff" grep -aq "ps2: mouse ready (IRQ12, stream mode)" "$LOG" || fail "no mouse-ready boot line" grep -aq "dx=+8 dy=+4 buttons=---" "$LOG" || fail "/dev/mouse did not drain the queued move (dx=+8 dy=+4)" grep -aq "dx=+20 dy=+7 buttons=---" "$LOG" || fail "mouse stream missed mouse_move 20 7" grep -aq "dx=+0 dy=+0 buttons=L--" "$LOG" || fail "left-button press did not decode" grep -aq "dx=-5 dy=-3 buttons=L--" "$LOG" || fail "held-button movement wrong (dx=-5 dy=-3 L)" grep -aq "mouse: stopped" "$LOG" || fail "^C did not stop the mouse stream" echo "" echo "PASS — PS/2 mouse round trip OK (IRQ12, /dev/mouse, live stream, buttons)" echo " events: 8,4 · 20,7 · L-press · -5,-3 held · release"