freeroute/tests/dsn/test_shapes.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

75 lines
2.0 KiB
Python

"""Tests for DSN shape parsing."""
from __future__ import annotations
from freeroute.dsn.sexp import parse
from freeroute.dsn.shapes import (
Circle,
Path,
Polygon,
Rectangle,
read_area,
read_shape,
)
def shape_of(text: str):
return read_shape(parse(text))
def test_rectangle():
rect = shape_of("(rect Top -400 -100 400 100)")
assert isinstance(rect, Rectangle)
assert rect.layer == "Top"
assert rect.coords == [-400.0, -100.0, 400.0, 100.0]
def test_circle_with_center():
circ = shape_of("(circle Bottom 600 10 20)")
assert isinstance(circ, Circle)
assert circ.diameter == 600.0
assert (circ.center_x, circ.center_y) == (10.0, 20.0)
def test_circle_without_center_defaults_to_origin():
circ = shape_of("(circle Top 600)")
assert isinstance(circ, Circle)
assert (circ.center_x, circ.center_y) == (0.0, 0.0)
def test_polygon():
poly = shape_of("(polygon signal 0 0 0 100 0 100 100 0 100)")
assert isinstance(poly, Polygon)
assert poly.aperture_width == 0.0
assert poly.coords == [0.0, 0.0, 100.0, 0.0, 100.0, 100.0, 0.0, 100.0]
def test_path_is_width_plus_points():
path = shape_of("(path pcb 0 0 0 150000 0 150000 -90000)")
assert isinstance(path, Path)
assert path.width == 0.0
assert path.coords == [0.0, 0.0, 150000.0, 0.0, 150000.0, -90000.0]
def test_unknown_shape_returns_none():
assert shape_of("(mystery Top 1 2)") is None
def test_area_border_name_and_clearance():
area = read_area(parse('(keepout "kz" (rect Top 0 0 10 10) (clearance_class special))'))
assert area is not None
assert area.name == "kz"
assert isinstance(area.border, Rectangle)
assert area.clearance_class == "special"
assert area.holes == []
def test_area_with_window_hole():
area = read_area(
parse("(keepout (polygon Top 0 0 0 0 100 100 100 100 0) (window (rect Top 10 10 20 20)))")
)
assert area is not None
assert isinstance(area.border, Polygon)
assert len(area.holes) == 1
assert isinstance(area.holes[0], Rectangle)