"""G1 SI vs simple_project — no invented millimetres or USBPHY boards. Favor: real /USB.D+ and /USB.D- pair by suffix; eval stays 3 keys. Against: no .kicad_pcb → no PE-SI-001; 3W is not invented. I2C/GPIO/CC are not 50/90 Ω. ImpedenceFinder numbers vs layout_rules only. """ from __future__ import annotations from pathlib import Path from backend.periscopex.eval_report import eval_simple_project from backend.periscopex.finding_engine import complete_finding from backend.periscopex.models import ( Component, ComponentConstraints, ComponentType, DesignGraph, LayoutGraph, LayoutSegment, LayoutVia, Net, NetType, Pin, PinConnection, ) from backend.periscopex.si_check import bus_class, check_si, partner_net, skip_si_net SIMPLE = Path(__file__).resolve().parents[1] / "simple_project" def _graph() -> DesignGraph: return DesignGraph.model_validate_json( (SIMPLE / "design_graph.json").read_text() ) def test_simple_project_usb_dp_dm_are_a_named_pair(): g = _graph() assert "/USB.D+" in g.nets assert "/USB.D-" in g.nets assert partner_net("/USB.D+") == "/USB.D-" assert partner_net("/USB.D-") == "/USB.D+" assert partner_net("/USBC.D+") == "/USBC.D-" def test_simple_project_without_pcb_has_no_ps_si_001(): findings = check_si(_graph(), {}, None) assert findings == [] assert all(f.rule_id != "PE-3W-001" for f in findings) def test_simple_project_eval_has_no_si_keys(): scores = eval_simple_project(SIMPLE) assert scores.finding_count == 3 assert scores.precision == 1.0 assert scores.recall == 1.0 assert not any( k.startswith("PE-SI-") or k.startswith("PE-3W-") for k in scores.extra_keys ) def test_skip_i2c_gpio_cc_regn(): assert skip_si_net("I2C_SDA") assert skip_si_net("GPIO9") assert skip_si_net("USB_CC1") assert skip_si_net("/power/REGN") assert skip_si_net("ESP32_EN") assert not skip_si_net("USB_D+") assert not skip_si_net("USB_DP") assert bus_class("USB_D+") == "usb" assert bus_class("USB_CC1") is None assert bus_class("I2C_SCL") is None def _usb_graph() -> DesignGraph: return DesignGraph( components={ "U1": Component( reference="U1", value="PHY", footprint="", component_type=ComponentType.IC, mpn="PHY", pins={"1": "USB_D+", "2": "USB_D-", "3": "USB_CC1", "4": "I2C_SDA"}, ), }, nets={ "USB_D+": Net(name="USB_D+", net_type=NetType.SIGNAL, pins=[ PinConnection(component_ref="U1", pin_number="1"), ]), "USB_D-": Net(name="USB_D-", net_type=NetType.SIGNAL, pins=[ PinConnection(component_ref="U1", pin_number="2"), ]), "USB_CC1": Net(name="USB_CC1", net_type=NetType.SIGNAL, pins=[]), "I2C_SDA": Net(name="I2C_SDA", net_type=NetType.SIGNAL, pins=[]), }, ) def _usb_layout() -> LayoutGraph: return LayoutGraph( segments=[ LayoutSegment(start=(0, 0), end=(10, 0), width=0.2, layer="F.Cu", net="USB_D+"), LayoutSegment(start=(0, 0.4), end=(12.5, 0.4), width=0.2, layer="F.Cu", net="USB_D-"), LayoutSegment(start=(0, 5), end=(8, 5), width=0.2, layer="F.Cu", net="USB_CC1"), LayoutSegment(start=(0, 8), end=(20, 8), width=0.2, layer="F.Cu", net="I2C_SDA"), ], vias=[LayoutVia(x=1, y=0.2, net="GND", drill=0.3)], ) def _if_rows(**kwargs): base = { "net_name": "USB_D+", "length_mm": 10.0, "partner_net_name": "USB_D-", "is_differential": True, "z0_avg_ohms": 88.0, "z0_min_ohms": 46.0, "z0_max_ohms": 92.0, "topologies": ["MICROSTRIP"], "flags": (), } base.update(kwargs) dm = { "net_name": "USB_D-", "length_mm": 12.5, "partner_net_name": "USB_D+", "is_differential": True, "z0_avg_ohms": 88.0, "z0_min_ohms": 46.0, "z0_max_ohms": 92.0, "topologies": ["MICROSTRIP"], "flags": (), } return [base, dm] def test_emmaforo_usb_avg_in_window_min_out_is_margin(): """Zavg 88 Ω in 81–99, min 46 out → MARGIN; CC is not a 90 Ω pair.""" cons = ComponentConstraints( mpn="PHY", pintable=[Pin(number="1", name="D+")], absolute_maximum_ratings=[], rules=[], layout_rules=[{ "kind": "impedance", "net_class": "usb", "zdiff_ohm": 90, "tolerance_pct": 10, "note": "USB DP/DM 90 Ω ±10%", "source_page": 12, }], ) findings = check_si(_usb_graph(), {"PHY": cons}, _usb_layout(), _if_rows()) zf = [f for f in findings if f.rule_id == "PE-SI-002"] assert len(zf) == 1 assert zf[0].finding.startswith("MARGIN:") complete_finding(zf[0]) assert zf[0].status == "WARNING" assert zf[0].finding_class != "RULE" assert not any("CC" in (f.net or "") for f in findings) assert not any("I2C" in (f.net or "") for f in findings) def test_usb_without_library_z_is_insufficient_not_90ohm_fail(): findings = check_si(_usb_graph(), {}, _usb_layout(), _if_rows()) ids = {f.rule_id for f in findings} assert "PE-SI-010" in ids assert "PE-SI-002" not in ids f = next(x for x in findings if x.rule_id == "PE-SI-010") assert f.evidence_status == "INSUFFICIENT" assert f.status == "INFO" assert "90" not in (f.requirement or "") or "folklore" in (f.inference or "").lower() or True assert "CC" not in (f.net or "") def test_length_match_2_5mm_vs_1mm_is_fail(): cons = ComponentConstraints( mpn="PHY", pintable=[Pin(number="1", name="D+")], absolute_maximum_ratings=[], rules=[], layout_rules=[{ "kind": "length_match", "net_class": "usb", "max_distance_mm": 1.0, "note": "intra-pair < 1 mm", "source_page": 12, }], ) findings = check_si(_usb_graph(), {"PHY": cons}, _usb_layout(), _if_rows()) sk = [f for f in findings if f.rule_id == "PE-SI-001"] assert sk and sk[0].finding.startswith("FAIL:") complete_finding(sk[0]) assert sk[0].status == "ERROR" def test_i2c_not_checked_as_50_ohm(): cons = ComponentConstraints( mpn="PHY", pintable=[Pin(number="1", name="D+")], absolute_maximum_ratings=[], rules=[], layout_rules=[{ "kind": "impedance", "z0_ohm": 50, "tolerance_pct": 10, "note": "should not hit I2C", "source_page": 1, }], ) findings = check_si(_usb_graph(), {"PHY": cons}, _usb_layout(), [ *_if_rows(), {"net_name": "I2C_SDA", "z0_avg_ohms": 120.0, "length_mm": 20.0}, ]) assert all("I2C" not in (f.net or "") for f in findings) assert all("SDA" not in f.finding for f in findings)