Phase A: stub, return-split, and BOM termination on buses present on the graph; DDR/CPU/FPGA class certifiers only when that device exists. Skip without evidence. Via, pad, track, and zone stay distinct. Not DRC or FEM.
625 lines
20 KiB
Python
625 lines
20 KiB
Python
"""Interface class certifiers — USB-C, Ethernet 10/100 vs GbE, PoE.
|
|
|
|
First slice: class + connection integrity. Not generic SI, not via ampacity,
|
|
not thermal FEM. Missing evidence is INSUFFICIENT or N/A, never invented
|
|
geometry / Z / I / PoE / SuperSpeed.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from backend.periscopex.finding_engine import complete_finding, lookup_rule
|
|
from backend.periscopex.interface_class_check import check_interface_classes
|
|
from backend.periscopex.models import (
|
|
Component,
|
|
ComponentType,
|
|
DesignGraph,
|
|
Net,
|
|
NetType,
|
|
PinConnection,
|
|
ResistorSpecs,
|
|
)
|
|
from backend.periscopex.pcb_checks import merge_schema_pcb_reports, run_pcb_checks
|
|
from tests.paths import SIMPLE_PROJECT
|
|
|
|
|
|
def _comp(
|
|
ref: str,
|
|
*,
|
|
ctype: ComponentType,
|
|
value: str = "",
|
|
footprint: str = "",
|
|
mpn: str = "",
|
|
pins: dict[str, str] | None = None,
|
|
subtype: str | None = None,
|
|
specs=None,
|
|
) -> Component:
|
|
return Component(
|
|
reference=ref,
|
|
value=value,
|
|
footprint=footprint,
|
|
component_type=ctype,
|
|
component_subtype=subtype,
|
|
mpn=mpn or None,
|
|
pins=pins or {},
|
|
specs=specs,
|
|
)
|
|
|
|
|
|
def _net(name: str, ntype: NetType, *refs_pins: tuple[str, str]) -> Net:
|
|
return Net(
|
|
name=name,
|
|
net_type=ntype,
|
|
pins=[PinConnection(component_ref=r, pin_number=p) for r, p in refs_pins],
|
|
)
|
|
|
|
|
|
def _by_rule(findings, rule_id: str, designator: str | None = None):
|
|
out = [
|
|
f for f in findings
|
|
if f.rule_id == rule_id and (designator is None or f.designator == designator)
|
|
]
|
|
return out
|
|
|
|
|
|
def _rd(ref: str, cc_net: str) -> Component:
|
|
return _comp(
|
|
ref,
|
|
ctype=ComponentType.RESISTOR,
|
|
value="5k1",
|
|
pins={"1": cc_net, "2": "GND"},
|
|
specs=ResistorSpecs(
|
|
value_ohms=5100.0, value_formatted="5.1k",
|
|
),
|
|
)
|
|
|
|
|
|
def _usbc_device_graph(*, ss: bool = False, usb2_value: bool = True) -> DesignGraph:
|
|
"""USB-C receptacle with Rd 5.1 kΩ on CC1/CC2, VBUS, GND."""
|
|
value = "USB_C_Receptacle_USB2.0_16P" if usb2_value else "USB_C_Receptacle_USB3.1"
|
|
pins = {
|
|
"A5": "USB_CC1",
|
|
"B5": "USB_CC2",
|
|
"A4": "VBUS",
|
|
"A9": "VBUS",
|
|
"A1": "GND",
|
|
"A12": "GND",
|
|
"A6": "USB_D+",
|
|
"A7": "USB_D-",
|
|
}
|
|
nets = {
|
|
"USB_CC1": _net("USB_CC1", NetType.SIGNAL, ("J2", "A5"), ("R10", "1")),
|
|
"USB_CC2": _net("USB_CC2", NetType.SIGNAL, ("J2", "B5"), ("R11", "1")),
|
|
"VBUS": _net("VBUS", NetType.POWER, ("J2", "A4"), ("J2", "A9")),
|
|
"GND": _net("GND", NetType.GROUND, ("J2", "A1"), ("J2", "A12"), ("R10", "2"), ("R11", "2")),
|
|
"USB_D+": _net("USB_D+", NetType.SIGNAL, ("J2", "A6")),
|
|
"USB_D-": _net("USB_D-", NetType.SIGNAL, ("J2", "A7")),
|
|
}
|
|
comps = {
|
|
"J2": _comp(
|
|
"J2",
|
|
ctype=ComponentType.CONNECTOR,
|
|
value=value,
|
|
footprint="Connector_USB:USB_C_Receptacle_HRO_TYPE-C-31-M-12",
|
|
mpn="TYPE-C-31-M-12",
|
|
subtype="connector.usb",
|
|
pins=pins,
|
|
),
|
|
"R10": _rd("R10", "USB_CC1"),
|
|
"R11": _rd("R11", "USB_CC2"),
|
|
}
|
|
if ss:
|
|
pins["A2"] = "USB_SSTX_P"
|
|
pins["A3"] = "USB_SSTX_N"
|
|
pins["B11"] = "USB_SSRX_P"
|
|
pins["B10"] = "USB_SSRX_N"
|
|
nets["USB_SSTX_P"] = _net("USB_SSTX_P", NetType.SIGNAL, ("J2", "A2"))
|
|
nets["USB_SSTX_N"] = _net("USB_SSTX_N", NetType.SIGNAL, ("J2", "A3"))
|
|
nets["USB_SSRX_P"] = _net("USB_SSRX_P", NetType.SIGNAL, ("J2", "B11"))
|
|
nets["USB_SSRX_N"] = _net("USB_SSRX_N", NetType.SIGNAL, ("J2", "B10"))
|
|
comps["J2"] = _comp(
|
|
"J2",
|
|
ctype=ComponentType.CONNECTOR,
|
|
value="USB_C_Receptacle_USB3.1",
|
|
footprint="Connector_USB:USB_C_Receptacle_24P",
|
|
mpn="USB3-C-24",
|
|
subtype="connector.usb",
|
|
pins=pins,
|
|
)
|
|
return DesignGraph(components=comps, nets=nets)
|
|
|
|
|
|
def test_no_connector_emits_nothing():
|
|
"""No USB-C / no RJ45 / no PoE evidence → do not emit that certifier."""
|
|
g = DesignGraph(
|
|
components={
|
|
"U1": _comp("U1", ctype=ComponentType.IC, value="MCU", pins={"1": "GND"}),
|
|
},
|
|
nets={"GND": _net("GND", NetType.GROUND, ("U1", "1"))},
|
|
)
|
|
findings = check_interface_classes(g)
|
|
assert findings == []
|
|
assert not any((f.rule_id or "").startswith("PE-DDR") for f in findings)
|
|
|
|
|
|
def test_usbc_rd_vbus_gnd_certified():
|
|
findings = check_interface_classes(_usbc_device_graph())
|
|
for f in findings:
|
|
complete_finding(f)
|
|
cls = _by_rule(findings, "PE-USBC-001", "J2")[0]
|
|
assert cls.status == "INFO"
|
|
assert "USB2" in cls.finding
|
|
assert _by_rule(findings, "PE-USBC-002", "J2")[0].status == "INFO"
|
|
rd = _by_rule(findings, "PE-USBC-003", "J2")[0]
|
|
assert rd.status == "INFO"
|
|
assert rd.evidence_status == "SUFFICIENT"
|
|
assert "5100" in rd.facts or "5.1" in rd.facts
|
|
assert _by_rule(findings, "PE-USBC-004", "J2")[0].status == "INFO"
|
|
ss = _by_rule(findings, "PE-USBC-005", "J2")[0]
|
|
assert ss.status != "ERROR"
|
|
assert ss.finding_class == "INFO"
|
|
assert "N/A" in ss.finding or "USB2" in ss.finding
|
|
assert all(f.status != "ERROR" for f in findings if f.designator == "J2")
|
|
assert not any((f.rule_id or "").startswith(("PE-ETH", "PE-POE", "PE-DDR")) for f in findings)
|
|
|
|
|
|
def test_usbc_missing_cc2_is_error():
|
|
g = _usbc_device_graph()
|
|
j2 = g.components["J2"]
|
|
pins = dict(j2.pins)
|
|
pins.pop("B5")
|
|
g.components["J2"] = j2.model_copy(update={"pins": pins})
|
|
del g.nets["USB_CC2"]
|
|
findings = check_interface_classes(g)
|
|
for f in findings:
|
|
complete_finding(f)
|
|
cc = _by_rule(findings, "PE-USBC-002", "J2")[0]
|
|
assert cc.status == "ERROR"
|
|
assert cc.finding_class == "RULE"
|
|
assert cc.evidence_status == "SUFFICIENT"
|
|
|
|
|
|
def test_usbc_wrong_rd_is_error():
|
|
g = _usbc_device_graph()
|
|
g.components["R10"] = _comp(
|
|
"R10",
|
|
ctype=ComponentType.RESISTOR,
|
|
value="10k",
|
|
pins={"1": "USB_CC1", "2": "GND"},
|
|
specs=ResistorSpecs(value_ohms=10000.0, value_formatted="10k"),
|
|
)
|
|
findings = check_interface_classes(g)
|
|
for f in findings:
|
|
complete_finding(f)
|
|
rd = _by_rule(findings, "PE-USBC-003", "J2")[0]
|
|
assert rd.status == "ERROR"
|
|
assert rd.finding_class == "RULE"
|
|
|
|
|
|
def test_usbc_cc_to_controller_without_r_is_insufficient_not_error():
|
|
g = _usbc_device_graph()
|
|
del g.components["R10"]
|
|
del g.components["R11"]
|
|
g.components["U5"] = _comp(
|
|
"U5",
|
|
ctype=ComponentType.IC,
|
|
value="CC controller",
|
|
mpn="CC-IC",
|
|
pins={"1": "USB_CC1", "2": "USB_CC2"},
|
|
)
|
|
g.nets["USB_CC1"] = _net("USB_CC1", NetType.SIGNAL, ("J2", "A5"), ("U5", "1"))
|
|
g.nets["USB_CC2"] = _net("USB_CC2", NetType.SIGNAL, ("J2", "B5"), ("U5", "2"))
|
|
findings = check_interface_classes(g)
|
|
for f in findings:
|
|
complete_finding(f)
|
|
rd = _by_rule(findings, "PE-USBC-003", "J2")[0]
|
|
assert rd.status != "ERROR"
|
|
assert rd.evidence_status == "INSUFFICIENT"
|
|
assert rd.finding_class == "INFO"
|
|
|
|
|
|
def test_usbc_unknown_r_value_is_insufficient():
|
|
g = _usbc_device_graph()
|
|
g.components["R10"] = _comp(
|
|
"R10",
|
|
ctype=ComponentType.RESISTOR,
|
|
value="",
|
|
pins={"1": "USB_CC1", "2": "GND"},
|
|
)
|
|
findings = check_interface_classes(g)
|
|
for f in findings:
|
|
complete_finding(f)
|
|
rd = _by_rule(findings, "PE-USBC-003", "J2")[0]
|
|
assert rd.status != "ERROR"
|
|
assert rd.evidence_status == "INSUFFICIENT"
|
|
|
|
|
|
def test_usb3_class_without_ss_pairs_is_error():
|
|
g = _usbc_device_graph(usb2_value=False)
|
|
j2 = g.components["J2"]
|
|
g.components["J2"] = j2.model_copy(update={
|
|
"value": "USB_C_Receptacle_USB3.1",
|
|
"footprint": "Connector_USB:USB_C_Receptacle_24P",
|
|
"mpn": "USB3-C",
|
|
})
|
|
findings = check_interface_classes(g)
|
|
for f in findings:
|
|
complete_finding(f)
|
|
ss = _by_rule(findings, "PE-USBC-005", "J2")[0]
|
|
assert ss.status == "ERROR"
|
|
assert ss.finding_class == "RULE"
|
|
assert "SuperSpeed" in ss.finding or "USB3" in ss.finding
|
|
|
|
|
|
def test_usb3_with_ss_pairs_is_certified():
|
|
findings = check_interface_classes(_usbc_device_graph(ss=True, usb2_value=False))
|
|
for f in findings:
|
|
complete_finding(f)
|
|
ss = _by_rule(findings, "PE-USBC-005", "J2")[0]
|
|
assert ss.status == "INFO"
|
|
assert ss.evidence_status == "SUFFICIENT"
|
|
assert ss.status != "ERROR"
|
|
|
|
|
|
def test_missing_vbus_is_error():
|
|
g = _usbc_device_graph()
|
|
j2 = g.components["J2"]
|
|
pins = {k: v for k, v in j2.pins.items() if v != "VBUS"}
|
|
g.components["J2"] = j2.model_copy(update={"pins": pins})
|
|
findings = check_interface_classes(g)
|
|
for f in findings:
|
|
complete_finding(f)
|
|
pwr = _by_rule(findings, "PE-USBC-004", "J2")[0]
|
|
assert pwr.status == "ERROR"
|
|
assert pwr.finding_class == "RULE"
|
|
|
|
|
|
def _rj45(
|
|
*,
|
|
value: str,
|
|
mpn: str,
|
|
pins: dict[str, str],
|
|
extra_comps: dict[str, Component] | None = None,
|
|
extra_nets: dict[str, Net] | None = None,
|
|
) -> DesignGraph:
|
|
comps = {
|
|
"J1": _comp(
|
|
"J1",
|
|
ctype=ComponentType.CONNECTOR,
|
|
value=value,
|
|
footprint="Connector_RJ:RJ45",
|
|
mpn=mpn,
|
|
pins=pins,
|
|
),
|
|
}
|
|
nets = {}
|
|
for pin, net in pins.items():
|
|
nets.setdefault(net, Net(name=net, net_type=NetType.SIGNAL, pins=[]))
|
|
nets[net].pins.append(PinConnection(component_ref="J1", pin_number=pin))
|
|
if net.upper() in {"GND", "AGND"}:
|
|
nets[net].net_type = NetType.GROUND
|
|
if extra_comps:
|
|
comps.update(extra_comps)
|
|
if extra_nets:
|
|
for n, net in extra_nets.items():
|
|
if n in nets:
|
|
nets[n].pins.extend(net.pins)
|
|
else:
|
|
nets[n] = net
|
|
return DesignGraph(components=comps, nets=nets)
|
|
|
|
|
|
def test_ethernet_10_100_magjack_no_gbe_magnetics_error():
|
|
g = _rj45(
|
|
value="RJ45 PoE 10/100 Base-TX Jack with Magnetic Module",
|
|
mpn="ARJP11A-MASA-B-A-EMU2",
|
|
pins={
|
|
"1": "ETH_TX+",
|
|
"2": "ETH_TX-",
|
|
"3": "ETH_RX+",
|
|
"6": "ETH_RX-",
|
|
},
|
|
)
|
|
findings = check_interface_classes(g)
|
|
for f in findings:
|
|
complete_finding(f)
|
|
cls = _by_rule(findings, "PE-ETH-001", "J1")[0]
|
|
assert cls.status == "INFO"
|
|
assert "10/100" in cls.finding
|
|
assert "GbE" not in cls.finding or "not GbE" in cls.finding.lower() or "10/100" in cls.finding
|
|
pairs = _by_rule(findings, "PE-ETH-002", "J1")[0]
|
|
assert pairs.status == "INFO"
|
|
mag = _by_rule(findings, "PE-ETH-003", "J1")[0]
|
|
assert mag.status != "ERROR"
|
|
assert mag.finding_class == "INFO"
|
|
assert "N/A" in mag.finding or "10/100" in mag.finding
|
|
|
|
|
|
def test_gbe_without_magnetics_is_error():
|
|
g = _rj45(
|
|
value="RJ45 1000BASE-T",
|
|
mpn="RJ45-GBE-BARE",
|
|
pins={
|
|
"1": "TRD0_P",
|
|
"2": "TRD0_N",
|
|
"3": "TRD1_P",
|
|
"6": "TRD1_N",
|
|
"4": "TRD2_P",
|
|
"5": "TRD2_N",
|
|
"7": "TRD3_P",
|
|
"8": "TRD3_N",
|
|
},
|
|
)
|
|
findings = check_interface_classes(g)
|
|
for f in findings:
|
|
complete_finding(f)
|
|
cls = _by_rule(findings, "PE-ETH-001", "J1")[0]
|
|
assert "GbE" in cls.finding or "1000" in cls.finding
|
|
mag = _by_rule(findings, "PE-ETH-003", "J1")[0]
|
|
assert mag.status == "ERROR"
|
|
assert mag.finding_class == "RULE"
|
|
assert mag.evidence_status == "SUFFICIENT"
|
|
|
|
|
|
def test_gbe_with_magnetics_is_certified():
|
|
g = _rj45(
|
|
value="RJ45 1000BASE-T MagJack",
|
|
mpn="PULSE-GBE-MAG",
|
|
pins={
|
|
"1": "TRD0_P",
|
|
"2": "TRD0_N",
|
|
"3": "TRD1_P",
|
|
"6": "TRD1_N",
|
|
"4": "TRD2_P",
|
|
"5": "TRD2_N",
|
|
"7": "TRD3_P",
|
|
"8": "TRD3_N",
|
|
},
|
|
extra_comps={
|
|
"T1": _comp(
|
|
"T1",
|
|
ctype=ComponentType.TRANSFORMER,
|
|
value="LAN magnetics",
|
|
mpn="HX1198FNL",
|
|
subtype="transformer.signal",
|
|
pins={"1": "TRD0_P", "2": "TRD0_N"},
|
|
),
|
|
},
|
|
extra_nets={
|
|
"TRD0_P": _net("TRD0_P", NetType.SIGNAL, ("T1", "1")),
|
|
},
|
|
)
|
|
findings = check_interface_classes(g)
|
|
for f in findings:
|
|
complete_finding(f)
|
|
mag = _by_rule(findings, "PE-ETH-003", "J1")[0]
|
|
assert mag.status == "INFO"
|
|
assert mag.evidence_status == "SUFFICIENT"
|
|
assert mag.status != "ERROR"
|
|
|
|
|
|
def test_rj45_without_speed_is_insufficient_not_invented_gbe():
|
|
g = _rj45(
|
|
value="RJ45",
|
|
mpn="8P8C",
|
|
pins={"1": "NET1", "2": "NET2", "3": "NET3", "6": "NET6"},
|
|
)
|
|
findings = check_interface_classes(g)
|
|
for f in findings:
|
|
complete_finding(f)
|
|
cls = _by_rule(findings, "PE-ETH-001", "J1")[0]
|
|
assert cls.status != "ERROR"
|
|
assert cls.evidence_status == "INSUFFICIENT"
|
|
mag = _by_rule(findings, "PE-ETH-003", "J1")[0]
|
|
assert mag.status != "ERROR"
|
|
assert mag.finding_class == "INFO"
|
|
|
|
|
|
def test_bare_rj45_does_not_invent_poe():
|
|
g = _rj45(
|
|
value="RJ45",
|
|
mpn="8P8C-BARE",
|
|
pins={"1": "ETH_TX+", "2": "ETH_TX-", "3": "ETH_RX+", "6": "ETH_RX-"},
|
|
)
|
|
findings = check_interface_classes(g)
|
|
for f in findings:
|
|
complete_finding(f)
|
|
assert _by_rule(findings, "PE-ETH-001", "J1")
|
|
assert not any((f.rule_id or "").startswith("PE-POE") for f in findings)
|
|
assert not any((f.rule_id or "").startswith("PE-USBC") for f in findings)
|
|
assert not any((f.rule_id or "").startswith("PE-DDR") for f in findings)
|
|
assert all(f.status != "ERROR" for f in findings if (f.rule_id or "").startswith("PE-ETH"))
|
|
|
|
|
|
def test_poe_with_evidence_requires_magnetics_isolation():
|
|
g = _rj45(
|
|
value="RJ45 PoE 10/100 Base-TX Jack with Magnetic Module",
|
|
mpn="ARJP11A-MASA-B-A-EMU2",
|
|
pins={
|
|
"1": "ETH_TX+",
|
|
"2": "ETH_TX-",
|
|
"3": "ETH_RX+",
|
|
"6": "ETH_RX-",
|
|
},
|
|
)
|
|
findings = check_interface_classes(g)
|
|
for f in findings:
|
|
complete_finding(f)
|
|
ev = _by_rule(findings, "PE-POE-001", "J1")[0]
|
|
assert ev.status == "INFO"
|
|
assert ev.evidence_status == "SUFFICIENT"
|
|
assert "PoE" in ev.finding
|
|
cls = _by_rule(findings, "PE-POE-002", "J1")[0]
|
|
assert cls.status != "ERROR"
|
|
assert cls.evidence_status == "INSUFFICIENT"
|
|
iso = _by_rule(findings, "PE-POE-003", "J1")[0]
|
|
assert iso.status == "INFO"
|
|
assert iso.evidence_status == "SUFFICIENT"
|
|
|
|
|
|
def test_poe_evidence_on_bare_jack_is_isolation_error():
|
|
g = _rj45(
|
|
value="RJ45 PoE 802.3af",
|
|
mpn="RJ45-POE-BARE",
|
|
pins={"1": "ETH_TX+", "2": "ETH_TX-", "3": "ETH_RX+", "6": "ETH_RX-"},
|
|
)
|
|
findings = check_interface_classes(g)
|
|
for f in findings:
|
|
complete_finding(f)
|
|
iso = _by_rule(findings, "PE-POE-003", "J1")[0]
|
|
assert iso.status == "ERROR"
|
|
assert iso.finding_class == "RULE"
|
|
|
|
|
|
def test_simple_project_usbc_not_false_error():
|
|
graph = DesignGraph.model_validate_json(
|
|
(SIMPLE_PROJECT / "design_graph.json").read_text()
|
|
)
|
|
findings = check_interface_classes(graph)
|
|
for f in findings:
|
|
complete_finding(f)
|
|
usbc = [f for f in findings if (f.rule_id or "").startswith("PE-USBC") and f.designator == "J1"]
|
|
assert usbc
|
|
assert all(f.status != "ERROR" for f in usbc)
|
|
rd = _by_rule(findings, "PE-USBC-003", "J1")[0]
|
|
assert rd.status == "INFO"
|
|
assert rd.evidence_status == "SUFFICIENT"
|
|
ss = _by_rule(findings, "PE-USBC-005", "J1")[0]
|
|
assert ss.status != "ERROR"
|
|
assert ss.evidence_status == "INSUFFICIENT" or "N/A" in ss.finding
|
|
assert not any((f.rule_id or "").startswith("PE-ETH") for f in findings)
|
|
assert not any((f.rule_id or "").startswith("PE-POE") for f in findings)
|
|
assert not any((f.rule_id or "").startswith("PE-DDR") for f in findings)
|
|
|
|
|
|
def test_hubaudio_like_usbc_usb2_and_poe_magjack():
|
|
"""HubAudio J2 is USB2 Type-C 16P; J1 is PoE 10/100 MagJack — no false SS/GbE/PoE class."""
|
|
usbc = _usbc_device_graph()
|
|
eth = _rj45(
|
|
value="RJ45 PoE 10/100 Base-TX Jack with Magnetic Module",
|
|
mpn="ARJP11A-MASA-B-A-EMU2",
|
|
pins={
|
|
"1": "ETH_TX+",
|
|
"2": "ETH_TX-",
|
|
"3": "ETH_RX+",
|
|
"6": "ETH_RX-",
|
|
},
|
|
)
|
|
comps = dict(usbc.components)
|
|
comps.update(eth.components)
|
|
nets = dict(usbc.nets)
|
|
for name, net in eth.nets.items():
|
|
if name in nets:
|
|
nets[name] = nets[name].model_copy(
|
|
update={"pins": list(nets[name].pins) + list(net.pins)},
|
|
)
|
|
else:
|
|
nets[name] = net
|
|
g = DesignGraph(components=comps, nets=nets)
|
|
findings = check_interface_classes(g)
|
|
for f in findings:
|
|
complete_finding(f)
|
|
assert _by_rule(findings, "PE-USBC-005", "J2")[0].status != "ERROR"
|
|
assert _by_rule(findings, "PE-ETH-001", "J1")[0].status != "ERROR"
|
|
assert _by_rule(findings, "PE-ETH-003", "J1")[0].status != "ERROR"
|
|
assert _by_rule(findings, "PE-POE-001", "J1")[0].status != "ERROR"
|
|
assert _by_rule(findings, "PE-POE-002", "J1")[0].status != "ERROR"
|
|
assert _by_rule(findings, "PE-POE-003", "J1")[0].status != "ERROR"
|
|
assert all(
|
|
f.status != "ERROR"
|
|
for f in findings
|
|
if (f.rule_id or "").startswith(("PE-USBC", "PE-ETH", "PE-POE"))
|
|
)
|
|
assert not any((f.rule_id or "").startswith("PE-DDR") for f in findings)
|
|
|
|
|
|
def test_hubaudio_bom_has_usbc_and_poe_rj45_not_ddr():
|
|
"""HubAudio actual parts: J2 USB-C USB2 16P, J1 PoE 10/100 MagJack, no DDR."""
|
|
from tests.paths import REPO_ROOT
|
|
bom = (REPO_ROOT / "tests" / "fixtures" / "hubaudio_kicad_bom.csv").read_text()
|
|
assert "USB_C_Receptacle_USB2.0_16P" in bom
|
|
assert "RJ45" in bom and "PoE" in bom
|
|
assert "DDR3" not in bom and "DDR2" not in bom and "DDR4" not in bom
|
|
assert "USB3" not in bom
|
|
|
|
|
|
def test_rule_catalog_shared_not_si():
|
|
for rid in (
|
|
"PE-USBC-001", "PE-USBC-002", "PE-USBC-003", "PE-USBC-004", "PE-USBC-005",
|
|
"PE-ETH-001", "PE-ETH-002", "PE-ETH-003", "PE-ETH-004",
|
|
"PE-POE-001", "PE-POE-002", "PE-POE-003",
|
|
):
|
|
rec = lookup_rule(rid)
|
|
assert rec is not None, rid
|
|
assert rec.domain == "shared"
|
|
|
|
|
|
def test_run_pcb_checks_skips_absent_interfaces():
|
|
findings = run_pcb_checks(DesignGraph(), {}, None)
|
|
ids = {f.rule_id for f in findings}
|
|
assert "PE-USBC-001" not in ids
|
|
assert "PE-ETH-001" not in ids
|
|
assert "PE-POE-001" not in ids
|
|
if_f = [f for f in findings if (f.rule_id or "").startswith(
|
|
("PE-USBC", "PE-ETH", "PE-POE", "PE-DDR", "PE-CPU", "PE-FPGA")
|
|
)]
|
|
assert if_f == []
|
|
|
|
|
|
def test_merge_drops_duplicate_pcb_interface_findings():
|
|
schema = {
|
|
"findings": [{
|
|
"finding_id": "J2-001",
|
|
"designator": "J2",
|
|
"finding": "class USB-C USB2",
|
|
"status": "INFO",
|
|
"rule_id": "PE-USBC-001",
|
|
"source": "interface_class_check",
|
|
}],
|
|
"summary": {"INFO": 1, "ERROR": 0, "WARNING": 0},
|
|
}
|
|
pcb = {
|
|
"findings": [{
|
|
"finding_id": "PCB-J2-001",
|
|
"designator": "J2",
|
|
"finding": "class USB-C USB2",
|
|
"status": "INFO",
|
|
"rule_id": "PE-USBC-001",
|
|
"source": "interface_class_check",
|
|
}],
|
|
"summary": {"INFO": 1, "ERROR": 0, "WARNING": 0},
|
|
}
|
|
merged = merge_schema_pcb_reports(schema, pcb)
|
|
usbc = [
|
|
f for f in merged["findings"]
|
|
if f.get("rule_id") == "PE-USBC-001" and f.get("designator") == "J2"
|
|
]
|
|
assert len(usbc) == 1
|
|
assert usbc[0]["finding_id"] == "J2-001"
|
|
|
|
|
|
def test_does_not_use_layout_geometry():
|
|
"""Certifier takes the netlist graph only — no invented mm / Z / I."""
|
|
import inspect
|
|
from backend.periscopex import interface_class_check as mod
|
|
|
|
sig = inspect.signature(mod.check_interface_classes)
|
|
assert list(sig.parameters) == ["graph"]
|
|
src = inspect.getsource(mod)
|
|
assert "LayoutVia" not in src
|
|
assert "LayoutPad" not in src
|
|
assert "LayoutSegment" not in src
|
|
assert "LayoutZone" not in src
|
|
|
|
|
|
def test_usbc_vbus_does_not_invent_usb_500ma():
|
|
"""No typical USB current and no PE-PWR check without datasheet I."""
|
|
findings = check_interface_classes(_usbc_device_graph())
|
|
blob = " ".join(
|
|
" ".join(filter(None, (f.finding, f.why, f.facts, f.requirement, f.inference)))
|
|
for f in findings
|
|
).lower()
|
|
assert "500 ma" not in blob
|
|
assert "500ma" not in blob
|
|
assert "0.5 a" not in blob
|
|
assert not any(f.rule_id in {"PE-PWR-001", "PE-VIA-001"} for f in findings)
|