"""End-to-end tests for the SES writer. Where possible these re-parse the emitted SES with the project's own tokenizer / S-expression parser (:mod:`freeroute.dsn.sexp`) and assert structural invariants, mirroring what FreeRouting's ``SesReader`` and ``kicad-cli pcb import specctra-ses`` walk: ``session > routes > network_out > net > wire(path)/via``. """ from __future__ import annotations from pathlib import Path import pytest from freeroute.dsn import parse_dsn from freeroute.dsn.sexp import SExp, parse from freeroute.ses import RoutedVia, RoutedWire, RoutingResult, write_ses FIXTURES = Path(__file__).parent.parent / "dsn" / "fixtures" def load_board(name: str): return parse_dsn((FIXTURES / name).read_text()) @pytest.fixture def board(): return load_board("smd_demo.dsn") def child(node: SExp, head: str) -> SExp: found = node.child(head) assert found is not None, f"missing ({head} ...) scope" return found # --- no-op session ----------------------------------------------------------- def test_noop_session_is_parseable_and_well_formed(board): text = write_ses(board) top = parse(text) # must be a single balanced S-expression assert top.head == "session" # session name argument: design name with .dsn -> .ses assert top.values()[0].text == "smd-demo.ses" def test_noop_session_has_required_scopes(board): top = parse(write_ses(board)) assert child(top, "base_design").values()[0].text == "smd-demo.dsn" assert top.child("placement") is not None assert top.child("was_is") is not None routes = child(top, "routes") # routes must carry a resolution (KiCad scales session coords by it) assert child(routes, "resolution").values()[0].text == "um" assert routes.child("network_out") is not None def test_noop_network_out_is_empty(board): routes = child(parse(write_ses(board)), "routes") network = child(routes, "network_out") assert network.children("net") == [] def test_placement_echoes_components(board): placement = child(parse(write_ses(board)), "placement") comps = placement.children("component") names = [c.values()[0].text for c in comps] assert names == ["MiniQFN-6", "0603"] # U1 place: coords rounded to int, side, rotation u1 = comps[0].child("place") vals = [v.text for v in u1.values()] assert vals == ["U1", "75000", "-45000", "front", "90"] def test_library_out_echoes_via_padstack(board): routes = child(parse(write_ses(board)), "routes") lib = child(routes, "library_out") pads = lib.children("padstack") assert [p.values()[0].text for p in pads] == ["Via[0-1]_600:300_um"] # --- routed geometry --------------------------------------------------------- @pytest.fixture def routed(): result = RoutingResult() result.add_wire("NET_A", RoutedWire(layer="Top", width=200, coords=[0, 0, 1000, -500])) result.add_via("NET_A", RoutedVia(padstack="Via[0-1]_600:300_um", x=1000, y=-500)) result.add_wire("NET_B", RoutedWire(layer="Bottom", width=250, coords=[10, 10, 20, 20])) return result def test_routed_network_out_has_nets(board, routed): routes = child(parse(write_ses(board, routed)), "routes") network = child(routes, "network_out") nets = network.children("net") assert [n.values()[0].text for n in nets] == ["NET_A", "NET_B"] def test_routed_wire_path_and_via(board, routed): routes = child(parse(write_ses(board, routed)), "routes") net_a = child(routes, "network_out").children("net")[0] wire = child(net_a, "wire") path = child(wire, "path") pvals = [v.text for v in path.values()] # (path x1 y1 x2 y2) assert pvals == ["Top", "200", "0", "0", "1000", "-500"] via = child(net_a, "via") vvals = [v.text for v in via.values()] assert vvals == ["Via[0-1]_600:300_um", "1000", "-500"] def test_coordinates_are_integers(board): result = RoutingResult() result.add_wire("NET_A", RoutedWire(layer="Top", width=199.6, coords=[0.4, -0.4, 2.5, 3.5])) routes = child(parse(write_ses(board, result)), "routes") path = child(child(child(routes, "network_out").children("net")[0], "wire"), "path") pvals = [v.text for v in path.values()] # width 199.6 -> 200; coords 0.4->0, -0.4->0, 2.5->3, 3.5->4 (round half up) assert pvals == ["Top", "200", "0", "0", "3", "4"] assert all("." not in v for v in pvals) def test_empty_result_matches_noop(board): assert write_ses(board, RoutingResult()) == write_ses(board) def test_round_trip_from_empty_board_fixture(): board = load_board("empty_board.dsn") top = parse(write_ses(board)) assert top.head == "session" assert top.values()[0].text == "freeroute-empty.ses" # empty board has no placements -> placement scope has no components assert child(top, "placement").children("component") == []