freeroute/tests/dsn/test_sexp.py
Ryan Malloy 3bf4f6bed2 Implement Specctra DSN parser with typed board model
Ports the read path of FreeRouting's io/specctra/parser package to a
Java-free Python implementation:

- tokenizer: S-expression lexer mirroring SpecctraFileDescription.flex
  (comments, quoted strings, the string_quote IGNORE_QUOTE directive,
  case-insensitive keywords, and hash-prefixed names)
- sexp: nested S-expression tree builder
- shapes: rect/circle/polygon/path plus area-with-holes scopes
- model: typed dataclasses for layers, padstacks, images, placements,
  nets, net classes, rules, keepouts
- reader: recursive-descent scope readers producing a DsnBoard, one
  _read_* function per FreeRouting read_scope method

46 pytest cases cover the tokenizer, tree, shapes, and end-to-end
parsing against hand-crafted fixtures modeled on FreeRouting's own
test DSN files. Parses 90 of 91 upstream fixtures (the one failure is
a binary OLE file, not text DSN).
2026-07-11 15:21:18 -06:00

61 lines
1.6 KiB
Python

"""Tests for the S-expression tree parser."""
from __future__ import annotations
import pytest
from freeroute.dsn.sexp import SExp, parse
from freeroute.dsn.tokenizer import DsnSyntaxError, Token
def test_parse_simple_head_and_values():
tree = parse("(resolution um 10)")
assert tree.head == "resolution"
vals = tree.values()
assert [v.text for v in vals] == ["um", "10"]
def test_nested_children():
tree = parse("(structure (layer Top (type signal)) (layer Bot (type signal)))")
assert tree.head == "structure"
layers = tree.children("layer")
assert len(layers) == 2
assert isinstance(layers[0], SExp)
assert layers[0].values()[0].text == "Top"
type_scope = layers[0].child("type")
assert type_scope is not None
assert type_scope.values()[0].text == "signal"
def test_child_missing_returns_none():
tree = parse("(a (b 1))")
assert tree.child("nope") is None
def test_values_skips_nested_lists():
tree = parse("(net NET_A (pins U1-1 R2-2))")
assert [v.text for v in tree.values()] == ["NET_A"]
pins = tree.child("pins")
assert pins is not None
assert [v.text for v in pins.values()] == ["U1-1", "R2-2"]
def test_leaf_items_are_tokens():
tree = parse("(a 1 2)")
assert all(isinstance(it, Token) for it in tree.items)
def test_unbalanced_raises():
with pytest.raises(DsnSyntaxError):
parse("(a (b )")
def test_trailing_token_raises():
with pytest.raises(DsnSyntaxError):
parse("(a) (b)")
def test_no_toplevel_list_raises():
with pytest.raises(DsnSyntaxError):
parse("just atoms no parens")