USB-C/RJ45/PoE/DDR3 are the method, not a catalog. No connector means no finding. Bare RJ45 is Ethernet only; PoE needs PoE evidence. HubAudio has USB-C and PoE MagJack, not DDR. Skip-I unchanged.
623 lines
20 KiB
Python
623 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"))]
|
|
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)
|