kicad-mcp/tests/test_power_symbols.py
Ryan Malloy 9dbb4cc0c7
Some checks are pending
CI / Lint and Format (push) Waiting to run
CI / Test Python 3.11 on macos-latest (push) Waiting to run
CI / Test Python 3.12 on macos-latest (push) Waiting to run
CI / Test Python 3.13 on macos-latest (push) Waiting to run
CI / Test Python 3.10 on ubuntu-latest (push) Waiting to run
CI / Test Python 3.11 on ubuntu-latest (push) Waiting to run
CI / Test Python 3.12 on ubuntu-latest (push) Waiting to run
CI / Test Python 3.13 on ubuntu-latest (push) Waiting to run
CI / Security Scan (push) Waiting to run
CI / Build Package (push) Blocked by required conditions
Fix stub_length quantization in power symbol placement
snap_to_grid() used 2.54mm default grid for symbol position,
silently rounding sub-2.54mm stub lengths up and causing shorts
on tightly-spaced connectors. Now uses 1.27mm fine grid.
2026-03-07 03:57:39 -07: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"] == "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
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