"""Std writer regression: synthetic EPRO2 docs → Option-2 Std-shaped JSON.""" import json from tools.epro2.replay import Document from tools.epro2.std.pcb_writer import write_pcb_std from tools.epro2.std.sch_writer import write_sch_std def _doc(typ, uuid="d") -> Document: d = Document(doc_uuid=uuid, doc_type=typ) d.head = {"docType": typ, "editVersion": "3.2.91"} return d # -- PCB --------------------------------------------------------------- def test_pcb_envelope_has_required_keys(): """Downstream's adapter checks for `success`/`code`/`result` at the top level and `head`/`BBox`/`layers`/`objects` inside `dataStr` — the ~100 LoC adapter's first job is to find those keys, so missing any is a hard failure for the entire pipeline.""" d = _doc("PCB", "p1") d.objects["META"] = {"_type": "META", "title": "Test"} payload = write_pcb_std(d) assert payload["success"] is True assert payload["code"] == 0 r = payload["result"] assert r["docType"] == 3 assert r["uuid"] == "p1" ds = r["dataStr"] for required in ("head", "BBox", "layers", "objects", "preference", "netColors", "DRCRULE"): assert required in ds, f"missing dataStr.{required}" def test_pcb_no_shape_field(): """No `shape` array in our output. Downstream said `shape` empty placeholder is misleading — they'll generate it themselves.""" d = _doc("PCB", "p1") payload = write_pcb_std(d) assert "shape" not in payload["result"]["dataStr"] def test_pcb_head_carries_units_and_editor_version(): """`head.units = "mil"` is the explicit hint the adapter keys off to skip mm conversion. `head.editorVersion` exposes the EPRO2 editor build the source was authored with — used by Wokwi to pick its parser branch.""" d = _doc("PCB", "p1") payload = write_pcb_std(d) head = payload["result"]["dataStr"]["head"] assert head["units"] == "mil" assert head["docType"] == "3" assert "3.2.91" in head["editorVersion"] def test_pcb_objects_dict_is_full_document_objects(): """The whole point of Option 2: pass the raw EPRO2 objects through untouched so the adapter can dispatch on `_type` and access every field without going through us. Verify the dict is preserved 1:1 (not just shallow keys).""" d = _doc("PCB", "p1") d.objects["e0"] = {"_type": "COMPONENT", "x": 100, "y": -50, "angle": 90} d.objects["ln1"] = { "_type": "LINE", "layerId": 1, "netName": "GND", "width": 6, "startX": 0, "startY": 0, "endX": 100, "endY": 0, } payload = write_pcb_std(d) objs = payload["result"]["dataStr"]["objects"] assert objs["e0"] == {"_type": "COMPONENT", "x": 100, "y": -50, "angle": 90} assert objs["ln1"]["netName"] == "GND" def test_pcb_bbox_is_min_max_of_numeric_x_y_pairs(): """BBox is best-effort min/max across known x/y/startX/startY/... fields. Adapter can refine by walking `path` arrays itself; we just give it a gross outer rectangle good enough for canvas centering. All coords stay in mil (no mm conversion) — `head.units` says so.""" d = _doc("PCB", "p1") d.objects["v1"] = {"_type": "VIA", "centerX": -100, "centerY": 50} d.objects["ln1"] = { "_type": "LINE", "startX": 0, "startY": -200, "endX": 300, "endY": 100, } payload = write_pcb_std(d) b = payload["result"]["dataStr"]["BBox"] assert b["x"] == -100 # min x across all points assert b["y"] == -200 # min y assert b["width"] == 400 # 300 - (-100) assert b["height"] == 300 # 100 - (-200) def test_pcb_layers_appends_used_inner_signals(): """An EPRO2 4-layer board with SIGNAL inner ids 15 + 16 carrying real geometry must add Std layer entries `21~Inner1...` and `22~Inner2...` — the adapter relies on these being present to know the stack-up. Unused SIGNAL inners (declared in LAYER ops but no primitives) don't get an entry; they'd just be noise.""" d = _doc("PCB", "p1") d.objects["ln1"] = {"_type": "LINE", "layerId": 15, "startX": 0, "startY": 0, "endX": 1, "endY": 0} d.objects["ln2"] = {"_type": "LINE", "layerId": 16, "startX": 0, "startY": 0, "endX": 1, "endY": 0} payload = write_pcb_std(d) layers = payload["result"]["dataStr"]["layers"] inner_lines = [s for s in layers if "Inner" in s] assert any(s.startswith("21~Inner1") for s in inner_lines) assert any(s.startswith("22~Inner2") for s in inner_lines) def test_pcb_non_pcb_doc_rejected(): d = _doc("SCH_PAGE", "x") try: write_pcb_std(d) except ValueError: return raise AssertionError("expected ValueError for non-PCB doc") # -- SCH --------------------------------------------------------------- def test_sch_envelope_carries_doctype_1(): """docType=1 routes the file to downstream's schematic parser instead of the PCB one.""" d = _doc("SCH_PAGE", "s1") d.objects["META"] = {"_type": "META", "title": "Test"} payload = write_sch_std(d) assert payload["result"]["docType"] == 1 assert payload["result"]["dataStr"]["head"]["docType"] == "1" def test_sch_layers_empty(): """Schematic has no copper stack-up; layers[] is empty.""" d = _doc("SCH_PAGE", "s1") payload = write_sch_std(d) assert payload["result"]["dataStr"]["layers"] == [] def test_sch_objects_dict_preserved(): d = _doc("SCH_PAGE", "s1") d.objects["e1"] = {"_type": "COMPONENT", "partId": "ABC.1", "x": 100, "y": 200, "rotation": 0} d.objects["a1"] = {"_type": "ATTR", "parentId": "e1", "key": "Designator", "value": "U1"} payload = write_sch_std(d) objs = payload["result"]["dataStr"]["objects"] assert objs["e1"]["partId"] == "ABC.1" assert objs["a1"]["value"] == "U1" # -- json round-trip --------------------------------------------------- def test_writers_round_trip_through_json_dump(): """Our payloads must survive json.dumps without TypeError — catches Decimal / datetime / bytes leaks early.""" d_pcb = _doc("PCB", "p1") d_pcb.objects["META"] = {"_type": "META", "title": "Test"} json.dumps(write_pcb_std(d_pcb)) d_sch = _doc("SCH_PAGE", "s1") d_sch.objects["META"] = {"_type": "META", "title": "Test"} json.dumps(write_sch_std(d_sch))