From 7958276c9f0d230053520ba20f2a00f5ecda18e4 Mon Sep 17 00:00:00 2001 From: salmonik Date: Sun, 1 Nov 2026 15:53:29 +0100 Subject: scripts: rebuild the dev loop around build.sh/run.sh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `make` did not parse at all (missing separator at line 139), so every make target was dead. The folder also carried three overlapping launchers (boot.sh, qemu.sh, boot-image.sh) plus a fourth partial one in the Makefile, `cargo build --package rsdos` copy-pasted eight times, the QEMU invocation copy-pasted per script, and the test helpers (fail/wait_log/wait_qemu_exit/qemu_hmp) duplicated in five tests. New layout: - scripts/lib.sh: single config (image, OVMF, mem/smp), the one cargo invocation (build_all), the one QEMU shape (qemu_args), and the test harness (test_boot, wait_log, wait_qemu_exit, qemu_hmp, sendkeys) - scripts/build.sh: kernel + every bins/* in one pass - scripts/mksystem.sh: freshness-aware image build that now stages every bins/*.efi into /bin/cli|gui (they were never copied before) - scripts/run.sh: build + stage + boot in one command; --headless, --debug (GDB stub), --monitor (HMP socket), --fresh, --mem/--smp/ --bios/--image; on Wayland it auto-selects XWayland plus grab-on-hover so the mouse can be captured (Ctrl+Alt+G toggles) - scripts/tests/: the five automated tests on the shared harness - deleted: boot.sh, qemu.sh, boot-image.sh, lib-boot-args.sh, build-image-release.sh, build-bootloader.sh (rsboot chain dropped) The Makefile is now a thin wrapper: make run, make test, make system, make inspect, make help. Two-command flow: ./scripts/build.sh then ./scripts/run.sh (run.sh alone also works). Stale test assumptions fixed so the suite is green again: test-gfx waited for "gfx: check" while the shell prints "gfx check: PASS"; test-mouse/test-msg hardcoded msgrecv = task 3 (wm-input took slot 3 — ids are parsed from the log now); the queue-full probe targeted task 0, whose exec pump drains its own queue as completion codes (moved to the cursor task, which never recvs). Verified in QEMU: make test -> test-disk, test-gfx, test-mouse, test-msg, test-fault all PASS. --- scripts/tests/sendkeys.py | 105 ++++++++++++++++++++++++++++++++++++++++++++ scripts/tests/test-disk.sh | 67 ++++++++++++++++++++++++++++ scripts/tests/test-fault.sh | 47 ++++++++++++++++++++ scripts/tests/test-focus.py | 57 ++++++++++++++++++++++++ scripts/tests/test-gfx.sh | 37 ++++++++++++++++ scripts/tests/test-mouse.sh | 83 ++++++++++++++++++++++++++++++++++ scripts/tests/test-msg.sh | 98 +++++++++++++++++++++++++++++++++++++++++ 7 files changed, 494 insertions(+) create mode 100755 scripts/tests/sendkeys.py create mode 100755 scripts/tests/test-disk.sh create mode 100755 scripts/tests/test-fault.sh create mode 100755 scripts/tests/test-focus.py create mode 100755 scripts/tests/test-gfx.sh create mode 100755 scripts/tests/test-mouse.sh create mode 100755 scripts/tests/test-msg.sh (limited to 'scripts/tests') diff --git a/scripts/tests/sendkeys.py b/scripts/tests/sendkeys.py new file mode 100755 index 0000000..9300242 --- /dev/null +++ b/scripts/tests/sendkeys.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +"""Type lines into a QEMU guest via the monitor's `sendkey` command. + +Each positional argument is one line: it is typed character by character +(US layout) followed by Enter. Use it to drive the rs-dos shell from a +test script without a display. + +Used by scripts/tests/test-*.sh, and manually against an interactive +session started with `./scripts/run.sh --monitor`: + + ./scripts/tests/sendkeys.py "ls /disk" "date" "poweroff" + ./scripts/tests/sendkeys.py --sock /tmp/rsdos-monitor.sock "date" + +Environment: QMON_SOCK (default: /tmp/rsdos-monitor.sock) +""" +import os +import socket +import sys +import time + +SOCK = os.environ.get("QMON_SOCK", "/tmp/rsdos-monitor.sock") + +# Unshifted punctuation needs QEMU's *named* keys — bare '-'/'.'/'/' are +# not valid sendkey names (and '-' is the modifier separator). +BASE = { + "-": "minus", "=": "equal", "[": "bracketleft", "]": "bracketright", + "\\": "backslash", ";": "semicolon", "'": "apostrophe", + ",": "comma", ".": "dot", "/": "slash", "`": "grave", +} + +# Characters that need a shift key on US layout, and the key QEMU's +# `sendkey` understands for them. +SHIFT = { + "~": "grave", "!": "1", "@": "2", "#": "3", "$": "4", "%": "5", + "^": "6", "&": "7", "*": "8", "(": "9", ")": "0", + "_": "minus", "+": "equal", "{": "bracketleft", "}": "bracketright", + "|": "backslash", ":": "semicolon", '"': "apostrophe", + "<": "comma", ">": "dot", "?": "slash", +} + +ALNUM = set("abcdefghijklmnopqrstuvwxyz0123456789") + + +def connect(path, timeout=60): + """Connect to the monitor socket, retrying until QEMU is up.""" + deadline = time.time() + timeout + while True: + try: + s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + s.connect(path) + return s + except OSError: + if time.time() > deadline: + raise + time.sleep(0.2) + + +def sendkey(sock, keys): + sock.sendall(("sendkey %s\n" % keys).encode()) + + +def type_line(sock, line): + for ch in line: + if ch == " ": + sendkey(sock, "spc") + elif ch in BASE: + sendkey(sock, BASE[ch]) + elif ch in SHIFT: + sendkey(sock, "shift-" + SHIFT[ch]) + elif ch.isupper(): + sendkey(sock, "shift-" + ch.lower()) + elif ch in ALNUM: + sendkey(sock, ch) + else: + sys.stderr.write("warning: no sendkey mapping for %r\n" % ch) + continue + time.sleep(0.25) + sendkey(sock, "ret") + time.sleep(1.5) + + +def main(): + args = [a for a in sys.argv[1:] if a != "--sock"] + sock_path = SOCK + if "--sock" in sys.argv: + i = sys.argv.index("--sock") + sock_path = sys.argv[i + 1] + args = sys.argv[i + 2:] + if not args: + sys.stderr.write(__doc__ or "") + return 1 + sock = connect(sock_path) + # Let the monitor drain any startup banner before typing. + time.sleep(0.5) + for line in args: + try: + type_line(sock, line) + except OSError: + # Guest powered off (last line was usually "poweroff"). + break + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/tests/test-disk.sh b/scripts/tests/test-disk.sh new file mode 100755 index 0000000..3836b6d --- /dev/null +++ b/scripts/tests/test-disk.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# +# scripts/tests/test-disk.sh — full automated data-disk round trip. +# +# Builds, stages the system image, boots QEMU headless, drives the shell +# via the monitor (ls / mkdir / echo WRITING / cat / poweroff), then +# verifies the results with mtools and fsck. +# +# Usage: +# ./scripts/tests/test-disk.sh reuse the existing image +# ./scripts/tests/test-disk.sh --fresh reformat + reseed first +# +# Env: SYSTEM_IMG, OVMF, QEMU (see scripts/lib.sh) +# +set -euo pipefail +TNAME="disk" +# shellcheck source=../lib.sh +source "$(dirname "$0")/../lib.sh" + +FRESH=0 +[ "${1:-}" = "--fresh" ] && FRESH=1 +STAMP="$(date +%s)" + +echo "==> 1/6 build (kernel + bins)" +build_all + +echo "==> 2/6 stage system image" +if [ "$FRESH" -eq 1 ]; then stage_image --force; else stage_image; fi + +echo "==> 3/6 boot QEMU (headless)" +test_boot +sleep 1 + +# Volumes mount under /disk// where UID = "LABEL-SERIAL" (e.g. +# RSDOS-5BD1E55F). Discover it from the boot log so the test works on +# any image without knowing the serial in advance. +VOL_UID="$(grep -a 'fat: mounted' "$LOG" | sed -n 's/.*at \/disk\/\([^ )]*\).*/\1/p' | head -1)" +[ -n "$VOL_UID" ] || fail "no FAT32 volume was mounted (look for 'fat: mounted' in the log)" +echo " mounted volume UID: $VOL_UID" + +echo "==> 4/6 drive the shell (ls / mkdir / echo WRITING / cat / poweroff)" +# The filename is deliberately >13 chars so the FAT LFN path gets +# multi-entry entries (they're stored last-first on disk). +TESTFILE="disk-test-$STAMP.txt" +sendkeys \ + "ls /disk" \ + "mkdir /disk/$VOL_UID/sub" \ + "echo disk-test-$STAMP WRITING /disk/$VOL_UID/sub/$TESTFILE" \ + "cat /disk/$VOL_UID/sub/$TESTFILE" \ + "poweroff" + +echo "==> 5/6 wait for shutdown" +wait_qemu_exit 30 || fail "QEMU did not exit after poweroff" + +echo "==> 6/6 verify" +grep -a "disk-test-$STAMP" "$LOG" >/dev/null || fail "cat output missing from serial log" +if ! fsck.fat -n "$SYSTEM_IMG" >/tmp/rsdos-fsck.out 2>&1; then + fail "fsck.fat reports problems: $(tail -3 /tmp/rsdos-fsck.out)" +fi +MT="$(mtype -i "$SYSTEM_IMG" "::/sub/$TESTFILE" 2>/dev/null || true)" +[ "$MT" = "disk-test-$STAMP" ] || fail "mtype mismatch: got '$MT'" + +echo "" +echo "PASS — disk round trip OK (fsck clean, file survived poweroff)" +echo " image: $SYSTEM_IMG" +echo " volume: /disk/$VOL_UID" +echo " written: /disk/$VOL_UID/sub/$TESTFILE = \"disk-test-$STAMP\"" diff --git a/scripts/tests/test-fault.sh b/scripts/tests/test-fault.sh new file mode 100755 index 0000000..0c39cde --- /dev/null +++ b/scripts/tests/test-fault.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# +# scripts/tests/test-fault.sh — fault-diagnostics verification. +# +# Proves the fault-dump path: every CPU exception prints a white-on-red +# band to the framebuffer and a parseable serial line before halting. +# Boots with a dedicated CRASHLOG FAT stick on ide.1 so the crash band +# has somewhere to write. +# +# Usage: ./scripts/tests/test-fault.sh +# Env: SYSTEM_IMG, OVMF, QEMU (see scripts/lib.sh) +# +set -euo pipefail +TNAME="fault" +# shellcheck source=../lib.sh +source "$(dirname "$0")/../lib.sh" + +echo "==> 1/5 build (kernel + bins)" +build_all + +echo "==> 2/5 stage system image" +stage_image --force + +echo "==> 3/5 boot QEMU (headless, + dedicated CRASHLOG stick)" +CRASH_IMG=/tmp/rsdos-crash.img +rm -f "$CRASH_IMG" +mkfs.fat -C -F 32 -n CRASHLOG "$CRASH_IMG" 65536 >/dev/null +test_boot \ + -drive "file=$CRASH_IMG,format=raw,if=none,id=crash" \ + -device ide-hd,drive=crash,bus=ide.1 + +echo "==> 4/5 boot diagnostics present" +grep -aq "gop: fb base=" "$LOG" || fail "fb geometry diagnostic missing" +grep -aq "gop: mode .* selected" "$LOG" || fail "mode-selection diagnostic missing" +grep -aq "CRASHLOG" "$LOG" || fail "CRASHLOG stick not mounted" + +echo "==> 5/5 inject a page fault (fault command)" +sendkeys "clear" "fault" +sleep 2 + +grep -aq "!!! page fault vec=14 .*rip=" "$LOG" || fail "page-fault dump line missing" +grep -aq "cr2=0x400000000000" "$LOG" || fail "CR2 not reported (faulting address)" +grep -aq "task=0 \"shell\"" "$LOG" || fail "faulting task not identified" +grep -aq "!!! halting" "$LOG" || fail "no halt message" + +echo "" +echo "PASS — fault dump verified" diff --git a/scripts/tests/test-focus.py b/scripts/tests/test-focus.py new file mode 100755 index 0000000..cfa317a --- /dev/null +++ b/scripts/tests/test-focus.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +"""Drive the focus-routing gate: 3 windows, type markers, verify routing. + +Connects to the QEMU monitor once and keeps the socket open (the per-call +sendkeys.py pattern loses the socket when the monitor resets). + +Usage: start the guest with `./scripts/run.sh --monitor`, then + python3 scripts/tests/test-focus.py [/tmp/rsdos-monitor.sock] +""" +import socket +import sys +import time + +sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) +sock.connect(sys.argv[1] if len(sys.argv) > 1 else "/tmp/rsdos-monitor.sock") + + +def sk(k, d=0.35): + sock.sendall(f"sendkey {k}\n".encode()) + time.sleep(d) + + +def line(txt, d=0.8): + for ch in txt: + if ch == " ": + sk("spc", 0.1) + elif ch.isupper(): + sk("shift-" + ch.lower(), 0.12) + else: + sk(ch, 0.12) + sk("ret", d) + + +time.sleep(0.5) + +# Home is focused at boot. +line("windows") # expect: home + nothing (or existing windows) +time.sleep(1.5) + +# F1 -> term1 (focused on creation) +sk("f1", 1.5) +line("echo TERM1MARK") +time.sleep(1.0) + +# F1 -> term2 (focused on creation) +sk("f1", 1.5) +line("echo TERM2MARK") +time.sleep(1.0) + +# F2 cycles z-order: term2 -> term1 +sk("f2", 0.8) +line("echo CYCLED") +time.sleep(1.0) + +# F2 again: term1 -> home (window 0, oldest) +sk("f2", 0.8) +line("windows") diff --git a/scripts/tests/test-gfx.sh b/scripts/tests/test-gfx.sh new file mode 100755 index 0000000..613318f --- /dev/null +++ b/scripts/tests/test-gfx.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# +# scripts/tests/test-gfx.sh — automated graphics round-trip test. +# +# Builds, boots headless, runs the in-kernel gfx self-test and the +# status-bar demo, then powers off. +# +# Usage: ./scripts/tests/test-gfx.sh +# Env: SYSTEM_IMG, OVMF, QEMU (see scripts/lib.sh) +# +set -euo pipefail +TNAME="gfx" +# shellcheck source=../lib.sh +source "$(dirname "$0")/../lib.sh" + +echo "==> 1/5 build (kernel + bins)" +build_all + +echo "==> 2/5 stage system image" +stage_image --force + +echo "==> 3/5 boot QEMU (headless)" +test_boot +wait_log "$LOG" "cursor: task " 10 || fail "cursor task did not spawn" + +echo "==> 4/5 in-kernel pixel self-test (gfx check)" +sendkeys "gfx check" +wait_log "$LOG" "gfx check: PASS" 30 || fail "gfx check did not run" + +echo "==> 5/5 gfx demo (draw scene)" +sendkeys "gfx" "poweroff" +wait_qemu_exit 30 || fail "QEMU did not exit after poweroff" + +grep -aq "gfx: drew status bar" "$LOG" || fail "gfx demo did not draw" + +echo "" +echo "PASS — gfx round trip" diff --git a/scripts/tests/test-mouse.sh b/scripts/tests/test-mouse.sh new file mode 100755 index 0000000..bb835be --- /dev/null +++ b/scripts/tests/test-mouse.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# +# scripts/tests/test-mouse.sh — automated PS/2 mouse round-trip test. +# +# Boots QEMU headless, injects mouse movement via the QEMU monitor, +# verifies the on-screen cursor tracks it, and tests the mouse stream +# routing command. +# +# Usage: ./scripts/tests/test-mouse.sh +# Env: SYSTEM_IMG, OVMF, QEMU (see scripts/lib.sh) +# +set -euo pipefail +TNAME="mouse" +# shellcheck source=../lib.sh +source "$(dirname "$0")/../lib.sh" + +# cursor_state — one-shot `cursor` command, parsed to "x y buttons". +cursor_state() { + sendkeys "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 + bins)" +build_all + +echo "==> 2/6 stage system image" +stage_image --force + +echo "==> 3/6 boot QEMU (headless)" +test_boot +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 " moved: x=$X1 y=$Y1 (delta +20,+7)" + +echo "==> 5/6 left button edge detection" +qemu_hmp "mouse_button 1" +S2=$(cursor_state) +IB2=$(echo "$S2" | awk '{print $3}') +[ "$IB2" = "L--" ] || fail "expected L--, got $IB2" + +qemu_hmp "mouse_button 0" +S3=$(cursor_state) +IB3=$(echo "$S3" | awk '{print $3}') +[ "$IB3" = "---" ] || fail "expected --- after release, got $IB3" +echo " button: $IB2 → $IB3" + +echo "==> 6/6 mouse stream routing to msgrecv's task" +# The spawned task id shifts with the task table (wm-input took slot 3) — +# parse it rather than hardcoding. +sendkeys "msgrecv" +sleep 1 +MID="$(sed -n 's/.*msgrecv: task \([0-9][0-9]*\) listening.*/\1/p' "$LOG" | tail -1)" +[ -n "$MID" ] || fail "msgrecv did not spawn" +echo " msgrecv task id: $MID" +sendkeys "mouse $MID" +qemu_hmp "mouse_move 20 7" +qemu_hmp "mouse_button 1" +sleep 0.5 +qemu_hmp "mouse_button 0" +sleep 0.5 +sendkeys "ctrl-c" +sleep 0.5 +grep -aq "Mouse(+20,+7" "$LOG" || fail "routed mouse move not logged" + +echo "" +echo "PASS — mouse round trip" diff --git a/scripts/tests/test-msg.sh b/scripts/tests/test-msg.sh new file mode 100755 index 0000000..7786beb --- /dev/null +++ b/scripts/tests/test-msg.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +# +# scripts/tests/test-msg.sh — automated scheduler message-queue round trip. +# +# Tests all message types (user, timer, key, mouse), real-input routing, +# and the queue-full boundary. +# +# Usage: ./scripts/tests/test-msg.sh +# Env: SYSTEM_IMG, OVMF, QEMU (see scripts/lib.sh) +# +set -euo pipefail +TNAME="msg" +# shellcheck source=../lib.sh +source "$(dirname "$0")/../lib.sh" + +echo "==> 1/5 build (kernel + bins)" +build_all + +echo "==> 2/5 stage system image" +stage_image --force + +echo "==> 3/5 boot QEMU (headless)" +test_boot + +echo "==> 4/5 drive the shell (msgrecv / send / mouse routing / queue-full)" +# msgrecv's task id is assigned at spawn time and shifts when the task +# table grows (the Windowed GUI added wm-input at slot 3) — read it from +# the log instead of hardcoding. +sendkeys "msgrecv" +sleep 1 +MID="$(sed -n 's/.*msgrecv: task \([0-9][0-9]*\) listening.*/\1/p' "$LOG" | tail -1)" +[ -n "$MID" ] || fail "msgrecv did not spawn (no 'msgrecv: task N listening' in the log)" +echo " msgrecv task id: $MID" + +sendkeys \ + "send $MID user 42" \ + "send $MID timer" \ + "send $MID key enter" \ + "send $MID key ctrl-c" \ + "send $MID key f3" \ + "send $MID mouse 5 -3 3" \ + "sleep 800" \ + "jobs" + +sendkeys "mouse $MID" +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" + +# Queue-full probe: overflow a queue that nobody drains. The shell used +# to be that task, but its exec pump now consumes its own User messages +# as completion codes (q=0 at the prompt), and msgrecv consumes on wake — +# the cursor task never calls recv, so its 16-slot ring fills for real. +CID="$(awk '$2 == "cursor" { print $1 }' "$LOG" | tail -1)" +[ -n "$CID" ] || fail "no cursor task in jobs output (cannot probe queue-full)" +echo " queue-full target: task $CID (cursor, never recv'd)" + +sendkeys \ + "send $CID user 1" \ + "send $CID user 2" \ + "send $CID user 3" \ + "send $CID user 4" \ + "send $CID user 5" \ + "send $CID user 6" \ + "send $CID user 7" \ + "send $CID user 8" \ + "send $CID user 9" \ + "send $CID user 10" \ + "send $CID user 11" \ + "send $CID user 12" \ + "send $CID user 13" \ + "send $CID user 14" \ + "send $CID user 15" \ + "send $CID user 16" \ + "send $CID user 17" \ + "sleep 800" \ + "jobs" \ + "poweroff" + +echo "==> 5/5 wait for shutdown + verify" +wait_qemu_exit 30 || fail "QEMU did not exit after poweroff" + +grep -aq "msgrecv: task $MID listening" "$LOG" || fail "msgrecv did not spawn" +grep -aq "msgrecv\[$MID\]: User(42)" "$LOG" || fail "user message missing" +grep -aq "msgrecv\[$MID\]: Timer" "$LOG" || fail "timer message missing" +grep -aq "msgrecv\[$MID\]: Key(Enter)" "$LOG" || fail "key enter missing" +grep -aq "msgrecv\[$MID\]: Key(Ctrl-c)" "$LOG" || fail "key ctrl-c missing" +grep -aq "msgrecv\[$MID\]: Key(F3)" "$LOG" || fail "key f3 missing" +grep -aq "msgrecv\[$MID\]: Mouse(+5,-3 L R)" "$LOG" || fail "synthesized mouse message missing" +grep -aq "msgrecv\[$MID\]: Mouse(+20,+7" "$LOG" || fail "real mouse move 20,7 not routed to queue" +grep -aq "msgrecv\[$MID\]: Mouse(-5,-3 L" "$LOG" || fail "held-button routed move missing" +grep -aq "queue full" "$LOG" || fail "17th send did not report queue full" +grep -aq "send: unknown type" "$LOG" && fail "a bad send slipped through (expected none)" + +echo "" +echo "PASS — message queues OK" -- cgit v1.2.3