qemu: wake verb, idle stat, and no fbcon cursor in the framebuffer
- tests/qmp.py: `wake` is a judged verb (the reply must be the channel's own ack) and `assert_stat` knows `idle` (0 awake, 1 dimmed, 2 asleep). warden-ui's sleep.c swallows the touch that wakes a dimmed or blanked panel, so every compiled flow script now opens with wake (flare-edge #148); tests cover both. - run.sh: vt.global_cursor_default=0. fbcon shares the virtio-gpu framebuffer with warden-ui and its cursor blinked an 8x2 block onto the top-left corner in some boots and not others, which made a structural pixel reference fail by one cell (#18). The panel has no fbcon on its display, so this only makes the rig match it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013aHKWzT5EF86RFKRMtAv9n
This commit is contained in:
@@ -67,6 +67,10 @@ README); `warden-flared`, `warden-modbus`, and `warden-ui` (the LVGL
|
|||||||
fbdev+evdev build from flare-edge `tools/build-ui-vm.sh`) are started by
|
fbdev+evdev build from flare-edge `tools/build-ui-vm.sh`) are started by
|
||||||
stage-2 init when present.
|
stage-2 init when present.
|
||||||
|
|
||||||
|
The VT cursor is kept off (`vt.global_cursor_default=0`): fbcon shares the
|
||||||
|
virtio-gpu framebuffer with warden-ui and its blinking cursor would otherwise
|
||||||
|
show up in screendumps at random (issue #18).
|
||||||
|
|
||||||
## Scenarios
|
## Scenarios
|
||||||
|
|
||||||
All take the virt-fragment `<zImage>`; `FLARE_EDGE=<checkout>` where noted.
|
All take the virt-fragment `<zImage>`; `FLARE_EDGE=<checkout>` where noted.
|
||||||
|
|||||||
+6
-1
@@ -82,7 +82,12 @@ fi
|
|||||||
|
|
||||||
# NOTE: never add `earlyprintk`: the config's DEBUG_UART_PHYS is the RV1106's
|
# NOTE: never add `earlyprintk`: the config's DEBUG_UART_PHYS is the RV1106's
|
||||||
# 0xff4c0000, which does not exist on -M virt.
|
# 0xff4c0000, which does not exist on -M virt.
|
||||||
APPEND="console=ttyAMA0 rdinit=/init"
|
# vt.global_cursor_default=0: the virt kernel binds fbcon to the virtio-gpu
|
||||||
|
# framebuffer, and its blinking cursor lands on /dev/fb0 under warden-ui (an
|
||||||
|
# 8x2 block at the top-left, present in one boot and absent in the next). The
|
||||||
|
# panel has no fbcon on its display, and a pixel reference captured with the
|
||||||
|
# cursor in frame fails without it (issue #18).
|
||||||
|
APPEND="console=ttyAMA0 rdinit=/init vt.global_cursor_default=0"
|
||||||
# Port 0 disables a forward. A boot smoke needs no host ports and must not
|
# Port 0 disables a forward. A boot smoke needs no host ports and must not
|
||||||
# fail on a busy default port.
|
# fail on a busy default port.
|
||||||
NETDEV="user,id=n0"
|
NETDEV="user,id=n0"
|
||||||
|
|||||||
+19
-6
@@ -26,6 +26,10 @@ warden_debug.c. The same channel is what tools/warden-ctl reaches over SSH on
|
|||||||
a real panel, so these verbs mean the same thing on the rig and on hardware:
|
a real panel, so these verbs mean the same thing on the rig and on hardware:
|
||||||
|
|
||||||
nav MENU[/TAB] jump to a page; fails if the page is unknown
|
nav MENU[/TAB] jump to a page; fails if the page is unknown
|
||||||
|
wake wake the UI as if touched, minus the swallow
|
||||||
|
(sleep.c dims and then blanks an idle panel and
|
||||||
|
eats the touch that ends either); every compiled
|
||||||
|
flow script opens with it
|
||||||
page | stats | hit X Y print the reply, judge nothing
|
page | stats | hit X Y print the reply, judge nothing
|
||||||
ctl WORDS... raw passthrough for anything the channel grows
|
ctl WORDS... raw passthrough for anything the channel grows
|
||||||
assert_page MENU/TAB the active page is exactly this
|
assert_page MENU/TAB the active page is exactly this
|
||||||
@@ -55,9 +59,10 @@ math, this file only drives it.
|
|||||||
document is a live snapshot and the key might
|
document is a live snapshot and the key might
|
||||||
simply not be built yet.
|
simply not be built yet.
|
||||||
assert_stat FIELD OP VALUE
|
assert_stat FIELD OP VALUE
|
||||||
FIELD is cpu, fps or render, read off a fresh
|
FIELD is cpu, fps, render or idle (0 awake,
|
||||||
`stats` reply (warden_debug.c). Same OP vocabulary
|
1 dimmed, 2 asleep), read off a fresh `stats`
|
||||||
as assert_json.
|
reply (warden_debug.c). Same OP vocabulary as
|
||||||
|
assert_json.
|
||||||
capture_region NAME X Y W H TOLERANCE
|
capture_region NAME X Y W H TOLERANCE
|
||||||
screendump now, crop to X,Y,WxH, and (over)write
|
screendump now, crop to X,Y,WxH, and (over)write
|
||||||
NAME in the refs file with both a phash and a
|
NAME in the refs file with both a phash and a
|
||||||
@@ -382,12 +387,13 @@ STATS_FIELD_RE = {
|
|||||||
"cpu": re.compile(r'^cpu:\s*(-?\d+(?:\.\d+)?)%?\s*$'),
|
"cpu": re.compile(r'^cpu:\s*(-?\d+(?:\.\d+)?)%?\s*$'),
|
||||||
"fps": re.compile(r'^fps:\s*(-?\d+(?:\.\d+)?)\s*$'),
|
"fps": re.compile(r'^fps:\s*(-?\d+(?:\.\d+)?)\s*$'),
|
||||||
"render": re.compile(r'^render:\s*(-?\d+(?:\.\d+)?)\s*ms/frame\s*$'),
|
"render": re.compile(r'^render:\s*(-?\d+(?:\.\d+)?)\s*ms/frame\s*$'),
|
||||||
|
"idle": re.compile(r'^idle:\s*(\d+)\s*$'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def parse_stats(reply):
|
def parse_stats(reply):
|
||||||
"""The `stats` reply (warden_debug.c: 'page: X\\ncpu: N%\\nfps: N\\n
|
"""The `stats` reply (warden_debug.c: 'page: X\\ncpu: N%\\nfps: N\\n
|
||||||
render: A.BB ms/frame\\nrga: N%') -> {'cpu'|'fps'|'render': float} for
|
render: A.BB ms/frame\\nrga: N%\\nidle: N') -> {'cpu'|'fps'|'render'|'idle': float} for
|
||||||
whichever lines are present. A field the reply lacks is simply absent
|
whichever lines are present. A field the reply lacks is simply absent
|
||||||
from the result -- the caller reports that as 'no such field', the same
|
from the result -- the caller reports that as 'no such field', the same
|
||||||
shape as resolve_path's 'no such path' for the json channel."""
|
shape as resolve_path's 'no such path' for the json channel."""
|
||||||
@@ -515,6 +521,13 @@ def drive(s, f, script_path, outdir, size, ctl_path=None, console_path=None, ref
|
|||||||
need_ctl(lineno, cmd)
|
need_ctl(lineno, cmd)
|
||||||
reply = ctl.send("nav " + " ".join(args))
|
reply = ctl.send("nav " + " ".join(args))
|
||||||
record(lineno, line, "ok" if reply.endswith(": ok") else "fail", reply)
|
record(lineno, line, "ok" if reply.endswith(": ok") else "fail", reply)
|
||||||
|
elif cmd == "wake":
|
||||||
|
# Judged, unlike the query verbs: a UI that does not answer the
|
||||||
|
# wake is one whose next tap may be swallowed, and that must not
|
||||||
|
# read as a passing step.
|
||||||
|
need_ctl(lineno, cmd)
|
||||||
|
reply = ctl.send("wake")
|
||||||
|
record(lineno, line, "ok" if reply == "wake: ok" else "fail", reply)
|
||||||
elif cmd in ("page", "hit", "stats", "ctl"):
|
elif cmd in ("page", "hit", "stats", "ctl"):
|
||||||
# Query verbs: print the reply, never judge it. `ctl` is a raw
|
# Query verbs: print the reply, never judge it. `ctl` is a raw
|
||||||
# passthrough for anything the channel grows later.
|
# passthrough for anything the channel grows later.
|
||||||
@@ -587,9 +600,9 @@ def drive(s, f, script_path, outdir, size, ctl_path=None, console_path=None, ref
|
|||||||
# assert_stat FIELD OP VALUE: FIELD off a fresh `stats` reply.
|
# assert_stat FIELD OP VALUE: FIELD off a fresh `stats` reply.
|
||||||
need_ctl(lineno, cmd)
|
need_ctl(lineno, cmd)
|
||||||
field, op, value = args[0], args[1], args[2]
|
field, op, value = args[0], args[1], args[2]
|
||||||
if field not in ("cpu", "fps", "render"):
|
if field not in ("cpu", "fps", "render", "idle"):
|
||||||
record(lineno, line, "fail", f"unknown stat field {field!r} "
|
record(lineno, line, "fail", f"unknown stat field {field!r} "
|
||||||
f"(known: cpu, fps, render)")
|
f"(known: cpu, fps, render, idle)")
|
||||||
else:
|
else:
|
||||||
stats = parse_stats(ctl.send("stats"))
|
stats = parse_stats(ctl.send("stats"))
|
||||||
if field not in stats:
|
if field not in stats:
|
||||||
|
|||||||
@@ -39,7 +39,9 @@ class FakeCtl:
|
|||||||
if cmd == "page":
|
if cmd == "page":
|
||||||
return "Demo/Rows"
|
return "Demo/Rows"
|
||||||
if cmd == "stats":
|
if cmd == "stats":
|
||||||
return "page: Demo/Rows\ncpu: 12%\nfps: 10\nrender: 3.20 ms/frame\nrga: 0%"
|
return "page: Demo/Rows\ncpu: 12%\nfps: 10\nrender: 3.20 ms/frame\nrga: 0%\nidle: 0"
|
||||||
|
if cmd == "wake":
|
||||||
|
return "wake: ok"
|
||||||
if cmd.startswith("@cat "):
|
if cmd.startswith("@cat "):
|
||||||
return json.dumps({"a": {"b": 1}, "list": [1, 2], "name": "warden"})
|
return json.dumps({"a": {"b": 1}, "list": [1, 2], "name": "warden"})
|
||||||
if cmd.startswith("hit "):
|
if cmd.startswith("hit "):
|
||||||
@@ -98,14 +100,16 @@ class PureHelpers(unittest.TestCase):
|
|||||||
|
|
||||||
def test_parse_stats(self):
|
def test_parse_stats(self):
|
||||||
got = qmp.parse_stats(FakeCtl("x").send("stats"))
|
got = qmp.parse_stats(FakeCtl("x").send("stats"))
|
||||||
self.assertEqual(got, {"cpu": 12.0, "fps": 10.0, "render": 3.2})
|
self.assertEqual(got, {"cpu": 12.0, "fps": 10.0, "render": 3.2, "idle": 0.0})
|
||||||
|
|
||||||
|
|
||||||
class DriveVerbs(unittest.TestCase):
|
class DriveVerbs(unittest.TestCase):
|
||||||
def test_every_channel_passes_on_a_healthy_ui(self):
|
def test_every_channel_passes_on_a_healthy_ui(self):
|
||||||
rc, by, rows = run_script(
|
rc, by, rows = run_script(
|
||||||
|
"wake\n"
|
||||||
"assert_page Demo/Rows\n"
|
"assert_page Demo/Rows\n"
|
||||||
"assert_hit 47 676 obj box=12,640,72x72\n"
|
"assert_hit 47 676 obj box=12,640,72x72\n"
|
||||||
|
"assert_stat idle eq 0\n"
|
||||||
"wait_json a.b eq 1 2\n"
|
"wait_json a.b eq 1 2\n"
|
||||||
"wait_json list len_ge 2 2\n"
|
"wait_json list len_ge 2 2\n"
|
||||||
"assert_json name eq warden\n"
|
"assert_json name eq warden\n"
|
||||||
@@ -116,7 +120,7 @@ class DriveVerbs(unittest.TestCase):
|
|||||||
"assert_region r1\n"
|
"assert_region r1\n"
|
||||||
)
|
)
|
||||||
self.assertIsNone(rc, [r for r in rows if r["status"] != "ok"])
|
self.assertIsNone(rc, [r for r in rows if r["status"] != "ok"])
|
||||||
self.assertEqual(len(rows), 10)
|
self.assertEqual(len(rows), 12)
|
||||||
self.assertTrue(all(r["status"] == "ok" for r in rows))
|
self.assertTrue(all(r["status"] == "ok" for r in rows))
|
||||||
|
|
||||||
def test_mismatches_are_fails_not_stops(self):
|
def test_mismatches_are_fails_not_stops(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user