diff --git a/qemu/README.md b/qemu/README.md index 065adbe..e4c3551 100644 --- a/qemu/README.md +++ b/qemu/README.md @@ -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 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 All take the virt-fragment ``; `FLARE_EDGE=` where noted. diff --git a/qemu/run.sh b/qemu/run.sh index d3a8b8e..7587b96 100755 --- a/qemu/run.sh +++ b/qemu/run.sh @@ -82,7 +82,12 @@ fi # NOTE: never add `earlyprintk`: the config's DEBUG_UART_PHYS is the RV1106's # 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 # fail on a busy default port. NETDEV="user,id=n0" diff --git a/qemu/tests/qmp.py b/qemu/tests/qmp.py index d60b172..f0e49c8 100755 --- a/qemu/tests/qmp.py +++ b/qemu/tests/qmp.py @@ -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: diff --git a/qemu/tests/test_qmp_drive.py b/qemu/tests/test_qmp_drive.py index c6e4261..acbd3ae 100755 --- a/qemu/tests/test_qmp_drive.py +++ b/qemu/tests/test_qmp_drive.py @@ -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):