ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
suppression_rebind_selftest.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"""Both-direction selftests for sanctioned suppression ledger operations.
5
6Imported by suppression_rebind.selftest() through a module binding so
7neither direction executes partially initialized names at import time.
8"""
9
10from __future__ import annotations
11
12import hashlib
13from dataclasses import replace
14
15import suppression_rebind as core
16from suppression_identity import binding_payload
17from suppression_ledger import LedgerRow
18from suppression_model import Suppression
19
20
21def _fixture_row(site: str, binding: str, ref: str, state: str = "retain") -> LedgerRow:
22 """Build one synthetic ledger row."""
23 return LedgerRow(2, site, binding, state, "r", "batch-1", ref)
24
25
26def _fixture_evidence(raw: object) -> tuple[str, ...]:
27 """Coerce fixture evidence to a string tuple."""
28 if isinstance(raw, tuple) and all(isinstance(item, str) for item in raw):
29 return raw
30 return ("decision-line:11", "standard:x")
31
32
33def _fixture_record(**fields: object) -> Suppression:
34 """Build one synthetic inventory record with a consistent binding."""
35 record = Suppression(
36 path=str(fields.get("path", "src/a.c")),
37 line=int(fields.get("line", 10)),
38 column=int(fields.get("column", 1)),
39 family=str(fields.get("family", "mcdc-deactivation")),
40 tool=str(fields.get("tool", "llvm-cov")),
41 rule=str(fields.get("rule", "deactivated-condition")),
42 directive=str(fields.get("directive", "mcdc-deactivated")),
43 scope=str(fields.get("scope", "decision-line:11")),
44 provenance=str(fields.get("provenance", "inline-comment")),
45 reason=str(fields.get("reason", "operands cannot vary")),
46 owner=str(fields.get("owner", "first-party")),
47 concerns=(),
48 evidence=_fixture_evidence(fields.get("evidence")),
49 match_count=int(fields.get("match_count", 1)),
50 recommendation=str(fields.get("recommendation", "revalidate-invariant")),
51 anchor=str(fields.get("anchor", "// mcdc-deactivated:")),
52 )
53 binding = hashlib.sha256(binding_payload(record, record.anchor)).hexdigest()
54 return replace(record, site_id=str(fields.get("site_id", "a" * 64)), binding_sha256=binding)
55
56
57def _selftest_move() -> list[str]:
58 """Prove a harmless move rebinds and unrelated rows survive."""
59 failures: list[str] = []
60 record = _fixture_record()
61 old_binding = hashlib.sha256(
62 binding_payload(
63 replace(
64 record,
65 scope="decision-line:19",
66 evidence=("decision-line:19", "standard:x"),
67 ),
68 record.anchor,
69 )
70 ).hexdigest()
71 ref = "review-decision src/a.c:18 mcdc-deactivated: operands cannot vary."
72 row = _fixture_row(record.site_id, old_binding, ref)
73 other = _fixture_row("b" * 64, "c" * 64, "review-decision other.")
74 plan, reason = core.plan_rebind([record], [row, other], record.site_id)
75 if plan is None:
76 return [f"harmless move refused: {reason}"]
77 if (plan.old_refs, plan.new_refs) != (("19",), ("11",)):
78 failures.append("move plan reports wrong decision refs")
79 if (plan.marker_old, plan.marker_new) != ("18", "10"):
80 failures.append("move plan reports wrong marker refs")
81 if plan.new_binding != record.binding_sha256 or plan.old_binding != old_binding:
82 failures.append("move plan reports wrong bindings")
83 header = "site_id\tbinding_sha256\tstate\trationale_id\tbatch_id\tevidence_ref\n"
84 ledger = (
85 f"{header}"
86 f"{record.site_id}\t{old_binding}\tretain\tr\tbatch-1\t{ref}\n"
87 f"{'b' * 64}\t{'c' * 64}\tretain\tr\tbatch-1\treview-decision other.\n"
88 )
89 batches = (
90 "batches:\n"
91 " - id: batch-1\n"
92 " authority: repository-suppression-review\n"
93 " date: 2026-09-12\n"
94 " identity_schema: 2-durable-site-identity\n"
95 " assigned_rows: 2\n"
96 f" rows_sha256: {core.batch_digest([row, other])}\n"
97 )
98 result = core.apply_plan(ledger, batches, plan)
99 if isinstance(result, str):
100 return [f"harmless move apply refused: {result}"]
101 new_ledger, new_batches = result
102 if f"b{'b' * 63}" not in new_ledger or "review-decision other." not in new_ledger:
103 failures.append("unrelated ledger row changed")
104 old_lines = ledger.split("\n")
105 new_lines = new_ledger.split("\n")
106 if [line for line in new_lines if record.site_id not in line] != [
107 line for line in old_lines if record.site_id not in line
108 ]:
109 failures.append("non-target ledger bytes changed")
110 if record.binding_sha256 not in new_ledger or "src/a.c:10" not in new_ledger:
111 failures.append("rebound row missing new binding or line ref")
112 if "assigned_rows: 2" not in new_batches:
113 failures.append("batch member count changed")
114 return failures
115
116
117def _selftest_retire() -> list[str]:
118 """Prove stale successions retire and bad ones refuse."""
119 failures: list[str] = []
120 live = _fixture_record(
121 path="scripts/checks/suppression_rebind_selftest.py", line=20, scope="decision-line:20"
122 )
123 stale_ref = (
124 "review-decision scripts/checks/suppression_rebind_selftest.py:19 "
125 "mcdc-deactivated: operands vary."
126 )
127 stale = LedgerRow(2, "d" * 64, live.binding_sha256, "retain", "r", "batch-1", stale_ref)
128 req = core.RetireRequest(
129 "d" * 64,
130 live.site_id,
131 "scripts/checks/suppression_rebind_selftest.py",
132 "mcdc-deactivated",
133 "doc move",
134 )
135 plan, reason = core.plan_retire([live], [stale], req)
136 if plan is None:
137 return [f"stale succession refused: {reason}"]
138 if plan.successor != live.site_id or plan.batch_id != "batch-1":
139 failures.append("retire plan misreports successor or batch")
140 probe_req = core.RetireRequest(
141 "d" * 64,
142 live.site_id,
143 "scripts/checks/suppression_ledger.py",
144 "mcdc-deactivated",
145 "doc move",
146 )
147 probe, _ = core.plan_retire([live], [stale], probe_req)
148 if probe is not None:
149 failures.append("coords mismatch retires")
150 ledger = (
151 "site_id\tbinding_sha256\tstate\trationale_id\tbatch_id\tevidence_ref\n"
152 f"{'d' * 64}\t{live.binding_sha256}\tretain\tr\tbatch-1\t{stale_ref}\n"
153 )
154 batches = (
155 "batches:\n"
156 " - id: batch-1\n"
157 " authority: repository-suppression-review\n"
158 " date: 2026-09-12\n"
159 " identity_schema: 2-durable-site-identity\n"
160 " assigned_rows: 1\n"
161 f" rows_sha256: {core.batch_digest([stale])}\n"
162 )
163 result = core.apply_retire(ledger, batches, plan)
164 if isinstance(result, str):
165 return [f"stale apply refused: {result}"]
166 new_ledger, _ = result
167 if "\tsuperseded\tsuperseded-identity-rebinding\t" not in new_ledger:
168 failures.append("retire did not flip state and rationale")
169 tokens = new_ledger.split()
170 if f"replaced-by:{live.site_id}" not in tokens:
171 failures.append("retire did not record a clean successor link")
172 live_req = core.RetireRequest(live.site_id, live.site_id, "src/a.c", "m", "x")
173 probe, _ = core.plan_retire([live], [stale], live_req)
174 if probe is not None:
175 failures.append("live site retires")
176 return failures
177
178
179def _selftest_relink_legacy(
180 failures: list[str], case: tuple[Suppression, LedgerRow, LedgerRow, str, str]
181) -> None:
182 """Prove legacy separators normalize during relink."""
183 live, row, dead, ref, batches = case
184 legacy_ref = ref + ";;;"
185 legacy_row = _fixture_row("e" * 64, live.binding_sha256, legacy_ref, state="superseded")
186 legacy_req = core.RelinkRequest(
187 "e" * 64, "d" * 64, live.site_id, "src/a.c", "mcdc-deactivated", "x"
188 )
189 legacy_plan, legacy_reason = core.plan_relink([live], [legacy_row, dead], legacy_req)
190 if legacy_plan is None:
191 failures.append(f"legacy separators refuse to plan: {legacy_reason}")
192 else:
193 legacy_ledger = (
194 "site_id\tbinding_sha256\tstate\trationale_id\tbatch_id\tevidence_ref\n"
195 f"{'e' * 64}\t{live.binding_sha256}\tsuperseded\tr\tbatch-1\t{legacy_ref}\n"
196 )
197 legacy_batches = batches.replace(core.batch_digest([row]), core.batch_digest([legacy_row]))
198 legacy_result = core.apply_relink(legacy_ledger, legacy_batches, legacy_plan)
199 if isinstance(legacy_result, str):
200 failures.append(f"legacy separators refuse to apply: {legacy_result}")
201 elif f"replaced-by:{live.site_id}" not in legacy_result[0].split():
202 failures.append("legacy separators did not normalize to a clean link")
203
204
205def _selftest_relink() -> list[str]:
206 """Prove dangling links refresh and bad ones refuse."""
207 failures: list[str] = []
208 live = _fixture_record()
209 ref = (
210 f"review-decision src/a.c:19 mcdc-deactivated: operands cannot vary. replaced-by:{'d' * 64}"
211 )
212 row = _fixture_row("e" * 64, live.binding_sha256, ref, state="superseded")
213 dead = _fixture_row("d" * 64, live.binding_sha256, ref)
214 rows = [row, dead]
215 req = core.RelinkRequest("e" * 64, "d" * 64, live.site_id, "src/a.c", "mcdc-deactivated", "x")
216 plan, reason = core.plan_relink([live], rows, req)
217 if plan is None:
218 return [f"dangling link refused: {reason}"]
219 if plan.new_target != live.site_id:
220 failures.append("relink plan misreports the successor")
221 ledger = (
222 "site_id\tbinding_sha256\tstate\trationale_id\tbatch_id\tevidence_ref\n"
223 f"{'e' * 64}\t{live.binding_sha256}\tsuperseded\tr\tbatch-1\t{ref}\n"
224 )
225 batches = (
226 "batches:\n"
227 " - id: batch-1\n"
228 " authority: repository-suppression-review\n"
229 " date: 2026-09-12\n"
230 " identity_schema: 2-durable-site-identity\n"
231 " assigned_rows: 1\n"
232 f" rows_sha256: {core.batch_digest([row])}\n"
233 )
234 result = core.apply_relink(ledger, batches, plan)
235 if isinstance(result, str):
236 return [f"relink apply refused: {result}"]
237 new_ledger, _ = result
238 if f"replaced-by:{live.site_id}" not in new_ledger:
239 failures.append("relink did not refresh the successor link")
240 if f"replaced-by:{'d' * 64}" in new_ledger:
241 failures.append("relink left the dead link behind")
242 _selftest_relink_legacy(failures, (live, row, dead, ref, batches))
243 stray = _fixture_row(
244 "d" * 64,
245 live.binding_sha256,
246 "review scripts/checks/suppression_model.py:9 directive:other-kind rule:x.",
247 )
248 stray_req = core.RelinkRequest("e" * 64, "d" * 64, live.site_id, "src/a.c", "m", "x")
249 plan, _ = core.plan_relink([live], [row, stray], stray_req)
250 if plan is not None:
251 failures.append("stale old-target coordinates relink")
252 return failures
253
254
255def _selftest_restore() -> list[str]:
256 """Prove live resolved rows reinstate and drift refuses."""
257 failures: list[str] = []
258 record = _fixture_record()
259 legacy = f"history replaced-by:{'e' * 64}; retirement note."
260 row = LedgerRow(2, record.site_id, record.binding_sha256, "resolved", "r", "batch-1", legacy)
261 rationales = {"r": {"state": "retain"}}
262 plan, reason = core.plan_restore(
263 [record], [row], rationales, core.RestoreRequest(record.site_id, "r", "back")
264 )
265 if plan is None:
266 return [f"live resolved row refused: {reason}"]
267 ledger = (
268 "site_id\tbinding_sha256\tstate\trationale_id\tbatch_id\tevidence_ref\n"
269 f"{record.site_id}\t{record.binding_sha256}\tresolved\tr\tbatch-1\t{legacy}\n"
270 )
271 batches = (
272 "batches:\n"
273 " - id: batch-1\n"
274 " authority: repository-suppression-review\n"
275 " date: 2026-09-12\n"
276 " identity_schema: 2-durable-site-identity\n"
277 " assigned_rows: 1\n"
278 f" rows_sha256: {core.batch_digest([row])}\n"
279 )
280 result = core.apply_restore(ledger, batches, plan)
281 if isinstance(result, str):
282 return [f"restore apply refused: {result}"]
283 new_ledger, _ = result
284 if "\tretain\tr\t" not in new_ledger or "reinstated: back" not in new_ledger:
285 failures.append("restore did not flip state and rationale")
286 if "retirement note. reinstated: back" not in new_ledger:
287 failures.append("restore note glued to trailing prose")
288 drifted = replace(record, binding_sha256="0" * 64)
289 drift_req = core.RestoreRequest(record.site_id, "r", "back")
290 plan, _ = core.plan_restore([drifted], [row], rationales, drift_req)
291 if plan is not None:
292 failures.append("drifted binding restores")
293 return failures
294
295
296def _selftest_refusals() -> list[str]:
297 """Prove semantic changes and scope violations fail closed."""
298 failures: list[str] = []
299 record = _fixture_record()
300 ref = "review-decision src/a.c:19 mcdc-deactivated: operands cannot vary."
301 old_binding = hashlib.sha256(
302 binding_payload(
303 replace(
304 record,
305 scope="decision-line:19",
306 evidence=("decision-line:19", "standard:x"),
307 ),
308 record.anchor,
309 )
310 ).hexdigest()
311 row = _fixture_row(record.site_id, old_binding, ref)
312 changed = replace(record, reason="totally different justification")
313 changed = replace(changed, site_id=record.site_id, binding_sha256="0" * 64)
314 plan, _ = core.plan_rebind([changed], [row], record.site_id)
315 if plan is not None:
316 failures.append("semantic reason change rebinds")
317 wrong_kind = replace(record, directive="other-kind", site_id=record.site_id)
318 plan, _ = core.plan_rebind([wrong_kind], [row], record.site_id)
319 if plan is not None:
320 failures.append("different suppression kind rebinds")
321 plan, _ = core.plan_rebind([record], [row], "d" * 64)
322 if plan is not None:
323 failures.append("unknown site_id rebinds")
324 twin = replace(record, line=11)
325 plan, _ = core.plan_rebind([record, twin], [row], record.site_id)
326 if plan is not None:
327 failures.append("ambiguous live site rebinds")
328 draft = LedgerRow(2, record.site_id, old_binding, "unreviewed", "r", "batch-1", ref)
329 plan, _ = core.plan_rebind([record], [draft], record.site_id)
330 if plan is not None:
331 failures.append("unreviewed site rebinds")
332 return failures
333
334
335def run_selftests() -> list[str]:
336 """Run every rebind selftest; return failure strings."""
337 failures: list[str] = []
338 failures.extend(_selftest_move())
339 failures.extend(_selftest_refusals())
340 failures.extend(_selftest_retire())
341 failures.extend(_selftest_relink())
342 failures.extend(_selftest_restore())
343 return failures