"""Tests for the native SES -> .kicad_pcb applier.""" from pathlib import Path import pytest from mckicad.utils.ses_apply import ( SesApplyError, _fmt, _padstack_dims, _transform, apply_ses_to_board, build_net_map, resolution_scale, ) from mckicad.utils.sexp_tree import find, parse, parse_file FIXTURES = Path(__file__).parent / "fixtures" BOARD = FIXTURES / "arduino_mega.kicad_pcb" ROUTED_SES = FIXTURES / "arduino_mega_routed.ses" # A hand-built SES with a single-segment wire and one via, in um/10 units. SMALL_SES = """\ (session small (routes (resolution um 10) (library_out (padstack "Via[0-1]_600:300_um" (shape (circle F.Cu 6000 0 0)) (shape (circle B.Cu 6000 0 0)) (attach off) ) ) (network_out (net GND (wire (path F.Cu 2000 1965200 -923800 1939800 -923800 ) ) (via "Via[0-1]_600:300_um" 1928283 -923800) ) (net NO_SUCH_NET (wire (path B.Cu 2000 100000 -100000 200000 -100000 ) ) ) ) ) ) """ # -------------------------------------------------------------------------- # Resolution parsing / scaling # -------------------------------------------------------------------------- @pytest.mark.unit def test_resolution_scale_um_10(): # (resolution um 10): 1 SES unit = 0.1 um = 1/10000 mm assert resolution_scale("um", 10) == pytest.approx(1e-4) @pytest.mark.unit def test_resolution_scale_units(): assert resolution_scale("mm", 1) == pytest.approx(1.0) assert resolution_scale("um", 1) == pytest.approx(0.001) assert resolution_scale("mil", 1) == pytest.approx(0.0254) assert resolution_scale("inch", 1) == pytest.approx(25.4) @pytest.mark.unit def test_resolution_scale_rejects_bad_unit(): with pytest.raises(SesApplyError): resolution_scale("furlong", 10) @pytest.mark.unit def test_resolution_scale_rejects_zero(): with pytest.raises(SesApplyError): resolution_scale("um", 0) # -------------------------------------------------------------------------- # Coordinate transform (the de-risked J7 anchor) # -------------------------------------------------------------------------- @pytest.mark.unit def test_transform_j7_anchor(): # SES (place J7 1939800 -923800) <-> board (at 193.98 92.38) scale = resolution_scale("um", 10) x, y = _transform(1939800, -923800, scale) assert x == "193.98" assert y == "92.38" @pytest.mark.unit def test_transform_y_axis_flip(): scale = resolution_scale("um", 10) # Positive SES y flips to negative KiCad y. _, y = _transform(0, 100000, scale) assert y == "-10" # Origin stays clean (no "-0"). assert _transform(0, 0, scale) == ("0", "0") @pytest.mark.unit def test_fmt_trims_trailing_zeros(): assert _fmt(0.2) == "0.2" assert _fmt(196.52) == "196.52" assert _fmt(-0.0) == "0" # -------------------------------------------------------------------------- # Net-name -> number mapping # -------------------------------------------------------------------------- @pytest.mark.unit def test_build_net_map(): root = parse_file(str(BOARD)) net_map = build_net_map(root) assert net_map["GND"] == 1 assert net_map["+5V"] == 34 assert net_map["VCC"] == 76 # The empty net 0 is still present. assert net_map[""] == 0 # -------------------------------------------------------------------------- # Padstack -> via geometry # -------------------------------------------------------------------------- @pytest.mark.unit def test_padstack_dims_from_name(): # "600:300_um" -> 0.6 mm pad, 0.3 mm drill size, drill = _padstack_dims("Via[0-1]_600:300_um", shape_diameter_mm=0.6) assert size == pytest.approx(0.6) assert drill == pytest.approx(0.3) @pytest.mark.unit def test_padstack_dims_fallback_to_shape_and_default(): size, drill = _padstack_dims("MysteryPad", shape_diameter_mm=0.8) assert size == pytest.approx(0.8) assert drill == pytest.approx(0.3) # default drill size, drill = _padstack_dims("MysteryPad", shape_diameter_mm=None) assert size == pytest.approx(0.6) # default size assert drill == pytest.approx(0.3) # -------------------------------------------------------------------------- # Small hand-built SES -> expected segment/via s-expr # -------------------------------------------------------------------------- @pytest.mark.unit def test_small_ses_emits_expected_segment_and_via(tmp_path): ses = tmp_path / "small.ses" ses.write_text(SMALL_SES) out = tmp_path / "out.kicad_pcb" result = apply_ses_to_board(str(BOARD), str(ses), str(out)) # One GND wire (1 segment) + one via; NO_SUCH_NET skipped as unknown. assert result["segments_added"] == 1 assert result["vias_added"] == 1 assert result["nets_routed"] == 1 assert result["unknown_nets"] == ["NO_SUCH_NET"] root = parse_file(str(out)) segments = find(root, "segment") vias = find(root, "via") assert len(segments) == 1 assert len(vias) == 1 seg = segments[0] def child_vals(node, tag): for c in node.children: if c.tag == tag: return c.values return None assert child_vals(seg, "start") == ["196.52", "92.38"] assert child_vals(seg, "end") == ["193.98", "92.38"] assert child_vals(seg, "width") == ["0.2"] assert child_vals(seg, "layer") == ["F.Cu"] assert child_vals(seg, "net") == ["1"] # GND via = vias[0] assert child_vals(via, "at") == ["192.8283", "92.38"] assert child_vals(via, "size") == ["0.6"] assert child_vals(via, "drill") == ["0.3"] assert child_vals(via, "layers") == ["F.Cu", "B.Cu"] assert child_vals(via, "net") == ["1"] @pytest.mark.unit def test_apply_preserves_original_board_bytes(tmp_path): ses = tmp_path / "small.ses" ses.write_text(SMALL_SES) out = tmp_path / "out.kicad_pcb" apply_ses_to_board(str(BOARD), str(ses), str(out)) original = BOARD.read_text() produced = out.read_text() # Everything up to the injected block is untouched. insert = produced.index("\t(segment") assert produced[:insert] == original[: len(produced[:insert])] # The board still closes cleanly. assert produced.rstrip().endswith(")") # -------------------------------------------------------------------------- # Integration: real routed SES applied to real board # -------------------------------------------------------------------------- @pytest.mark.integration def test_apply_real_routed_ses(tmp_path): out = tmp_path / "routed.kicad_pcb" result = apply_ses_to_board(str(BOARD), str(ROUTED_SES), str(out)) # The oracle SES routes GND (11 segments) and +5V (12 segments), no vias. assert result["segments_added"] == 23 assert result["vias_added"] == 0 assert result["nets_routed"] == 2 assert result["unknown_nets"] == [] assert out.exists() @pytest.mark.integration def test_applied_segments_reference_valid_nets(tmp_path): out = tmp_path / "routed.kicad_pcb" apply_ses_to_board(str(BOARD), str(ROUTED_SES), str(out)) root = parse_file(str(out)) net_numbers = {int(n.values[0]) for n in find(root, "net") if n.values} segments = find(root, "segment") assert len(segments) > 0 for seg in segments: net_child = next((c for c in seg.children if c.tag == "net"), None) assert net_child is not None assert int(net_child.values[0]) in net_numbers @pytest.mark.integration def test_applied_board_reparses(tmp_path): out = tmp_path / "routed.kicad_pcb" apply_ses_to_board(str(BOARD), str(ROUTED_SES), str(out)) # The produced file must still be a single well-formed s-expression. root = parse(out.read_text()) assert root.tag == "kicad_pcb"