"""Tests for the S-expression parser utilities. These tests do NOT require kicad-sch-api — they test raw file parsing. """ import os import tempfile import pytest from mckicad.utils.sexp_parser import ( parse_global_labels, parse_lib_symbol_pins, transform_pin_to_schematic, ) # Minimal .kicad_sch content with global labels and lib_symbols SAMPLE_SCHEMATIC = """\ (kicad_sch (version 20231120) (generator "eeschema") (uuid "abc123") (paper "A4") (lib_symbols (symbol "Device:R" (pin_numbers hide) (pin_names (offset 0) ) (exclude_from_sim no) (in_bom yes) (on_board yes) (property "Reference" "R" (at 2.032 0 90) (effects (font (size 1.27 1.27) ) ) ) (symbol "Device:R_0_1" (polyline (pts (xy -1.016 -2.54) (xy -1.016 2.54) ) ) ) (symbol "Device:R_1_1" (pin passive line (at 0 3.81 270) (length 2.54) (name "~" (effects (font (size 1.27 1.27) ) ) ) (number "1" (effects (font (size 1.27 1.27) ) ) ) ) (pin passive line (at 0 -3.81 90) (length 2.54) (name "~" (effects (font (size 1.27 1.27) ) ) ) (number "2" (effects (font (size 1.27 1.27) ) ) ) ) ) ) (symbol "Espressif:ESP32-P4" (pin_names (offset 1.016) ) (exclude_from_sim no) (in_bom yes) (on_board yes) (symbol "Espressif:ESP32-P4_0_1" (pin input line (at -25.4 22.86 0) (length 2.54) (name "GPIO0" (effects (font (size 1.27 1.27) ) ) ) (number "1" (effects (font (size 1.27 1.27) ) ) ) ) (pin power_in line (at 0 30.48 270) (length 2.54) (name "VDD" (effects (font (size 1.27 1.27) ) ) ) (number "2" (effects (font (size 1.27 1.27) ) ) ) ) (pin output line (at 25.4 22.86 180) (length 2.54) (name "TX" (effects (font (size 1.27 1.27) ) ) ) (number "3" (effects (font (size 1.27 1.27) ) ) ) ) ) ) ) (global_label "ESP_3V3" (shape input) (at 127 95.25 180) (uuid "def456") (effects (font (size 1.27 1.27) ) (justify right) ) ) (global_label "GND" (shape input) (at 200.5 150.75 0) (uuid "ghi789") ) (global_label "SPI_CLK" (shape output) (at 300 200 90) (uuid "jkl012") ) (label "LOCAL_NET" (at 100 100 0) (uuid "mno345") ) ) """ @pytest.fixture def sample_schematic_file(): """Write the sample schematic to a temp file and return its path.""" with tempfile.NamedTemporaryFile( mode="w", suffix=".kicad_sch", delete=False, encoding="utf-8" ) as f: f.write(SAMPLE_SCHEMATIC) path = f.name yield path os.unlink(path) class TestParseGlobalLabels: def test_finds_all_global_labels(self, sample_schematic_file): labels = parse_global_labels(sample_schematic_file) assert len(labels) == 3 def test_extracts_text_and_position(self, sample_schematic_file): labels = parse_global_labels(sample_schematic_file) texts = {lbl["text"] for lbl in labels} assert texts == {"ESP_3V3", "GND", "SPI_CLK"} esp = next(lbl for lbl in labels if lbl["text"] == "ESP_3V3") assert esp["x"] == pytest.approx(127.0) assert esp["y"] == pytest.approx(95.25) gnd = next(lbl for lbl in labels if lbl["text"] == "GND") assert gnd["x"] == pytest.approx(200.5) assert gnd["y"] == pytest.approx(150.75) def test_does_not_include_local_labels(self, sample_schematic_file): labels = parse_global_labels(sample_schematic_file) texts = {lbl["text"] for lbl in labels} assert "LOCAL_NET" not in texts def test_nonexistent_file_returns_empty(self): labels = parse_global_labels("/nonexistent/path.kicad_sch") assert labels == [] class TestParseLibSymbolPins: def test_finds_resistor_pins(self, sample_schematic_file): pins = parse_lib_symbol_pins(sample_schematic_file, "Device:R") assert len(pins) == 2 nums = {p["number"] for p in pins} assert nums == {"1", "2"} pin1 = next(p for p in pins if p["number"] == "1") assert pin1["name"] == "~" assert pin1["type"] == "passive" assert pin1["x"] == pytest.approx(0.0) assert pin1["y"] == pytest.approx(3.81) def test_finds_custom_ic_pins(self, sample_schematic_file): pins = parse_lib_symbol_pins(sample_schematic_file, "Espressif:ESP32-P4") assert len(pins) == 3 names = {p["name"] for p in pins} assert names == {"GPIO0", "VDD", "TX"} gpio = next(p for p in pins if p["name"] == "GPIO0") assert gpio["number"] == "1" assert gpio["type"] == "input" assert gpio["x"] == pytest.approx(-25.4) assert gpio["y"] == pytest.approx(22.86) assert gpio["rotation"] == pytest.approx(0.0) vdd = next(p for p in pins if p["name"] == "VDD") assert vdd["type"] == "power_in" def test_does_not_match_subunit_prefix(self, sample_schematic_file): # "Espressif:ESP32-P4_0_1" is a sub-unit, not the top-level symbol pins = parse_lib_symbol_pins(sample_schematic_file, "Espressif:ESP32-P4_0") assert len(pins) == 0 def test_nonexistent_lib_id_returns_empty(self, sample_schematic_file): pins = parse_lib_symbol_pins(sample_schematic_file, "NoSuchLib:Missing") assert pins == [] def test_nonexistent_file_returns_empty(self): pins = parse_lib_symbol_pins("/nonexistent/path.kicad_sch", "Device:R") assert pins == [] class TestTransformPinToSchematic: def test_zero_rotation(self): sx, sy = transform_pin_to_schematic(0, 3.81, 100, 100, 0) assert sx == pytest.approx(100.0) assert sy == pytest.approx(103.81) def test_90_degree_rotation(self): sx, sy = transform_pin_to_schematic(0, 3.81, 100, 100, 90) assert sx == pytest.approx(100 - 3.81, abs=0.01) assert sy == pytest.approx(100.0, abs=0.01) def test_180_degree_rotation(self): sx, sy = transform_pin_to_schematic(0, 3.81, 100, 100, 180) assert sx == pytest.approx(100.0, abs=0.01) assert sy == pytest.approx(100 - 3.81, abs=0.01) def test_270_degree_rotation(self): sx, sy = transform_pin_to_schematic(0, 3.81, 100, 100, 270) assert sx == pytest.approx(100 + 3.81, abs=0.01) assert sy == pytest.approx(100.0, abs=0.01) def test_mirror_x(self): sx, sy = transform_pin_to_schematic(5, 0, 100, 100, 0, mirror_x=True) assert sx == pytest.approx(95.0) assert sy == pytest.approx(100.0)