kicad-mcp/tests/test_power_symbols.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

225 lines
7.4 KiB
Python

"""Tests for the power symbol tool and geometry helper."""
import os
import pytest
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")
assert pin_pos is not None
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_short_stub_length_honored(self, tmp_output_dir):
"""stub_length=1.27 should produce a 1.27mm stub, not snap to 2.54."""
from kicad_sch_api import create_schematic
from mckicad.patterns._geometry import add_power_symbol_to_pin
sch = create_schematic("pwr_short_stub")
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",
stub_length=1.27,
)
# Ground goes below (positive Y direction), stub should be exactly 1.27mm
actual_stub = abs(result["symbol_position"]["y"] - pin_pos.y)
assert actual_stub == pytest.approx(1.27, abs=0.01), (
f"Expected 1.27mm stub, got {actual_stub}mm"
)
sch.save(os.path.join(tmp_output_dir, "pwr_short_stub.kicad_sch"))
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")
assert pin1 is not None
assert pin2 is not None
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"] == "sch_document"
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
class TestPowerSymbolSexpFallback:
"""Test that add_power_symbol uses sexp pin fallback for custom symbols."""
def test_resolve_pin_position_used_on_api_failure(self):
"""Verify resolve_pin_position handles None from API gracefully."""
from unittest.mock import MagicMock
from mckicad.utils.sexp_parser import resolve_pin_position
sch = MagicMock()
sch.get_component_pin_position.return_value = None
sch.components.get.return_value = None
result = resolve_pin_position(sch, "/fake.kicad_sch", "D1", "1")
assert result is None