qemu: json/stat/region channels, seeding, driver tests
- rootfs/sbin/init: the control bridge answers `@cat PATH` locally so the driver can read /tmp/warden-web-status.json out of the guest. That file has no trailing newline; the bridge adds one so the sentinel stays on its own line and the line-based reader never blocks. - tests/qmp.py: wait_json/assert_json (dotted paths, eq/ne/contains/ len_eq/len_ge/gt/lt), assert_stat off the FIFO's stats reply, capture_region, assert_region NAME [TOLERANCE] and assert_ocr. A tolerance other than the captured one, a reference box that does not fit the screendump, a missing reference or a missing tesseract is FATAL for that step and the run continues (flare-edge #147). - tests/imgtools.py: P6 reader, crop, perceptual and structural hashes, compare, with a self-test. - tests/test_qmp_drive.py: drive() with QMP and the control channel faked, pinning the per-step ok/fail/fatal contract. - mkimage.sh SEED_DIR and ui-drive.sh --seed/--refs: settings fixtures staged into userdata before warden-ui starts, and a reference store handed to the driver. - ci: the driver tests and the imgtools self-test run in qemu-tools. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013aHKWzT5EF86RFKRMtAv9n
This commit is contained in:
+55
-9
@@ -2,8 +2,8 @@
|
||||
# Drive the LVGL UI through a scripted interaction and collect screenshots.
|
||||
#
|
||||
# ui-shot.sh proves touch reaches the UI in one tap; this is the same rig for
|
||||
# work that needs a SEQUENCE — swipe through the app rows, open a submenu, tap a
|
||||
# tab, bring up the keyboard — with a screendump wherever the script asks for
|
||||
# work that needs a SEQUENCE (swipe through the app rows, open a submenu, tap a
|
||||
# tab, bring up the keyboard) with a screendump wherever the script asks for
|
||||
# one. One boot serves the whole script, because booting per step (TCG, no KVM)
|
||||
# costs about a minute and a real interaction is thirty steps.
|
||||
#
|
||||
@@ -14,23 +14,53 @@
|
||||
#
|
||||
# FAILS CLOSED on missing prerequisites.
|
||||
#
|
||||
# Usage: ui-drive.sh <zImage-virt> <script> [out-dir]
|
||||
# Usage: ui-drive.sh [--seed FILE] [--refs FILE] <zImage-virt> <script> [out-dir]
|
||||
# <script> is a qmp.py `drive` script: see its docstring for the commands.
|
||||
# --seed FILE a seed manifest (flat YAML key: value, see flare-edge
|
||||
# tools/seed-fixtures.py) run through that tool and staged
|
||||
# into userdata/warden BEFORE boot, via mkimage.sh's SEED_DIR
|
||||
# hook -- so a screen's first read at startup already sees
|
||||
# it, not a value written after the race is already lost.
|
||||
# Needs FLARE_EDGE=<checkout> to find the tool (same
|
||||
# convention as ota-apply.sh / portal-scenario.sh).
|
||||
# --refs FILE passed straight through to qmp.py drive's --refs (the
|
||||
# region/ocr reference JSON); see qmp.py's own docstring.
|
||||
set -euo pipefail
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # qemu/tests/
|
||||
QDIR="$(cd "$HERE/.." && pwd)" # qemu/
|
||||
ZIMAGE="${1:-}"
|
||||
SCRIPT="${2:-}"
|
||||
OUTDIR="${3:-$QDIR/out/ui-drive}"
|
||||
|
||||
SEED_FILE=""
|
||||
REFS_FILE=""
|
||||
ARGS=()
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--seed) SEED_FILE="${2:?--seed needs a manifest path}"; shift 2 ;;
|
||||
--refs) REFS_FILE="${2:?--refs needs a path}"; shift 2 ;;
|
||||
--) shift; ARGS+=("$@"); break ;;
|
||||
-*) echo "FATAL: unknown option '$1' (usage: $0 [--seed FILE] [--refs FILE] <zImage> <script> [out-dir])" >&2; exit 1 ;;
|
||||
*) ARGS+=("$1"); shift ;;
|
||||
esac
|
||||
done
|
||||
ZIMAGE="${ARGS[0]:-}"
|
||||
SCRIPT="${ARGS[1]:-}"
|
||||
OUTDIR="${ARGS[2]:-$QDIR/out/ui-drive}"
|
||||
|
||||
if [ -z "$ZIMAGE" ] || [ ! -f "$ZIMAGE" ]; then
|
||||
echo "FATAL: usage: $0 <zImage> <script> [out-dir]: the virt.fragment kernel variant" >&2
|
||||
echo "FATAL: usage: $0 [--seed FILE] [--refs FILE] <zImage> <script> [out-dir]: the virt.fragment kernel variant" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$SCRIPT" ] || [ ! -f "$SCRIPT" ]; then
|
||||
echo "FATAL: no drive script at '$SCRIPT'" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -n "$SEED_FILE" ] && [ ! -f "$SEED_FILE" ]; then
|
||||
echo "FATAL: no seed manifest at '$SEED_FILE'" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -n "$SEED_FILE" ] && { [ -z "${FLARE_EDGE:-}" ] || [ ! -f "$FLARE_EDGE/tools/seed-fixtures.py" ]; }; then
|
||||
echo "FATAL: --seed needs FLARE_EDGE to point at a flare-edge checkout (seed-fixtures.py not found under '${FLARE_EDGE:-}')" >&2
|
||||
exit 1
|
||||
fi
|
||||
[ -x "$QDIR/payload/warden-ui" ] || {
|
||||
echo "FATAL: no qemu/payload/warden-ui: build it with flare-edge tools/build-ui-vm.sh" >&2
|
||||
exit 1
|
||||
@@ -56,7 +86,17 @@ cleanup() {
|
||||
trap cleanup EXIT
|
||||
|
||||
bash "$QDIR/mkinitramfs.sh"
|
||||
bash "$QDIR/mkimage.sh"
|
||||
if [ -n "$SEED_FILE" ]; then
|
||||
# Stage the settings files BEFORE mkimage.sh builds the disk: userdata is
|
||||
# baked into the image up front (see qemu/README.md's boundary table --
|
||||
# there is no mount-after-boot step this rig could inject files through),
|
||||
# so anything a screen reads at startup has to be in place before -kernel
|
||||
# even runs, not written into a running VM.
|
||||
python3 "$FLARE_EDGE/tools/seed-fixtures.py" "$SEED_FILE" "$WORK/seed"
|
||||
SEED_DIR="$WORK/seed" bash "$QDIR/mkimage.sh"
|
||||
else
|
||||
bash "$QDIR/mkimage.sh"
|
||||
fi
|
||||
|
||||
# Random hostfwd ports can collide. Detect qemu's early bind failure and retry
|
||||
# with a fresh base rather than failing spuriously.
|
||||
@@ -141,8 +181,14 @@ echo "== driving $SCRIPT"
|
||||
# expectation. The driver's exit status is the verdict; capture it rather than
|
||||
# let `set -e` skip the backstop and the summary below.
|
||||
drive_rc=0
|
||||
DRIVE_ARGS=(--ctl "$WORK/ctl.sock" --console "$WORK/console.log")
|
||||
# Only passed when given: qmp.py's own default (<outdir>/refs.json, see its
|
||||
# docstring) is right for the common case of one refs file living next to a
|
||||
# flow's other fixtures, and forcing a path here would just duplicate that
|
||||
# default in two places.
|
||||
[ -n "$REFS_FILE" ] && DRIVE_ARGS+=(--refs "$REFS_FILE")
|
||||
python3 "$HERE/qmp.py" "$WORK/qmp.sock" drive "$SCRIPT" "$OUTDIR" \
|
||||
--ctl "$WORK/ctl.sock" --console "$WORK/console.log" || drive_rc=$?
|
||||
"${DRIVE_ARGS[@]}" || drive_rc=$?
|
||||
|
||||
# The UI must still be alive: see the header. The driver checks this after
|
||||
# every step and pins a crash to the step that caused it; this is the
|
||||
|
||||
Reference in New Issue
Block a user