#!/usr/bin/env bash # # scripts/test-mouse.sh — automated mouse round trip (cursor model). # # Since v0.5.0 the cursor task owns the PS/2 ring: mouse input shows up # as *cursor position + buttons* on the screen, not as an event drain. # This script boots QEMU headless, injects movement/buttons via the # monitor, and verifies: # - the `cursor` command reports a position that moves with `mouse_move` # - button presses ride along (L while held) # - `/dev/mouse` is a position device (one snapshot line) # - the `mouse` watch command prints live changes and stops on ^C # - a `mouse ` forward routes real events to a task's queue # # Usage: ./scripts/test-mouse.sh # Env: OVMF, QEMU (same defaults as other test scripts) # 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 trap 'kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true' EXIT fail() { echo "FAIL: $1" [ -f "$LOG" ] && tail -30 "$LOG" exit 1 } 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() { 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() { 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 } # cursor_state — run `cursor`, return "x y buttons" of the LAST occurrence. cursor_state() { python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" "cursor" sleep 0.5 sed -n 's/.*cursor: x=\([0-9]*\) y=\([0-9]*\) buttons=\([A-Za-z-]*\).*/\1 \2 \3/p' "$LOG" | tail -1 } echo "==> 1/6 build kernel (debug)" cargo build --package rsdos --target x86_64-unknown-uefi echo "==> 2/6 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/6 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" wait_log "$LOG" "cursor: task " 10 || fail "cursor task did not spawn" echo "==> 4/6 cursor position tracks injected movement" INIT=$(cursor_state) [ -n "$INIT" ] || fail "no initial cursor position" IX=$(echo "$INIT" | awk '{print $1}') IY=$(echo "$INIT" | awk '{print $2}') IB=$(echo "$INIT" | awk '{print $3}') echo " initial: x=$IX y=$IY buttons=$IB" [ "$IB" = "---" ] || fail "expected no buttons at boot, got $IB" qemu_hmp "mouse_move 20 7" S1=$(cursor_state) X1=$(echo "$S1" | awk '{print $1}') Y1=$(echo "$S1" | awk '{print $2}') [ "$X1" -eq $((IX + 20)) ] || fail "x did not move +20 (was $IX, now $X1)" [ "$Y1" -eq $((IY + 7)) ] || fail "y did not move +7 (was $IY, now $Y1)" echo "==> 5/6 buttons ride along; /dev/mouse is a position device" qemu_hmp "mouse_button 1" S2=$(cursor_state) B2=$(echo "$S2" | awk '{print $3}') [ "$B2" = "L--" ] || fail "left press not seen (got $B2)" qemu_hmp "mouse_move -5 -3" S3=$(cursor_state) X3=$(echo "$S3" | awk '{print $1}') Y3=$(echo "$S3" | awk '{print $2}') B3=$(echo "$S3" | awk '{print $3}') [ "$X3" -eq $((X1 - 5)) ] || fail "held move x wrong ($X1 -5 = $((X1 - 5)), got $X3)" [ "$Y3" -eq $((Y1 - 3)) ] || fail "held move y wrong ($Y1 -3 = $((Y1 - 3)), got $Y3)" [ "$B3" = "L--" ] || fail "button state dropped during held move (got $B3)" qemu_hmp "mouse_button 0" S4=$(cursor_state) B4=$(echo "$S4" | awk '{print $3}') [ "$B4" = "---" ] || fail "release not seen (got $B4)" python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" "cat /dev/mouse" sleep 0.5 grep -aq "x=$X3 y=$Y3 buttons=---" "$LOG" || fail "/dev/mouse does not report position snapshot (x=$X3 y=$Y3)" grep -aq "mouse: no events" "$LOG" && fail "/dev/mouse is still an event drain" echo "==> 6/6 live watch + forwarding + poweroff" python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" \ "msgrecv" \ "mouse 3" # Live watch running: a move must appear as a changed line. qemu_hmp "mouse_move 3 2" # Forwarding live: the same move must reach the msgrecv task (spawned as # task 3; task 2 is the cursor task itself). sleep 1 qemu_hmp "sendkey ctrl-c" grep -aq "mouse: routing events to task 3" "$LOG" || fail "mouse 3 did not arm routing" grep -aq "msgrecv\[3\]: Mouse(+" "$LOG" || fail "task 3 queue did not receive the routed Mouse event" # Watch line: x/y/buttons format (any occurrence after watching started). grep -aq "mouse: x=$((X3 + 3)) y=$((Y3 + 2)) buttons=---" "$LOG" || fail "watch did not print the moved cursor (x=$((X3 + 3)) y=$((Y3 + 2)))" grep -aq "mouse: stopped" "$LOG" || fail "^C did not stop the mouse watch" python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" "poweroff" echo "==> done: wait for shutdown + verify" wait_qemu_exit 30 || fail "QEMU did not exit after poweroff" echo "" echo "PASS — mouse round trip OK (cursor task, /dev/mouse, watch, forward)" echo " moves: +20,+7 · -5,-3 held · +3,+2 watch" echo " path: IRQ12 → ring → cursor task → overlay + queue"