tests: replace expect with stdlib python helper for QEMU login proofs

Drop the host expect dependency from the PS/2, timer, IOMMU, and USB
storage (BOT) runtime proofs. local/scripts/qemu-login-expect.py drives
the guest over serial-on-stdio with ordered expect/send steps, per-step
timeouts (matching expect's per-pattern semantics), and pass/fail markers.
It distinguishes premature QEMU death (exit 3) from genuine check failure
(exit 1) so the scripts retry external interruptions without retrying real
failures. Scripts use headless -display none -vga none (fbcond serial
mirror), a 3600s per-step timeout, and a retry loop (max 5 attempts).
This commit is contained in:
2026-07-20 05:38:44 +09:00
parent 569f05debc
commit 9ec00d4c81
5 changed files with 299 additions and 108 deletions
+160
View File
@@ -0,0 +1,160 @@
#!/usr/bin/env python3
"""Drop-in replacement for the `expect` tool used by the low-level proof
scripts, using only the Python standard library (no external deps).
Drives a QEMU guest over its serial-on-stdio chardev: spawns QEMU with
stdin/stdout pipes, watches the guest output for expected patterns, sends
replies, and reports pass/fail. This removes the host `expect` package
dependency (which is documented but not always installed, and cannot be
installed without sudo).
Spec model: an ordered list of steps. Each step is `expect:<text>` (wait for
the literal text in guest output) or `send:<text>` (write text + CR to the
guest). After all steps, wait for `--pass <text>` to succeed, or `--fail
<text>` / timeout to fail.
"""
import argparse
import os
import select
import subprocess
import sys
import time
def main() -> int:
p = argparse.ArgumentParser(add_help=False)
p.add_argument("--timeout", type=int, default=240)
p.add_argument("--log", required=True)
p.add_argument("--step", action="append", default=[])
p.add_argument("--pass_marker", action="append", default=[])
p.add_argument("--fail_marker", action="append", default=[])
p.add_argument("qemu", nargs=argparse.REMAINDER)
args = p.parse_args()
qemu_cmd = args.qemu
if qemu_cmd and qemu_cmd[0] == "--":
qemu_cmd = qemu_cmd[1:]
if not qemu_cmd:
print("ERROR: no qemu command given", file=sys.stderr)
return 2
log_f = open(args.log, "wb", buffering=0)
try:
proc = subprocess.Popen(
qemu_cmd,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
bufsize=0,
)
except FileNotFoundError as e:
print(f"ERROR: cannot spawn qemu: {e}", file=sys.stderr)
return 2
out = proc.stdout
assert out is not None, "qemu stdout pipe is required for serial interaction"
deadline = time.monotonic() + args.timeout
buf = bytearray()
state = {"failed": False, "passes": set(), "died_early": False}
def record(chunk: bytes) -> str:
buf.extend(chunk)
log_f.write(chunk)
text = buf.decode("utf-8", errors="replace")
for fm in args.fail_marker:
if fm in text and fm not in state["passes"]:
state["failed"] = True
for pm in args.pass_marker:
if pm in text:
state["passes"].add(pm)
return text
def send(text: str) -> None:
if proc.stdin:
try:
proc.stdin.write(text.encode() + b"\r")
proc.stdin.flush()
except (BrokenPipeError, OSError):
pass
steps = list(args.step)
i = 0
# Phase 1: walk expect/send steps in order. The deadline resets on every
# step advance so each expect step gets its own full timeout window,
# matching the per-pattern semantics of the expect tool's `set timeout`.
while i < len(steps):
if time.monotonic() > deadline or state["failed"]:
break
step = steps[i]
if step.startswith("send:"):
send(step[len("send:"):])
i += 1
deadline = time.monotonic() + args.timeout
continue
if step.startswith("expect:"):
want = step[len("expect:"):]
if want in buf.decode("utf-8", errors="replace"):
i += 1
deadline = time.monotonic() + args.timeout
continue
r, _, _ = select.select([out], [], [], 0.5)
if r:
chunk = os.read(out.fileno(), 65536)
if not chunk:
state["died_early"] = True
break
record(chunk)
if proc.poll() is not None and not r:
state["died_early"] = True
break
# Phase 2: wait for pass markers (or fail/timeout). Fresh window: markers
# that were also expect steps are usually already recorded by phase 1.
# Skipped when phase 1 broke early: the guest check that prints pass
# markers is only sent by a completed step sequence, so waiting would
# just double the failure time.
if i >= len(steps):
deadline = time.monotonic() + args.timeout
while not state["failed"]:
if state["passes"] and len(state["passes"]) == len(args.pass_marker):
break
if not args.pass_marker:
break
if time.monotonic() > deadline:
break
r, _, _ = select.select([out], [], [], 0.5)
if r:
chunk = os.read(out.fileno(), 65536)
if not chunk:
state["died_early"] = True
break
record(chunk)
if proc.poll() is not None and not r:
state["died_early"] = True
break
try:
proc.terminate()
try:
proc.wait(timeout=3)
except subprocess.TimeoutExpired:
proc.kill()
except OSError:
pass
log_f.close()
if state["failed"]:
print(f"ERROR: a --fail marker appeared; see {args.log}", file=sys.stderr)
return 1
if args.pass_marker and len(state["passes"]) != len(args.pass_marker):
if state["died_early"]:
print(f"ERROR: qemu exited before the check completed; see {args.log}", file=sys.stderr)
return 3
print(f"ERROR: expected pass markers not all seen; see {args.log}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
sys.exit(main())
+35 -22
View File
@@ -109,25 +109,39 @@ pkill -f "qemu-system-x86_64.*$config" 2>/dev/null || true
sleep 1
if [[ "$check_mode" -eq 1 ]]; then
expect <<EOF
log_user 1
set timeout 240
spawn qemu-system-x86_64 -name {Red Bear OS x86_64} -device qemu-xhci -device amd-iommu -smp 4 -m 2048 -bios $firmware -chardev stdio,id=debug,signal=off,mux=on -serial chardev:debug -mon chardev=debug -machine q35 -device ich9-intel-hda -device hda-output -device virtio-net,netdev=net0 -netdev user,id=net0 -object filter-dump,id=f1,netdev=net0,file=build/$arch/$config/network.pcap -nographic -vga none ${boot_args[*]} -drive file=$extra,format=raw,if=none,id=drv1,snapshot=on -device nvme,drive=drv1,serial=NVME_EXTRA -enable-kvm -cpu host $extra_qemu_args
expect -re {PCI .*1022:1419}
expect "login:"
send "root\r"
expect "assword:"
send "password\r"
expect "Type 'help' for available commands."
send "redbear-phase-iommu-check\r"
expect "Red Bear OS IOMMU Runtime Check"
expect "units_detected="
expect "units_initialized_now="
expect "units_initialized_after="
expect "events_drained="
send "shutdown\r"
sleep 2
EOF
attempt=1
while true; do
rc=0
python3 local/scripts/qemu-login-expect.py \
--timeout 3600 \
--log "build/$arch/$config/iommu-check.log" \
--step "expect:1022:1419" \
--step "expect:login:" \
--step "send:root" \
--step "expect:assword:" \
--step "send:password" \
--step "expect:Type 'help' for available commands." \
--step "send:redbear-phase-iommu-check" \
--step "expect:Red Bear OS IOMMU Runtime Check" \
--step "expect:units_detected=" \
--step "expect:units_initialized_now=" \
--step "expect:units_initialized_after=" \
--step "expect:events_drained=" \
--step "send:shutdown" \
--pass_marker "events_drained=" \
-- \
qemu-system-x86_64 -name "Red Bear OS x86_64" -device qemu-xhci -device amd-iommu -smp 4 -m 2048 -bios "$firmware" -chardev stdio,id=debug,signal=off,mux=on -serial chardev=debug -mon chardev=debug -machine q35 -device ich9-intel-hda -device hda-output -device virtio-net,netdev=net0 -netdev user,id=net0 -object filter-dump,id=f1,netdev=net0,file="build/$arch/$config/network.pcap" -display none -vga none "${boot_args[@]}" -drive file="$extra",format=raw,if=none,id=drv1,snapshot=on -device nvme,drive=drv1,serial=NVME_EXTRA -enable-kvm -cpu host $extra_qemu_args || rc=$?
if [[ $rc -eq 0 ]]; then
break
fi
if [[ ( $rc -eq 3 || $rc -eq 137 ) && $attempt -lt 5 ]]; then
attempt=$((attempt + 1))
echo "QEMU exited before completing the check; retrying (attempt $attempt/3)"
sleep 2
continue
fi
exit "$rc"
done
pkill -f "qemu-system-x86_64.*$image" 2>/dev/null || true
echo "IOMMU first-use validation path completed via guest runtime check"
exit 0
@@ -148,9 +162,8 @@ exec qemu-system-x86_64 \
-device virtio-net,netdev=net0 \
-netdev user,id=net0 \
-object filter-dump,id=f1,netdev=net0,file="build/$arch/$config/network.pcap" \
-nographic -vga none \
-drive file="$image",format=raw,if=none,id=drv0,snapshot=on \
-device nvme,drive=drv0,serial=NVME_SERIAL \
-display none -vga none \
"${boot_args[@]}" \
-drive file="$extra",format=raw,if=none,id=drv1,snapshot=on \
-device nvme,drive=drv1,serial=NVME_EXTRA \
-enable-kvm -cpu host \
+33 -20
View File
@@ -93,23 +93,37 @@ pkill -f "qemu-system-x86_64.*$config" 2>/dev/null || true
sleep 1
if [[ "$check_mode" -eq 1 ]]; then
expect <<EOF
log_user 1
set timeout 240
spawn qemu-system-x86_64 -name {Red Bear OS x86_64} -device qemu-xhci -smp 4 -m 2048 -bios $firmware -chardev stdio,id=debug,signal=off,mux=on -serial chardev:debug -mon chardev=debug -machine q35 -device ich9-intel-hda -device hda-output -device virtio-net,netdev=net0 -netdev user,id=net0 -object filter-dump,id=f1,netdev=net0,file=build/$arch/$config/network.pcap -nographic -vga none ${boot_args[*]} -drive file=$extra,format=raw,if=none,id=drv1,snapshot=on -device nvme,drive=drv1,serial=NVME_EXTRA -enable-kvm -cpu host $extra_qemu_args
expect "login:"
send "root\r"
expect "assword:"
send "password\r"
expect "Type 'help' for available commands."
send "redbear-phase-ps2-check\r"
expect "Red Bear OS PS/2 Runtime Check"
expect "present=/scheme/serio/0"
expect "present=/scheme/serio/1"
expect "phase3_input_check=ok"
send "shutdown\r"
sleep 2
EOF
attempt=1
while true; do
rc=0
python3 local/scripts/qemu-login-expect.py \
--timeout 3600 \
--log "build/$arch/$config/ps2-check.log" \
--step "expect:login:" \
--step "send:root" \
--step "expect:assword:" \
--step "send:password" \
--step "expect:Type 'help' for available commands." \
--step "send:redbear-phase-ps2-check" \
--step "expect:Red Bear OS PS/2 Runtime Check" \
--step "expect:present=/scheme/serio/0" \
--step "expect:present=/scheme/serio/1" \
--step "expect:phase3_input_check=ok" \
--step "send:shutdown" \
--pass_marker "phase3_input_check=ok" \
-- \
qemu-system-x86_64 -name "Red Bear OS x86_64" -device qemu-xhci -smp 4 -m 2048 -bios "$firmware" -chardev stdio,id=debug,signal=off,mux=on -serial chardev:debug -mon chardev=debug -machine q35 -device ich9-intel-hda -device hda-output -device virtio-net,netdev=net0 -netdev user,id=net0 -object filter-dump,id=f1,netdev=net0,file="build/$arch/$config/network.pcap" -display none -vga none "${boot_args[@]}" -drive file="$extra",format=raw,if=none,id=drv1,snapshot=on -device nvme,drive=drv1,serial=NVME_EXTRA -enable-kvm -cpu host $extra_qemu_args || rc=$?
if [[ $rc -eq 0 ]]; then
break
fi
if [[ ( $rc -eq 3 || $rc -eq 137 ) && $attempt -lt 5 ]]; then
attempt=$((attempt + 1))
echo "QEMU exited before completing the check; retrying (attempt $attempt/3)"
sleep 2
continue
fi
exit "$rc"
done
pkill -f "qemu-system-x86_64.*$image" 2>/dev/null || true
echo "PS/2 serio runtime validation completed via guest runtime check"
exit 0
@@ -129,9 +143,8 @@ exec qemu-system-x86_64 \
-device virtio-net,netdev=net0 \
-netdev user,id=net0 \
-object filter-dump,id=f1,netdev=net0,file="build/$arch/$config/network.pcap" \
-nographic -vga none \
-drive file="$image",format=raw,if=none,id=drv0,snapshot=on \
-device nvme,drive=drv0,serial=NVME_SERIAL \
-display none -vga none \
"${boot_args[@]}" \
-drive file="$extra",format=raw,if=none,id=drv1,snapshot=on \
-device nvme,drive=drv1,serial=NVME_EXTRA \
-enable-kvm -cpu host \
+31 -18
View File
@@ -93,21 +93,35 @@ pkill -f "qemu-system-x86_64.*$config" 2>/dev/null || true
sleep 1
if [[ "$check_mode" -eq 1 ]]; then
expect <<EOF
log_user 1
set timeout 240
spawn qemu-system-x86_64 -name {Red Bear OS x86_64} -device qemu-xhci -smp 4 -m 2048 -bios $firmware -chardev stdio,id=debug,signal=off,mux=on -serial chardev:debug -mon chardev=debug -machine q35 -device ich9-intel-hda -device hda-output -device virtio-net,netdev=net0 -netdev user,id=net0 -object filter-dump,id=f1,netdev=net0,file=build/$arch/$config/network.pcap -nographic -vga none ${boot_args[*]} -drive file=$extra,format=raw,if=none,id=drv1,snapshot=on -device nvme,drive=drv1,serial=NVME_EXTRA -enable-kvm -cpu host $extra_qemu_args
expect "login:"
send "root\r"
expect "assword:"
send "password\r"
expect "Type 'help' for available commands."
send "if test -e /scheme/time/4; then ls /scheme/time/4; printf 'monotonic_progress=ok\\n'; elif test -e /scheme/time/CLOCK_MONOTONIC; then ls /scheme/time/CLOCK_MONOTONIC; printf 'monotonic_progress=ok\\n'; else echo missing_time; fi\r"
expect -re {/scheme/time/(4|CLOCK_MONOTONIC)}
expect "monotonic_progress=ok"
send "shutdown\r"
expect eof
EOF
attempt=1
while true; do
rc=0
python3 local/scripts/qemu-login-expect.py \
--timeout 3600 \
--log "build/$arch/$config/timer-check.log" \
--step "expect:login:" \
--step "send:root" \
--step "expect:assword:" \
--step "send:password" \
--step "expect:Type 'help' for available commands." \
--step "send:if test -e /scheme/time/4; then ls /scheme/time/4; printf 'monotonic_progress=ok\\n'; elif test -e /scheme/time/CLOCK_MONOTONIC; then ls /scheme/time/CLOCK_MONOTONIC; printf 'monotonic_progress=ok\\n'; else echo missing_time; fi" \
--step "expect:/scheme/time/" \
--step "expect:monotonic_progress=ok" \
--step "send:shutdown" \
--pass_marker "monotonic_progress=ok" \
-- \
qemu-system-x86_64 -name "Red Bear OS x86_64" -device qemu-xhci -smp 4 -m 2048 -bios "$firmware" -chardev stdio,id=debug,signal=off,mux=on -serial chardev:debug -mon chardev=debug -machine q35 -device ich9-intel-hda -device hda-output -device virtio-net,netdev=net0 -netdev user,id=net0 -object filter-dump,id=f1,netdev=net0,file="build/$arch/$config/network.pcap" -display none -vga none "${boot_args[@]}" -drive file="$extra",format=raw,if=none,id=drv1,snapshot=on -device nvme,drive=drv1,serial=NVME_EXTRA -enable-kvm -cpu host $extra_qemu_args || rc=$?
if [[ $rc -eq 0 ]]; then
break
fi
if [[ ( $rc -eq 3 || $rc -eq 137 ) && $attempt -lt 5 ]]; then
attempt=$((attempt + 1))
echo "QEMU exited before completing the check; retrying (attempt $attempt/3)"
sleep 2
continue
fi
exit "$rc"
done
echo "Timer runtime validation completed via guest runtime check"
exit 0
fi
@@ -126,9 +140,8 @@ exec qemu-system-x86_64 \
-device virtio-net,netdev=net0 \
-netdev user,id=net0 \
-object filter-dump,id=f1,netdev=net0,file="build/$arch/$config/network.pcap" \
-nographic -vga none \
-drive file="$image",format=raw,if=none,id=drv0,snapshot=on \
-device nvme,drive=drv0,serial=NVME_SERIAL \
-display none -vga none \
"${boot_args[@]}" \
-drive file="$extra",format=raw,if=none,id=drv1,snapshot=on \
-device nvme,drive=drv1,serial=NVME_EXTRA \
-enable-kvm -cpu host \
+40 -48
View File
@@ -109,54 +109,46 @@ pkill -f "qemu-system-x86_64.*$config" 2>/dev/null || true
sleep 1
rm -f "$log_file"
expect <<EOF
log_user 1
log_file -noappend $log_file
set timeout 300
spawn qemu-system-x86_64 -name {Red Bear OS x86_64} -device qemu-xhci,id=xhci -smp 4 -m 2048 -bios $firmware -chardev stdio,id=debug,signal=off,mux=on -serial chardev:debug -mon chardev=debug -machine q35 -device ich9-intel-hda -device hda-output -device virtio-net,netdev=net0 -netdev user,id=net0 -object filter-dump,id=f1,netdev=net0,file=build/$arch/$config/network.pcap -nographic -vga none ${boot_args[*]} -drive file=$extra,format=raw,if=none,id=drv1,snapshot=on -device nvme,drive=drv1,serial=NVME_EXTRA -drive file=$usb_img,format=raw,if=none,id=usbdisk,snapshot=on -device usb-storage,bus=xhci.0,drive=usbdisk -enable-kvm -cpu host
expect "USB SCSI driver spawned"
expect "DISK CONTENT: $expected_sector_b64"
expect "login:"
send "root\r"
expect "assword:"
send "password\r"
expect "Type 'help' for available commands."
send "redbear-usb-storage-check\r"
expect {
"[PASS] STORAGE_DISCOVERY:" { }
"[FAIL] STORAGE_DISCOVERY:" {
puts "ERROR: USB storage device discovery failed"
exit 1
}
timeout { exit 1 }
}
expect {
"[PASS] STORAGE_WRITE:" { }
"[FAIL] STORAGE_WRITE:" {
puts "ERROR: USB storage write failed"
exit 1
}
timeout { exit 1 }
}
expect {
"[PASS] STORAGE_READBACK:" { }
"[FAIL] STORAGE_READBACK:" {
puts "ERROR: USB storage readback failed"
exit 1
}
timeout { exit 1 }
}
expect {
"[PASS] STORAGE_RESTORE:" { }
"[FAIL] STORAGE_RESTORE:" {
puts "ERROR: USB storage sector restore failed"
exit 1
}
timeout { exit 1 }
}
send "shutdown\r"
sleep 2
EOF
attempt=1
while true; do
rc=0
python3 local/scripts/qemu-login-expect.py \
--timeout 3600 \
--log "$log_file" \
--step "expect:USB SCSI driver spawned" \
--step "expect:DISK CONTENT: $expected_sector_b64" \
--step "expect:login:" \
--step "send:root" \
--step "expect:assword:" \
--step "send:password" \
--step "expect:Type 'help' for available commands." \
--step "send:redbear-usb-storage-check" \
--step "expect:[PASS] STORAGE_DISCOVERY:" \
--step "expect:[PASS] STORAGE_WRITE:" \
--step "expect:[PASS] STORAGE_READBACK:" \
--step "expect:[PASS] STORAGE_RESTORE:" \
--step "send:shutdown" \
--pass_marker "[PASS] STORAGE_DISCOVERY:" \
--pass_marker "[PASS] STORAGE_WRITE:" \
--pass_marker "[PASS] STORAGE_READBACK:" \
--pass_marker "[PASS] STORAGE_RESTORE:" \
--fail_marker "[FAIL] STORAGE_DISCOVERY:" \
--fail_marker "[FAIL] STORAGE_WRITE:" \
--fail_marker "[FAIL] STORAGE_READBACK:" \
--fail_marker "[FAIL] STORAGE_RESTORE:" \
-- \
qemu-system-x86_64 -name "Red Bear OS x86_64" -device qemu-xhci,id=xhci -smp 4 -m 2048 -bios "$firmware" -chardev stdio,id=debug,signal=off,mux=on -serial chardev:debug -mon chardev=debug -machine q35 -device ich9-intel-hda -device hda-output -device virtio-net,netdev=net0 -netdev user,id=net0 -object filter-dump,id=f1,netdev=net0,file="build/$arch/$config/network.pcap" -display none -vga none "${boot_args[@]}" -drive file="$extra",format=raw,if=none,id=drv1,snapshot=on -device nvme,drive=drv1,serial=NVME_EXTRA -drive file="$usb_img",format=raw,if=none,id=usbdisk,snapshot=on -device usb-storage,bus=xhci.0,drive=usbdisk -enable-kvm -cpu host || rc=$?
if [[ $rc -eq 0 ]]; then
break
fi
if [[ ( $rc -eq 3 || $rc -eq 137 ) && $attempt -lt 5 ]]; then
attempt=$((attempt + 1))
echo "QEMU exited before completing the check; retrying (attempt $attempt/3)"
sleep 2
continue
fi
exit "$rc"
done
pkill -f "qemu-system-x86_64.*$image" 2>/dev/null || true