kicad-mcp/tests/test_patterns.py
Ryan Malloy 57872e59c1 Replace kicad-sch-api with internal SchDocument engine
Eliminate the external kicad-sch-api dependency by migrating all MCP
tools to the internal SchDocument class built on sexp_tree.py. This
fixes three serialization bugs (dropped global labels, TypeError on
local labels, mis-quoted property private keywords) and removes ~1,900
lines of workaround code.

New modules:
- sexp_tree.py: S-expression parser and round-trip serializer
- sch_document.py: schematic read/write/mutate API
- lib_resolver.py: symbol library search and resolution
- sch_helpers.py: shared load/validate/expand helpers

Migrated all 9 tool files, resources, and tests. Removed 5 workaround
functions from sexp_parser.py. 531 tests pass, ruff + mypy clean.
2026-07-11 16:47:22 -06:00

245 lines
7.4 KiB
Python

"""Tests for the pattern library and MCP wrapper tools."""
import os
import pytest
from tests.conftest import requires_sch_api
@requires_sch_api
class TestDecouplingBankPattern:
"""Tests for the decoupling bank pattern library function."""
def test_place_single_cap(self, tmp_output_dir):
from kicad_sch_api import create_schematic
from mckicad.patterns.decoupling_bank import place_decoupling_bank
path = os.path.join(tmp_output_dir, "decoup_test.kicad_sch")
sch = create_schematic("decoup_test")
result = place_decoupling_bank(
sch=sch,
caps=[{"value": "100nF"}],
power_net="+3V3",
x=100,
y=100,
)
assert result["cap_count"] == 1
assert result["placed_refs"] == ["C1"]
assert result["power_net"] == "+3V3"
assert result["ground_net"] == "GND"
# Should have 2 power symbols (one VCC, one GND per cap)
assert len(result["power_symbols"]) == 2
sch.save(path)
def test_place_multiple_caps_grid(self, tmp_output_dir):
from kicad_sch_api import create_schematic
from mckicad.patterns.decoupling_bank import place_decoupling_bank
sch = create_schematic("decoup_multi")
result = place_decoupling_bank(
sch=sch,
caps=[{"value": "100nF"}, {"value": "100nF"}, {"value": "10uF"}],
power_net="+3V3",
x=100,
y=100,
cols=3,
)
assert result["cap_count"] == 3
assert len(result["placed_refs"]) == 3
# 2 power symbols per cap = 6 total
assert len(result["power_symbols"]) == 6
def test_custom_references(self, tmp_output_dir):
from kicad_sch_api import create_schematic
from mckicad.patterns.decoupling_bank import place_decoupling_bank
sch = create_schematic("decoup_refs")
result = place_decoupling_bank(
sch=sch,
caps=[
{"value": "100nF", "reference": "C101"},
{"value": "10uF", "reference": "C102"},
],
power_net="VCC",
x=100,
y=100,
)
assert result["placed_refs"] == ["C101", "C102"]
@requires_sch_api
class TestPullResistorPattern:
"""Tests for the pull resistor pattern library function."""
def test_place_pull_up(self, tmp_output_dir):
from kicad_sch_api import create_schematic
from mckicad.patterns.pull_resistor import place_pull_resistor
sch = create_schematic("pull_test")
sch.components.add(lib_id="Device:R", reference="R1", value="10k", position=(100, 100))
result = place_pull_resistor(
sch=sch,
signal_ref="R1",
signal_pin="1",
rail_net="+3V3",
value="10k",
)
assert result["resistor_ref"].startswith("R")
assert result["value"] == "10k"
assert result["rail_net"] == "+3V3"
assert result["wire_id"] is not None
assert result["power_symbol"] is not None
assert result["power_symbol"]["direction"] == "up"
def test_place_pull_down(self, tmp_output_dir):
from kicad_sch_api import create_schematic
from mckicad.patterns.pull_resistor import place_pull_resistor
sch = create_schematic("pulldown_test")
sch.components.add(lib_id="Device:R", reference="R1", value="10k", position=(100, 100))
result = place_pull_resistor(
sch=sch,
signal_ref="R1",
signal_pin="1",
rail_net="GND",
)
assert result["power_symbol"]["direction"] == "down"
def test_invalid_pin_raises(self, tmp_output_dir):
from kicad_sch_api import create_schematic
from mckicad.patterns.pull_resistor import place_pull_resistor
sch = create_schematic("pull_bad")
sch.components.add(lib_id="Device:R", reference="R1", value="10k", position=(100, 100))
with pytest.raises(ValueError, match="not found"):
place_pull_resistor(
sch=sch,
signal_ref="R1",
signal_pin="99", # nonexistent pin
rail_net="+3V3",
)
@requires_sch_api
class TestCrystalPattern:
"""Tests for the crystal oscillator pattern library function."""
def test_place_crystal_standalone(self, tmp_output_dir):
from kicad_sch_api import create_schematic
from mckicad.patterns.crystal_oscillator import place_crystal_with_caps
sch = create_schematic("xtal_test")
result = place_crystal_with_caps(
sch=sch,
xtal_value="16MHz",
cap_value="22pF",
x=200,
y=200,
)
assert result["crystal_ref"] == "Y1"
assert result["cap_xin_ref"] == "C1"
assert result["cap_xout_ref"] == "C2"
assert result["xtal_value"] == "16MHz"
assert result["cap_value"] == "22pF"
assert len(result["gnd_symbols"]) == 2
assert len(result["internal_wires"]) >= 0 # may vary with pin geometry
assert result["ic_wires"] == []
path = os.path.join(tmp_output_dir, "xtal_test.kicad_sch")
sch.save(path)
@requires_sch_api
class TestDecouplingBankMCPTool:
"""Tests for the place_decoupling_bank_pattern MCP tool wrapper."""
def test_comma_separated_values(self, populated_schematic_with_ic):
from mckicad.tools.schematic_patterns import place_decoupling_bank_pattern
result = place_decoupling_bank_pattern(
schematic_path=populated_schematic_with_ic,
cap_values="100nF,100nF,10uF",
power_net="+3V3",
x=300,
y=300,
)
assert result["success"] is True
assert result["cap_count"] == 3
assert result["engine"] == "sch_document"
def test_empty_values_rejected(self, populated_schematic_with_ic):
from mckicad.tools.schematic_patterns import place_decoupling_bank_pattern
result = place_decoupling_bank_pattern(
schematic_path=populated_schematic_with_ic,
cap_values="",
power_net="+3V3",
x=100,
y=100,
)
assert result["success"] is False
@requires_sch_api
class TestPullResistorMCPTool:
"""Tests for the place_pull_resistor_pattern MCP tool wrapper."""
def test_pull_up_via_tool(self, populated_schematic_with_ic):
from mckicad.tools.schematic_patterns import place_pull_resistor_pattern
result = place_pull_resistor_pattern(
schematic_path=populated_schematic_with_ic,
signal_ref="R1",
signal_pin="1",
rail_net="+3V3",
value="4.7k",
)
assert result["success"] is True
assert result["value"] == "4.7k"
assert result["rail_net"] == "+3V3"
@requires_sch_api
class TestCrystalMCPTool:
"""Tests for the place_crystal_pattern MCP tool wrapper."""
def test_crystal_via_tool(self, populated_schematic_with_ic):
from mckicad.tools.schematic_patterns import place_crystal_pattern
result = place_crystal_pattern(
schematic_path=populated_schematic_with_ic,
xtal_value="16MHz",
cap_value="22pF",
x=400,
y=400,
)
assert result["success"] is True
assert result["crystal_ref"] == "Y1"
assert result["cap_value"] == "22pF"