"""Schematic writer regression: synthetic SCH_PAGE → kicad_sch → re-parse.""" from tools.epro2.kicad._sexpr_reader import parse from tools.epro2.kicad.sch_writer import MIL_TO_MM, write_sch_page from tools.epro2.replay import Document def _doc(objs, doc_uuid="d_test"): d = Document(doc_uuid=doc_uuid, doc_type="SCH_PAGE") d.head = {"docType": "SCH_PAGE", "editVersion": "3.2.91"} for k, v in objs: d.objects[k] = v return d def _block(parsed, name): """Return all top-level child blocks of a kicad_sch named ``name``.""" return [c for c in parsed if isinstance(c, list) and c and c[0] == name] def test_writer_emits_paper_title_and_lib_symbols(): d = _doc([("META", {"_type": "META", "title": "Test"})]) text = write_sch_page(d) p = parse(text) assert p[0] == "kicad_sch" assert _block(p, "paper")[0][1] == "A4" title = _block(p, "title_block")[0] assert title[1] == ["title", "Test"] # lib_symbols stub present (Phase 1) assert _block(p, "lib_symbols") == [["lib_symbols"]] def test_wires_emit_one_block_per_line_with_mil_to_mm_conversion(): d = _doc([ ("ln1", {"_type": "LINE", "lineGroup": "w1", "startX": 0, "startY": 0, "endX": 1000, "endY": 0}), ]) text = write_sch_page(d, sheet_origin_mm=(0.0, 0.0)) p = parse(text) wires = _block(p, "wire") assert len(wires) == 1 pts = wires[0][1] # ['pts', ['xy', x1, y1], ['xy', x2, y2]] assert pts[0] == "pts" assert pts[1][1:] == [0.0, 0.0] assert pts[2][1] == 1000 * MIL_TO_MM # 25.4 mm assert pts[2][2] == 0.0 assert getattr(write_sch_page, "last_stats").wires == 1 def test_zero_length_wire_skipped(): d = _doc([ ("ln1", {"_type": "LINE", "startX": 5, "startY": 5, "endX": 5, "endY": 5}), ]) text = write_sch_page(d) p = parse(text) assert _block(p, "wire") == [] stats = getattr(write_sch_page, "last_stats") assert stats.wires == 0 assert stats.skipped == 1 def test_component_emits_symbol_placement_with_designator(): d = _doc([ ("e1", {"_type": "COMPONENT", "partId": "MyPart.1", "x": 100, "y": 200, "rotation": 90}), ("a1", {"_type": "ATTR", "parentId": "e1", "key": "Designator", "value": "R1"}), ("a2", {"_type": "ATTR", "parentId": "e1", "key": "Value", "value": "10kΩ"}), ]) text = write_sch_page(d, sheet_origin_mm=(0.0, 0.0)) p = parse(text) syms = _block(p, "symbol") assert len(syms) == 1 sym = syms[0] # lib_id contains our partId lib_id = next(c for c in sym if isinstance(c, list) and c[0] == "lib_id") assert "MyPart.1" in lib_id[1] # at xy applied at = next(c for c in sym if isinstance(c, list) and c[0] == "at") assert at[1] == 100 * MIL_TO_MM assert at[2] == 200 * MIL_TO_MM assert at[3] == 90.0 # properties carry ATTR values props = [c for c in sym if isinstance(c, list) and c[0] == "property"] by_name = {p[1]: p[2] for p in props} assert by_name["Reference"] == "R1" assert by_name["Value"] == "10kΩ" def test_text_object_emits_text_block_when_non_empty(): d = _doc([ ("t1", {"_type": "TEXT", "value": "Hello", "x": 50, "y": 50}), ("t2", {"_type": "TEXT", "value": "", "x": 60, "y": 60}), # skipped ]) text = write_sch_page(d, sheet_origin_mm=(0.0, 0.0)) p = parse(text) texts = _block(p, "text") assert len(texts) == 1 assert texts[0][1] == "Hello" def test_named_wire_emits_global_label_at_line_start(): """EPRO2 binds wire segments into nets by NAME (WIRE.NET attr), not by geometry alone. Each LINE whose lineGroup points to a WIRE with a NET attr gets a (global_label "") at one endpoint — global, not local, because EPRO2 nets span every page of the schematic and (via PCB) the whole project; local (label) on a single page would always be flagged `label_dangling` for cross-sheet nets.""" d = _doc([ ("w1", {"_type": "WIRE"}), ("a1", {"_type": "ATTR", "parentId": "w1", "key": "NET", "value": "GND"}), ("ln1", {"_type": "LINE", "lineGroup": "w1", "startX": 100, "startY": 0, "endX": 200, "endY": 0}), ("ln2", {"_type": "LINE", "lineGroup": "w1", "startX": 300, "startY": 0, "endX": 400, "endY": 0}), ]) text = write_sch_page(d, sheet_origin_mm=(0.0, 0.0)) p = parse(text) labels = _block(p, "global_label") assert len(labels) == 2 # one label per non-degenerate LINE assert all(lab[1] == "GND" for lab in labels) # First label sits at the first LINE's start endpoint at = next(c for c in labels[0] if isinstance(c, list) and c[0] == "at") assert at[1] == 100 * MIL_TO_MM assert at[2] == 0.0 assert getattr(write_sch_page, "last_stats").labels == 2 def test_unnamed_wire_emits_no_label(): """A WIRE without a NET attr (or a LINE without a lineGroup) gets no label — emitting a label without a name would be syntactically invalid and semantically meaningless.""" d = _doc([ ("w1", {"_type": "WIRE"}), # no NET attr ("ln1", {"_type": "LINE", "lineGroup": "w1", "startX": 0, "startY": 0, "endX": 100, "endY": 0}), ]) text = write_sch_page(d, sheet_origin_mm=(0.0, 0.0)) p = parse(text) assert _block(p, "global_label") == [] assert getattr(write_sch_page, "last_stats").labels == 0 def test_power_port_component_emits_global_label_at_placement(): """EPRO2 represents power rails (VCC/GND/VBUS/...) as generic placeholder COMPONENTs whose net name lives in a `Global Net Name` ATTR on the placement (not on the underlying PART). Without an explicit (global_label) at the pin tip, every such instance reads as pin_not_connected even when the symbol's pin sits on a wire.""" d = _doc([ ("e1", {"_type": "COMPONENT", "partId": "pid8a0e77bacb214e", "x": 100, "y": 50, "rotation": 0}), ("a1", {"_type": "ATTR", "parentId": "e1", "key": "Global Net Name", "value": "VBUS"}), ]) text = write_sch_page(d, sheet_origin_mm=(0.0, 0.0)) p = parse(text) labels = _block(p, "global_label") assert len(labels) == 1 assert labels[0][1] == "VBUS" at = next(c for c in labels[0] if isinstance(c, list) and c[0] == "at") assert at[1] == 100 * MIL_TO_MM assert at[2] == 50 * MIL_TO_MM def test_unnamed_power_port_emits_no_label(): """Unnamed placeholder power-ports (Global Net Name absent) stay as bare symbol placements — we have no rail name to bind them to.""" d = _doc([ ("e1", {"_type": "COMPONENT", "partId": "pid8a0e77bacb214e", "x": 0, "y": 0}), ]) text = write_sch_page(d, sheet_origin_mm=(0.0, 0.0)) p = parse(text) assert _block(p, "global_label") == [] def test_sheet_path_and_page_num_propagate_to_sheet_instances(): """When a page is written as a child of a hierarchical root, its (sheet_instances) must echo the uuid the root assigned (path "/") and its hierarchy page number — without that the root can't bind the child and ERC treats it as standalone.""" d = _doc([("META", {"_type": "META", "title": "child"})]) text = write_sch_page( d, sheet_path="/22222222-3333-4444-5555-666666666666", page_num=3, ) p = parse(text) inst = _block(p, "sheet_instances")[0] path = next(c for c in inst if isinstance(c, list) and c[0] == "path") assert path[1] == "/22222222-3333-4444-5555-666666666666" page = next(c for c in path if isinstance(c, list) and c[0] == "page") assert page[1] == "3" def test_non_sch_page_doc_rejected(): d = Document(doc_uuid="x", doc_type="PCB") try: write_sch_page(d) except ValueError: return raise AssertionError("expected ValueError for non-SCH_PAGE doc")