Wait for QEMU input releases (#546)

This commit is contained in:
Noah
2026-09-11 11:15:02 -06:00
parent 55660b7c01
commit 2efe36f582
2 changed files with 45 additions and 35 deletions
+34 -29
View File
@@ -217,26 +217,36 @@ def do_tap(s, f, ax, ay, hold=TAP_HOLD_S):
send_events(s, f, [btn_ev(False)]) send_events(s, f, [btn_ev(False)])
def do_observed_tap(ctx, ax, ay): def wait_input_count(ctx, field, before):
"""Press until the guest reports consuming it, then release.""" """Wait until one guest input counter advances."""
before = parse_stats(ctx.ctl.send("stats")).get("presses")
if before is None: if before is None:
time.sleep(0.05)
return True
deadline = time.monotonic() + TAP_OBSERVE_TIMEOUT_S
while time.monotonic() < deadline:
now = parse_stats(ctx.ctl.send("stats")).get(field)
if now is not None and now != before:
return True
time.sleep(0.02)
return False
def do_observed_tap(ctx, ax, ay):
"""Wait until the guest consumes both halves of a tap."""
before = parse_stats(ctx.ctl.send("stats"))
if before.get("presses") is None:
do_tap(ctx.s, ctx.f, ax, ay) do_tap(ctx.s, ctx.f, ax, ay)
return True return True
send_events(ctx.s, ctx.f, [abs_ev("x", ax), abs_ev("y", ay), btn_ev(True)]) send_events(ctx.s, ctx.f, [abs_ev("x", ax), abs_ev("y", ay), btn_ev(True)])
observed = False pressed = False
deadline = time.monotonic() + TAP_OBSERVE_TIMEOUT_S
try: try:
while time.monotonic() < deadline: pressed = wait_input_count(ctx, "presses", before.get("presses"))
now = parse_stats(ctx.ctl.send("stats")).get("presses")
if now is not None and now != before:
observed = True
break
time.sleep(0.02)
finally: finally:
send_events(ctx.s, ctx.f, [btn_ev(False)]) send_events(ctx.s, ctx.f, [btn_ev(False)])
return observed if not pressed:
return False
return wait_input_count(ctx, "releases", before.get("releases"))
def _move_swipe(s, f, ax1, ay1, ax2, ay2, ms, steps): def _move_swipe(s, f, ax1, ay1, ax2, ay2, ms, steps):
@@ -278,27 +288,18 @@ def do_observed_swipe(ctx, x1, y1, x2, y2, ms=400, steps=None):
if steps is None: if steps is None:
steps = max(6, int(ms / 25)) steps = max(6, int(ms / 25))
before = parse_stats(ctx.ctl.send("stats")).get("presses") before = parse_stats(ctx.ctl.send("stats"))
send_events(ctx.s, ctx.f, [abs_ev("x", ax1), abs_ev("y", ay1), btn_ev(True)]) send_events(ctx.s, ctx.f, [abs_ev("x", ax1), abs_ev("y", ay1), btn_ev(True)])
observed = before is None pressed = False
try: try:
if observed: pressed = wait_input_count(ctx, "presses", before.get("presses"))
time.sleep(0.05) if not pressed:
else:
deadline = time.monotonic() + TAP_OBSERVE_TIMEOUT_S
while time.monotonic() < deadline:
now = parse_stats(ctx.ctl.send("stats")).get("presses")
if now is not None and now != before:
observed = True
break
time.sleep(0.02)
if not observed:
return False return False
_move_swipe(ctx.s, ctx.f, ax1, ay1, ax2, ay2, ms, steps) _move_swipe(ctx.s, ctx.f, ax1, ay1, ax2, ay2, ms, steps)
return True
finally: finally:
send_events(ctx.s, ctx.f, [btn_ev(False)]) send_events(ctx.s, ctx.f, [btn_ev(False)])
return wait_input_count(ctx, "releases", before.get("releases"))
class Ctl: class Ctl:
@@ -593,6 +594,7 @@ STATS_FIELD_RE = {
"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*$'), "idle": re.compile(r'^idle:\s*(\d+)\s*$'),
"presses": re.compile(r'^presses:\s*(\d+)\s*$'), "presses": re.compile(r'^presses:\s*(\d+)\s*$'),
"releases": re.compile(r'^releases:\s*(\d+)\s*$'),
"termbusy": re.compile(r'^termbusy:\s*(\d+)\s*$'), "termbusy": re.compile(r'^termbusy:\s*(\d+)\s*$'),
"termintr": re.compile(r'^termintr:\s*(\d+)\s*$'), "termintr": re.compile(r'^termintr:\s*(\d+)\s*$'),
"termfg": re.compile(r'^termfg:\s*(-?\d+)\s*$'), "termfg": re.compile(r'^termfg:\s*(-?\d+)\s*$'),
@@ -755,20 +757,23 @@ def verb_shot(ctx, lineno, cmd, args, line):
def verb_tap(ctx, lineno, cmd, args, line): def verb_tap(ctx, lineno, cmd, args, line):
x, y = int(args[0]), int(args[1]) x, y = int(args[0]), int(args[1])
observed = do_observed_tap(ctx, to_axis(x, ctx.size), to_axis(y, ctx.size)) observed = do_observed_tap(ctx, to_axis(x, ctx.size), to_axis(y, ctx.size))
return ("ok", "") if observed else ("fail", "press was not consumed within 2 seconds") return (("ok", "") if observed else
("fail", "input press or release was not consumed within 2 seconds"))
def verb_swipe(ctx, lineno, cmd, args, line): def verb_swipe(ctx, lineno, cmd, args, line):
ms = int(args[4]) if len(args) > 4 else 400 ms = int(args[4]) if len(args) > 4 else 400
observed = do_observed_swipe( observed = do_observed_swipe(
ctx, int(args[0]), int(args[1]), int(args[2]), int(args[3]), ms) ctx, int(args[0]), int(args[1]), int(args[2]), int(args[3]), ms)
return ("ok", "") if observed else ("fail", "press was not consumed within 2 seconds") return (("ok", "") if observed else
("fail", "input press or release was not consumed within 2 seconds"))
def verb_fling(ctx, lineno, cmd, args, line): def verb_fling(ctx, lineno, cmd, args, line):
observed = do_observed_swipe( observed = do_observed_swipe(
ctx, int(args[0]), int(args[1]), int(args[2]), int(args[3]), ms=120) ctx, int(args[0]), int(args[1]), int(args[2]), int(args[3]), ms=120)
return ("ok", "") if observed else ("fail", "press was not consumed within 2 seconds") return (("ok", "") if observed else
("fail", "input press or release was not consumed within 2 seconds"))
def verb_sleep(ctx, lineno, cmd, args, line): def verb_sleep(ctx, lineno, cmd, args, line):
+11 -6
View File
@@ -48,7 +48,8 @@ class FakeCtl:
if cmd == "stats": if cmd == "stats":
self.stats_calls += 1 self.stats_calls += 1
return ("page: Demo/Rows\ncpu: 12%\nfps: 10\nrender: 3.20 ms/frame\n" return ("page: Demo/Rows\ncpu: 12%\nfps: 10\nrender: 3.20 ms/frame\n"
f"rga: 0%\nidle: 0\npresses: {self.stats_calls}\ntermbusy: 1\n" f"rga: 0%\nidle: 0\npresses: {self.stats_calls}\n"
f"releases: {self.stats_calls}\ntermbusy: 1\n"
"termintr: 2\ntermfg: -1\ntermsig: 3") "termintr: 2\ntermfg: -1\ntermsig: 3")
if cmd == "wake": if cmd == "wake":
return "wake: ok" return "wake: ok"
@@ -241,7 +242,9 @@ class PureHelpers(unittest.TestCase):
def send(self, cmd): def send(self, cmd):
self.calls += 1 self.calls += 1
return f"presses: {0 if self.calls < 3 else 1}" presses = 0 if self.calls < 3 else 1
releases = 0 if self.calls < 4 else 1
return f"presses: {presses}\nreleases: {releases}"
saved_send, saved_sleep = qmp.send_events, qmp.time.sleep saved_send, saved_sleep = qmp.send_events, qmp.time.sleep
qmp.send_events = lambda _s, _f, events: calls.append(events) qmp.send_events = lambda _s, _f, events: calls.append(events)
@@ -264,7 +267,7 @@ class PureHelpers(unittest.TestCase):
class UnobservingCtl: class UnobservingCtl:
def send(self, cmd): def send(self, cmd):
return "presses: 0" return "presses: 0\nreleases: 0"
saved_send, saved_sleep, saved_monotonic = ( saved_send, saved_sleep, saved_monotonic = (
qmp.send_events, qmp.time.sleep, qmp.time.monotonic) qmp.send_events, qmp.time.sleep, qmp.time.monotonic)
@@ -291,7 +294,9 @@ class PureHelpers(unittest.TestCase):
def send(self, cmd): def send(self, cmd):
self.calls += 1 self.calls += 1
return f"presses: {0 if self.calls < 3 else 1}" presses = 0 if self.calls < 3 else 1
releases = 0 if self.calls < 4 else 1
return f"presses: {presses}\nreleases: {releases}"
saved_send, saved_sleep = qmp.send_events, qmp.time.sleep saved_send, saved_sleep = qmp.send_events, qmp.time.sleep
qmp.send_events = lambda _s, _f, events: calls.append(events) qmp.send_events = lambda _s, _f, events: calls.append(events)
@@ -314,7 +319,7 @@ class PureHelpers(unittest.TestCase):
class UnobservingCtl: class UnobservingCtl:
def send(self, cmd): def send(self, cmd):
return "presses: 0" return "presses: 0\nreleases: 0"
saved_send, saved_sleep, saved_monotonic = ( saved_send, saved_sleep, saved_monotonic = (
qmp.send_events, qmp.time.sleep, qmp.time.monotonic) qmp.send_events, qmp.time.sleep, qmp.time.monotonic)
@@ -378,7 +383,7 @@ 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, "presses": 1.0, "idle": 0.0, "presses": 1.0, "releases": 1.0,
"termbusy": 1.0, "termintr": 2.0, "termbusy": 1.0, "termintr": 2.0,
"termfg": -1.0, "termsig": 3.0}) "termfg": -1.0, "termsig": 3.0})