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).
75 lines
2.0 KiB
Python
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)
|