Add tools/epro2 — EPRO2 parser + replay prototype
为 Pro 3.x .epro2 工程源数据写解析骨架,下游做 EPRO2→KiCad 转换器
前的基础设施。在 ESP-VoCat (278 docs / 7.5 MB) + 220V 桌面电源
(771 docs / 26 MB) 端到端跑通,0 parse errors。
模块结构:
tools/epro2/parser.py 单行 → Op:rstrip("|") + split("||") + json.loads
tools/epro2/replay.py state-machine:DOCHEAD 设头;其它 op 按 id 做
upsert(payload=None 当 delete);EDIT_HEAD/
META/CANVAS/PREFERENCE/PANELIZE 当 doc 级单
例存
tools/epro2/__main__.py CLI:传项目目录走 manifest.json 重放每个 doc,
按 docType 聚合输出 + 可选 --dump-doc 看单文
档详情
tools/epro2/tests/ 6 个单测 pin 死 trailing-pipe / 三段消息 /
id-only-no-payload / 嵌入管道符等坑
ESP-VoCat 输出示例:
Documents: 278 (parse_errors=0)
count docType objects ops deletes untyped_ops
105 SYMBOL 4124 4439 0 0
88 DEVICE 88 264 0 0
55 FOOTPRINT 4641 4855 0 0
9 SCH_PAGE 7982 8167 42 0
6 PCB 8428 8547 38 0
6 BOARD 9 18 0 0
6 SCH 9 26 0 0
1 BLOB 4 8 0 0
1 FONT 16 28 0 0
1 CONFIG 2 3 0 0
Top ops: ATTR 7035 / ELE_PLACEHOLDER 4225 / LINE 3005 / LAYER 2318 ...
PCB 文档单 dump 验证语义正确:META 含 title (PCB-EchoEar-CoreBoard-V1_0)
+ board 引用;CANVAS 含 origin/grid/unit (mm);LAYER 1/2/3 = TOP/BOTTOM/
TOP_SILK 配色齐全。
跑法:
uv run python -m tools.epro2 data/raw/oshwhub/<project_uuid>
uv run python -m tools.epro2 data/raw/oshwhub/<uuid> --dump-doc <doc_uuid>
下一步(不在本 commit):
1. 把对象间关系建起来(COMPONENT.partId → PART;LINE.lineGroup → WIRE;
PAD_NET id → PAD + NET 三方关联)—— 当前 replay 只做扁平 dict
2. EPRO2 → KiCad 序列化层(Forge 投影硬门槛)
3. 在 Pro 3.x 三个项目做整体回归(X86 主板 7374 docs 可作压力测试)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
0
tools/epro2/tests/__init__.py
Normal file
0
tools/epro2/tests/__init__.py
Normal file
56
tools/epro2/tests/test_parser.py
Normal file
56
tools/epro2/tests/test_parser.py
Normal file
@@ -0,0 +1,56 @@
|
||||
"""Parser regression tests — pin down the ``rstrip("|")`` invariant that took
|
||||
us two debugging rounds to find (see docs/sources/easyeda_pro_source.md §3.1).
|
||||
"""
|
||||
|
||||
from tools.epro2.parser import parse_line, Epro2ParseError
|
||||
|
||||
|
||||
def test_dochead_with_trailing_pipe():
|
||||
ln = b'{"type":"DOCHEAD","ticket":2}||{"docType":"BOARD","uuid":"35086b7d90787675","editVersion":"3.2.127"}|'
|
||||
op = parse_line(ln)
|
||||
assert op.type == "DOCHEAD"
|
||||
assert op.ticket == 2
|
||||
assert op.payload["docType"] == "BOARD"
|
||||
assert op.payload["uuid"] == "35086b7d90787675"
|
||||
assert op.payload["editVersion"] == "3.2.127"
|
||||
|
||||
|
||||
def test_three_part_message():
|
||||
"""Some types carry an `extra` third field after the second `||`."""
|
||||
ln = b'{"type":"WIRE","ticket":1009,"id":"e3514"}||{"groupId":""}||{"meta":1}|'
|
||||
op = parse_line(ln)
|
||||
assert op.type == "WIRE"
|
||||
assert op.id == "e3514"
|
||||
assert op.payload == {"groupId": ""}
|
||||
assert op.extra == {"meta": 1}
|
||||
|
||||
|
||||
def test_id_only_no_payload_yields_none_payload():
|
||||
ln = b'{"type":"COMPONENT","ticket":7,"id":"e1"}|'
|
||||
op = parse_line(ln)
|
||||
assert op.type == "COMPONENT"
|
||||
assert op.id == "e1"
|
||||
assert op.payload is None # downstream replay treats this as a delete
|
||||
|
||||
|
||||
def test_bad_head_raises():
|
||||
try:
|
||||
parse_line(b"not json||{}|")
|
||||
except Epro2ParseError:
|
||||
return
|
||||
raise AssertionError("expected Epro2ParseError")
|
||||
|
||||
|
||||
def test_empty_line_raises():
|
||||
try:
|
||||
parse_line(b"|")
|
||||
except Epro2ParseError:
|
||||
return
|
||||
raise AssertionError("expected Epro2ParseError")
|
||||
|
||||
|
||||
def test_payload_with_embedded_pipes():
|
||||
"""An embedded `|` inside a JSON string must NOT be treated as a separator."""
|
||||
ln = b'{"type":"ATTR","ticket":3,"id":"e1"}||{"key":"Symbol","value":"a|b|c"}|'
|
||||
op = parse_line(ln)
|
||||
assert op.payload["value"] == "a|b|c"
|
||||
Reference in New Issue
Block a user