Skip to content

ADR-002: Add a Host-Side pytest-bdd Acceptance Tier via the Mocked-HAL Test Double

Field Value
Date 2026-09-29
Status Accepted
Deciders TODO
Supersedes —
Superseded by —

Context and Problem Statement

A host-side build of src/app.cpp against a mocked HAL (test/mocks/hal_mock.hpp) is tested with Catch2, while on-target/HIL testing stays deferred. The .feature files in test/bdd/features/ hold the scenarios derived from the acceptance criteria of the capabilities, but nothing executes them by itself: there is no running process for pytest-bdd to poll over HTTP (unlike service), and no end-user binary to invoke (unlike tool). Without an acceptance tier of its own, this repository's .feature files would stay pure documentation, with nothing to notice if the implementation drifted away from what they describe.

The obvious candidate for a host-side harness is src/app_c_api.h/app_c_api.cpp — the extern "C" bridge that vendor/cubemx/Core/Src/main.c calls on real hardware. It looks attractive because it exercises the exact entry points production code calls, not a re-implementation of them. But app_c_api.cpp wires a concrete TargetHal (see its own TODO comment). That class exists to grow real calls into CubeMX-generated HAL functions (e.g. HAL_GPIO_WritePin), which need CubeMX/vendor headers only available when cross-compiling for the target MCU. The moment TargetHal gets its first real implementation — the intended next step for this firmware, not an edge case — app_c_api.cpp stops compiling on the host at all. A harness built on top of it would work only in the initial, not-yet-implemented scaffold state and break as soon as the project does what it was scaffolded to do next.

Decision Drivers

  • Reuse, not replace, the existing host-side mocked-HAL build — no second Hal double, no separate build configuration.
  • Must stay host-compilable indefinitely, regardless of how much real hardware logic TargetHal grows — ruling out anything that links app_c_api.cpp.
  • Must not imply this substitutes for on-target/HIL testing — that stays deferred.

Considered Options

  1. No acceptance tier beyond the existing Catch2 app_test.cpp.
  2. A thin, subprocess-drivable harness executable (test/bdd/harness/main.cpp) that links app_c_api.cpp and calls app_init()/app_poll() through app_c_api.h, exercising TargetHal's production wiring directly.
  3. A thin, subprocess-drivable harness executable (test/bdd/harness/main.cpp) that wires App to HalMock directly — the same test double app_test.cpp already uses — built alongside app_test, driven by pytest-bdd via subprocess exactly like the library/tool acceptance tiers.
  4. Introduce on-target/HIL testing with pytest-bdd now.

Decision Outcome

Chosen option: 3. test/bdd/harness/main.cpp wires App to HalMock — identical to app_test.cpp's setup — calls app.init()/app.poll(), prints an observable result, and exits 0. test/CMakeLists.txt compiles it alongside app_test, linking app plus the same test/mocks/hal_mock.hpp header app_test uses. pytest-bdd drives the built bdd_harness binary via subprocess, the same pattern as the library/tool acceptance tiers.

Trigger to revisit: the introduction of on-target/HIL testing — this host-side harness is additive and does not replace it.

Positive Consequences

  • Closes the same spec/test drift gap already closed for service/tool/library, now here too.
  • No second Hal double: reuses the same HalMock app_test.cpp already uses, rather than introducing a third Hal implementation.
  • Stays host-compilable indefinitely: HalMock has no reason to ever call real hardware, unlike TargetHal, so this harness cannot be broken by this firmware's own intended evolution.
  • Structurally cannot affect the ARM cross-build under firmware/: test/ is never referenced by firmware/CMakeLists.txt.

Negative Consequences / Risks

  • Adds pytest-bdd (Python) as a devbox dependency, the same cost service/tool/library already carry for their own acceptance tiers.
  • Does not verify the app_c_api.h extern "C" bridge or TargetHal's real hardware calls — only App's logic against a mock, exactly like the Catch2 tier it sits alongside. That bridge is exercised by the ARM cross-build (make firmware-build) instead, the only place CubeMX's real HAL headers are available.
  • Limitations: this harness only proves hardware-independent logic against the mocked HAL — real timing, interrupt latency, and actual peripheral behaviour remain unverified by it, exactly as for the Catch2 tier. Not every scenario in test/bdd/features/ maps sensibly onto the mocked HAL either; such scenarios stay documentation without a test binding until a genuine on-target/HIL tier exists.

Pros and Cons of the Options

Option 1 — No acceptance tier beyond Catch2

  • Pro: Simplest option; no new devbox dependency.
  • Con: Leaves the spec/test drift gap open here, inconsistent with how service/tool/ library handle it.

Option 2 — Host-side app_c_api harness, subprocess-driven

  • Pro: Would exercise the exact production entry points app_c_api.h exposes, not just App's logic in isolation.
  • Con: Only works while TargetHal is still the empty scaffold placeholder (see Context) — breaks on the host the moment TargetHal gets its real CubeMX HAL implementation.

Option 3 — Host-side HalMock harness, subprocess-driven (chosen)

  • Pro: Reuses the existing mocked-HAL host build and test double unchanged; stays host-compilable no matter how TargetHal evolves; consistent with the other project types' acceptance-tier pattern.
  • Con: Still only a host-side smoke test — see Limitations above — and does not itself prove the app_c_api bridge builds (the ARM cross-build already covers that).

Option 4 — On-target/HIL testing now

  • Pro: Would close the timing/interrupt/peripheral gap this ADR's harness cannot.
  • Con: Contradicts the deferral of on-target/HIL testing without a concrete trigger having been hit; disproportionate investment at this stage.

Notes

This ADR concerns the host-side build only. It complements the Catch2 tier — the two stay structurally identical (same App, same HalMock), differing only in how they are driven: Catch2 macros in-process for app_test, pytest-bdd over a subprocess for bdd_harness.