m5-hotreload.sh
raw
#!/usr/bin/env bash
# M5 hot-reload test: runs the dev example under quibble against a scratch
# copy of Counter.gdc, then edits the file live and asserts
# 1. the reload applies (pixels change, "reloaded" printed),
# 2. counter state survives (next click reaches count=4, not 1),
# 3. a broken save keeps the UI alive,
# 4. idle renders zero frames throughout.
set -euo pipefail
cd "$(dirname "$0")/.."
cargo build -p guiduck --example m5_dev
cargo build -p guiduck-render-tinyskia --bin png-diff
workdir=$(mktemp -d)
sock="$workdir/quibble.sock"
cleanup() {
quibblectl --socket "$sock" quit >/dev/null 2>&1 || true
rm -rf "$workdir"
}
trap cleanup EXIT
cp crates/guiduck/components/Counter.gdc "$workdir/Counter.gdc"
CLICK_X=76
CLICK_Y=80
GUIDUCK_BACKEND=software GUIDUCK_TRACE_FRAMES=1 \
GUIDUCK_DEV_FILE="$workdir/Counter.gdc" \
quibble --socket "$sock" --width 400 --height 300 \
target/debug/examples/m5_dev \
>"$workdir/out.log" 2>"$workdir/err.log" &
for _ in $(seq 1 50); do
quibblectl --socket "$sock" ping >/dev/null 2>&1 && break
sleep 0.2
done
quibblectl --socket "$sock" wait-window --timeout-ms 30000 >/dev/null
sleep 1
frames() { grep -c '^guiduck-frame' "$workdir/err.log" || true; }
fail=0
# Three clicks to build up state.
quibblectl --socket "$sock" pointer-move $CLICK_X $CLICK_Y >/dev/null
for _ in 1 2 3; do quibblectl --socket "$sock" click >/dev/null; done
sleep 0.5
grep -q '^count=3$' "$workdir/out.log" || { echo "FAIL: count=3 not reached" >&2; fail=1; }
# Park the pointer away from the button so screenshots show base colors.
quibblectl --socket "$sock" pointer-move 350 250 >/dev/null
sleep 0.3
quibblectl --socket "$sock" screenshot -o "$workdir/before.png" >/dev/null
# Live edit: new label text and a new button color.
sed -i 's/Count: {count}/Tally: {count}/; s/#4682b4/#2e8b57/' "$workdir/Counter.gdc"
sleep 1.5
if grep -q '^reloaded$' "$workdir/out.log"; then
echo "reload: applied"
else
echo "FAIL: no reload detected" >&2
fail=1
fi
quibblectl --socket "$sock" screenshot -o "$workdir/after.png" >/dev/null
# Same renderer, deterministic output: byte comparison is the right bar.
if cmp -s "$workdir/before.png" "$workdir/after.png"; then
echo "FAIL: reload did not change pixels" >&2
fail=1
else
echo "reload: pixels changed"
fi
# State survived: the next click continues from 3.
quibblectl --socket "$sock" pointer-move $CLICK_X $CLICK_Y >/dev/null
quibblectl --socket "$sock" click >/dev/null
sleep 0.5
if grep -q '^count=4$' "$workdir/out.log"; then
echo "state: count survived the reload (3 -> 4)"
else
echo "FAIL: expected count=4 after reload; got $(grep '^count=' "$workdir/out.log" | tail -1)" >&2
fail=1
fi
# Broken save: UI stays up and responsive.
echo "(component Broken (container" > "$workdir/Counter.gdc"
sleep 1.5
quibblectl --socket "$sock" click >/dev/null
sleep 0.5
if grep -q '^count=5$' "$workdir/out.log"; then
echo "broken save: previous UI still live"
else
echo "FAIL: UI died after a broken save" >&2
fail=1
fi
grep -q 'reload failed' "$workdir/err.log" && echo "broken save: diagnostics reported"
# Quiescence.
q0=$(frames); sleep 2; q1=$(frames)
echo "idle frames: $q0 -> $q1"
[ "$q0" = "$q1" ] || { echo "FAIL: rendering while idle" >&2; fail=1; }
cp "$workdir/before.png" "$workdir/after.png" target/ 2>/dev/null || true
[ "$fail" -eq 0 ] && echo "M5 hot-reload test PASSED"
exit $fail