"""M6/M7 USB-C/Ethernet/HDMI packs: UNKNOWN + needed_document, no invented ohms.""" from __future__ import annotations import copy import json import pytest from backend.periscopex.models import ( Component, ComponentType, DesignGraph, Net, NetType, PinConnection, ) from backend.periscopex.protocol_catalog import ( ProtocolCatalogError, build_m0_catalog, load_catalog, parse_catalog, ) from backend.periscopex.protocol_recognize import recognize_physical_buses def _ic(ref: str, pins: dict[str, str], *, mpn: str = "", value: str = "") -> Component: return Component( reference=ref, value=value or mpn, footprint="", component_type=ComponentType.IC, mpn=mpn or None, 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 test_unknown_requires_needed_document(): raw = build_m0_catalog() iface = copy.deepcopy(raw["physical_interfaces"][0]) iface["id"] = "usb2-hs-no-doc" bad = copy.deepcopy(iface["constraints"][0]) bad["id"] = "no-doc" bad["value_kind"] = "UNKNOWN" bad["value"] = None bad["needed_document"] = None iface["constraints"] = [bad] raw["physical_interfaces"].append(iface) with pytest.raises(ProtocolCatalogError, match="needed_document"): parse_catalog(raw) def test_reject_typical_as_standard_still_holds(): raw = build_m0_catalog() iface = copy.deepcopy(raw["physical_interfaces"][0]) typical = copy.deepcopy(iface["constraints"][0]) typical["id"] = "bad-typical-m67" typical["typical"] = True typical["value_origin"] = "TYPICAL" typical["source_type"] = "STANDARD" typical["source_class"] = "NORMATIVE" typical["mandatory"] = "MANDATORY" typical["value_kind"] = "UNKNOWN" typical["value"] = None typical["needed_document"] = "USB-IF Base Specification" iface["id"] = "usb2-hs-typical-m67" iface["constraints"] = [typical] raw["physical_interfaces"].append(iface) with pytest.raises(ProtocolCatalogError, match="typical"): parse_catalog(raw) def test_usb2_and_usb_c_unknown_with_usbif_needed_document_no_90ohm(): cat = load_catalog() logical = {p.id for p in cat.logical_protocols} assert {"usb2-ls", "usb2-fs", "usb2-hs", "usb-c", "usb3-x", "usb4"} <= logical assert "mini-hdmi" not in logical usb_ifaces = [ p for p in cat.physical_interfaces if p.logical_protocol_id in {"usb2-ls", "usb2-fs", "usb2-hs", "usb2-ls-fs", "usb-c", "usb3-x", "usb4"} or p.id.startswith("usb-") ] assert usb_ifaces z_blob = json.dumps([ c.model_dump() for p in usb_ifaces for c in p.constraints if c.parameter == "differential_impedance" ]) assert "90" not in z_blob for p in usb_ifaces: for c in p.constraints: if c.parameter == "differential_impedance": assert c.value is None assert c.value_kind != "NUMERIC" if c.value_kind in {"UNKNOWN", "MISSING_SOURCE"}: assert c.needed_document assert "USB-IF" in c.needed_document or "specification" in c.needed_document.lower() or "Specification" in c.needed_document if p.logical_protocol_id == "usb-c" and c.parameter in {"rp", "rd"}: assert c.value_kind == "NUMERIC" assert c.source and "Type-C" in (c.source.document or "") over = next(p for p in cat.physical_interfaces if p.id == "usb-c-usb2-receptacle") assert over.logical_protocol_id == "usb2-hs" assert over.connector == "Type-C" sys = next(p for p in cat.physical_interfaces if p.id == "usb-c-receptacle") assert sys.logical_protocol_id == "usb-c" assert sys.connector == "Type-C" def test_usb_c_connector_alone_is_not_usb3_or_usb2_protocol(): g = DesignGraph( components={ "J1": Component( reference="J1", value="USB Type-C receptacle", footprint="USB_C_Receptacle", component_type=ComponentType.CONNECTOR, pins={"A5": "CC1"}, ), }, nets={"CC1": _net("CC1", ("J1", "A5"))}, ) insts = recognize_physical_buses(g) logs = {i.logical_protocol_id for i in insts} assert "usb3-x" not in logs assert "usb4" not in logs assert "usb2-hs" not in logs assert any(i.logical_protocol_id == "usb-c" for i in insts) blob = json.dumps([i.model_dump() for i in insts]) assert "90" not in blob def test_usb2_over_type_c_from_connector_nets_silicon_not_usb3(): g = DesignGraph( components={ "U1": _ic("U1", {"1": "USB_D+", "2": "USB_D-"}, mpn="CH340E"), "J2": Component( reference="J2", value="USB_C_Receptacle_USB2.0_16P", footprint="USB_C_Receptacle_USB2.0_16P", component_type=ComponentType.CONNECTOR, pins={"A6": "USB_D+", "A7": "USB_D-"}, ), }, nets={ "USB_D+": _net("USB_D+", ("U1", "1"), ("J2", "A6")), "USB_D-": _net("USB_D-", ("U1", "2"), ("J2", "A7")), }, ) insts = recognize_physical_buses(g) usb = [i for i in insts if i.logical_protocol_id.startswith("usb2")] assert usb one = usb[0] assert one.connector == "Type-C" assert "USB 2.0 over USB Type-C" in one.physical_implementation assert one.logical_protocol_id != "usb3-x" assert all(i.logical_protocol_id != "usb3-x" for i in insts) assert "mux not assumed" in one.notes.lower() or "mux/switch not assumed" in one.notes.lower() blob = json.dumps(one.model_dump()) assert "90" not in blob def test_rj45_alone_is_not_1000base_t(): g = DesignGraph( components={ "J9": Component( reference="J9", value="RJ45", footprint="RJ45", component_type=ComponentType.CONNECTOR, pins={"1": "TD+"}, ), }, nets={"TD+": _net("TD+", ("J9", "1"))}, ) insts = recognize_physical_buses(g) logs = {i.logical_protocol_id for i in insts} assert "1000base-t" not in logs assert "100base-tx" not in logs assert any(i.logical_protocol_id == "ethernet-unspecified" for i in insts) def test_mini_hdmi_is_connector_not_protocol_id(): cat = load_catalog() logical = {p.id for p in cat.logical_protocols} assert "mini-hdmi" not in logical assert "hdmi-mini" not in logical mini = next(p for p in cat.physical_interfaces if p.id == "hdmi-1.4-tmds-type-c-mini") assert mini.logical_protocol_id == "hdmi-1.4" assert mini.connector == "HDMI-C-mini" assert "CONNECTOR" in mini.notes or "connector" in mini.notes.lower() g = DesignGraph( components={ "J8": Component( reference="J8", value="Mini HDMI", footprint="HDMI_C_Mini", component_type=ComponentType.CONNECTOR, pins={"1": "HPD"}, ), }, nets={"HPD": _net("HPD", ("J8", "1"))}, ) insts = recognize_physical_buses(g) assert all(i.logical_protocol_id != "mini-hdmi" for i in insts) hdmi = [i for i in insts if "hdmi" in i.logical_protocol_id] assert hdmi assert hdmi[0].connector == "HDMI-C-mini" assert hdmi[0].logical_protocol_id.startswith("hdmi-") def test_ethernet_hdmi_skeletons_unknown_needed_document(): cat = load_catalog() for lid in ( "1000base-t", "2.5gbase-t", "5gbase-t", "10gbase-t", "sgmii", "1000base-x", "10gbase-r", "hdmi-1.4", "hdmi-2.0", "hdmi-frl", ): ifaces = [p for p in cat.physical_interfaces if p.logical_protocol_id == lid] assert ifaces, lid assert all(p.incomplete for p in ifaces) for p in ifaces: for c in p.constraints: assert c.value is None assert c.value_kind != "NUMERIC" if c.value_kind in {"UNKNOWN", "MISSING_SOURCE"}: assert c.needed_document if lid.startswith("hdmi"): assert "HDMI" in c.needed_document else: assert "IEEE" in c.needed_document or "PHY" in (c.needed_document or "") or "SGMII" in (c.needed_document or "") tmds = next(p for p in cat.physical_interfaces if p.id == "hdmi-1.4-tmds-type-a") frl = next(p for p in cat.physical_interfaces if p.id == "hdmi-frl-pcb") assert tmds.logical_protocol_id != frl.logical_protocol_id