ui-shot.sh proves touch reaches the UI in one tap. Verifying a UI change needs a SEQUENCE -- swipe through the app rows, open a submenu, tap a tab, bring up the keyboard -- and booting per step costs about a minute under TCG, so: - qmp.py gains a `drive` mode: one connection, one boot, a script of tap/swipe/fling/shot/sleep steps in PANEL PIXELS rather than the raw 0..32767 tablet axis. Swipes interpolate their motion, because LVGL decides a gesture from the movement between indev polls and a press-then-release with nothing in between is a click, not a scroll. - ui-drive.sh runs such a script against a booted VM and collects the screenshots. It also FAILS on a UI that died mid-script. warden-ui crashing leaves its last frame in the framebuffer, so screendumps keep returning a plausible picture of a program that no longer exists; stage-2 init now announces the exit and its status on the console, and ui-drive.sh greps for that after the run. This is what caught the SIGSEGV behind flare-edge#125. Stage-2 init also mounts devpts. The UI's Terminal page opens a PTY, so without it that page could only ever report "no PTY available" -- it rendered, which made a screenshot scenario look fine while the one thing the page does was untestable. tests/scripts/nav-stress.txt is the first committed drive script: the navigation sequence that reproduces flare-edge#125. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T2D2KtdgwbhbF6Mo64eUrn
133 lines
4.7 KiB
Bash
Executable File
133 lines
4.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# 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
|
|
# 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.
|
|
#
|
|
# It also FAILS on a UI that died mid-script. The framebuffer keeps its last
|
|
# frame when warden-ui crashes, so screendumps carry on returning a plausible
|
|
# picture of a program that no longer exists; stage-2 init announces the exit on
|
|
# the console (see rootfs/sbin/init) and this greps for it after the run.
|
|
#
|
|
# FAILS CLOSED on missing prerequisites.
|
|
#
|
|
# Usage: ui-drive.sh <zImage-virt> <script> [out-dir]
|
|
# <script> is a qmp.py `drive` script: see its docstring for the commands.
|
|
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}"
|
|
|
|
if [ -z "$ZIMAGE" ] || [ ! -f "$ZIMAGE" ]; then
|
|
echo "FATAL: usage: $0 <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
|
|
[ -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
|
|
}
|
|
command -v qemu-system-arm >/dev/null || {
|
|
echo "FATAL: qemu-system-arm not on PATH: see qemu/README.md" >&2
|
|
exit 1
|
|
}
|
|
|
|
mkdir -p "$OUTDIR"
|
|
# Short-named scratch: AF_UNIX socket paths are capped at ~108 chars.
|
|
WORK="$(mktemp -d /tmp/wqd.XXXXXX)"
|
|
QEMU_PID=""
|
|
cleanup() {
|
|
if [ -n "$QEMU_PID" ]; then
|
|
python3 "$HERE/qmp.py" "$WORK/qmp.sock" quit 2>/dev/null || true
|
|
sleep 1
|
|
kill "$QEMU_PID" 2>/dev/null || true
|
|
fi
|
|
cp "$WORK/console.log" "$OUTDIR/console.log" 2>/dev/null || true
|
|
rm -rf "$WORK"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
bash "$QDIR/mkinitramfs.sh"
|
|
bash "$QDIR/mkimage.sh"
|
|
|
|
# Random hostfwd ports can collide. Detect qemu's early bind failure and retry
|
|
# with a fresh base rather than failing spuriously.
|
|
for _attempt in 1 2 3; do
|
|
PORT=$((21000 + RANDOM % 20000))
|
|
: > "$WORK/console.log"
|
|
bash "$QDIR/run.sh" --kernel "$ZIMAGE" --display headless --qmp "$WORK/qmp.sock" \
|
|
--ssh-port "$PORT" --http-port $((PORT + 1)) --api-port $((PORT + 2)) \
|
|
> "$WORK/console.log" 2>&1 &
|
|
QEMU_PID=$!
|
|
sleep 3
|
|
kill -0 "$QEMU_PID" 2>/dev/null && break
|
|
if grep -aq 'Could not set up host forwarding' "$WORK/console.log"; then
|
|
echo "== hostfwd port collision on base $PORT, retrying"
|
|
QEMU_PID=""
|
|
continue
|
|
fi
|
|
echo "FATAL: VM died at launch:" >&2
|
|
tail -20 "$WORK/console.log" >&2
|
|
exit 1
|
|
done
|
|
if [ -z "$QEMU_PID" ] || ! kill -0 "$QEMU_PID" 2>/dev/null; then
|
|
echo "FATAL: could not launch the VM after 3 port attempts" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "== waiting for warden-ui"
|
|
deadline=$((SECONDS + 180))
|
|
while [ $SECONDS -lt $deadline ]; do
|
|
grep -aq 'init: starting warden-ui' "$WORK/console.log" && break
|
|
kill -0 "$QEMU_PID" 2>/dev/null || { echo "FATAL: VM exited early" >&2; tail -25 "$WORK/console.log" >&2; exit 1; }
|
|
sleep 2
|
|
done
|
|
grep -aq 'init: starting warden-ui' "$WORK/console.log" || {
|
|
echo "FATAL: warden-ui never started (no fb0? wrong kernel?)" >&2
|
|
tail -25 "$WORK/console.log" >&2
|
|
exit 1
|
|
}
|
|
|
|
# A started process is not a rendered frame. Poll screendumps until the panel
|
|
# stops being a single flat colour, on a bounded deadline: TCG renders CPU-bound
|
|
# and a loaded host can be arbitrarily slow, so this is never a fixed sleep.
|
|
echo "== waiting for the first real frame"
|
|
deadline=$((SECONDS + 180))
|
|
ready=0
|
|
while [ $SECONDS -lt $deadline ]; do
|
|
python3 "$HERE/qmp.py" "$WORK/qmp.sock" screendump "$WORK/probe.ppm" 2>/dev/null || { sleep 2; continue; }
|
|
colors="$(python3 -c "
|
|
import sys
|
|
d=open('$WORK/probe.ppm','rb').read()
|
|
print(len(set(d[i:i+3] for i in range(15, len(d), 3))))
|
|
" 2>/dev/null || echo 0)"
|
|
[ "${colors:-0}" -gt 32 ] && { ready=1; break; }
|
|
sleep 3
|
|
done
|
|
[ "$ready" = 1 ] || {
|
|
echo "FATAL: the UI never rendered a real frame" >&2
|
|
tail -25 "$WORK/console.log" >&2
|
|
exit 1
|
|
}
|
|
|
|
echo "== driving $SCRIPT"
|
|
python3 "$HERE/qmp.py" "$WORK/qmp.sock" drive "$SCRIPT" "$OUTDIR"
|
|
|
|
# The UI must still be alive: see the header. Checked AFTER the script so a
|
|
# crash caused by the interaction is caught, which is the usual case.
|
|
if grep -aq 'warden-ui EXITED' "$WORK/console.log"; then
|
|
echo "FATAL: warden-ui DIED during the run:" >&2
|
|
grep -a -A22 'warden-ui EXITED' "$WORK/console.log" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "UI-DRIVE-PASS (screenshots in $OUTDIR)"
|