Ports the data model of the board Item hierarchy (Item base, Pin, ObstacleArea/ConductionArea, Via, Trace) and BasicBoard (layers, nets, clearance, bounding box, items with query helpers), then build_board: the load-bearing integration that constructs a BasicBoard from a parsed DsnBoard. build_board maps layers -> LayerStructure, resolution -> transform, default clearance rule -> ClearanceMatrix, nets -> Nets plus a (component,pin)->net map, padstacks x placement -> Pin items, and keepouts -> ObstacleArea items. Rectangle pads become exact IntBoxes, convex polygon pads exact Simplexes, circle pads their bounding box (documented approximation); every pad is centred on its pin location so it contains that location by construction. Trace/Via are router-produced and lightweight here (an imported unrouted board has none). Validated on a real KiCad export (kicad_routable.dsn): layer/net/pin counts match the parsed DSN, every pin's pad shape contains its origin, every pin reports a valid net, and pin locations lie in the board bounding box. An oracle-gated test cross-checks that every net the reference FreeRouting JAR routes exists on the constructed board.
132 lines
4.1 KiB
Python
132 lines
4.1 KiB
Python
"""DSN -> board construction invariants, validated on a real KiCad board.
|
|
|
|
There is no unit-test oracle for the board model, so these assert structural
|
|
invariants against the parsed DSN (the source of truth) and — when a JVM + JAR
|
|
are available — cross-check the board's net set against the reference router's
|
|
routed net set.
|
|
|
|
Fixture: ``tests/dsn/fixtures/kicad_routable.dsn`` (a KiCad pcbnew export).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
from freeroute.board import BasicBoard, ObstacleArea, Pin, build_board
|
|
from freeroute.dsn import parse_dsn
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
|
|
FIXTURES = Path(__file__).resolve().parent.parent / "dsn" / "fixtures"
|
|
ROUTABLE = FIXTURES / "kicad_routable.dsn"
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def dsn():
|
|
return parse_dsn(ROUTABLE.read_text())
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def board(dsn) -> BasicBoard:
|
|
return build_board(dsn)
|
|
|
|
|
|
# --- structural counts match the DSN ----------------------------------------
|
|
|
|
|
|
def test_layer_count_matches_dsn(dsn, board):
|
|
assert board.get_layer_count() == len(dsn.layers)
|
|
assert [layer.name for layer in board.layer_structure.arr] == [
|
|
layer.name for layer in dsn.layers
|
|
]
|
|
|
|
|
|
def test_net_count_matches_dsn(dsn, board):
|
|
assert len(board.nets) == len(dsn.nets)
|
|
assert board.nets.names() == {n.name for n in dsn.nets}
|
|
|
|
|
|
def test_pin_count_matches_placement(dsn, board):
|
|
images = {img.name: img for img in dsn.images}
|
|
expected = sum(
|
|
len(images[pl.lib_name].pins)
|
|
for pl in dsn.placements
|
|
for place in pl.places
|
|
if place.x is not None and pl.lib_name in images
|
|
)
|
|
assert len(board.get_pins()) == expected
|
|
assert expected > 0
|
|
|
|
|
|
def test_obstacle_count_matches_keepouts(dsn, board):
|
|
# each keepout expands to one obstacle per applicable layer; with no keepouts
|
|
# in this fixture the count is zero
|
|
assert len(board.get_obstacle_areas()) >= len(dsn.keepouts)
|
|
assert len(dsn.keepouts) == 0
|
|
assert board.get_obstacle_areas() == []
|
|
|
|
|
|
# --- per-item invariants ----------------------------------------------------
|
|
|
|
|
|
def test_every_pin_shape_contains_its_origin(board):
|
|
pins = board.get_pins()
|
|
assert pins
|
|
for pin in pins:
|
|
assert pin.shape_contains_origin(), f"pad shape missed origin for pin {pin.name}"
|
|
|
|
|
|
def test_every_pin_reports_a_net(dsn, board):
|
|
# this board's netlist connects every pad, so every pin has a net number
|
|
for pin in board.get_pins():
|
|
assert pin.net_nos, f"pin {pin.name} has no net"
|
|
for net_no in pin.net_nos:
|
|
assert board.nets.get_by_number(net_no) is not None
|
|
|
|
|
|
def test_items_report_correct_net_membership(board):
|
|
for pin in board.get_pins():
|
|
for net_no in pin.net_nos:
|
|
assert pin in board.get_connectable_items(net_no)
|
|
|
|
|
|
def test_pin_layers_are_valid(board):
|
|
n_layers = board.get_layer_count()
|
|
for pin in board.get_pins():
|
|
assert pin.layers
|
|
assert all(0 <= layer < n_layers for layer in pin.layers)
|
|
|
|
|
|
def test_bounding_box_is_non_degenerate(board):
|
|
assert not board.bounding_box.is_empty()
|
|
assert board.bounding_box.dimension() == 2
|
|
# every pin location lies within the board bounding box
|
|
for pin in board.get_pins():
|
|
assert board.bounding_box.contains(pin.location)
|
|
|
|
|
|
def test_item_types(board):
|
|
for item in board.get_items():
|
|
assert isinstance(item, (Pin, ObstacleArea))
|
|
|
|
|
|
# --- oracle cross-check ------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.oracle
|
|
def test_board_net_set_covers_oracle_routed_nets(board):
|
|
from oracle import HAS_ORACLE, route_dsn, routed_net_set
|
|
|
|
if not HAS_ORACLE:
|
|
pytest.skip("FreeRouting oracle unavailable")
|
|
# Routing the full board is slow; a couple of passes is enough to route most
|
|
# nets, and a generous timeout absorbs JVM start-up variance.
|
|
ses = route_dsn(ROUTABLE, max_passes=2, timeout=420)
|
|
routed = routed_net_set(ses)
|
|
assert routed, "oracle produced no routed nets"
|
|
# every net the reference router routed exists on our constructed board
|
|
assert routed <= board.nets.names()
|