ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
check_chapter_map_freshness.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"""Regenerate the tracked HUM chapter map and require byte identity."""
5
6from __future__ import annotations
7
8import argparse
9import re
10import shutil
11import subprocess
12import sys
13import tempfile
14from pathlib import Path
15
16REPO_ROOT = Path(__file__).resolve().parents[2]
17ARTEFACT = Path("docs/reference/CHAPTER_MAP.md")
18SOURCE_PDF = Path("docs/reference/ra8d2-hardware-user-manual.pdf")
19GENERATOR = Path("scripts/gen/build_chapter_map.sh")
20CHAPTER_ROW_RE = re.compile(r"^\|\s+\d+\s+\|")
21EXPECTED_CHAPTERS = 69
22
23
24class GenerationError(RuntimeError):
25 """The canonical generator could not produce trustworthy bytes."""
26
27
28def _run(root: Path, *argv: str) -> subprocess.CompletedProcess[bytes]:
29 """Run fixed repository tooling without a shell."""
30 return subprocess.run( # noqa: S603 -- fixed executable and caller-owned argv
31 [*argv], cwd=root, capture_output=True, check=False
32 )
33
34
35def _is_tracked(root: Path, path: Path) -> bool:
36 """Return whether Git owns ``path`` in the candidate index."""
37 git = shutil.which("git")
38 if git is None:
39 return False
40 result = _run(root, git, "ls-files", "--error-unmatch", "--", path.as_posix())
41 return result.returncode == 0
42
43
44def _generate(root: Path) -> bytes:
45 """Run the canonical generator into an isolated temporary output."""
46 bash = shutil.which("bash")
47 if bash is None:
48 message = "bash is required"
49 raise GenerationError(message)
50 with tempfile.TemporaryDirectory() as raw_tmp:
51 output = Path(raw_tmp) / ARTEFACT.name
52 result = _run(root, bash, str(root / GENERATOR), "--output", str(output))
53 if result.returncode != 0:
54 detail = result.stderr.decode("utf-8", errors="replace").strip()
55 message = detail or "chapter-map generator failed"
56 raise GenerationError(message)
57 if not output.is_file():
58 message = "chapter-map generator wrote no output"
59 raise GenerationError(message)
60 return output.read_bytes()
61
62
63def freshness_reason(candidate: bytes | None, fresh: bytes) -> str | None:
64 """Return a failure reason for missing/drifted bytes, or ``None``."""
65 if candidate is None:
66 return "tracked candidate chapter map is missing"
67 if candidate != fresh:
68 return f"candidate is {len(candidate)} bytes; regenerate is {len(fresh)} bytes"
69 return None
70
71
72def _chapter_count(rendered: bytes) -> int:
73 """Count structurally rendered chapter-table rows."""
74 text = rendered.decode("ascii")
75 return sum(CHAPTER_ROW_RE.match(line) is not None for line in text.splitlines())
76
77
78def selftest() -> int:
79 """Prove both verdict directions and the live generator's invariants."""
80 failures: list[str] = []
81 sample = b"chapter-map\n"
82 if freshness_reason(sample, sample) is not None:
83 failures.append("equal bytes were rejected")
84 if freshness_reason(sample + b"drift\n", sample) is None:
85 failures.append("drifted bytes were accepted")
86 if freshness_reason(None, sample) is None:
87 failures.append("missing candidate was accepted")
88 if not _is_tracked(REPO_ROOT, SOURCE_PDF):
89 failures.append(f"source PDF is not tracked: {SOURCE_PDF}")
90 first = _generate(REPO_ROOT)
91 second = _generate(REPO_ROOT)
92 if first != second:
93 failures.append("two clean regenerations differ")
94 count = _chapter_count(first)
95 if count != EXPECTED_CHAPTERS:
96 failures.append(f"chapter census drifted: {count} != {EXPECTED_CHAPTERS}")
97 if failures:
98 for failure in failures:
99 print(f"check_chapter_map_freshness.py: selftest FAIL: {failure}", file=sys.stderr)
100 return 1
101 print("check_chapter_map_freshness.py: selftest OK (both directions; 69 chapters)")
102 return 0
103
104
105def check() -> int:
106 """Compare the tracked candidate map with a clean regeneration."""
107 if not _is_tracked(REPO_ROOT, ARTEFACT):
108 print(f"check_chapter_map_freshness.py: FAIL: {ARTEFACT} is not tracked", file=sys.stderr)
109 return 1
110 try:
111 fresh = _generate(REPO_ROOT)
112 except GenerationError as exc:
113 print(f"check_chapter_map_freshness.py: ERROR: {exc}", file=sys.stderr)
114 return 2
115 reason = freshness_reason((REPO_ROOT / ARTEFACT).read_bytes(), fresh)
116 if reason is not None:
117 print(
118 f"check_chapter_map_freshness.py: FAIL: {reason}; run scripts/gen/build_chapter_map.sh",
119 file=sys.stderr,
120 )
121 return 1
122 print(f"Chapter map fresh ({EXPECTED_CHAPTERS} chapters from tracked full PDF)")
123 return 0
124
125
126def main() -> int:
127 """Dispatch live check or selftest."""
128 parser = argparse.ArgumentParser(description=__doc__)
129 parser.add_argument("--selftest", action="store_true")
130 args = parser.parse_args()
131 return selftest() if args.selftest else check()
132
133
134if __name__ == "__main__":
135 sys.exit(main())
-copyright
void main(void)
The application entry point Reset_Handler hands control to.
Definition main.c:298