"""ProjectRelations regression tests with synthetic micro-projects. Covers the cross-document resolution rules empirically derived from ESP-VoCat: - SCH COMPONENT.partId → SYMBOL doc(s) with matching PART.id - PCB COMPONENT → FOOTPRINT doc via ATTR(parent=comp, key=Footprint, value=fp_uuid) - PCB PAD_NET[comp,pin,pad] → pad payload inside FOOTPRINT (cross-doc) """ from tools.epro2.project_relations import ProjectRelations from tools.epro2.replay import Document, Project def _doc(uuid, doc_type, objs): d = Document(doc_uuid=uuid, doc_type=doc_type) for k, v in objs: d.objects[k] = v return d def _project(*docs): p = Project(project_uuid="testproj") for d in docs: p.documents[d.doc_uuid] = d return p def test_partid_to_symbol_resolution(): sym1 = _doc("sym_uuid_1", "SYMBOL", [ ("MyPart.1", {"_type": "PART", "BBOX": [0, 0, 10, 10], "title": "MyPart.1"}), ]) sym2 = _doc("sym_uuid_2", "SYMBOL", [ ("MyPart.1", {"_type": "PART", "BBOX": [0, 0, 10, 10], "title": "MyPart.1"}), # dup ]) sym3 = _doc("sym_uuid_3", "SYMBOL", [ ("OtherPart.1", {"_type": "PART", "title": "OtherPart.1"}), ]) sch = _doc("sch_uuid_1", "SCH_PAGE", [ ("e1", {"_type": "COMPONENT", "partId": "MyPart.1", "x": 0, "y": 0}), ("e2", {"_type": "COMPONENT", "partId": "OtherPart.1", "x": 5, "y": 5}), ]) pr = ProjectRelations.build(_project(sym1, sym2, sym3, sch)) syms = pr.resolve_symbol_docs("sch_uuid_1", "e1") assert sorted(syms) == ["sym_uuid_1", "sym_uuid_2"] assert pr.resolve_symbol_docs("sch_uuid_1", "e2") == ["sym_uuid_3"] s = pr.summary() assert s["distinct_parts"] == 2 assert s["duplicated_parts"] == 1 # MyPart.1 lives in 2 syms assert s["sch_components_with_partid"] == 2 assert s["sch_components_unresolved_part"] == 0 def test_pcb_component_to_footprint(): fp = _doc("fp_uuid_X", "FOOTPRINT", [ ("e7", {"_type": "PAD", "centerX": 1.0, "centerY": 2.0, "layerId": 1}), ]) pcb = _doc("pcb_uuid_1", "PCB", [ ("e0", {"_type": "COMPONENT", "x": 0, "y": 0, "attrs": {}}), ("attr_fp", {"_type": "ATTR", "parentId": "e0", "key": "Footprint", "value": "fp_uuid_X"}), ("attr_des", {"_type": "ATTR", "parentId": "e0", "key": "Designator", "value": "R1"}), ('["PAD_NET","e0","1","e7"]', {"_type": "PAD_NET", "padNet": "GND"}), ]) pr = ProjectRelations.build(_project(fp, pcb)) assert pr.resolve_footprint_doc("pcb_uuid_1", "e0") == "fp_uuid_X" assert pr.attrs_for_pcb_component("pcb_uuid_1", "e0") == { "Footprint": "fp_uuid_X", "Designator": "R1", } def test_pad_net_cross_doc_resolution(): fp = _doc("fp_uuid_X", "FOOTPRINT", [ ("e7", {"_type": "PAD", "centerX": 1.5, "centerY": -2.5, "num": "1"}), ("e8", {"_type": "PAD", "centerX": 3.0, "centerY": 0.0, "num": "2"}), ]) pcb = _doc("pcb_uuid_1", "PCB", [ ("e0", {"_type": "COMPONENT", "x": 0, "y": 0}), ("attr_fp", {"_type": "ATTR", "parentId": "e0", "key": "Footprint", "value": "fp_uuid_X"}), ('["PAD_NET","e0","1","e7"]', {"_type": "PAD_NET", "padNet": "GND"}), ('["PAD_NET","e0","2","e8"]', {"_type": "PAD_NET", "padNet": "VCC"}), ]) pr = ProjectRelations.build(_project(fp, pcb)) pad1 = pr.resolve_pcb_pad_net("pcb_uuid_1", "e0", "1", "e7") assert pad1 is not None assert pad1["footprint"] == "fp_uuid_X" assert pad1["pad"]["centerX"] == 1.5 pad2 = pr.resolve_pcb_pad_net("pcb_uuid_1", "e0", "2", "e8") assert pad2["pad"]["centerY"] == 0.0 # Unknown pad on known footprint → None assert pr.resolve_pcb_pad_net("pcb_uuid_1", "e0", "99", "e_ghost") is None def test_unresolved_counts_get_recorded(): # PCB component without Footprint ATTR → should count unresolved pcb = _doc("pcb_uuid_1", "PCB", [ ("e0", {"_type": "COMPONENT", "x": 0, "y": 0}), # no ATTR Footprint ]) sch = _doc("sch_uuid_1", "SCH_PAGE", [ ("e1", {"_type": "COMPONENT", "partId": "GhostPart.1"}), ]) pr = ProjectRelations.build(_project(pcb, sch)) s = pr.summary() assert s["pcb_components_unresolved_footprint"] == 1 assert s["sch_components_unresolved_part"] == 1 def test_attrs_for_pcb_component_collapses_multiple_attrs(): pcb = _doc("pcb_uuid_1", "PCB", [ ("e0", {"_type": "COMPONENT", "x": 0, "y": 0}), ("a1", {"_type": "ATTR", "parentId": "e0", "key": "Designator", "value": "R1"}), ("a2", {"_type": "ATTR", "parentId": "e0", "key": "Value", "value": "10kΩ"}), ("a3", {"_type": "ATTR", "parentId": "e0", "key": "Designator", "value": "R2"}), # later wins ]) pr = ProjectRelations.build(_project(pcb)) flat = pr.attrs_for_pcb_component("pcb_uuid_1", "e0") assert flat == {"Designator": "R2", "Value": "10kΩ"} def test_summary_keys_present(): pr = ProjectRelations.build(_project()) s = pr.summary() for k in ("documents", "doc_types", "distinct_parts", "duplicated_parts", "pcb_components_with_footprint", "pcb_components_unresolved_footprint", "sch_components_with_partid", "sch_components_unresolved_part"): assert k in s