Files
FacereDataset/tools/epro2/tests/test_sexpr.py
Knowit fb577cc89f tools/epro2/kicad: fix two KiCad 8 parse blockers (newline + pin_numbers)
装 kicad 8.0.9 (apt PPA) 后跑 kicad-cli sch erc 校验我们 emit 的
.kicad_sch 文件,发现 9/9 sheets 一开始全部报 "Failed to load schematic
file" — 父节点解析就挂掉。Bisect 找到两个语法 bug:

1. **(pin_numbers (hide no)) 不被 KiCad 8 接受**
   KiCad 8 lib_symbols 里 `pin_numbers` 是 token-form,不接受 (hide
   yes/no) 子块。要么省略整个 block 默认 visible,要么 `(pin_numbers
   hide)` 表示隐藏。原来的 `(hide no)` 风格是 KiCad 7 旧语法。

   Fix: tools/epro2/kicad/sym_writer.py 删掉 (pin_numbers (hide no))
        行;KiCad 默认 visible 行为正是我们想要的。

2. **String 里的字面 \n / \r / \t 让 KiCad 解析器中止**
   ESP-VoCat 的 Overview sheet 有 TEXT "Battary\n3.7V 700mAH"(多行
   电池标签),EPRO2 里以**字面 0x0a 字符**存储。我们把它原样 emit
   成 "..." 包住的字符串 → KiCad reader 在 quoted string 内遇到 \n
   就报 parse error 不给 message。

   Fix: tools/epro2/kicad/sexpr.py 在 str escape 路径加 \n / \r / \t
        转义;reader 加 \r 解码(roundtrip 用)。

修完后:

  9/9 sheets parse OK in KiCad 8.0.9
  ERC 跑通,9 个 sheet 共 2793 violations,分布:
     1372 endpoint_off_grid        (49%, cosmetic — 30-mil EPRO2 grid 不
                                    snap KiCad 默认 50-mil grid)
      571 lib_symbol_issues        (20%, cosmetic — facere 库未注册到
                                    user library table;库已 embed 在
                                    .kicad_sch 内联可用)
      444 wire_dangling            (16%, real — wire 端点没精确对齐 pin)
      406 pin_not_connected        (15%, 同上的另一面)

  Cosmetic 占 70%,real connectivity 30%,下个 phase 处理:
    - grid 校准(把 coord 精确 round 到统一 grid 上)
    - pin tip 端点匹配(KiCad 需要 wire 端点 == pin (at) 字段对应的
      绝对坐标,浮点必须精确相等)
    - 生成 sym-lib-table 注册 facere 库(消 lib_symbol_issues)

测试:
  + test_string_escapes_newlines_and_tabs
  + test_lib_symbol_omits_pin_numbers_block
  reader 加 \r 解码

41/41 通过(39 旧 + 2 新)。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 23:04:58 +08:00

73 lines
2.2 KiB
Python

"""S-expression emitter tests."""
from tools.epro2.kicad._sexpr_reader import parse
from tools.epro2.kicad.sexpr import Sym, to_sexpr
def test_round_trip_simple():
src = [Sym("kicad_sch"), [Sym("version"), 20231120], [Sym("paper"), "A4"]]
text = to_sexpr(src, pretty=False)
parsed = parse(text)
assert parsed[0] == "kicad_sch"
assert parsed[1] == ["version", 20231120]
assert parsed[2] == ["paper", "A4"]
def test_string_escaping():
text = to_sexpr([Sym("title"), 'a "quote" + \\backslash'], pretty=False)
parsed = parse(text)
assert parsed[1] == 'a "quote" + \\backslash'
def test_string_escapes_newlines_and_tabs():
"""KiCad 8 rejects literal newlines/CR/tabs inside quoted strings, so the
emitter must escape them. Round-trip restores the original characters."""
src = "Battary\n3.7V 700mAH\twith\rtab"
text = to_sexpr([Sym("text"), src], pretty=False)
# Raw text must NOT contain literal newline / CR / tab inside quotes
assert "\\n" in text
assert "\\r" in text
assert "\\t" in text
quoted = text[text.index('"'):text.rindex('"') + 1]
assert "\n" not in quoted
assert "\r" not in quoted
assert "\t" not in quoted
# Round-trip restores the original (our reader handles these escapes)
parsed = parse(text)
assert parsed[1] == src
def test_float_formatting_strips_trailing_zeros():
text = to_sexpr([Sym("x"), 1.500000], pretty=False)
assert "1.5" in text
assert "1.500000" not in text
def test_bool_renders_as_yes_no():
text = to_sexpr([Sym("hide"), True, False], pretty=False)
parsed = parse(text)
assert parsed == ["hide", "yes", "no"]
def test_pretty_indent_keeps_round_trip():
src = [Sym("kicad_sch"),
[Sym("wire"), [Sym("pts"), [Sym("xy"), 1.0, 2.0], [Sym("xy"), 3.0, 4.0]]]]
pretty = to_sexpr(src, pretty=True)
flat = to_sexpr(src, pretty=False)
assert parse(pretty) == parse(flat)
def test_nan_inf_rejected():
import math
try:
to_sexpr([Sym("x"), float("nan")])
except ValueError:
pass
else:
raise AssertionError("expected ValueError on NaN")
try:
to_sexpr([Sym("x"), math.inf])
except ValueError:
return
raise AssertionError("expected ValueError on Inf")