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.
245 lines
7.4 KiB
Python
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"
|