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).
61 lines
1.6 KiB
Python
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")
|