"""M5 protocol report: measured/limit/margin/source/method, chain, FAIL first, skips visible.""" from __future__ import annotations import json from backend.periscopex.models import ( Component, ComponentType, DesignGraph, Net, NetType, PinConnection, ) from backend.periscopex.protocol_catalog import ProtocolCatalogError, parse_catalog from backend.periscopex.protocol_l0 import protocol_exam from backend.periscopex.protocol_report import ( instance_worst_result, result_rank, ) 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 _usb_connected() -> DesignGraph: return DesignGraph( components={ "U1": _ic("U1", {"1": "USB_D+", "2": "USB_D-"}, mpn="CH340E"), "J2": Component( reference="J2", value="USB2_TypeA", footprint="USB_A", component_type=ComponentType.CONNECTOR, pins={"2": "USB_D+", "3": "USB_D-"}, ), }, nets={ "USB_D+": _net("USB_D+", ("U1", "1"), ("J2", "2")), "USB_D-": _net("USB_D-", ("U1", "2"), ("J2", "3")), }, ) def _usb_dangling_minus() -> DesignGraph: return DesignGraph( components={ "U1": _ic("U1", {"1": "USB_D+"}, mpn="CH340E"), "J2": Component( reference="J2", value="USB2_TypeA", footprint="USB_A", component_type=ComponentType.CONNECTOR, pins={"2": "USB_D+", "3": "USB_D-"}, ), }, nets={ "USB_D+": _net("USB_D+", ("U1", "1"), ("J2", "2")), "USB_D-": _net("USB_D-", ("J2", "3")), }, ) def _axi_graph() -> DesignGraph: return DesignGraph( components={ "U3": _ic("U3", {"1": "AXI_AWVALID"}, mpn="XC7A100T", value="AXI4 interconnect"), }, nets={"AXI_AWVALID": _net("AXI_AWVALID", ("U3", "1"))}, ) def _ddr_no_grouping() -> DesignGraph: pins = {str(i): f"MEM_DAT{i}" for i in range(8)} return DesignGraph( components={"U5": _ic("U5", pins, mpn="MT41K256M16TW", value="DDR4")}, nets={n: _net(n, ("U5", p)) for p, n in pins.items()}, ) def test_result_rank_fail_before_warning(): assert result_rank("FAIL") < result_rank("WARNING") assert result_rank("FAIL") < result_rank("UNKNOWN") assert instance_worst_result(["UNKNOWN", "FAIL", "PASS"]) == "FAIL" def test_usb2_pair_l0_pass_l2_z_unknown_no_invented_ohm(): sec, _ = protocol_exam(_usb_connected()) assert sec.max_level_reached == "L3" assert sec.macrophase == "M9" usb = next( r for r in sec.recognized_instances if "usb" in str(r.get("logical_protocol_id")) ) checks = usb["report_checks"] l0_topo = [c for c in checks if c["level"] == "L0" and c["check"] == "topology"] assert l0_topo and l0_topo[0]["result"] == "PASS" z = [c for c in checks if c["check"] == "differential_impedance"] assert z assert z[0]["result"] in {"MISSING_SOURCE", "UNKNOWN"} assert z[0]["skip_visible"] is True assert z[0]["measured"] is None assert z[0]["limit"] is None blob = json.dumps(z[0]) assert "90" not in blob chain = z[0]["chain"] assert chain["physical_interface_id"] assert chain["logical_protocol_id"] assert chain["physical_interface_id"] != chain["logical_protocol_id"] l3 = [c for c in checks if c["level"] == "L3"] assert l3 and l3[0]["skip_visible"] assert l3[0]["result"] != "PASS" assert "OpenEMS" in l3[0]["notes"] def test_fail_listed_before_warning_in_report_checks(): sec, findings = protocol_exam(_usb_dangling_minus()) usb = next( r for r in sec.recognized_instances if "usb" in str(r.get("logical_protocol_id")) ) assert usb["worst_result"] == "FAIL" assert usb["fail_count"] >= 1 results = [c["result"] for c in usb["report_checks"]] assert "FAIL" in results assert results.index("FAIL") < next( i for i, r in enumerate(results) if r in {"UNKNOWN", "MISSING_SOURCE", "WARNING"} ) assert any(f.status == "ERROR" and f.rule_id == "PE-PRT-L0-001" for f in findings) def test_axi_na_visible_not_fail(): sec, _ = protocol_exam(_axi_graph()) row = next( r for r in sec.recognized_instances if "axi" in str(r.get("logical_protocol_id")) ) checks = row["report_checks"] assert any(c["result"] == "NOT_APPLICABLE" for c in checks) assert row["worst_result"] != "FAIL" assert all(c["result"] != "FAIL" for c in checks if c["level"] != "L3") def test_ddr_grouping_skip_visible_not_pass(): sec, findings = protocol_exam(_ddr_no_grouping()) ddr = next( r for r in sec.recognized_instances if str(r.get("logical_protocol_id")).startswith("ddr") ) grouping = [ c for c in ddr["report_checks"] if c["check"] in {"byte_lane_mapping", "dq_to_dqs_skew", "byte_lane_skew"} ] assert grouping assert all(c["result"] != "PASS" for c in grouping) assert ddr["worst_result"] == "FAIL" or any(c["skip_visible"] for c in grouping) notes = " ".join((c.get("notes") or "") for c in grouping).lower() assert "byte-lane" in notes or "grouping" in notes or "geometria" in notes or "dq" in notes assert any( f.rule_id in {"PE-PRT-L0-001", "PE-PRT-L1-002", "PE-PRT-L1-001"} for f in findings ) def test_report_check_has_measured_limit_margin_source_method_keys(): sec, _ = protocol_exam(_usb_connected()) usb = sec.recognized_instances[0] row = usb["report_checks"][0] for key in ( "measured", "limit", "margin", "source", "method", "chain", "skip_visible", "result", "level", ): assert key in row chain = row["chain"] for key in ( "net", "group", "physical_interface_id", "logical_protocol_id", "constraint_id", "document", "section", ): assert key in chain def test_typical_still_rejected_as_standard(): raw = { "schema_version": "1.0.0", "logical_protocols": [{ "id": "usb2-hs", "name": "USB2 HS", "bus_type": "SERIAL", }], "physical_interfaces": [{ "id": "usb2-hs-dpair", "logical_protocol_id": "usb2-hs", "bus_type": "SERIAL", "physical_layer": "SERIAL_DIFFERENTIAL", "pcb_relevant": "YES", "required_checks": ["differential_impedance"], "constraints": [{ "id": "bad", "parameter": "differential_impedance", "value_kind": "NUMERIC", "mandatory": "MANDATORY", "source_type": "STANDARD", "source_class": "NORMATIVE", "value": 90, "unit": "ohm", "typical": True, "source": {"document": "blog", "organization": "web"}, }], }], } try: parse_catalog(raw) raise AssertionError("typical-as-standard must be rejected") except ProtocolCatalogError: pass