add_label bypasses kicad-sch-api serializer entirely — generates s-expression strings and inserts them directly into the .kicad_sch file via atomic write. Fixes two upstream bugs: global labels silently dropped on save (serializer never iterates "global_label" key), and local labels raising TypeError (parameter signature mismatch in LabelCollection.add()). add_power_symbol now falls back to sexp pin parsing when the API returns None for custom library symbols (e.g. SMF5.0CA). Extracts shared resolve_pin_position() utility used by both add_power_symbol and batch operations. Batch labels also fixed — collected as sexp strings during the batch loop and inserted after sch.save() so the serializer can't overwrite them.
196 lines
6.4 KiB
Python
196 lines
6.4 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")
|
|
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_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
|