Files
periscope/tests/pcb/test_bom_011_named_join.py
T
michele cd7a7ca3a6 Join PE-BOM-011 pins by name, not pin_count (2.63.2).
USB-C A5/B5 are contacts, not EP. Datasheet pintable, KiCad
.kicad_mod, and embedded PCB pads join by pin identity. Missing
pintable skips; scalar pin_count is never an ERROR authority.
2026-09-22 00:47:58 +02:00

280 lines
9.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""PE-BOM-011 joins pintable × lib × PCB by name. pin_count is not authority."""
from __future__ import annotations
from pathlib import Path
from backend.periscopex.bom_pcb_check import (
_is_ep_land,
check_bom_pcb_datasheet,
)
from backend.periscopex.models import (
Component,
ComponentConstraints,
ComponentType,
DesignGraph,
LayoutFootprint,
LayoutGraph,
LayoutPad,
PackageInfo,
Pin,
)
from backend.periscopex.parsers_kicad_pcb import parse_kicad_pcb
_HUB_PCB = Path(
"/Users/michelebigi/Development/HubAudio/hardware/kicad/HubAudio/HubAudio.kicad_pcb"
)
_USB_C_PINS = [
"A1", "A4", "A5", "A6", "A7", "A8", "A9", "A12",
"B1", "B4", "B5", "B6", "B7", "B8", "B9", "B12",
]
def _usb_c_cons(*, pintable: bool) -> ComponentConstraints:
pins = [Pin(number=n, name=n) for n in _USB_C_PINS] if pintable else []
return ComponentConstraints(
mpn="TYPE-C-31-M-12",
package_info=PackageInfo(
base_family="USB", package="USB-C-16P", pin_count=16,
),
pintable=pins,
absolute_maximum_ratings=[],
rules=[],
)
def _j2_graph(pins: dict[str, str] | None = None) -> DesignGraph:
keys = pins or {n: "SIG" for n in _USB_C_PINS}
keys.setdefault("SH", "GND")
return DesignGraph(
components={
"J2": Component(
reference="J2",
value="USB_C_Receptacle_USB2.0_16P",
footprint="Connector_USB:USB_C_Receptacle_HRO_TYPE-C-31-M-12",
component_type=ComponentType.CONNECTOR,
mpn="TYPE-C-31-M-12",
pins=keys,
),
},
)
def _j2_layout() -> LayoutGraph:
pads = [
LayoutPad(number=n, x=0, y=0, net="SIG", pinfunction=f"PIN_{n}")
for n in _USB_C_PINS
]
pads.extend(
LayoutPad(number="SH", x=i, y=0, net="GND", pinfunction="SHIELD_SH")
for i in range(4)
)
return LayoutGraph(
footprints={
"J2": LayoutFootprint(
reference="J2",
footprint="Connector_USB:USB_C_Receptacle_HRO_TYPE-C-31-M-12",
x=0, y=0, layer="F.Cu",
pads=pads,
),
},
)
def _ids(findings) -> set[str]:
return {f.rule_id for f in findings}
def test_a5_b5_are_not_ep():
assert _is_ep_land("A5", "CC1_A5", pin_count=16) is False
assert _is_ep_land("B5", "CC2_B5", pin_count=16) is False
assert _is_ep_land("EP", pin_count=8) is True
assert _is_ep_land("9", "EP", pin_count=8) is True
assert _is_ep_land("9", pin_count=8) is True
assert _is_ep_land("9_1", pin_count=8) is True
def test_usb_c_pin_count_16_without_pintable_is_not_error():
"""The 16 vs 1 SH false ERROR: pin_count alone must not fire PE-BOM-011."""
findings = check_bom_pcb_datasheet(
_j2_graph(), {"TYPE-C-31-M-12": _usb_c_cons(pintable=False)}, _j2_layout(),
)
assert "PE-BOM-011" not in _ids(findings)
assert not any("pin_count=16" in (f.finding or "") for f in findings)
def test_usb_c_pintable_matches_contacts_ignores_sh_and_mount():
findings = check_bom_pcb_datasheet(
_j2_graph(), {"TYPE-C-31-M-12": _usb_c_cons(pintable=True)}, _j2_layout(),
)
assert "PE-BOM-011" not in _ids(findings)
def test_hubaudio_j2_is_not_16_vs_1_signal_pad():
if not _HUB_PCB.is_file():
return
layout = parse_kicad_pcb(_HUB_PCB)
j2 = layout.footprints.get("J2")
assert j2 is not None
numbers = {p.number for p in j2.pads}
assert "A5" in numbers and "B5" in numbers
assert "SH" in numbers
slim = LayoutGraph(footprints={"J2": j2})
findings = check_bom_pcb_datasheet(
_j2_graph(), {"TYPE-C-31-M-12": _usb_c_cons(pintable=False)}, slim,
)
bom011 = [f for f in findings if f.rule_id == "PE-BOM-011"]
assert bom011 == []
findings_named = check_bom_pcb_datasheet(
_j2_graph(), {"TYPE-C-31-M-12": _usb_c_cons(pintable=True)}, slim,
)
assert "PE-BOM-011" not in _ids(findings_named)
def test_qfn_ep_still_excluded_from_join():
cons = ComponentConstraints(
mpn="SW",
package_info=PackageInfo(base_family="TI", package="SON-8-EP", pin_count=8),
pintable=[Pin(number=str(i), name=f"P{i}") for i in range(1, 9)],
absolute_maximum_ratings=[],
rules=[],
)
pads = [LayoutPad(number=str(i), x=0, y=0, net="GND") for i in range(1, 9)]
pads += [
LayoutPad(number="9", x=0, y=0, net="GND", pinfunction="EP"),
LayoutPad(number="9_1", x=0.2, y=0, net="GND"),
LayoutPad(number="9_2", x=0.4, y=0, net="GND"),
]
graph = DesignGraph(
components={
"U1": Component(
reference="U1", value="SW", footprint="SON-8",
component_type=ComponentType.IC, mpn="SW",
pins={str(i): "GND" for i in range(1, 9)},
),
},
)
layout = LayoutGraph(
footprints={"U1": LayoutFootprint(reference="U1", x=0, y=0, pads=pads)},
)
assert "PE-BOM-011" not in _ids(
check_bom_pcb_datasheet(graph, {"SW": cons}, layout),
)
def test_missing_signal_pad_still_fires():
cons = ComponentConstraints(
mpn="IC24",
package_info=PackageInfo(base_family="QFN", package="QFN-24", pin_count=24),
pintable=[Pin(number=str(i), name=f"P{i}") for i in range(1, 25)],
absolute_maximum_ratings=[],
rules=[],
)
pads = [LayoutPad(number=str(i), x=0, y=0, net="SIG") for i in range(1, 24)]
graph = DesignGraph(
components={
"U1": Component(
reference="U1", value="IC", footprint="QFN-24",
component_type=ComponentType.IC, mpn="IC24",
pins={str(i): "SIG" for i in range(1, 25)},
),
},
)
layout = LayoutGraph(
footprints={"U1": LayoutFootprint(reference="U1", x=0, y=0, pads=pads)},
)
findings = check_bom_pcb_datasheet(graph, {"IC24": cons}, layout)
assert "PE-BOM-011" in _ids(findings)
assert any("24" in (f.finding or "") for f in findings if f.rule_id == "PE-BOM-011")
def _write_mod(root: Path, lib: str, name: str, pads: list[str]) -> Path:
pretty = root / f"{lib}.pretty"
pretty.mkdir(parents=True, exist_ok=True)
body = [f'(footprint "{lib}:{name}" (version 20240108) (layer "F.Cu")\n']
for i, n in enumerate(pads):
body.append(
f' (pad "{n}" smd rect (at {i} 0) (size 0.3 0.8) '
f'(layers "F.Cu" "F.Paste" "F.Mask") (pinfunction "{n}"))\n'
)
body.append(")\n")
path = pretty / f"{name}.kicad_mod"
path.write_text("".join(body))
return path
def test_lib_vs_datasheet_is_lib_finding_not_pcb_pad_count(tmp_path: Path):
_write_mod(tmp_path, "Pkg", "QFN-8", [str(i) for i in range(1, 8)])
cons = ComponentConstraints(
mpn="IC8",
package_info=PackageInfo(base_family="QFN", package="QFN-8", pin_count=8),
pintable=[Pin(number=str(i), name=f"P{i}") for i in range(1, 9)],
absolute_maximum_ratings=[],
rules=[],
)
pads = [LayoutPad(number=str(i), x=0, y=0, net="SIG") for i in range(1, 9)]
graph = DesignGraph(
components={
"U1": Component(
reference="U1", value="IC", footprint="Pkg:QFN-8",
component_type=ComponentType.IC, mpn="IC8",
pins={str(i): "SIG" for i in range(1, 9)},
),
},
)
layout = LayoutGraph(
footprints={
"U1": LayoutFootprint(
reference="U1", footprint="Pkg:QFN-8", x=0, y=0, pads=pads,
),
},
)
findings = [
f for f in check_bom_pcb_datasheet(
graph, {"IC8": cons}, layout, footprint_dirs=[tmp_path],
)
if f.rule_id == "PE-BOM-011"
]
assert findings
text = " ".join(f.finding for f in findings)
assert "library" in text.lower()
assert "pin_count=" not in text
assert "signal pads=" not in text
def test_lib_vs_embedded_pcb_is_separate_finding(tmp_path: Path):
_write_mod(tmp_path, "Pkg", "QFN-8", [str(i) for i in range(1, 9)])
cons = ComponentConstraints(
mpn="IC8",
package_info=PackageInfo(base_family="QFN", package="QFN-8", pin_count=8),
pintable=[Pin(number=str(i), name=f"P{i}") for i in range(1, 9)],
absolute_maximum_ratings=[],
rules=[],
)
pads = [LayoutPad(number=str(i), x=0, y=0, net="SIG") for i in range(1, 8)]
pads.append(LayoutPad(number="99", x=1, y=0, net="SIG"))
graph = DesignGraph(
components={
"U1": Component(
reference="U1", value="IC", footprint="Pkg:QFN-8",
component_type=ComponentType.IC, mpn="IC8",
pins={str(i): "SIG" for i in range(1, 9)},
),
},
)
layout = LayoutGraph(
footprints={
"U1": LayoutFootprint(
reference="U1", footprint="Pkg:QFN-8", x=0, y=0, pads=pads,
),
},
)
findings = [
f for f in check_bom_pcb_datasheet(
graph, {"IC8": cons}, layout, footprint_dirs=[tmp_path],
)
if f.rule_id == "PE-BOM-011"
]
assert any("embedded" in f.finding.lower() or "PCB" in f.finding for f in findings)