3"""Selftest for the pinout generator, asserting BOTH directions.
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.
13Run via ``gen_pinouts.py --selftest``; the ``pinout-freshness`` gate runs it
17from __future__
import annotations
20from collections.abc
import Sequence
21from pathlib
import Path
23sys.path.insert(0, str(Path(__file__).resolve().parent))
24from pinout_model
import (
30from pinout_parse
import figure_port_sets, parse_block
31from pinout_render
import wrap_cell
35ORIGINS = (0, 9, 18, 27, 36, 50, 58, 67, 79, 105, 125, 137)
41 """Collects pass/fail so every case runs even after one fails."""
43 def __init__(self) -> None:
44 """Create an empty failure collection."""
45 self.failures: list[str] = []
47 def expect(self, name: str, *, condition: bool, detail: str =
"") ->
None:
48 """Record and print whether one named expectation passed."""
52 self.failures.append(f
"{name}: {detail}")
53 print(f
" FAIL {name}: {detail}")
56def lay(cells: Sequence[str]) -> str:
57 """Lay cells out at ORIGINS, the way pdftotext renders a table row."""
59 for origin, cell
in zip(ORIGINS, cells, strict=
False):
60 line = line.ljust(origin) + cell
64def fixture_page() -> list[str]:
65 """A miniature Standard-product page, built from cells not typed text.
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.
73 "BGA289 MIPI BGA224 MIPI Debug, CAC ports",
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]),
95 "R01DS0493EJ0130 Rev.1.30 Page 26 of 292",
99def check_table_parse(rec: Recorder) ->
None:
100 """The pin-list page parser, both directions."""
101 rows = parse_block(fixture_page(), 4)
103 "parses every data row",
104 condition=len(rows) == FIXTURE_ROW_COUNT,
105 detail=f
"got {len(rows)}",
107 if len(rows) == FIXTURE_ROW_COUNT:
109 "joins a wrapped cell",
110 condition=rows[0][8] ==
"TXD0_C/SDA0_C/MOSI0_C",
111 detail=f
"got {rows[0][8]!r}",
114 "joins a wrapped trailing cell",
115 condition=rows[0][11] ==
"LCD_DATA6_A",
116 detail=f
"got {rows[0][11]!r}",
119 "keeps the dashed ball column",
120 condition=rows[1][1] == DASH,
121 detail=f
"got {rows[1][1]!r}",
124 "ignores the page footer",
125 condition=rows[2][0] ==
"A3" and rows[2][5] ==
"P610",
126 detail=f
"got {rows[2]!r}",
131 parse_block([
"A1 A1 C4 P609",
"A2 A2",
"A3 A3 C6"], 4)
133 "rejects a ragged page",
135 detail=
"no ParseError raised",
138 rec.expect(
"rejects a ragged page", condition=
True)
143 narrow = [lay([
"A1",
"A1",
"C4",
"C4", DASH,
"P609"])
for _
in range(4)]
145 parse_block(narrow, 4)
146 rec.expect(
"rejects a page with too few columns", condition=
False, detail=
"accepted")
148 rec.expect(
"rejects a page with too few columns", condition=
True)
151def check_part_numbers(rec: Recorder) ->
None:
152 """Part-number decoding, both directions."""
153 part = decode_part(
"R7KA8D2KFLCAC")
155 "decodes the EK-RA8D2 part",
156 condition=part.group ==
"RA8D2"
157 and part.cores ==
"dual"
159 and part.package ==
"AC",
160 detail=f
"got {part}",
165 decode_part(
"R7KA8D2ADLCAZ")
166 rec.expect(
"rejects an unknown package code", condition=
False, detail=
"accepted AZ")
168 rec.expect(
"rejects an unknown package code", condition=
True)
172 decode_part(
"R7KA8D2JRLSAJ")
173 rec.expect(
"rejects inconsistent SiP encoding", condition=
False, detail=
"accepted")
175 rec.expect(
"rejects inconsistent SiP encoding", condition=
True)
178def check_figures(rec: Recorder) ->
None:
179 """The section 1.6 figure cross-check, both directions."""
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)
184 figures = figure_port_sets(doc)
186 "reads every pin-assignment figure",
187 condition=set(figures) == set(FIGURE_VARIANTS.values()),
188 detail=f
"got {sorted(figures)}",
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']]}",
199 figure_port_sets(doc.replace(
"Figure 1.3 Pin assignment for BGA 289-pin",
""))
201 "rejects a missing pin-assignment figure",
206 rec.expect(
"rejects a missing pin-assignment figure", condition=
True)
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)
213 "wrap_cell honours the width",
214 condition=all(len(line) <= WRAP_WIDTH
for line
in wrapped),
215 detail=f
"got {wrapped}",
218 "wrap_cell is lossless",
219 condition=
"".join(wrapped) ==
"A" * 30 +
"/B/C",
220 detail=f
"got {wrapped}",
222 empty = wrap_cell(
"", WRAP_WIDTH)
224 "wrap_cell marks an empty field",
225 condition=empty == [
"-"],
226 detail=f
"got {empty}",
231 """Run every case and return a process exit code."""
232 print(
"gen_pinouts selftest")
234 for case
in (check_table_parse, check_part_numbers, check_figures, check_rendering):
237 print(f
"gen_pinouts selftest: {len(rec.failures)} failure(s)")
239 print(
"gen_pinouts selftest: all checks passed")
243if __name__ ==
"__main__":