"""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"