Host-side unit tests for fields.c and page.c (#68, partial) #82

Merged
robert merged 1 commit from tooling/host-test-harness into main 2026-09-04 00:57:49 +02:00
Owner

What this is

Partial progress on #68 ("Host-side unit tests for the watchapp logic"), scoped down to what
currently exists in watchapp/src/c/ after #58/PR #81 (fields.c/fields.h, page.c/page.h). Several
of #68's own acceptance criteria depend on issues that haven't landed yet — see "Deferred" below.
This PR does not close #68; a comment on the issue lists exactly what's covered vs. still blocked.

Layout

  • watchapp/tests/CMakeLists.txt — host build, two CTest binaries (test_fields, test_page)
  • watchapp/tests/harness.h — dependency-free assertion harness (~90 lines). No C test framework,
    per the issue's own instruction. Analogue for a C#/xUnit or Jest background: TH_ASSERT_* macros
    play the role Assert.*/expect() play, plus a TH_RUN/TH_REPORT runner convention.
  • watchapp/tests/stubs/pebble.h + pebble_stub.c — a fake in-memory persist_* backing.

Note on path: I used watchapp/tests/ (plural), not watchapp/test/ as #68's own "Files"
section names it. .forgejo/workflows/fast-lane.yml's host-c-tests job checks specifically for
watchapp/tests/CMakeLists.txt (plural) as its skip guard, and getting that job to activate is the
highest-value part of this issue — so I matched the workflow rather than the issue text. Confirmed:
that guard now finds this CMakeLists.txt and the job will build+run it once a runner is registered
(no runner is registered yet per tonight's other PRs, so this hasn't executed for real in CI, only
verified locally — see below).

page.c: stubbed, not split

page.c mixes pure logic with two persist_* calls (page_store_load/page_store_save). Issue #68
and PR #81 both flag this as a decision point: stub the SDK calls, or split the file. I stubbed
(tests/stubs/pebble.h supplies just the persist_exists/get_size/read_data/write_data surface
page.c touches, backed by an in-memory fake). page.c compiles completely unmodified this way —
it's not aware it's being tested — which seemed simpler to keep in sync than maintaining a second
persistence-free copy of the file. This also made page_store_load/save's fallback-to-defaults
behaviour testable for free, so I included it as bonus coverage even though it wasn't explicitly
asked for tonight.

Covered (44 assertions, 2 binaries)

  • Field store: set, read, absent-renders---, zero-is-not-absent
  • Per-type sentinel handling: u8/u16/u32 sentinels each mark unavailable; zero is never mistaken
    for any of them
  • Formatter table: every non-blank FieldId (22 of them) formats correctly and renders --
    when unavailable — metric only. fields.c as merged has no imperial unit support (that's #44,
    still open); I did not fabricate imperial formatting to satisfy #68's "metric and imperial"
    bullet — noted as blocked below instead.
  • Staleness: tests the actual group-heartbeat model PR #81 built (D44) — greys past the group's
    threshold, recovers on a fresh touch, stays false while a field is unavailable (not merely
    untouched), stays false before the group has ever been touched, ignores a backwards clock, and a
    constant value under changed-keys-only updates stays fresh as long as its group keeps ticking.
  • Descriptor pack/unpack: byte-layout check, round-trips every default page, rejects an
    out-of-range template id, rejects an unknown field id in any of the 6 slots, rejects an all-0xFF
    malformed blob. (page_descriptor_unpack's signature is a fixed
    uint8_t[PAGE_DESCRIPTOR_WIRE_SIZE] — there's no length to get "wrong" at this API, so the
    issue's "wrong slot counts" phrase doesn't apply until whatever decodes a variable-length
    CONFIG_PAGES payload into this fixed array exists (#61); noted in a code comment, not tested.)
  • page_has_live_field (including that it counts a merely-stale field as live, not just an
    available one) and page_default_descriptor.

Deliberately deferred — not built or faked here

  • Template geometry / slot bounds on emery, gabbro, basalt — #59, not yet built
  • Page carousel skip rules ("sourceless page skipped", "last enabled page can't be skipped away") —
    #60, not yet built
  • Map projection anchor-relative offset math — #42, not yet built
  • Imperial unit formatting — #44, not yet built

Verification

No cmake in this sandbox, so I couldn't run the CTest path directly. I built and ran both test
binaries by hand with the same sources/flags the CMakeLists.txt declares:

gcc -std=c11 -Wall -Wextra -Werror -Iwatchapp/src/c -Iwatchapp/tests   watchapp/tests/test_fields.c watchapp/src/c/fields.c -o test_fields
gcc -std=c11 -Wall -Wextra -Werror -Iwatchapp/src/c -Iwatchapp/tests/stubs -Iwatchapp/tests   watchapp/tests/test_page.c watchapp/src/c/page.c watchapp/src/c/fields.c   watchapp/tests/stubs/pebble_stub.c -o test_page

Both compile clean under -Wall -Wextra -Werror and both pass clean (28 + 16 = 44 assertions, 0
failures, exit 0). I then deliberately broke one assertion's expected value and reran: it failed
loudly with a file:line message and a nonzero exit code, confirming the harness doesn't silently
swallow a failure.

watchapp/wscript's pbl_build only globs src/c/**/*.c, so watchapp/tests/ doesn't affect the
real pebble build/pebble-build CI job.

Since no runner is registered yet (per tonight's other PRs), host-c-tests hasn't actually executed
in CI for this PR — only the skip-guard logic and local build were verified.

https://claude.ai/code/session_01DAoXbRmJUf2uxNYBfdAXPt

## What this is Partial progress on #68 ("Host-side unit tests for the watchapp logic"), scoped down to what currently exists in `watchapp/src/c/` after #58/PR #81 (fields.c/fields.h, page.c/page.h). Several of #68's own acceptance criteria depend on issues that haven't landed yet — see "Deferred" below. This PR does not close #68; a comment on the issue lists exactly what's covered vs. still blocked. ## Layout - `watchapp/tests/CMakeLists.txt` — host build, two CTest binaries (`test_fields`, `test_page`) - `watchapp/tests/harness.h` — dependency-free assertion harness (~90 lines). No C test framework, per the issue's own instruction. Analogue for a C#/xUnit or Jest background: `TH_ASSERT_*` macros play the role `Assert.*`/`expect()` play, plus a `TH_RUN`/`TH_REPORT` runner convention. - `watchapp/tests/stubs/pebble.h` + `pebble_stub.c` — a fake in-memory `persist_*` backing. **Note on path**: I used `watchapp/tests/` (plural), not `watchapp/test/` as #68's own "Files" section names it. `.forgejo/workflows/fast-lane.yml`'s `host-c-tests` job checks specifically for `watchapp/tests/CMakeLists.txt` (plural) as its skip guard, and getting that job to activate is the highest-value part of this issue — so I matched the workflow rather than the issue text. Confirmed: that guard now finds this CMakeLists.txt and the job will build+run it once a runner is registered (no runner is registered yet per tonight's other PRs, so this hasn't executed for real in CI, only verified locally — see below). ## page.c: stubbed, not split page.c mixes pure logic with two `persist_*` calls (`page_store_load`/`page_store_save`). Issue #68 and PR #81 both flag this as a decision point: stub the SDK calls, or split the file. I stubbed (`tests/stubs/pebble.h` supplies just the `persist_exists/get_size/read_data/write_data` surface page.c touches, backed by an in-memory fake). page.c compiles **completely unmodified** this way — it's not aware it's being tested — which seemed simpler to keep in sync than maintaining a second persistence-free copy of the file. This also made `page_store_load`/`save`'s fallback-to-defaults behaviour testable for free, so I included it as bonus coverage even though it wasn't explicitly asked for tonight. ## Covered (44 assertions, 2 binaries) - **Field store**: set, read, absent-renders-`--`, zero-is-not-absent - **Per-type sentinel handling**: u8/u16/u32 sentinels each mark unavailable; zero is never mistaken for any of them - **Formatter table**: every non-blank `FieldId` (22 of them) formats correctly and renders `--` when unavailable — **metric only**. fields.c as merged has no imperial unit support (that's #44, still open); I did not fabricate imperial formatting to satisfy #68's "metric and imperial" bullet — noted as blocked below instead. - **Staleness**: tests the actual group-heartbeat model PR #81 built (D44) — greys past the group's threshold, recovers on a fresh touch, stays false while a field is unavailable (not merely untouched), stays false before the group has ever been touched, ignores a backwards clock, and a constant value under changed-keys-only updates stays fresh as long as its group keeps ticking. - **Descriptor pack/unpack**: byte-layout check, round-trips every default page, rejects an out-of-range template id, rejects an unknown field id in any of the 6 slots, rejects an all-0xFF malformed blob. (`page_descriptor_unpack`'s signature is a fixed `uint8_t[PAGE_DESCRIPTOR_WIRE_SIZE]` — there's no length to get "wrong" at this API, so the issue's "wrong slot counts" phrase doesn't apply until whatever decodes a variable-length CONFIG_PAGES payload into this fixed array exists (#61); noted in a code comment, not tested.) - `page_has_live_field` (including that it counts a merely-stale field as live, not just an available one) and `page_default_descriptor`. ## Deliberately deferred — not built or faked here - Template geometry / slot bounds on emery, gabbro, basalt — **#59**, not yet built - Page carousel skip rules ("sourceless page skipped", "last enabled page can't be skipped away") — **#60**, not yet built - Map projection anchor-relative offset math — **#42**, not yet built - Imperial unit formatting — **#44**, not yet built ## Verification No `cmake` in this sandbox, so I couldn't run the CTest path directly. I built and ran both test binaries by hand with the same sources/flags the CMakeLists.txt declares: ``` gcc -std=c11 -Wall -Wextra -Werror -Iwatchapp/src/c -Iwatchapp/tests watchapp/tests/test_fields.c watchapp/src/c/fields.c -o test_fields gcc -std=c11 -Wall -Wextra -Werror -Iwatchapp/src/c -Iwatchapp/tests/stubs -Iwatchapp/tests watchapp/tests/test_page.c watchapp/src/c/page.c watchapp/src/c/fields.c watchapp/tests/stubs/pebble_stub.c -o test_page ``` Both compile clean under `-Wall -Wextra -Werror` and both pass clean (28 + 16 = 44 assertions, 0 failures, exit 0). I then deliberately broke one assertion's expected value and reran: it failed loudly with a `file:line` message and a nonzero exit code, confirming the harness doesn't silently swallow a failure. `watchapp/wscript`'s `pbl_build` only globs `src/c/**/*.c`, so `watchapp/tests/` doesn't affect the real `pebble build`/`pebble-build` CI job. Since no runner is registered yet (per tonight's other PRs), `host-c-tests` hasn't actually executed in CI for this PR — only the skip-guard logic and local build were verified. https://claude.ai/code/session_01DAoXbRmJUf2uxNYBfdAXPt
Add host-side unit tests for fields.c and page.c (#68, partial)
Some checks failed
dev-artifact / build-pbw (push) Has been cancelled
dev-artifact / build-apk (push) Has been cancelled
dev-artifact / publish (push) Has been cancelled
fast-lane / host-c-tests (push) Has been cancelled
fast-lane / jvm-tests (push) Has been cancelled
fast-lane / pebble-build (push) Has been cancelled
fast-lane / lint-and-secrets (push) Has been cancelled
fast-lane / meta-declares-required-jobs (push) Has been cancelled
fast-lane / host-c-tests (pull_request) Has been cancelled
fast-lane / jvm-tests (pull_request) Has been cancelled
fast-lane / pebble-build (pull_request) Has been cancelled
fast-lane / lint-and-secrets (pull_request) Has been cancelled
fast-lane / meta-declares-required-jobs (pull_request) Has been cancelled
fab9d2584f
Host build of the currently-existing testable watchapp C logic from #58/PR
#81: the field value store, formatter table, group-heartbeat staleness rule,
and page descriptor pack/unpack. A dependency-free assertion harness
(watchapp/tests/harness.h) plus a fake persist_* backing
(watchapp/tests/stubs/) let page.c compile completely unmodified for the
host, rather than splitting it into a persistence-free core and an
SDK-touching shell.

44 assertions across two CTest binaries (test_fields, test_page), verified
by hand with plain gcc -Wall -Wextra -Werror -std=c11 (no cmake available
in this sandbox) and confirmed to fail loudly (nonzero exit, file:line) on
a deliberately broken assertion.

watchapp/tests/CMakeLists.txt existing activates fast-lane.yml's
host-c-tests job, which currently skips itself on that file's absence.

Covered:
- Field store: set, read, absent-renders-"--", zero-is-not-absent
- Per-type sentinel handling (u8/u16/u32), zero never mistaken for sentinel
- Formatter table: every FieldId, metric only (fields.c has no imperial
  support yet - #44)
- Staleness: group-heartbeat model (D44) - greys past threshold, recovers,
  never stale while unavailable, a constant value under changed-keys-only
  updates stays fresh
- Descriptor pack/unpack: round-trips every default page, rejects
  out-of-range template ids and unknown field ids in any slot, rejects an
  all-0xFF malformed blob
- page_has_live_field, page_default_descriptor
- Bonus (free from the persist stub): page_store_load/save fallback-to-
  defaults behaviour

Deliberately deferred (issues not yet built - noted in the PR, not faked
here): template geometry (#59), carousel skip rules (#60), map projection
(#42), imperial units (#44).

Uses watchapp/tests/ (plural) to match fast-lane.yml's existing skip guard,
not watchapp/test/ as originally written in #68's own "Files" section.

Claude-Session: https://claude.ai/code/session_01DAoXbRmJUf2uxNYBfdAXPt
robert merged commit 7aa728b8cc into main 2026-09-04 00:57:49 +02:00
Sign in to join this conversation.
No description provided.