kicad-mcp/tests/test_patterns.py
Ryan Malloy ce65035a17 Add batch operations, power symbols, pattern templates, and schematic editing
New modules:
- patterns/ library: decoupling bank, pull resistor, crystal oscillator
  placement with power symbol attachment and grid math helpers
- tools/batch.py: atomic file-based batch operations with dry_run
- tools/power_symbols.py: add_power_symbol with auto #PWR refs
- tools/schematic_patterns.py: MCP wrappers for pattern library
- tools/schematic_edit.py: modify/remove components, title blocks, annotations
- resources/schematic.py: schematic data resources

43 new tests (99 total), lint clean.
2026-03-04 16:55:09 -07: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"] == "kicad-sch-api"
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"