Files
rsp2k a6aa1991a1 Rename to rpmroute
The router began as a port of FreeRouting and has grown its own engine
(negotiated congestion, tree nets, exact checks, axis escapes), and
'freeroute' was one letter from the project it no longer follows. The
package, module and command are now rpmroute; the freeroute command stays
as an alias, and the RPMROUTE / RPMROUTE_ORACLE_JAVA variables fall back
to the old FREEROUTE names. Version 2026.10.07. Routing is unchanged: a
PCBench board routes to a byte-identical session either way.
2026-10-07 14:10:36 -06:00

182 lines
5.5 KiB
Python

"""End-to-end tests for parse_dsn against fixture files and inline snippets."""
from __future__ import annotations
from pathlib import Path
import pytest
from rpmroute.dsn import parse_dsn
from rpmroute.dsn.reader import DsnParseError
from rpmroute.dsn.shapes import Circle, Rectangle
from rpmroute.dsn.shapes import Path as DsnPath
FIXTURES = Path(__file__).parent / "fixtures"
def load(name: str) -> str:
return (FIXTURES / name).read_text()
# --- empty_board.dsn ---------------------------------------------------------
def test_empty_board_header_and_resolution():
board = parse_dsn(load("empty_board.dsn"))
assert board.name == "freeroute-empty.dsn"
assert board.parser.string_quote == '"'
assert board.resolution.unit == "um"
assert board.resolution.value == 10
assert board.unit == "um"
def test_empty_board_layers():
board = parse_dsn(load("empty_board.dsn"))
assert board.layer_names() == ["F.Cu", "B.Cu"]
assert all(layer.is_signal for layer in board.layers)
assert [layer.index for layer in board.layers] == [0, 1]
def test_empty_board_boundary_is_pcb_path():
board = parse_dsn(load("empty_board.dsn"))
assert isinstance(board.boundary, DsnPath)
assert board.boundary.layer == "pcb"
# closed rectangle: 5 points (10 coords), width prefix stripped
assert len(board.boundary.coords) == 10
assert board.outlines == []
# --- smd_demo.dsn ------------------------------------------------------------
@pytest.fixture
def smd():
return parse_dsn(load("smd_demo.dsn"))
def test_smd_parser_scope(smd):
assert smd.name == "smd-demo.dsn"
assert smd.parser.host_cad == "freeroute-test"
assert smd.parser.host_version == "1.0"
def test_smd_structure_via_and_rules(smd):
assert smd.via_padstack_names == ["Via[0-1]_600:300_um"]
widths = [r.value for r in smd.structure_rules.width_rules]
assert widths == [200.0]
clearances = smd.structure_rules.clearance_rules
assert clearances[0].value == 200.0
assert clearances[0].class_pairs == []
assert clearances[1].value == 50.0
assert clearances[1].class_pairs == ["smd_smd"]
def test_smd_keepout(smd):
assert len(smd.keepouts) == 1
ko = smd.keepouts[0]
assert ko.kind == "keepout"
assert ko.area.name == "no_route_zone"
assert isinstance(ko.area.border, Rectangle)
def test_smd_padstacks(smd):
assert [p.name for p in smd.padstacks] == [
"Rect[T]Pad_800x200_um",
"Via[0-1]_600:300_um",
]
rect_pad = smd.padstack("Rect[T]Pad_800x200_um")
assert isinstance(rect_pad.shapes[0], Rectangle)
assert rect_pad.attach_allowed is False
via_pad = smd.padstack("Via[0-1]_600:300_um")
assert len(via_pad.shapes) == 2
assert all(isinstance(s, Circle) for s in via_pad.shapes)
assert [s.layer for s in via_pad.shapes] == ["Top", "Bottom"]
def test_smd_images_and_pins(smd):
assert [img.name for img in smd.images] == ["MiniQFN-6", "0603"]
qfn = smd.images[0]
assert qfn.is_front is True
assert len(qfn.pins) == 3
pin1 = qfn.pins[0]
assert pin1.name == "1"
assert pin1.padstack_name == "Rect[T]Pad_800x200_um"
assert (pin1.x, pin1.y) == (-3000.0, 400.0)
def test_smd_placement(smd):
comps = {p.lib_name: p for p in smd.placements}
assert set(comps) == {"MiniQFN-6", "0603"}
u1 = comps["MiniQFN-6"].places[0]
assert u1.name == "U1"
assert (u1.x, u1.y) == (75000.0, -45000.0)
assert u1.is_front is True
assert u1.rotation == 90.0
r2 = comps["0603"].places[1]
assert r2.name == "R2"
assert r2.is_front is False
assert r2.rotation == 180.0
def test_smd_nets_and_pin_refs(smd):
assert [n.name for n in smd.nets] == ["NET_A", "NET_B"]
net_a = smd.net("NET_A")
assert [(p.component, p.pin) for p in net_a.pins] == [("U1", "1"), ("R2", "2")]
def test_smd_net_class(smd):
assert len(smd.net_classes) == 1
cls = smd.net_classes[0]
assert cls.name == "default"
assert cls.use_via == ["Via[0-1]_600:300_um"]
assert [r.value for r in cls.width_rules] == [200.0]
# --- error handling ----------------------------------------------------------
def test_non_pcb_top_scope_raises():
with pytest.raises(DsnParseError):
parse_dsn("(session foo)")
def test_pin_ref_splits_on_first_hyphen():
dsn = "(pcb b (network (net N (pins A-B-C X-1))))"
board = parse_dsn(dsn)
pins = board.net("N").pins
assert (pins[0].component, pins[0].pin) == ("A", "B-C")
assert (pins[1].component, pins[1].pin) == ("X", "1")
def test_unplaced_component_has_no_coords():
dsn = '(pcb b (placement (component "LIB" (place U9))))'
board = parse_dsn(dsn)
place = board.placements[0].places[0]
assert place.name == "U9"
assert place.x is None and place.y is None
# --- string_quote ----------------------------------------------------------
_QUOTED = """(pcb q.dsn
(parser (string_quote {q}) (space_in_quoted_tokens on))
(resolution um 10) (unit um)
(structure (layer F.Cu (type signal)) (boundary (path pcb 0 0 0 1000 0 1000 1000 0 1000 0 0)))
(placement (component "SW1" (place SW1 100 100 front 0 (PN {pn}))))
(library)
(network)
)"""
def test_a_bare_apostrophe_is_an_atom_when_the_file_quotes_with_double_quotes():
# KiCad declares " and writes a keycap's part number as a bare '
board = parse_dsn(_QUOTED.format(q='"', pn="'"))
assert board.parser.string_quote == '"'
def test_the_declared_quote_character_is_the_one_that_opens_strings():
board = parse_dsn(_QUOTED.format(q="'", pn="'10k 1%'").replace('"SW1"', "'SW1'"))
assert board.parser.string_quote == "'"