aboutsummaryrefslogtreecommitdiff
path: root/tools/ui_flow.py
diff options
context:
space:
mode:
authorcirakg <cirakg@cirakg.xyz>2026-10-21 03:55:10 +0200
committercirakg <cirakg@cirakg.xyz>2026-10-21 03:55:10 +0200
commit56e0c351bb95cfdf63aa76ab8c5d725a694bad63 (patch)
treee99c7af6eca0656948279055c20a5f87d855d4c8 /tools/ui_flow.py
parent5e8ac1e2f5a660e55ea132996e1d5730cfa4fa64 (diff)
test(build): add make watchdog, the glance memory budget and the glance screenshot
- make watchdog (in make ci): a stress build encodes, packs, turns into runs and draws the largest code of every 2D symbology at two steps per event on the Forerunner 255S and 70; it fails on a watchdog trip, and the last code left on the screen must decode (bitmap path on the 70). - make memtest checks the glance (DIAG glance, budget 70 % of 64 KB). - screenshots.sh launches the app again after the flow and compares the glance with the favourite's code (glance_code.png on 7 watches); the About goldens show 0.8.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'tools/ui_flow.py')
-rw-r--r--tools/ui_flow.py74
1 files changed, 65 insertions, 9 deletions
diff --git a/tools/ui_flow.py b/tools/ui_flow.py
index edfc9a4..a17d435 100644
--- a/tools/ui_flow.py
+++ b/tools/ui_flow.py
@@ -28,8 +28,11 @@ waiting for the PIN and confirmed from the menu (watches with buttons), the
list, a TOTP entry, a HOTP entry and its next code, the
PIN screen, a wrong PIN, a correct PIN opening a locked entry, auto-lock
closing it, the menu, the About screen (watches with buttons; touch-only
-menus have no focus to move), the lockout after 5 wrong PINs, and an EAN-13
-code whose screenshot must decode with zxing-cpp (README 12.4).
+menus have no focus to move), the lockout after 5 wrong PINs, the barcodes
+whose screenshots must decode with zxing-cpp (README 12.4). The glance with
+the favourite's code is a separate launch (--shot glance_code, from
+screenshots.sh): the simulator doesn't return to the glance when the app
+exits.
Each screenshot is compared with test/golden/<device>/<screen>.png like
sim_screenshot.py does (or saved there with --approve).
@@ -50,6 +53,7 @@ from pathlib import Path
import sim_screenshot as shots
import zxingcpp
+from PIL import Image
EXIT_OK, EXIT_DIFFERENT, EXIT_FLOW = 0, 1, 2
SCREEN_TIMEOUT_S = 20.0
@@ -90,6 +94,11 @@ class Key:
hold: bool
+def barcodes_on(screen: Image.Image) -> list[str]:
+ """The texts of every barcode zxing-cpp finds on a screenshot."""
+ return [r.text for r in zxingcpp.read_barcodes(screen)]
+
+
def load_keys(device_dir: Path) -> dict[str, Key]:
"""Button centres in device-drawing coordinates, by button id (enter, up, down, esc, menu)."""
sim = json.loads((device_dir / "simulator.json").read_text(encoding="utf-8"))
@@ -326,7 +335,7 @@ class Flow:
return
if expect_code is not None:
# README 12.4 check 3: the code on the screenshot must decode.
- read = [r.text for r in zxingcpp.read_barcodes(screen)]
+ read = barcodes_on(screen)
if expect_code not in read:
print(f"[{label}] the barcode didn't decode to {expect_code!r}: read {read}")
self.results[name] = EXIT_DIFFERENT
@@ -569,6 +578,50 @@ def open_from_glance(window: str, device_dir: Path, raw_dir: Path) -> None:
sim.press("enter")
+def decode_screen(window: str, device_dir: Path, out: Path, text: str) -> int:
+ """Screenshot the simulator; EXIT_OK when a barcode on it reads `text`."""
+ name = device_dir.name
+ try:
+ sim = Simulator(window, device_dir, out / "raw")
+ screen = shots.extract_screen(sim.capture("decode"), device_dir, out / "decode.png")
+ except (FlowError, shots.ScreenshotError, subprocess.CalledProcessError, OSError) as err:
+ print(f"[{name}] no screenshot to decode: {err}")
+ return EXIT_FLOW
+ read = barcodes_on(screen)
+ if text not in read:
+ print(f"[{name}] the barcode on the screen didn't decode to {text!r}: read {read}")
+ return EXIT_DIFFERENT
+ print(f"[{name}] the barcode on the screen decodes to {text!r}")
+ return EXIT_OK
+
+
+def open_app(window: str, device_dir: Path, out: Path) -> int:
+ """--open-from-glance: EXIT_OK once the input was sent."""
+ try:
+ open_from_glance(window, device_dir, out / "raw")
+ except (FlowError, subprocess.CalledProcessError, OSError) as err:
+ print(f"[{device_dir.name}] could not open the app from its glance: {err}")
+ return EXIT_FLOW
+ return EXIT_OK
+
+
+def single_shot(args: argparse.Namespace) -> int:
+ """Screenshot the simulator as args.shot and compare it with its golden image (or approve it)."""
+ window, device_dir, out, golden, name = args.window, args.device_dir, args.out, args.golden, args.shot
+ label = f"{device_dir.name}/{name}"
+ try:
+ sim = Simulator(window, device_dir, out / "raw")
+ screen = shots.extract_screen(sim.capture(name), device_dir, out / f"{name}.png")
+ except (FlowError, shots.ScreenshotError, subprocess.CalledProcessError, OSError) as err:
+ print(f"[{label}] {err}")
+ return EXIT_FLOW
+ if args.approve:
+ shots.save_png(screen, golden / f"{name}.png", f"Approved simulator screen '{name}' of {device_dir.name}")
+ print(f"[{label}] approved")
+ return EXIT_OK
+ return shots.check_golden(screen, label, out / f"{name}.png", golden / f"{name}.png")
+
+
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Scripted UI flow with screenshot checks.")
parser.add_argument("--device-dir", type=Path, required=True)
@@ -578,15 +631,18 @@ def main(argv: list[str] | None = None) -> int:
parser.add_argument("--golden", type=Path, help="directory of the approved images")
parser.add_argument("--approve", action="store_true")
parser.add_argument("--open-from-glance", action="store_true", help="only open the app from its glance")
+ parser.add_argument("--decode", metavar="TEXT", help="only check that the screen shows a barcode reading TEXT")
+ parser.add_argument("--shot", metavar="NAME", help="only screenshot the current screen as NAME (needs --golden)")
args = parser.parse_args(argv)
name = args.device_dir.name
+ if args.decode is not None:
+ return decode_screen(args.window, args.device_dir, args.out, args.decode)
+ if args.shot is not None:
+ if args.golden is None:
+ parser.error("--shot needs --golden")
+ return single_shot(args)
if args.open_from_glance:
- try:
- open_from_glance(args.window, args.device_dir, args.out / "raw")
- except (FlowError, subprocess.CalledProcessError, OSError) as err:
- print(f"[{name}] could not open the app from its glance: {err}")
- return EXIT_FLOW
- return EXIT_OK
+ return open_app(args.window, args.device_dir, args.out)
if args.log is None or args.golden is None:
parser.error("--log and --golden are required for the flow")
try: