"""HubAudio false findings: exposed-pad names, ESD cites, CC-via-net, Ethernet Z.""" from __future__ import annotations import pytest from backend.periscopex.af_trace_check import check_af_traces from backend.periscopex.bom_pcb_check import check_bom_pcb_datasheet from backend.periscopex.esd_return_check import check_esd from backend.periscopex.models import ( Component, ComponentConstraints, ComponentType, DesignGraph, LayoutFootprint, LayoutGraph, LayoutPad, LayoutSegment, Net, NetType, PackageInfo, Pin, PinConnection, ) from backend.periscopex.protocol_l0 import _run_l0_check, l0_findings from backend.periscopex.protocol_recognize import PhysicalBusInstance def _ic(ref: str, mpn: str, pins: dict[str, str], *, footprint: str = "") -> Component: return Component( reference=ref, value=mpn, footprint=footprint, component_type=ComponentType.IC, mpn=mpn, pins=pins, ) def _net(name: str, *pairs: tuple[str, str]) -> Net: return Net( name=name, net_type=NetType.SIGNAL, pins=[PinConnection(component_ref=r, pin_number=p) for r, p in pairs], ) def _cons(mpn: str, numbers: list[str], pin_count: int) -> ComponentConstraints: return ComponentConstraints( mpn=mpn, package_info=PackageInfo(base_family="QFN", package=f"QFN-{pin_count}", pin_count=pin_count), pintable=[Pin(number=n, name=n) for n in numbers], absolute_maximum_ratings=[], rules=[], ) def _layout(ref: str, pads: list[tuple[str, str]], *, footprint: str) -> LayoutGraph: return LayoutGraph( footprints={ ref: LayoutFootprint( reference=ref, footprint=footprint, x=0, y=0, pads=[ LayoutPad(number=n, x=0, y=0, net="GND", pinfunction=pf) for n, pf in pads ], ), }, ) def _bom(ref: str, mpn: str, numbers: list[str], pads: list[tuple[str, str]], pin_count: int): graph = DesignGraph(components={ ref: _ic(ref, mpn, {n: "N" for n, _pf in pads if n}, footprint="Lib:Part"), }) # Schematic pins follow the pintable numbers that are real contacts. graph.components[ref].pins = {n: "N" for n in numbers if n not in {"EP", "THERMAL PAD"}} layout = _layout(ref, pads, footprint="Lib:Part") findings = check_bom_pcb_datasheet(graph, {mpn: _cons(mpn, numbers, pin_count)}, layout) return [f for f in findings if f.rule_id == "PE-BOM-011"] def test_u16_ep_matches_pad_41(): """AXP2101: pintable EP, footprint pad 41 / EP_41.""" hits = _bom( "U16", "AXP2101", ["1", "EP"], [("1", "CHGLED_1"), ("41", "EP_41")], 40, ) assert hits == [] def test_u18_ep_matches_pad_89(): """ADAU1467: pintable EP, footprint pad 89 / EP_89.""" hits = _bom( "U18", "ADAU1467", ["1", "EP"], [("1", "DGND_1_1"), ("89", "EP_89")], 88, ) assert hits == [] def test_u19_ep_matches_pad_25_vss(): """LAN8720A: pintable EP, VQFN-24 land is pad 25 / VSS_25.""" hits = _bom( "U19", "LAN8720A", ["1", "EP"], [("1", "VDD2A_1"), ("25", "VSS_25")], 24, ) assert hits == [] def test_u5_thermal_pad_matches_epad_29(): """TAC5212: pintable THERMAL PAD, footprint pad 29 / EPAD_29.""" hits = _bom( "U5", "XTAC5212", ["1", "THERMAL PAD"], [("1", "DREG_1"), ("29", "EPAD_29")], 24, ) assert hits == [] def test_exposed_pad_absent_from_footprint_stays_error(): hits = _bom( "U9", "BARE", ["1", "EP"], [("1", "PIN_1")], 24, ) assert hits assert any("EP" in (f.finding or "") for f in hits) def _esd_graph(net: str, *, j_fp: str = "", j_value: str = "JACK") -> DesignGraph: return DesignGraph( components={ "J1": Component( reference="J1", value=j_value, footprint=j_fp, component_type=ComponentType.CONNECTOR, mpn="", pins={"13": net}, ), "U19": _ic("U19", "LAN8720A", {"1": net}), }, nets={net: _net(net, ("J1", "13"), ("U19", "1"))}, ) def test_esd_does_not_say_datasheet_specified_without_a_cite(): findings = check_esd(_esd_graph("USB_DP")) assert findings and findings[0].rule_id == "PE-ESD-001" blob = f"{findings[0].action} {findings[0].recommendation}" assert "datasheet-specified" not in blob def test_esd_quotes_datasheet_when_part_and_section_exist(): cons = ComponentConstraints( mpn="LAN8720A", pintable=[], absolute_maximum_ratings=[], rules=[], layout_rules=[{ "kind": "esd", "document": "LAN8720A datasheet", "section": "3.2", "page": "12", "note": "ESD on USB_DP", }], ) findings = check_esd(_esd_graph("USB_DP"), {"LAN8720A": cons}) assert findings blob = f"{findings[0].action} {findings[0].finding}" assert "datasheet-specified" in blob assert "LAN8720A datasheet" in blob assert "3.2" in blob def test_3v3_ethernet_rail_is_not_an_esd_warning(): assert check_esd(_esd_graph("3V3_ETHERNET")) == [] def test_vbus_without_cite_is_not_an_esd_warning(): graph = _esd_graph("VBUS") graph.components["U16"] = _ic("U16", "AXP2101", {"37": "VBUS"}) graph.components["J2"] = Component( reference="J2", value="USB_C", footprint="Connector_USB:USB_C_Receptacle", component_type=ComponentType.CONNECTOR, pins={"A4": "VBUS"}, ) graph.nets = {"VBUS": _net("VBUS", ("J2", "A4"), ("U16", "37"))} findings = check_esd(graph) assert findings == [] blob = " ".join(f.action or "" for f in findings) assert "datasheet-specified" not in blob def test_optical_spdif_is_not_an_esd_warning(): findings = check_esd(_esd_graph( "SPIF_IN", j_fp="OptoDevice:Broadcom_AFBR-16xxZ_Horizontal", j_value="TOSLINK", )) assert findings == [] def test_unverified_spdif_warns_it_was_not_seen_as_optical(): findings = check_esd(_esd_graph("SPIF_OUT", j_fp="Connector:PinHeader")) assert findings assert findings[0].status == "WARNING" assert "not verified as optical" in (findings[0].finding or "") assert "datasheet-specified" not in (findings[0].action or "") def _cc_inst() -> PhysicalBusInstance: return PhysicalBusInstance( instance_id="usb-c-usb2-receptacle:J2", logical_protocol_id="usb2-hs", physical_interface_id="usb-c-usb2-receptacle", pcb_relevant="YES", confidence=1.0, evidence_kind="pinout", recognition_status="RECOGNIZED", host_ref="J2", nets=["Net-(J2-A5)", "Net-(J2-B5)"], ) def _cc_graph(*, both: bool) -> DesignGraph: comps = { "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, pins={"A5": "Net-(J2-A5)", "B5": "Net-(J2-B5)", "A4": "VBUS"}, ), "R1": Component( reference="R1", value="5.1k", footprint="R_0603", component_type=ComponentType.RESISTOR, pins={"1": "Net-(J2-A5)", "2": "GND"}, ), } nets = { "Net-(J2-A5)": _net("Net-(J2-A5)", ("J2", "A5"), ("R1", "1")), "Net-(J2-B5)": _net("Net-(J2-B5)", ("J2", "B5")), "GND": _net("GND", ("R1", "2")), } if both: comps["R2"] = Component( reference="R2", value="5.1k", footprint="R_0603", component_type=ComponentType.RESISTOR, pins={"1": "Net-(J2-B5)", "2": "GND"}, ) nets["Net-(J2-B5)"] = _net("Net-(J2-B5)", ("J2", "B5"), ("R2", "1")) nets["GND"] = _net("GND", ("R1", "2"), ("R2", "2")) return DesignGraph(components=comps, nets=nets) def test_cc_rd_present_on_unnamed_nets_does_not_fail(): row = _run_l0_check(_cc_graph(both=True), _cc_inst(), "cc_rd_rp", "MANDATORY") assert row.result == "PASS" assert "5.1" in row.notes assert "CC1/CC2 net not present" not in row.notes findings = l0_findings(_cc_inst(), [row]) assert not any(f.rule_id == "PE-PRT-L0-001" for f in findings) def test_cc_rd_missing_on_pin_net_fails(): row = _run_l0_check(_cc_graph(both=False), _cc_inst(), "cc_rd_rp", "MANDATORY") assert row.result == "FAIL" assert "B5" in row.notes assert "CC1/CC2 net not present" not in row.notes assert "5.1" in row.notes findings = l0_findings(_cc_inst(), [row]) assert any(f.rule_id == "PE-PRT-L0-001" and f.status == "ERROR" for f in findings) def _eth_graph(mpn: str, *, value: str = "") -> DesignGraph: return DesignGraph( components={ "J1": Component( reference="J1", value="RJ45", footprint="Connector_RJ:RJ45_Abracon_ARJP11A-MA_Horizontal", component_type=ComponentType.CONNECTOR, pins={"9": "RJ45_TXP", "10": "RJ45_TXN"}, ), "U19": _ic("U19", mpn, {"21": "RJ45_TXP", "20": "RJ45_TXN"}, footprint="VQFN-24"), }, nets={ "RJ45_TXP": _net("RJ45_TXP", ("J1", "9"), ("U19", "21")), "RJ45_TXN": _net("RJ45_TXN", ("J1", "10"), ("U19", "20")), }, ) if not value else DesignGraph( components={ "J1": Component( reference="J1", value="RJ45", footprint="Connector_RJ:RJ45", component_type=ComponentType.CONNECTOR, pins={"9": "RJ45_TXP", "10": "RJ45_TXN"}, ), "U19": Component( reference="U19", value=value, footprint="", component_type=ComponentType.IC, mpn=mpn, pins={"21": "RJ45_TXP", "20": "RJ45_TXN"}, ), }, nets={ "RJ45_TXP": _net("RJ45_TXP", ("J1", "9"), ("U19", "21")), "RJ45_TXN": _net("RJ45_TXN", ("J1", "10"), ("U19", "20")), }, ) def _eth_layout() -> LayoutGraph: return LayoutGraph( segments=[ LayoutSegment(start=(0, 0), end=(20, 0), width=0.2, layer="F.Cu", net="RJ45_TXP"), LayoutSegment(start=(0, 0.2), end=(20, 0.2), width=0.2, layer="F.Cu", net="RJ45_TXN"), ], ) def _eth_findings(graph: DesignGraph, monkeypatch, z: float | None): if z is not None: monkeypatch.setattr( "backend.periscopex.af_trace_check._pair_z_ohm", lambda layout, unit: z, ) return [f for f in check_af_traces(graph, {}, _eth_layout()) if f.rule_id == "PE-AF-002"] def test_lan8720_pair_without_z_is_error_with_100base_cite(monkeypatch): findings = _eth_findings(_eth_graph("LAN8720A"), monkeypatch, None) assert len(findings) == 1 f = findings[0] assert f.status == "ERROR" blob = f"{f.finding} {f.action} {f.requirement}" assert blob.startswith("ERROR:") or "ERROR:" in f.finding assert "100" in blob and "25.4.9" in blob assert "stackup" in blob assert "Fornire" not in blob assert "tr" not in blob.lower() assert "90" not in blob assert "Clause 40" not in blob assert "1000BASE" not in blob assert "PASS" not in f.finding def test_lan8720_pair_z_inside_cite_is_pass(monkeypatch): findings = _eth_findings(_eth_graph("LAN8720A"), monkeypatch, 100.0) assert len(findings) == 1 assert findings[0].finding.startswith("PASS:") assert findings[0].status == "INFO" assert "RJ45_TXP" in findings[0].finding and "RJ45_TXN" in findings[0].finding assert "25.4.9" in findings[0].finding def test_lan8720_pair_z_outside_cite_is_fail(monkeypatch): findings = _eth_findings(_eth_graph("LAN8720A"), monkeypatch, 90.0) assert len(findings) == 1 assert findings[0].finding.startswith("FAIL:") assert findings[0].status == "ERROR" assert "PASS" not in findings[0].finding assert "25.4.9" in findings[0].finding def test_gigabit_phy_uses_clause_40_not_100base(monkeypatch): findings = _eth_findings( _eth_graph("RTL8211F", value="1000BASE-T"), monkeypatch, None, ) assert len(findings) == 1 blob = findings[0].finding assert findings[0].status == "ERROR" assert "Clause 40" in blob assert "25.4.9" not in blob assert "PASS" not in blob