From 81a5b903297e59d599dd0d6f580b89597630909d Mon Sep 17 00:00:00 2001 From: cirakg Date: Mon, 19 Oct 2026 22:28:09 +0200 Subject: docs: record M4 results and simulator findings; bump to 0.4.0 Co-Authored-By: Claude Opus 5.5 --- docs/DEVELOPER_GUIDE.md | 40 ++++++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 8 deletions(-) (limited to 'docs/DEVELOPER_GUIDE.md') diff --git a/docs/DEVELOPER_GUIDE.md b/docs/DEVELOPER_GUIDE.md index 283e51a..7b56f3b 100644 --- a/docs/DEVELOPER_GUIDE.md +++ b/docs/DEVELOPER_GUIDE.md @@ -38,10 +38,10 @@ Use other devices with `TEST_DEVICES="fr70 venu3"` on any target that takes a de | --- | --- | | `source/` | The watch app (Monkey C), one folder per module (README section 5) | | `test/` | Monkey C unit tests (`(:test)` functions), compiled only into test builds | -| `test/golden/` | Approved simulator screenshots, one per test device | -| `resources/` | Strings, the default launcher icon and, from M7, the settings | +| `test/golden/` | Approved simulator screenshots: one folder per device, one image per screen of the UI flow | +| `resources/` | Strings, the default launcher icon, the option properties and, from M7, the settings form | | `resources-icons/`, `icons.jungle` | Launcher icons at each size the products need, generated by `make icons` | -| `manifest.xml`, `monkey.jungle`, `test.jungle` | App manifest and build configuration | +| `manifest.xml`, `monkey.jungle`, `test.jungle`, `uitest.jungle` | App manifest and build configuration | | `docker/` | Images (`Dockerfile`, `docs.Dockerfile`), hash-locked dependencies, and the container scripts | | `tools/` | Python tools (test-log judge, screenshot checker, linters, icon generator) and their tests | | `docs/` | This guide and the other documents | @@ -53,7 +53,8 @@ Use other devices with `TEST_DEVICES="fr70 venu3"` on any target that takes a de `docker/scripts/build-app.sh` compiles one device in one mode: -- `debug`: what `make build`, `make sim`, `make perf` and `make screenshots` use. It contains the `(:debug)` diagnostics. +- `debug`: what `make build`, `make sim` and `make perf` use. It contains the `(:debug)` diagnostics, the debug menu items (demo entries, performance overlay, on-watch benchmark) and the fixtures. +- `uitest`: a debug build plus `uitest.jungle`, for `make screenshots`. It keeps the `(:uitest)` half of `source/util/TestMode.mc`, so the app starts with a wiped store, fake entries, the PIN 123456, a frozen clock and a 10 s auto-lock. - `release`: `monkeyc -r` removes every `(:debug)` declaration; used by `make package`. - `test`: adds `test.jungle` and `--unit-test`. @@ -67,6 +68,9 @@ The simulator is a GTK program. The scripts start it on a virtual display (Xvfb - It keeps a single-instance lock file, `~/Sim-dev`, holding its process ID. That file survives in `.cache/home`, and a later container often reuses the process ID for something else, so the simulator would believe it's already running. `ciq_start_simulator` deletes the stale lock first. - `monkeydo` exits with 1 even when every unit test passed, so `tools/ciq_test_report.py` judges the console output instead. - For a normal (non-test) run, `monkeydo` blocks while the app runs and streams the app's `System.println` output. Quotes in that output arrive escaped, which is why the diagnostics use plain `key=value` pairs. +- **It starts apps that have a glance in the glance.** `ciq_launch_app` waits for the glance's `DIAG shown glance` line, then opens the app the way the simulator accepts. On touch screens that's a tap on the glance; on button-only watches it's DOWN (which selects the glance) and then START. START alone does nothing in the simulator. The press is retried if the app doesn't open. +- **A second simulator in the same container opens a second window**, and input may go to the wrong one. `ciq_start_simulator` reuses one that's already listening. +- Both `monkeydo` and the harness append to the app log; with `>` one would overwrite the other's lines. ### Monkey C facts that matter here (verified with SDK 9.2.0) @@ -74,14 +78,32 @@ The simulator is a GTK program. The scripts start it on a virtual display (Xvfb - Assigning a value above 255 to a ByteArray element throws; mask with `& 0xFF`. - Native HMAC supports only SHA-256; HMAC-SHA1 and HMAC-SHA512 go through `Hmac.generic`. - Function calls are expensive in the VM; hot loops inline small helpers (SHA-512 went 37 % faster that way). -- The simulator doesn't enforce the watchdog in test runs. Anything long runs as a `Jobs.Job` with a bounded step. +- **The watchdog** ("Code Executed Too Long") is not enforced in unit-test runs, but it is in normal simulator runs, with the device's `watchdogCount` per event: 120,000 bytecodes on the Forerunner 255/255S, 240,000 on the others. M4 measured what it means. 200 byte-wise PBKDF2 iterations in one event tripped it on the 255S, and so did two SHA-512 codes in one draw. So: + - Anything long runs as a `Jobs.Job` with a bounded step (`Kdf.ITERATIONS_PER_STEP` = 100). + - Screens compute at most one SHA-512 code per event, in a timer event separate from drawing. + - The scripted UI flow runs on the 255S profile, so a regression trips there. - `StringUtil.convertEncodedString` stops the app with an uncatchable "System Error" on invalid UTF-8; `try` doesn't help. Decode untrusted bytes with `Bytes.toUtf8`, which validates first. - `Application.Storage` stores ByteArrays (its `ValueType` includes them, unlike `Application.PropertyValueType`), so records are stored as single ByteArrays of known size. Dictionaries passed to it must be typed `Dictionary`; `Dictionary` doesn't type-check. - `Array.slice` returns `Array`; cast it back (`as Array`). +- `Application.Properties.getValue` on a property that isn't in `properties.xml` stops the app with an uncatchable "Unexpected Type Error". Every option is therefore declared in `resources/settings/properties.xml`. +- **On touch screens, a tap is delivered as the `onSelect` behaviour first**, and `onTap` only runs if `onSelect` returns false. Delegates that need the tap position (the list, the PIN keypad) return false from `onSelect` on touch screens, and handle the START button in `onKey`. +- **Glances:** because `getGlanceView` is `(:glance)`, the compiler checks the whole app class against the glance scope. Methods that never run in glance mode opt out with `(:typecheck(disableGlanceCheck))`. + - `AppBase.initialize` runs before non-glance code is loaded, so `Diag` (which it calls) is `(:glance)`. + - `onStop` also runs after the glance, so it only touches `AppState` once the full app has started. +- `Attention.vibrate` makes the simulator shake the device drawing, which spoils screenshots; the UI-test build doesn't vibrate. +- A `ConfirmationDelegate` closes its dialog after `onResponse`, so a view switch from `onResponse` must wait for a short timer. +- `Attention has :vibrate` inside a class that has a private `vibrate` method warns (the symbol lookup finds the method); the helper is called `buzz`. -### Screenshots +### Screenshots and the UI flow -`tools/sim_screenshot.py` doesn't assume where the watch is drawn in the simulator window. It searches for the device drawing (named in the device's `simulator.json`) in the window capture, comparing only the bezel, because the app draws inside the display area. The match is near-exact (mean difference below 0.3 out of 255); the drawing sits at offset (1, 26) because of the window border and the menu bar. The display rectangle from `simulator.json` is then cut out, round screens get their corners blacked out, and the result is compared with `test/golden/.png`. +`make screenshots` builds the UI-test variant and runs `tools/ui_flow.py` on every device in `TEST_DEVICES` and `SPOT_DEVICES`. + +1. **Input.** The flow clicks the buttons where the device's `simulator.json` draws them. It taps and swipes on touch screens, and long-presses for the menu where there's no MENU button. +2. **Waiting.** After each input it waits for the screen's `DIAG shown ` line. The list and the OTP screen print it only once every visible code is drawn. +3. **Touch targets.** It reads where to tap from `DIAG list_focus` (the focused row) and `DIAG pin_keys` (the keypad). +4. **Comparison.** Each screen is cut out of the window capture by `tools/sim_screenshot.py` and compared with `test/golden//.png`. That tool finds the device drawing by its bezel, so it doesn't assume a window layout. Differences go to `build/screenshots//-diff.png`. + +Because the entry IDs, the clock and the codes are fixed in the UI-test build, every run is pixel-identical. Native `Menu2` menus print nothing, so the flow waits a fixed 1.5 s for them. Touch-only watches show menus as a freely scrolling list without focus, so About is reached only on watches with buttons. ### Performance and memory @@ -100,5 +122,7 @@ Debug builds print `DIAG first_frame startup_ms=... mem_used=... mem_total=...` ## Adding things - **A device**: add `` to `manifest.xml`, then `make sdk` (downloads its files), `make icons` and `make products` (checks it meets the target rule: a watch, API 5.0 or newer, at least 512 KB). -- **A test device**: add it to `TEST_DEVICES` in the `Makefile`, run `make screenshots`, review `build/screenshots/.png` and run `make screenshots-approve`. +- **A test device**: add it to `TEST_DEVICES` (unit tests, perf and screenshots) or `SPOT_DEVICES` (screenshots only) in the `Makefile`. Then run `make screenshots`, review `build/screenshots//`, and run `make screenshots-approve`. +- **A screen in the UI flow**: print `Diag.shown("")` after its first complete draw, add the steps to `run_flow` in `tools/ui_flow.py`, and approve the new goldens after reviewing them. +- **On-watch numbers**: sideload a debug build (`make build DEVICE=fr70`, copy `build/debug/fr70.prg` to the watch's `GARMIN/APPS` folder over USB). Then open the menu and choose "Debug: benchmark". - **A barcode symbology** (from M6): one encoder module under `source/codes/`, its validator, unit tests with reference patterns, and the checks in [README 12.4](../README.md#124-how-each-symbology-is-proven-scannable). -- cgit v1.2.3