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:
+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:
|
||||
|
||||
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
|
||||
ctl WORDS... raw passthrough for anything the channel grows
|
||||
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
|
||||
simply not be built yet.
|
||||
assert_stat FIELD OP VALUE
|
||||
FIELD is cpu, fps or render, read off a fresh
|
||||
`stats` reply (warden_debug.c). Same OP vocabulary
|
||||
as assert_json.
|
||||
FIELD is cpu, fps, render or idle (0 awake,
|
||||
1 dimmed, 2 asleep), read off a fresh `stats`
|
||||
reply (warden_debug.c). Same OP vocabulary as
|
||||
assert_json.
|
||||
capture_region NAME X Y W H TOLERANCE
|
||||
screendump now, crop to X,Y,WxH, and (over)write
|
||||
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*$'),
|
||||
"fps": re.compile(r'^fps:\s*(-?\d+(?:\.\d+)?)\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):
|
||||
"""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
|
||||
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."""
|
||||
@@ -515,6 +521,13 @@ def drive(s, f, script_path, outdir, size, ctl_path=None, console_path=None, ref
|
||||
need_ctl(lineno, cmd)
|
||||
reply = ctl.send("nav " + " ".join(args))
|
||||
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"):
|
||||
# Query verbs: print the reply, never judge it. `ctl` is a raw
|
||||
# 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.
|
||||
need_ctl(lineno, cmd)
|
||||
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} "
|
||||
f"(known: cpu, fps, render)")
|
||||
f"(known: cpu, fps, render, idle)")
|
||||
else:
|
||||
stats = parse_stats(ctl.send("stats"))
|
||||
if field not in stats:
|
||||
|
||||
@@ -39,7 +39,9 @@ class FakeCtl:
|
||||
if cmd == "page":
|
||||
return "Demo/Rows"
|
||||
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 "):
|
||||
return json.dumps({"a": {"b": 1}, "list": [1, 2], "name": "warden"})
|
||||
if cmd.startswith("hit "):
|
||||
@@ -98,14 +100,16 @@ class PureHelpers(unittest.TestCase):
|
||||
|
||||
def test_parse_stats(self):
|
||||
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):
|
||||
def test_every_channel_passes_on_a_healthy_ui(self):
|
||||
rc, by, rows = run_script(
|
||||
"wake\n"
|
||||
"assert_page Demo/Rows\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 list len_ge 2 2\n"
|
||||
"assert_json name eq warden\n"
|
||||
@@ -116,7 +120,7 @@ class DriveVerbs(unittest.TestCase):
|
||||
"assert_region r1\n"
|
||||
)
|
||||
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))
|
||||
|
||||
def test_mismatches_are_fails_not_stops(self):
|
||||
|
||||
Reference in New Issue
Block a user