ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
check_hil_alive_policy.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# SPDX-License-Identifier: MIT
3# Copyright (c) 2026 Brighton Sikarskie
4"""Enforce the HIL "honest contract" gate.
5
6Every hil.conf under examples/ek_ra8d2/hw_validated/hil/ must declare
7HIL_MODE to one of:
8 - uart_scrape (UART success-banner assertion)
9 - usb_cdc (Pi-side USB host probe)
10 - usb_hid (Pi-side USB HID host-binding probe)
11 - usb_msc (Pi-side USB Mass Storage SCSI-attach probe)
12 - jlink_memprobe (named-symbol counter advance via SWD)
13 - hil_eth_tcp (Pi-as-peer TCP/UDP/HTTP probe)
14 - rtt_scrape (SEGGER RTT banner assertion via JLink mem dump)
15 - alive ONLY if HIL_FAULT_EXPECTED=1 is also set
16 (legitimate fault-recovery test path -- the
17 handler is required to latch CFSR != 0 and the
18 app must emit a positive UART banner that is
19 not in hil_check_alive.sh's negative regex)
20
21Plain HIL_MODE=alive without HIL_FAULT_EXPECTED is the historical
22"PC happens to be in MRAM" loose check; it lets silently-broken apps
23pass CI and is forbidden under hw_validated/hil/. Apps that genuinely
24have no observable signal yet (panic-halt at init, no
25instrumentation) must live in hw_pending/ until they are fixed AND
26instrumented.
27
28Exit:
29 0 every hil.conf in hw_validated/hil/ satisfies the policy
30 1 one or more hil.confs violate
31 2 usage / unreachable repo root, or a collapsed scan (see HIL_CONF_FLOOR)
32"""
33
34from __future__ import annotations
35
36import pathlib
37import re
38import sys
39import tempfile
40from collections.abc import Iterable
41
42# A hw_validated HIL suite this size cannot legitimately collapse to a handful
43# of apps. If the glob returns less than this, something broke (a bad repo
44# root, a renamed hil/ directory) and reporting "0 findings" would be a lie:
45# every app would be trivially compliant because none was read. Measured
46# 2026-07-28: 114 hil.conf files. Same trip-wire as check_ruff.py.
47HIL_CONF_FLOOR = 90
48
49ALLOWED_MODES: frozenset[str] = frozenset(
50 {
51 "uart_scrape",
52 "usb_cdc",
53 "usb_hid",
54 "usb_msc",
55 "jlink_memprobe",
56 "hil_eth_tcp",
57 "rtt_scrape",
58 }
59)
60
61
62def _iter_hil_confs(repo_root: pathlib.Path) -> Iterable[pathlib.Path]:
63 """Yield every hil.conf under examples/ek_ra8d2/hw_validated/hil/."""
64 hil_dir = repo_root / "examples" / "ek_ra8d2" / "hw_validated" / "hil"
65 if not hil_dir.is_dir():
66 return ()
67 return hil_dir.glob("*/hil.conf")
68
69
70def _parse_kv(conf: pathlib.Path) -> dict[str, str]:
71 """Extract simple KEY=VALUE lines (no shell expansion) from a hil.conf."""
72 out: dict[str, str] = {}
73 pat = re.compile(r"^\s*([A-Z_][A-Z_0-9]*)\s*=\s*(.*?)\s*$")
74 for raw in conf.read_text().splitlines():
75 line = raw.split("#", 1)[0].rstrip()
76 m = pat.match(line)
77 if not m:
78 continue
79 key, val = m.group(1), m.group(2)
80 if val.startswith(('"', "'")) and val.endswith(val[0]) and len(val) >= 2: # noqa: PLR2004 # min quoted-string length
81 val = val[1:-1]
82 out[key] = val
83 return out
84
85
86def _policy_violations(repo_root: pathlib.Path, confs: Iterable[pathlib.Path]) -> list[str]:
87 """Return one honest-contract violation for each non-asserting config."""
88 violations: list[str] = []
89 for conf in confs:
90 kv = _parse_kv(conf)
91 mode = kv.get("HIL_MODE", "")
92 fault_expected = kv.get("HIL_FAULT_EXPECTED", "0")
93 app = conf.parent.name
94
95 if mode in ALLOWED_MODES or (mode == "alive" and fault_expected == "1"):
96 continue
97 if mode == "alive":
98 violations.append(
99 f"{conf.relative_to(repo_root)}: HIL_MODE=alive without "
100 f"HIL_FAULT_EXPECTED=1 is forbidden under "
101 f"hw_validated/hil/. Either:\n"
102 f" - instrument the app (g_<x>_match symbol + "
103 f"HIL_MODE=jlink_memprobe), OR\n"
104 f" - add a UART success banner + HIL_MODE=uart_scrape, OR\n"
105 f" - move the app to examples/ek_ra8d2/hw_pending/{app}/."
106 )
107 elif mode == "":
108 violations.append(f"{conf.relative_to(repo_root)}: missing HIL_MODE")
109 else:
110 violations.append(
111 f"{conf.relative_to(repo_root)}: HIL_MODE={mode!r} is not "
112 f"in the allowed set "
113 f"({[*sorted(ALLOWED_MODES), 'alive (with HIL_FAULT_EXPECTED=1)']})"
114 )
115 return violations
116
117
118def selftest() -> int:
119 """Prove asserting modes pass and every loose/unknown form is rejected."""
120 with tempfile.TemporaryDirectory(prefix="hil-alive-policy-selftest-") as raw:
121 root = pathlib.Path(raw)
122
123 def conf(name: str, text: str) -> pathlib.Path:
124 path = root / name / "hil.conf"
125 path.parent.mkdir()
126 path.write_text(text, encoding="ascii")
127 return path
128
129 good = [
130 conf("uart", "HIL_MODE=uart_scrape\n"),
131 conf("fault", "HIL_MODE=alive\nHIL_FAULT_EXPECTED=1\n"),
132 ]
133 bad = [
134 conf("loose", "HIL_MODE=alive\n"),
135 conf("missing", "HIL_FAULT_EXPECTED=1\n"),
136 conf("unknown", "HIL_MODE=not_a_probe\n"),
137 ]
138 good_findings = _policy_violations(root, good)
139 bad_findings = _policy_violations(root, bad)
140 cases = (
141 (not good_findings, "asserting and fault-expected modes stay quiet"),
142 (len(bad_findings) == len(bad), "loose, missing, and unknown modes fire"),
143 )
144 failed = [label for passed, label in cases if not passed]
145 for passed, label in cases:
146 print(f" [{'ok' if passed else 'FAIL'}] {label}")
147 if failed:
148 print(f"check_hil_alive_policy.py --selftest: {len(failed)} failure(s)", file=sys.stderr)
149 return 1
150 print("check_hil_alive_policy.py --selftest: all cases pass (both directions).")
151 return 0
152
153
154def main() -> int:
155 """Fail any hw_validated HIL app whose hil.conf does not assert a real outcome.
156
157 The rule being enforced is that ``HIL_MODE=alive`` proves only that the
158 board did not hang -- it cannot distinguish a working app from one that
159 booted and did nothing. It is therefore allowed under hw_validated/ only
160 when ``HIL_FAULT_EXPECTED=1``, i.e. when not faulting IS the assertion.
161 Any other app claiming validation must scrape a banner or probe a symbol,
162 or move to hw_pending/.
163
164 A missing HIL_MODE is treated as a violation rather than a default, since
165 a silently defaulted mode is how an unasserted app would slip in.
166
167 Enforces HIL_CONF_FLOOR before reading anything and exits 2 below it. An
168 empty glob would otherwise report "0 findings" -- indistinguishable from a
169 fully compliant suite, and produced by the scan having read nothing.
170
171 Returns 1 with one remediation-bearing message per offending hil.conf, 0
172 when every conf under hw_validated/hil/ declares an asserting mode, 2 when
173 the enumeration is too small to trust.
174 """
175 args = sys.argv[1:]
176 if args == ["--selftest"]:
177 return selftest()
178 if args:
179 print("usage: check_hil_alive_policy.py [--selftest]", file=sys.stderr)
180 return 2
181 repo_root = pathlib.Path(__file__).resolve().parents[2]
182
183 confs = sorted(_iter_hil_confs(repo_root))
184 if len(confs) < HIL_CONF_FLOOR:
185 sys.stderr.write(
186 f"check_hil_alive_policy.py: FATAL -- only {len(confs)} hil.conf file(s) in "
187 f"scope, floor is {HIL_CONF_FLOOR}.\n"
188 " A collapsed scope reports a compliant suite because it checked nothing.\n"
189 )
190 return 2
191
192 violations = _policy_violations(repo_root, confs)
193
194 if violations:
195 sys.stderr.write(
196 f"check_hil_alive_policy.py: {len(violations)} violation(s) in hw_validated/hil/:\n"
197 )
198 for v in violations:
199 sys.stderr.write(f" - {v}\n")
200 sys.stderr.write(
201 "\nThe HIL 'honest contract' rule: every app under "
202 "hw_validated/hil/ must prove its feature works end-to-end on "
203 "real hardware, not just that the chip booted.\n"
204 )
205 return 1
206
207 print(f"check_hil_alive_policy.py: 0 findings across {len(confs)} hil.conf file(s).")
208 return 0
209
210
211if __name__ == "__main__":
212 sys.exit(main())
void main(void)
The application entry point Reset_Handler hands control to.
Definition main.c:298