"""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 from freeroute.geometry import IntPoint 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): # Board-level keepouts plus every placed instance of a footprint-level one. # This fixture has none of the first and six plated mounting holes, whose # copper KiCad exports as a keepout on each layer. images = {img.name: img for img in dsn.images} placed = 0 for placement in dsn.placements: image = images.get(placement.lib_name) if image is None: continue per_part = len(image.keepouts) + len(image.via_keepouts) + len(image.wire_keepouts) placed += per_part * sum(p.x is not None for p in placement.places) assert len(dsn.keepouts) == 0 assert placed == 12 assert len(board.get_obstacle_areas()) == placed # --- 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() # --- keepouts as KiCad exports them ------------------------------------------ # Found on the TerraHawk charge-node boards (2026-09-27): a no-track ring along # the board edge arrives as one keyhole polygon, "keep router off" rule areas # arrive as wire_keepout, and neither may block the board's interior. _RING_DSN = """(pcb ring (parser) (resolution um 10) (unit um) (structure (layer F.Cu (type signal)) (layer B.Cu (type signal)) (boundary (path pcb 0 0 0 40000 0 40000 -30000 0 -30000 0 0)) (keepout "" (polygon F.Cu 0 0 0 40000 0 40000 -30000 0 -30000 0 0 500 -500 500 -29500 39500 -29500 39500 -500 500 -500 0 0)) (wire_keepout "pour" (polygon F.Cu 0 10000 -10000 20000 -10000 20000 -20000 10000 -20000)) (place_keepout "courtyard" (polygon F.Cu 0 30000 -10000 35000 -10000 35000 -20000 30000 -20000)) (rule (width 250) (clearance 150))) (placement) (library) (network))""" def _areas(dsn_text): return build_board(parse_dsn(dsn_text)).get_obstacle_areas() def _blocked(areas, x_um, y_um): p = IntPoint(x_um * 10, y_um * 10) return any(t.contains(p) for a in areas for t in a.tiles) def test_keyhole_ring_keepout_blocks_only_the_border(): areas = _areas(_RING_DSN) assert _blocked(areas, 200, -15000) # inside the 0.5 mm edge strip assert not _blocked(areas, 5000, -5000) # open board interior def test_wire_keepout_is_read_and_blocks_its_area(): dsn = parse_dsn(_RING_DSN) assert [k.kind for k in dsn.keepouts].count("wire_keepout") == 1 assert _blocked(_areas(_RING_DSN), 15000, -15000) def test_place_keepout_does_not_block_routing(): assert not _blocked(_areas(_RING_DSN), 32500, -15000) def test_grid_router_leaves_the_interior_of_a_ring_keepout_open(): from freeroute.route.pipeline import route_dsn_board ring_only = "\n".join( line for line in _RING_DSN.splitlines() if "wire_keepout" not in line ).replace( "(placement) (library) (network)", "(placement (component P (place P1 5000 -15000 front 0) (place P2 35000 -15000 front 0)))\n" " (library (image P (pin pad 1 0 0)) (padstack pad (shape (circle F.Cu 600)) (attach off)))\n" " (network (net N (pins P1-1 P2-1)))", ) result, _, _ = route_dsn_board(parse_dsn(ring_only), max_passes=2) assert result.routed_net_numbers # --- pads placed as KiCad places them (2026-09-27, TerraHawk rc4) ------------ _PAD_DSN = """(pcb pads (parser) (resolution um 10) (unit um) (structure (layer F.Cu (type signal)) (layer B.Cu (type signal)) (boundary (path pcb 0 0 0 40000 0 40000 -30000 0 -30000 0 0)) (via "Via[0-1]_600:300_um") (rule (width 250) (clearance 150))) (placement (component R (place TOP 10000 -10000 front 0) (place BOT 20000 -10000 back 0) (place ROT 30000 -10000 front 90))) (library (image R (pin rect 1 -1000 0)) (padstack rect (shape (rect F.Cu -400 -250 400 250)) (attach off)) (padstack "Via[0-1]_600:300_um" (shape (circle F.Cu 600)) (shape (circle B.Cu 600)) (attach off))) (network))""" def _pins_by_ref(text): board = build_board(parse_dsn(text)) return {p.component_ref: p for p in board.get_pins()}, board def test_back_side_pad_moves_to_the_bottom_layer(): pins, board = _pins_by_ref(_PAD_DSN) names = [layer.name for layer in board.layer_structure.arr] assert [names[i] for i in pins["TOP"].layers] == ["F.Cu"] assert [names[i] for i in pins["BOT"].layers] == ["B.Cu"] def test_back_side_pin_offset_is_mirrored(): pins, _ = _pins_by_ref(_PAD_DSN) assert pins["TOP"].location.x == (10000 - 1000) * 10 assert pins["BOT"].location.x == (20000 + 1000) * 10 def test_rotated_part_rotates_its_pad_outline(): pins, _ = _pins_by_ref(_PAD_DSN) flat = pins["TOP"].shape.bounding_box() turned = pins["ROT"].shape.bounding_box() assert (flat.ur.x - flat.ll.x, flat.ur.y - flat.ll.y) == (8000, 5000) assert (turned.ur.x - turned.ll.x, turned.ur.y - turned.ll.y) == (5000, 8000) def test_oval_pad_keeps_its_width(): oval = _PAD_DSN.replace("(shape (rect F.Cu -400 -250 400 250))", "(shape (path F.Cu 500 -300 0 300 0))") pins, _ = _pins_by_ref(oval) box = pins["TOP"].shape.bounding_box() assert box.ur.y - box.ll.y == 5000 # the aperture, not a zero-height line assert box.ur.x - box.ll.x == 11000 # centreline 600 um plus the aperture def test_via_size_comes_from_the_via_padstack(): from freeroute.route.pipeline import _via_half_board assert _via_half_board(parse_dsn(_PAD_DSN), 10) == 3000 # 0.6 mm via, board units