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.
176 lines
5.7 KiB
Python
176 lines
5.7 KiB
Python
"""Tests for the power symbol tool and geometry helper."""
|
|
|
|
import os
|
|
|
|
from tests.conftest import requires_sch_api
|
|
|
|
|
|
@requires_sch_api
|
|
class TestAddPowerSymbolToPin:
|
|
"""Tests for the _geometry.add_power_symbol_to_pin() helper."""
|
|
|
|
def test_ground_symbol_placed_below_pin(self, tmp_output_dir):
|
|
from kicad_sch_api import create_schematic
|
|
|
|
from mckicad.patterns._geometry import add_power_symbol_to_pin
|
|
|
|
path = os.path.join(tmp_output_dir, "pwr_test.kicad_sch")
|
|
sch = create_schematic("pwr_test")
|
|
sch.components.add(lib_id="Device:R", reference="R1", value="10k", position=(100, 100))
|
|
|
|
pin_pos = sch.get_component_pin_position("R1", "2")
|
|
assert pin_pos is not None
|
|
|
|
result = add_power_symbol_to_pin(
|
|
sch=sch,
|
|
pin_position=(pin_pos.x, pin_pos.y),
|
|
net="GND",
|
|
)
|
|
|
|
assert result["net"] == "GND"
|
|
assert result["direction"] == "down"
|
|
assert result["reference"].startswith("#PWR")
|
|
assert result["lib_id"] == "power:GND"
|
|
assert result["wire_id"] is not None
|
|
|
|
# Ground symbol should be below the pin (higher Y in KiCad coords)
|
|
assert result["symbol_position"]["y"] > pin_pos.y
|
|
|
|
sch.save(path)
|
|
|
|
def test_supply_symbol_placed_above_pin(self, tmp_output_dir):
|
|
from kicad_sch_api import create_schematic
|
|
|
|
from mckicad.patterns._geometry import add_power_symbol_to_pin
|
|
|
|
path = os.path.join(tmp_output_dir, "pwr_test2.kicad_sch")
|
|
sch = create_schematic("pwr_test2")
|
|
sch.components.add(lib_id="Device:R", reference="R1", value="10k", position=(100, 100))
|
|
|
|
pin_pos = sch.get_component_pin_position("R1", "1")
|
|
assert pin_pos is not None
|
|
|
|
result = add_power_symbol_to_pin(
|
|
sch=sch,
|
|
pin_position=(pin_pos.x, pin_pos.y),
|
|
net="+3V3",
|
|
)
|
|
|
|
assert result["net"] == "+3V3"
|
|
assert result["direction"] == "up"
|
|
assert result["lib_id"] == "power:+3V3"
|
|
|
|
# Supply symbol should be above the pin (lower Y in KiCad coords)
|
|
assert result["symbol_position"]["y"] < pin_pos.y
|
|
|
|
sch.save(path)
|
|
|
|
def test_custom_lib_id_override(self, tmp_output_dir):
|
|
from kicad_sch_api import create_schematic
|
|
|
|
from mckicad.patterns._geometry import add_power_symbol_to_pin
|
|
|
|
sch = create_schematic("pwr_test3")
|
|
sch.components.add(lib_id="Device:C", reference="C1", value="100nF", position=(100, 100))
|
|
|
|
pin_pos = sch.get_component_pin_position("C1", "1")
|
|
result = add_power_symbol_to_pin(
|
|
sch=sch,
|
|
pin_position=(pin_pos.x, pin_pos.y),
|
|
net="VCC",
|
|
lib_id="power:VCC",
|
|
)
|
|
|
|
assert result["lib_id"] == "power:VCC"
|
|
|
|
def test_sequential_pwr_references(self, tmp_output_dir):
|
|
from kicad_sch_api import create_schematic
|
|
|
|
from mckicad.patterns._geometry import add_power_symbol_to_pin
|
|
|
|
sch = create_schematic("pwr_seq")
|
|
sch.components.add(lib_id="Device:R", reference="R1", value="10k", position=(100, 100))
|
|
|
|
pin1 = sch.get_component_pin_position("R1", "1")
|
|
pin2 = sch.get_component_pin_position("R1", "2")
|
|
|
|
r1 = add_power_symbol_to_pin(sch, (pin1.x, pin1.y), "+3V3")
|
|
r2 = add_power_symbol_to_pin(sch, (pin2.x, pin2.y), "GND")
|
|
|
|
# References should be sequential
|
|
assert r1["reference"] != r2["reference"]
|
|
assert r1["reference"].startswith("#PWR")
|
|
assert r2["reference"].startswith("#PWR")
|
|
|
|
|
|
@requires_sch_api
|
|
class TestAddPowerSymbolTool:
|
|
"""Integration tests for the add_power_symbol MCP tool."""
|
|
|
|
def test_add_gnd_to_resistor_pin(self, populated_schematic_with_ic):
|
|
from mckicad.tools.power_symbols import add_power_symbol
|
|
|
|
result = add_power_symbol(
|
|
schematic_path=populated_schematic_with_ic,
|
|
net="GND",
|
|
pin_ref="R1",
|
|
pin_number="2",
|
|
)
|
|
|
|
assert result["success"] is True
|
|
assert result["net"] == "GND"
|
|
assert result["target_component"] == "R1"
|
|
assert result["target_pin"] == "2"
|
|
assert result["reference"].startswith("#PWR")
|
|
assert result["engine"] == "kicad-sch-api"
|
|
|
|
def test_add_vcc_to_cap_pin(self, populated_schematic_with_ic):
|
|
from mckicad.tools.power_symbols import add_power_symbol
|
|
|
|
result = add_power_symbol(
|
|
schematic_path=populated_schematic_with_ic,
|
|
net="VCC",
|
|
pin_ref="C1",
|
|
pin_number="1",
|
|
)
|
|
|
|
assert result["success"] is True
|
|
assert result["net"] == "VCC"
|
|
|
|
def test_invalid_pin_ref(self, populated_schematic_with_ic):
|
|
from mckicad.tools.power_symbols import add_power_symbol
|
|
|
|
result = add_power_symbol(
|
|
schematic_path=populated_schematic_with_ic,
|
|
net="GND",
|
|
pin_ref="NONEXISTENT",
|
|
pin_number="1",
|
|
)
|
|
|
|
assert result["success"] is False
|
|
assert "not found" in result["error"].lower() or "NONEXISTENT" in result["error"]
|
|
|
|
def test_empty_net_rejected(self, populated_schematic_with_ic):
|
|
from mckicad.tools.power_symbols import add_power_symbol
|
|
|
|
result = add_power_symbol(
|
|
schematic_path=populated_schematic_with_ic,
|
|
net="",
|
|
pin_ref="R1",
|
|
pin_number="1",
|
|
)
|
|
|
|
assert result["success"] is False
|
|
|
|
def test_bad_schematic_path(self):
|
|
from mckicad.tools.power_symbols import add_power_symbol
|
|
|
|
result = add_power_symbol(
|
|
schematic_path="/nonexistent/path.kicad_sch",
|
|
net="GND",
|
|
pin_ref="R1",
|
|
pin_number="1",
|
|
)
|
|
|
|
assert result["success"] is False
|