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
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.
225 lines
7.4 KiB
Python
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
|