ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
pinout_selftest.py
Go to the documentation of this file.
1# SPDX-License-Identifier: MIT
2# Copyright (c) 2026 Brighton Sikarskie
3"""Selftest for the pinout generator, asserting BOTH directions.
4
5A checker that has quietly stopped matching is also perfectly silent, so
6every case below is paired: something that must fire, and something that
7must stay quiet. The failure mode that matters most for this generator is
8not a crash but a *collapse* -- a table that still parses, into fewer
9columns than it has, losing alternate functions without a word. That case is
10asserted explicitly (``must fire: too few columns``), as is a figure the
11cross-check would otherwise skip.
12
13Run via ``gen_pinouts.py --selftest``; the ``pinout-freshness`` gate runs it
14before the scan.
15"""
16
17from __future__ import annotations
18
19import sys
20from collections.abc import Sequence
21from pathlib import Path
22
23sys.path.insert(0, str(Path(__file__).resolve().parent))
24from pinout_model import (
25 DASH,
26 FIGURE_VARIANTS,
27 ParseError,
28 decode_part,
29)
30from pinout_parse import figure_port_sets, parse_block
31from pinout_render import wrap_cell
32
33# Column origins of the miniature Standard-product page below. Twelve
34# left-aligned columns, the shape pdftotext produces.
35ORIGINS = (0, 9, 18, 27, 36, 50, 58, 67, 79, 105, 125, 137)
36FIXTURE_ROW_COUNT = 3
37WRAP_WIDTH = 10
38
39
40class Recorder:
41 """Collects pass/fail so every case runs even after one fails."""
42
43 def __init__(self) -> None:
44 """Create an empty failure collection."""
45 self.failures: list[str] = []
46
47 def expect(self, name: str, *, condition: bool, detail: str = "") -> None:
48 """Record and print whether one named expectation passed."""
49 if condition:
50 print(f" ok {name}")
51 else:
52 self.failures.append(f"{name}: {detail}")
53 print(f" FAIL {name}: {detail}")
54
55
56def lay(cells: Sequence[str]) -> str:
57 """Lay cells out at ORIGINS, the way pdftotext renders a table row."""
58 line = ""
59 for origin, cell in zip(ORIGINS, cells, strict=False):
60 line = line.ljust(origin) + cell
61 return line
62
63
64def fixture_page() -> list[str]:
65 """A miniature Standard-product page, built from cells not typed text.
66
67 Hand-typed fixed-width fixtures drift out of alignment silently; this one
68 cannot, because it is laid out by the same rule the assertions assume.
69 """
70 d = DASH
71 return [
72 " BGA289 BGA224",
73 "BGA289 MIPI BGA224 MIPI Debug, CAC ports",
74 "",
75 lay(
76 [
77 "A1",
78 "A1",
79 "C4",
80 "C4",
81 d,
82 "P609",
83 "D7/DQ7",
84 "IRQ29",
85 "TXD0_C/SDA0_C/",
86 "GTIU/",
87 d,
88 "LCD_DA",
89 ]
90 ),
91 lay(["", "", "", "", "", "", "", "", "MOSI0_C", "GTIOC5B", "", "TA6_A"]),
92 lay(["A2", d, "C5", "C5", "VSS", d, d, d, d, d, d, d]),
93 lay(["A3", "A3", d, d, d, "P610", d, "IRQ2", d, d, d, d]),
94 "",
95 "R01DS0493EJ0130 Rev.1.30 Page 26 of 292",
96 ]
97
98
99def check_table_parse(rec: Recorder) -> None:
100 """The pin-list page parser, both directions."""
101 rows = parse_block(fixture_page(), 4)
102 rec.expect(
103 "parses every data row",
104 condition=len(rows) == FIXTURE_ROW_COUNT,
105 detail=f"got {len(rows)}",
106 )
107 if len(rows) == FIXTURE_ROW_COUNT:
108 rec.expect(
109 "joins a wrapped cell",
110 condition=rows[0][8] == "TXD0_C/SDA0_C/MOSI0_C",
111 detail=f"got {rows[0][8]!r}",
112 )
113 rec.expect(
114 "joins a wrapped trailing cell",
115 condition=rows[0][11] == "LCD_DATA6_A",
116 detail=f"got {rows[0][11]!r}",
117 )
118 rec.expect(
119 "keeps the dashed ball column",
120 condition=rows[1][1] == DASH,
121 detail=f"got {rows[1][1]!r}",
122 )
123 rec.expect(
124 "ignores the page footer",
125 condition=rows[2][0] == "A3" and rows[2][5] == "P610",
126 detail=f"got {rows[2]!r}",
127 )
128
129 # MUST FIRE: rows that do not agree on a layout.
130 try:
131 parse_block(["A1 A1 C4 P609", "A2 A2", "A3 A3 C6"], 4)
132 rec.expect(
133 "rejects a ragged page",
134 condition=False,
135 detail="no ParseError raised",
136 )
137 except ParseError:
138 rec.expect("rejects a ragged page", condition=True)
139
140 # MUST FIRE: rows that agree on the WRONG layout. This is the collapse
141 # mode -- a table that parses cleanly into too few columns silently
142 # drops every alternate function past the cut.
143 narrow = [lay(["A1", "A1", "C4", "C4", DASH, "P609"]) for _ in range(4)]
144 try:
145 parse_block(narrow, 4)
146 rec.expect("rejects a page with too few columns", condition=False, detail="accepted")
147 except ParseError:
148 rec.expect("rejects a page with too few columns", condition=True)
149
150
151def check_part_numbers(rec: Recorder) -> None:
152 """Part-number decoding, both directions."""
153 part = decode_part("R7KA8D2KFLCAC")
154 rec.expect(
155 "decodes the EK-RA8D2 part",
156 condition=part.group == "RA8D2"
157 and part.cores == "dual"
158 and part.mipi
159 and part.package == "AC",
160 detail=f"got {part}",
161 )
162
163 # MUST FIRE: a package code the product matrix does not define.
164 try:
165 decode_part("R7KA8D2ADLCAZ")
166 rec.expect("rejects an unknown package code", condition=False, detail="accepted AZ")
167 except ParseError:
168 rec.expect("rejects an unknown package code", condition=True)
169
170 # MUST FIRE: the three fields that encode SiP-ness disagreeing.
171 try:
172 decode_part("R7KA8D2JRLSAJ") # R7K says MRAM, S/AJ say SiP
173 rec.expect("rejects inconsistent SiP encoding", condition=False, detail="accepted")
174 except ParseError:
175 rec.expect("rejects inconsistent SiP encoding", condition=True)
176
177
178def check_figures(rec: Recorder) -> None:
179 """The section 1.6 figure cross-check, both directions."""
180 doc = "\n".join(
181 f" {label} grid P0{i:02d} P1{i:02d}\n Figure 1.{i + 3} Pin assignment for {label}"
182 for i, label in enumerate(FIGURE_VARIANTS)
183 )
184 figures = figure_port_sets(doc)
185 rec.expect(
186 "reads every pin-assignment figure",
187 condition=set(figures) == set(FIGURE_VARIANTS.values()),
188 detail=f"got {sorted(figures)}",
189 )
190 rec.expect(
191 "reads a figure's port pins",
192 condition=figures[FIGURE_VARIANTS["BGA 289-pin"]] == {"P000", "P100"},
193 detail=f"got {figures[FIGURE_VARIANTS['BGA 289-pin']]}",
194 )
195
196 # MUST FIRE: a renamed or dropped figure. Skipping one would disarm the
197 # cross-check for that variant while still reporting success.
198 try:
199 figure_port_sets(doc.replace("Figure 1.3 Pin assignment for BGA 289-pin", ""))
200 rec.expect(
201 "rejects a missing pin-assignment figure",
202 condition=False,
203 detail="accepted",
204 )
205 except ParseError:
206 rec.expect("rejects a missing pin-assignment figure", condition=True)
207
208
209def check_rendering(rec: Recorder) -> None:
210 """Cell wrapping: never over width, never loses a character."""
211 wrapped = wrap_cell("A" * 30 + "/B/C", WRAP_WIDTH)
212 rec.expect(
213 "wrap_cell honours the width",
214 condition=all(len(line) <= WRAP_WIDTH for line in wrapped),
215 detail=f"got {wrapped}",
216 )
217 rec.expect(
218 "wrap_cell is lossless",
219 condition="".join(wrapped) == "A" * 30 + "/B/C",
220 detail=f"got {wrapped}",
221 )
222 empty = wrap_cell("", WRAP_WIDTH)
223 rec.expect(
224 "wrap_cell marks an empty field",
225 condition=empty == ["-"],
226 detail=f"got {empty}",
227 )
228
229
230def run() -> int:
231 """Run every case and return a process exit code."""
232 print("gen_pinouts selftest")
233 rec = Recorder()
234 for case in (check_table_parse, check_part_numbers, check_figures, check_rendering):
235 case(rec)
236 if rec.failures:
237 print(f"gen_pinouts selftest: {len(rec.failures)} failure(s)")
238 return 1
239 print("gen_pinouts selftest: all checks passed")
240 return 0
241
242
243if __name__ == "__main__":
244 sys.exit(run())