4"""Gate the MISRA deviation register's derived claims against committed evidence.
6``docs/qualification/MISRA_DEVIATIONS.md`` is the certification artefact that
7says which MISRA-C 2012 findings the project has formally dispositioned. Its
8"current tree" inventories used to be live prose that nothing re-derived, and
9they rotted spectacularly: D-005 claimed 166 Rule 8.4 findings while the
10committed baseline held 1873 across 313 files (#632). This checker makes that
11class of rot a gate failure by re-deriving every machine-checkable claim in
12the register from the two committed evidence files:
14* ``.github/misra-baseline.txt`` -- the per-file-per-rule ratchet baseline
15 (parsed by the one authoritative parser, ``misra_ratchet.load_baseline``).
16* ``.cppcheck-suppressions`` -- the ``misra-c2012-*`` suppression rows the
17 audit converts to ``--suppress=`` flags.
19Machine-checked structures in the register (each is a SINGLE physical line,
20so prose re-wrapping cannot silently detach a number from its pattern):
221. Provenance: ``Baseline: <N> findings across <M> file/rule rows
23 (Cppcheck <V>).`` -- N must equal the baseline's ``# total findings:``
24 header AND the recomputed sum (a three-way check, so a truncated baseline
25 is a refusal to compare, never a burn-down); M is the data-row count and V
26 the ``# cppcheck:`` header value.
272. Register rows: ``| D-NNN | misra-c2012-R | <category> | <class> |
28 <status> | <MAR> | <findings> | <files> |`` -- the deviation-index
29 table, exactly one row per deviation, whose last two columns are
30 re-derived per rule from the baseline. Any ``| D-...`` line that
31 does not parse as this 8-column shape -- including one with a
32 non-three-digit ID like ``D-11`` -- is malformed, and so is any
33 ``## D-`` heading that does not parse canonically, so the register
34 cannot silently change shape out from under the checker.
353. Residual: ``Residual (no deviation record): <R> rules, <N> findings,
36 <M> rows.`` -- everything in the baseline outside the register.
374. Ownership: ``- `misra-c2012-R` (<N> rows, <M> paths): <owner>`` -- one
38 bullet per rule family present in ``.cppcheck-suppressions``, both counts
39 re-derived; an unlisted family or a ghost bullet fails.
405. Excerpts: ``Highest-count files for misra-c2012-R (top <N>, derived):``
41 followed by ``| `path` | <count> |`` rows that must equal the true top-N
42 (count descending, path ascending). An excerpt-shaped row outside a
43 parsed excerpt table -- the footprint of a wrapped or deleted intro
456. Populations: any ``<N> findings across|in <M> files`` phrase is
46 re-derived against the rule of the ``## D-NNN`` section it sits in, and
47 is MALFORMED outside such a section, where no rule could derive it. An
48 unanchored population sentence in the top matter is how the #632 rot was
49 worded, so the shape is refused rather than ignored.
51Structural cross-checks: the ``## D-NNN: Rule R`` section headings and the
52register rows must describe the same (ID, rule) set; that set must be exactly
53the contiguous range the document header declares as active, so a deviation
54cannot be retired, added or renumbered without amending the header; and every
55row's rule must exist in the baseline, so an invented or typo'd rule id
56cannot pass by claiming zero findings.
58Non-vacuity floors (documented to SHRINK with the debt, never grow): the
59baseline must parse to at least ``MIN_BASELINE_RULES`` distinct rules, and the
60register to at least ``MIN_REGISTER_ROWS`` deviations,
61``MIN_OWNERSHIP_FAMILIES`` ownership bullets and ``MIN_EXCERPTS`` excerpt
62tables, else the scan is treated as collapsed (exit 2), not clean -- deleting
63a claim class outright must never read as verifying it. There is no
64suppressive baseline of its own: every discrepancy is a failure the register
65must absorb by being corrected.
67What this checker does NOT verify, so the register's own wording must not
68claim it does: the index's Category / Class / Status / MAR columns, prose that
69does not use one of the shapes above, and the identity (as opposed to the
70count) of the suppression rows behind each ownership bullet.
72Exit codes: 0 clean, 1 drift (a claim disagrees with the evidence), 2
73malformed input or collapsed/vacuous scan.
77 python3 scripts/checks/check_misra_deviations.py --check
78 python3 scripts/checks/check_misra_deviations.py --selftest
80``--selftest`` builds a fixture register + baseline + suppressions in a
81temporary tree and drives ``run_check`` -- the identical entry point
82``--check`` uses -- through every failure class: stale count, missing /
83extra / misshapen register row, short deviation ID in a row or heading, a
84row naming a rule absent from the baseline, a retirement that leaves the
85header's ID range stale, a missing ID range, malformed and missing
86documents, tampered / vacuous / two-column / below-floor baselines,
87suppression drift, unowned family, ghost bullet, heading mismatch,
88misordered excerpt, wrapped excerpt intro, a deleted excerpt block, a
89deleted ownership list, and a population claim that is stale, reworded
90``in``, or stranded outside any deviation section -- plus the
91must-stay-quiet direction, and finally asserts the live tree clears the
95from __future__
import annotations
101from collections
import Counter
102from dataclasses
import dataclass, field
103from pathlib
import Path
105sys.path.insert(0, str(Path(__file__).resolve().parent))
107from misra_deviations_fixtures
import fixture_files, selftest_cases
108from misra_ratchet
import load_baseline
110REPO_ROOT = Path(__file__).resolve().parents[2]
111DOC_PATH = REPO_ROOT /
"docs" /
"qualification" /
"MISRA_DEVIATIONS.md"
112BASELINE_PATH = REPO_ROOT /
".github" /
"misra-baseline.txt"
113SUPPRESSIONS_PATH = REPO_ROOT /
".cppcheck-suppressions"
119MIN_BASELINE_RULES = 30
120"""Floor on distinct rules parsed from the baseline (63 at introduction).
122A collapsed parse reports FEWER rules, which would otherwise read as a clean
123register. Shrink this constant with the debt as burn-down retires whole
124rules; never raise it to paper over a parse regression.
128"""Floor on derived-population rows parsed from the register (10 today).
130The register only ever loses rows when a deviation is formally retired, so a
131parse that suddenly sees fewer than this is a collapsed scan, not progress.
132The header's declared ID range is the tighter guard: a retirement that does
133not also amend the range fails, so this floor only has to catch a collapse.
136MIN_OWNERSHIP_FAMILIES = 10
137"""Floor on suppression-ownership bullets parsed from the register (15 today).
139Without it, commenting out every ``misra-c2012-*`` suppression row and
140deleting the bullet list verifies nothing and still announces PASS. Shrink
141it as families are genuinely retired; never raise it to hide a parse break.
145"""Floor on highest-count-file excerpt tables parsed from the register.
147An excerpt is the one claim class a maintainer could delete outright rather
148than let it drift, so a floor is what keeps "0 excerpt(s) verified" from
149reading as a clean run.
152RULE_PATTERN =
r"misra-c2012-\d+\.\d+"
154HEADER_RANGE_RE = re.compile(
r"\(D-(\d{3})\.\.D-(\d{3}) active\)")
155HEADING_RE = re.compile(
r"^## (D-\d{3}): Rule (\d+\.\d+) ")
156ANY_HEADING_RE = re.compile(
r"^## D-")
157ANY_REGISTER_ROW_RE = re.compile(
r"^\|\s*D-\d+")
158REGISTER_ROW_RE = re.compile(
159 r"^\|\s*(D-\d{3})\s*\|\s*("
161 +
r")\s*\|(?:[^|]*\|){4}\s*(\d+)\s*\|\s*(\d+)\s*\|\s*$"
163PROVENANCE_RE = re.compile(
164 r"^Baseline: (\d+) findings across (\d+) file/rule rows \((Cppcheck [0-9.]+)\)\.$"
166RESIDUAL_RE = re.compile(
167 r"^Residual \(no deviation record\): (\d+) rules, (\d+) findings, (\d+) rows\.$"
169OWNERSHIP_RE = re.compile(
r"^- `(" + RULE_PATTERN +
r")` \((\d+) rows?, (\d+) paths?\): \S")
170EXCERPT_INTRO_RE = re.compile(
171 r"^Highest-count files for (" + RULE_PATTERN +
r") \(top (\d+), derived\):$"
173EXCERPT_ROW_RE = re.compile(
r"^\|\s*`([^`|]+)`\s*\|\s*(\d+)\s*\|$")
174TABLE_DECOR_RE = re.compile(
r"^\|[\s:|-]+\|$")
175TOTAL_HEADER_RE = re.compile(
r"^# total findings: (\d+)$")
176POPULATION_RE = re.compile(
r"\b(\d+)(?: spurious)? findings (?:across|in) (\d+) files?(?![\w/])")
177"""A per-rule population restated in prose.
179Matched ANYWHERE in the register: inside a ``## D-NNN`` section it is
180re-derived against that section's rule, and outside one it is malformed --
181there is no rule to derive it against, and an unanchored population sentence
182in the top matter is exactly how the #632 rot was worded. A deliberately
183HISTORICAL figure must therefore not use this phrasing; the register says
184"751 violations in the 2026-05-02 baseline" for those.
190 """Every machine-checkable claim parsed out of the deviation register."""
192 provenance: list[tuple[int, int, str]] = field(default_factory=list)
193 derived: list[tuple[str, str, int, int]] = field(default_factory=list)
194 residual: list[tuple[int, int, int]] = field(default_factory=list)
195 headings: dict[str, str] = field(default_factory=dict)
196 declared_range: tuple[str, str] |
None =
None
197 populations: list[tuple[str, int, int]] = field(default_factory=list)
198 ownership: dict[str, tuple[int, int]] = field(default_factory=dict)
199 excerpts: list[tuple[str, int, list[tuple[str, int]]]] = field(default_factory=list)
200 malformed: list[str] = field(default_factory=list)
203def _parse_excerpt_table(lines: list[str], start: int) -> tuple[list[tuple[str, int]], int]:
204 """Consume one excerpt table after its intro line.
207 lines: The whole document as physical lines.
208 start: Index of the first line after the intro sentence.
211 The parsed ``(path, count)`` rows and the index of the first line
214 rows: list[tuple[str, int]] = []
216 while i < len(lines):
218 if not line.strip()
or TABLE_DECOR_RE.match(line)
or line.startswith(
"| File"):
221 m = EXCERPT_ROW_RE.match(line)
224 rows.append((m.group(1), int(m.group(2))))
229def _classify_line(claims: DocClaims, line: str) ->
None:
230 """Match one document line against every single-line claim pattern."""
231 if m := PROVENANCE_RE.match(line):
232 claims.provenance.append((int(m.group(1)), int(m.group(2)), m.group(3)))
233 elif m := RESIDUAL_RE.match(line):
234 claims.residual.append((int(m.group(1)), int(m.group(2)), int(m.group(3))))
235 elif m := REGISTER_ROW_RE.match(line):
236 claims.derived.append((m.group(1), m.group(2), int(m.group(3)), int(m.group(4))))
237 elif ANY_REGISTER_ROW_RE.match(line):
238 claims.malformed.append(f
"register row does not match the 8-column shape: {line!r}")
239 elif m := HEADING_RE.match(line):
240 dev_id, rule = m.group(1),
"misra-c2012-" + m.group(2)
241 if dev_id
in claims.headings:
242 claims.malformed.append(f
"duplicate section heading for {dev_id}")
243 claims.headings[dev_id] = rule
244 elif ANY_HEADING_RE.match(line):
245 claims.malformed.append(f
"deviation heading does not match '## D-NNN: Rule R': {line!r}")
246 elif m := OWNERSHIP_RE.match(line):
248 if rule
in claims.ownership:
249 claims.malformed.append(f
"duplicate suppression-ownership bullet for {rule}")
250 claims.ownership[rule] = (int(m.group(2)), int(m.group(3)))
251 elif EXCERPT_ROW_RE.match(line):
252 claims.malformed.append(
253 f
"excerpt-shaped row outside an excerpt table (wrapped or missing intro?): {line!r}"
257def parse_doc(path: Path) -> DocClaims:
258 """Parse the deviation register into its machine-checkable claims.
261 path: The register document (``MISRA_DEVIATIONS.md``).
264 The parsed claims; structural defects land in ``claims.malformed``.
267 lines = path.read_text(encoding=
"utf-8").splitlines()
269 current_rule: str |
None =
None
270 while i < len(lines):
272 if m := EXCERPT_INTRO_RE.match(line):
273 rows, i = _parse_excerpt_table(lines, i + 1)
275 claims.malformed.append(f
"excerpt for {m.group(1)} has an intro but no table rows")
276 claims.excerpts.append((m.group(1), int(m.group(2)), rows))
278 if line.startswith(
"## "):
279 heading = HEADING_RE.match(line)
280 current_rule =
"misra-c2012-" + heading.group(2)
if heading
else None
281 if claims.declared_range
is None and (rm := HEADER_RANGE_RE.search(line)):
282 claims.declared_range = (
"D-" + rm.group(1),
"D-" + rm.group(2))
283 _classify_line(claims, line)
284 for pm
in POPULATION_RE.finditer(line):
285 if current_rule
is None:
286 claims.malformed.append(
287 f
"population claim outside any deviation section (no rule to derive "
288 f
"it against): {line.strip()!r}"
291 claims.populations.append((current_rule, int(pm.group(1)), int(pm.group(2))))
293 if len(claims.provenance) != 1:
294 claims.malformed.append(
295 f
"expected exactly one 'Baseline: ...' provenance line, found {len(claims.provenance)}"
297 if len(claims.residual) != 1:
298 claims.malformed.append(
299 f
"expected exactly one 'Residual ...' line, found {len(claims.residual)}"
301 seen: set[str] = set()
302 for dev_id, _rule, _f, _n
in claims.derived:
304 claims.malformed.append(f
"duplicate derived-population row for {dev_id}")
309def read_baseline(path: Path) -> tuple[Counter[tuple[str, str]], str, int |
None, list[str]]:
310 """Load the committed baseline via the ratchet's own parser.
313 path: The committed ``misra-baseline.txt``.
316 The per-(file, rule) counts, the recorded cppcheck version, the
317 ``# total findings:`` header value (None when absent) and any
318 malformed-input problems.
320 problems: list[str] = []
321 header_total: int |
None =
None
323 counts, version = load_baseline(path)
324 except (SystemExit, ValueError):
325 return Counter(),
"unknown",
None, [f
"{path.name}: malformed baseline row"]
326 except OSError
as err:
327 return Counter(),
"unknown",
None, [f
"{path.name}: unreadable -- {err}"]
328 for raw
in path.read_text(encoding=
"utf-8").splitlines():
329 if m := TOTAL_HEADER_RE.match(raw):
330 header_total = int(m.group(1))
331 if header_total
is None:
332 problems.append(f
"{path.name}: missing '# total findings:' header")
333 elif header_total != sum(counts.values()):
335 f
"{path.name}: header claims {header_total} findings but the rows sum to "
336 f
"{sum(counts.values())} -- truncated or tampered; refusing to compare"
338 return counts, version, header_total, problems
341def parse_suppressions(path: Path) -> tuple[dict[str, tuple[int, int]], list[str]]:
342 """Derive per-rule row and path counts from the suppression list.
345 path: The committed ``.cppcheck-suppressions``.
348 A map of rule id to ``(row count, distinct path-pattern count)`` and
349 any malformed-input problems.
352 text = path.read_text(encoding=
"utf-8")
353 except OSError
as err:
354 return {}, [f
"{path.name}: unreadable -- {err}"]
355 rows: dict[str, int] = {}
356 paths: dict[str, set[str]] = {}
357 for raw
in text.splitlines():
359 if not line
or line.startswith(
"#")
or not line.startswith(
"misra-c2012-"):
361 parts = line.split(
":")
363 rows[rule] = rows.get(rule, 0) + 1
365 paths.setdefault(rule, set()).add(parts[1])
366 return {rule: (rows[rule], len(paths.get(rule, set())))
for rule
in rows}, []
369def _per_rule(counts: Counter[tuple[str, str]]) -> dict[str, tuple[int, int]]:
370 """Aggregate the per-(file, rule) baseline into per-rule (findings, files)."""
371 agg: dict[str, list[int]] = {}
372 for (_fname, rule), count
in counts.items():
373 bucket = agg.setdefault(rule, [0, 0])
376 return {rule: (f, n)
for rule, (f, n)
in agg.items()}
379def _range_problems(claims: DocClaims) -> list[str]:
380 """Check the header's declared ID range against the rows actually present.
383 claims: The parsed register claims.
386 Problems describing a silently retired, added or renumbered deviation.
388 if claims.declared_range
is None:
389 return [
"header declares no '(D-NNN..D-NNN active)' range -- cannot bound the register"]
390 first, last = claims.declared_range
391 want = [f
"D-{n:03d}" for n
in range(int(first[2:]), int(last[2:]) + 1)]
392 got = sorted({dev_id
for dev_id, _r, _f, _n
in claims.derived})
394 missing = [d
for d
in want
if d
not in got]
395 extra = [d
for d
in got
if d
not in want]
397 f
"header declares {first}..{last} active but the index holds "
399 + (f
"; missing {missing}" if missing
else "")
400 + (f
"; unexpected {extra}" if extra
else "")
405def _register_problems(claims: DocClaims, per_rule: dict[str, tuple[int, int]]) -> list[str]:
406 """Cross-check section headings and register rows against the baseline."""
407 problems: list[str] = []
408 derived = {dev_id: (rule, f, n)
for dev_id, rule, f, n
in claims.derived}
409 for dev_id
in sorted(set(claims.headings) | set(derived)):
410 if dev_id
not in claims.headings:
411 problems.append(f
"{dev_id}: register row without a section heading (extra row)")
412 if dev_id
not in derived:
413 problems.append(f
"{dev_id}: section heading without a register row (missing row)")
415 rule, doc_f, doc_n = derived[dev_id]
416 if claims.headings.get(dev_id, rule) != rule:
417 problems.append(f
"{dev_id}: rule mismatch between section heading and register row")
418 if rule
not in per_rule:
420 f
"{dev_id}: rule {rule} appears nowhere in the baseline -- a typo, an "
421 f
"invented id, or a deviation whose debt is gone and which must be retired"
424 real_f, real_n = per_rule[rule]
425 if (doc_f, doc_n) != (real_f, real_n):
427 f
"{dev_id} ({rule}): register claims {doc_f} findings / {doc_n} files, "
428 f
"baseline derives {real_f} / {real_n} (stale count)"
433def _population_problems(claims: DocClaims, per_rule: dict[str, tuple[int, int]]) -> list[str]:
434 """Check in-section 'N findings across M files' restatements against the baseline."""
436 for rule, doc_f, doc_n
in claims.populations:
437 real_f, real_n = per_rule.get(rule, (0, 0))
438 if (doc_f, doc_n) != (real_f, real_n):
440 f
"in-section population claim under {rule}: register says {doc_f} findings / "
441 f
"{doc_n} files, baseline derives {real_f} / {real_n} (stale count)"
446def _residual_problems(claims: DocClaims, per_rule: dict[str, tuple[int, int]]) -> list[str]:
447 """Check the residual line covers exactly the rules outside the register."""
448 if not claims.residual
or not claims.provenance:
450 register_rules = {rule
for _id, rule, _f, _n
in claims.derived}
451 resid = [rule
for rule
in per_rule
if rule
not in register_rules]
454 sum(per_rule[r][0]
for r
in resid),
455 sum(per_rule[r][1]
for r
in resid),
457 got = claims.residual[0]
460 f
"residual line claims {got[0]} rules / {got[1]} findings / {got[2]} rows, "
461 f
"baseline derives {want[0]} / {want[1]} / {want[2]} (stale count)"
466def _ownership_problems(claims: DocClaims, families: dict[str, tuple[int, int]]) -> list[str]:
467 """Check the suppression-ownership bullets against the suppression list."""
469 for rule
in sorted(set(families) | set(claims.ownership)):
470 if rule
not in claims.ownership:
472 f
"suppression family {rule} ({families[rule][0]} row(s)) has no ownership "
473 f
"bullet in the register (unowned waiver)"
475 elif rule
not in families:
476 problems.append(f
"ownership bullet for {rule} matches no suppression row (ghost row)")
477 elif claims.ownership[rule] != families[rule]:
479 f
"ownership bullet for {rule} claims {claims.ownership[rule]}, suppression "
480 f
"list derives {families[rule]} (rows, paths)"
485def _excerpt_problems(claims: DocClaims, counts: Counter[tuple[str, str]]) -> list[str]:
486 """Check every highest-count-files excerpt equals the true top-N."""
488 for rule, top_n, rows
in claims.excerpts:
489 per_file = [(fname, c)
for (fname, r), c
in counts.items()
if r == rule]
490 want = sorted(per_file, key=
lambda fc: (-fc[1], fc[0]))[:top_n]
492 problems.append(f
"excerpt for {rule}: register lists {rows}, baseline derives {want}")
496def _provenance_problems(
497 claims: DocClaims, counts: Counter[tuple[str, str]], version: str
499 """Check the provenance line's totals and tool version."""
500 if not claims.provenance:
502 doc_f, doc_rows, doc_version = claims.provenance[0]
504 if (doc_f, doc_rows) != (sum(counts.values()), len(counts)):
506 f
"provenance claims {doc_f} findings / {doc_rows} rows, baseline derives "
507 f
"{sum(counts.values())} / {len(counts)} (stale count)"
509 if doc_version != version:
511 f
"provenance names {doc_version!r} but the baseline header records {version!r}"
517 doc_path: Path, baseline_path: Path, suppressions_path: Path
518) -> tuple[list[str], list[str]]:
519 """Re-derive every register claim from the committed evidence.
522 doc_path: The deviation register.
523 baseline_path: The committed MISRA ratchet baseline.
524 suppressions_path: The committed cppcheck suppression list.
527 ``(drift, malformed)`` problem lists; both empty means clean.
530 claims = parse_doc(doc_path)
531 except OSError
as err:
532 return [], [f
"{doc_path.name}: unreadable -- {err}"]
533 counts, version, _header_total, base_problems = read_baseline(baseline_path)
534 families, supp_problems = parse_suppressions(suppressions_path)
535 malformed = claims.malformed + base_problems + supp_problems
536 per_rule = _per_rule(counts)
537 if len(per_rule) < MIN_BASELINE_RULES:
539 f
"baseline parsed to {len(per_rule)} distinct rules, below the "
540 f
"non-vacuity floor of {MIN_BASELINE_RULES} -- collapsed scan, refusing to pass"
542 for parsed, floor, what
in (
543 (len(claims.derived), MIN_REGISTER_ROWS,
"derived-population rows"),
544 (len(claims.ownership), MIN_OWNERSHIP_FAMILIES,
"suppression-ownership bullets"),
545 (len(claims.excerpts), MIN_EXCERPTS,
"excerpt tables"),
549 f
"register parsed to {parsed} {what}, below the non-vacuity floor of "
550 f
"{floor} -- collapsed scan, refusing to pass"
555 _provenance_problems(claims, counts, version)
556 + _range_problems(claims)
557 + _register_problems(claims, per_rule)
558 + _population_problems(claims, per_rule)
559 + _residual_problems(claims, per_rule)
560 + _ownership_problems(claims, families)
561 + _excerpt_problems(claims, counts)
566def run_check(doc_path: Path, baseline_path: Path, suppressions_path: Path) -> int:
567 """Evaluate the register and report; the single entry point both modes use.
570 doc_path: The deviation register.
571 baseline_path: The committed MISRA ratchet baseline.
572 suppressions_path: The committed cppcheck suppression list.
575 ``EXIT_OK``, ``EXIT_DRIFT`` or ``EXIT_MALFORMED``.
577 tag =
"check_misra_deviations.py"
578 drift, malformed = evaluate(doc_path, baseline_path, suppressions_path)
579 for problem
in malformed:
580 print(f
"{tag}: MALFORMED -- {problem}", file=sys.stderr)
582 return EXIT_MALFORMED
583 for problem
in drift:
584 print(f
"{tag}: DRIFT -- {problem}", file=sys.stderr)
587 f
"{tag}: {len(drift)} claim(s) in {doc_path.name} disagree with the committed "
588 f
"evidence. Re-derive the numbers from .github/misra-baseline.txt and "
589 f
".cppcheck-suppressions and correct the register -- never the evidence.",
593 claims = parse_doc(doc_path)
595 f
"{tag}: PASS -- {len(claims.derived)} deviations, {len(claims.ownership)} "
596 f
"suppression families and {len(claims.excerpts)} excerpt(s) verified against "
597 f
"the committed baseline."
602def _fixture_files() -> dict[str, str]:
603 """Build the mutually consistent self-test fixture."""
604 return fixture_files()
607def _selftest_cases() -> list[tuple[str, str, str, str, int]]:
608 """Enumerate every self-test mutation and the quiet control."""
609 return selftest_cases(EXIT_OK, EXIT_DRIFT, EXIT_MALFORMED)
612def _run_fixture_case(tmp: Path, files: dict[str, str], name: str, expected: int) -> str |
None:
613 """Materialise one fixture variant and drive ``run_check`` over it.
616 tmp: Scratch directory to write the variant into.
617 files: The three fixture file bodies, already mutated for this case.
618 name: Case label for the failure report.
619 expected: The exit code the case must produce.
622 A failure description, or None when the case behaves as expected.
624 doc = tmp /
"MISRA_DEVIATIONS.md"
625 baseline = tmp /
"misra-baseline.txt"
626 suppressions = tmp /
"cppcheck-suppressions"
627 doc.write_text(files[
"doc"], encoding=
"utf-8")
628 baseline.write_text(files[
"baseline"], encoding=
"utf-8")
629 suppressions.write_text(files[
"suppressions"], encoding=
"utf-8")
630 got = run_check(doc, baseline, suppressions)
632 return f
"{name}: expected exit {expected}, got {got}"
636def selftest() -> int:
637 """Drive every failure class and the quiet direction through ``run_check``."""
638 tag =
"check_misra_deviations.py --selftest"
639 failures: list[str] = []
640 with tempfile.TemporaryDirectory()
as tmpdir:
642 for name, key, old, new, expected
in _selftest_cases():
643 files = dict(_fixture_files())
644 if old
and old
not in files[key]:
645 failures.append(f
"{name}: fixture mutation target {old!r} not found")
648 files[key] = files[key].replace(old, new)
649 problem = _run_fixture_case(tmp, files, name, expected)
651 failures.append(problem)
652 floor_files = dict(_fixture_files())
653 floor_files[
"baseline"] = (
654 "# fixture baseline\n"
655 "# cppcheck: Cppcheck 9.9.9\n"
656 "# total findings: 3\n"
657 "a.c\tmisra-c2012-1.1\t3\n"
659 problem = _run_fixture_case(tmp, floor_files,
"below-floor baseline", EXIT_MALFORMED)
661 failures.append(problem)
662 stripped = dict(_fixture_files())
663 stripped[
"doc"] =
"\n".join(
664 line
for line
in stripped[
"doc"].splitlines()
if not line.startswith(
"- `misra-c2012-")
666 problem = _run_fixture_case(tmp, stripped,
"no ownership bullets at all", EXIT_MALFORMED)
668 failures.append(problem)
669 good = _fixture_files()
670 (tmp /
"misra-baseline.txt").write_text(good[
"baseline"], encoding=
"utf-8")
671 (tmp /
"cppcheck-suppressions").write_text(good[
"suppressions"], encoding=
"utf-8")
673 tmp /
"absent.md", tmp /
"misra-baseline.txt", tmp /
"cppcheck-suppressions"
675 if got != EXIT_MALFORMED:
676 failures.append(f
"missing register document: expected exit {EXIT_MALFORMED}, got {got}")
677 _live_drift, live_malformed = evaluate(DOC_PATH, BASELINE_PATH, SUPPRESSIONS_PATH)
679 failures.append(f
"live tree trips the floors or fails to parse: {live_malformed[:3]}")
680 for failure
in failures:
681 print(f
"{tag}: FAIL: {failure}", file=sys.stderr)
684 print(f
"{tag}: PASS ({len(_selftest_cases()) + 3} both-direction cases + live floors)")
688def main(argv: list[str] |
None =
None) -> int:
689 """Entry point: parse the mode flag and run the register check or selftest.
692 argv: Argument vector override for tests; None uses ``sys.argv``.
695 The process exit code.
697 parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
698 mode = parser.add_mutually_exclusive_group()
702 help=
"re-derive every register claim from committed evidence (default)",
707 help=
"prove every failure class fires and the clean fixture stays quiet",
709 args = parser.parse_args(argv)
712 return run_check(DOC_PATH, BASELINE_PATH, SUPPRESSIONS_PATH)
715if __name__ ==
"__main__":
void main(void)
The application entry point Reset_Handler hands control to.