Files
kicad-mcp-server/tests/test_wire_connectivity.py
Eugene Mikhantyev 5de932e2b3 refactor: consolidate python/tests/ into tests/ directory
Move all Python test files from python/tests/ to the top-level tests/
directory to match the project's CONTRIBUTING.md guidelines. Update
sys.path inserts and template path references to reflect the new
location.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 11:22:39 +01:00

333 lines
12 KiB
Python

"""
Tests for the wire_connectivity module and the get_wire_connections handler.
Covers:
- Schema shape (TestSchema)
- Handler dispatch registration (TestHandlerDispatch)
- Parameter validation in the handler (TestHandlerParamValidation)
- Core logic: _to_iu, _parse_wires, _build_adjacency, _find_connected_wires,
get_wire_connections (TestCoreLogic)
"""
import sys
from pathlib import Path
from typing import Any
from unittest.mock import MagicMock, patch
import pytest
# Ensure the python package root is importable
sys.path.insert(0, str(Path(__file__).parent.parent / "python"))
# ---------------------------------------------------------------------------
# Module under test
# ---------------------------------------------------------------------------
from commands.wire_connectivity import (
_build_adjacency,
_find_connected_wires,
_parse_wires,
_to_iu,
get_wire_connections,
)
# ---------------------------------------------------------------------------
# Helpers to build minimal mock schematic objects
# ---------------------------------------------------------------------------
def _make_point(x: float, y: float) -> MagicMock:
pt = MagicMock()
pt.value = [x, y]
return pt
def _make_wire(x1: float, y1: float, x2: float, y2: float) -> MagicMock:
wire = MagicMock()
wire.pts = MagicMock()
wire.pts.xy = [_make_point(x1, y1), _make_point(x2, y2)]
return wire
def _make_schematic(*wires: Any) -> MagicMock:
sch = MagicMock()
sch.wire = list(wires)
# No net labels, no symbols by default
del sch.label # make hasattr(..., "label") return False
del sch.symbol # make hasattr(..., "symbol") return False
return sch
# ---------------------------------------------------------------------------
# TestSchema
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestSchema:
"""Verify the get_wire_connections tool schema is present and well-formed."""
def test_schema_registered(self) -> None:
from schemas.tool_schemas import TOOL_SCHEMAS
assert "get_wire_connections" in TOOL_SCHEMAS
def test_schema_required_fields(self) -> None:
from schemas.tool_schemas import TOOL_SCHEMAS
schema = TOOL_SCHEMAS["get_wire_connections"]
required = schema["inputSchema"]["required"]
assert "schematicPath" in required
assert "x" in required
assert "y" in required
def test_schema_has_title_and_description(self) -> None:
from schemas.tool_schemas import TOOL_SCHEMAS
schema = TOOL_SCHEMAS["get_wire_connections"]
assert schema.get("title")
assert schema.get("description")
# ---------------------------------------------------------------------------
# TestHandlerDispatch
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestHandlerDispatch:
"""Verify the handler is wired into KiCadInterface.command_routes."""
def test_get_wire_connections_in_routes(self) -> None:
# Import lazily to avoid heavy side-effects at collection time
with patch("kicad_interface.USE_IPC_BACKEND", False):
from kicad_interface import KiCADInterface
iface = KiCADInterface.__new__(KiCADInterface)
iface.board = None
iface.project_filename = None
iface.use_ipc = False
iface.ipc_backend = MagicMock()
iface.ipc_board_api = None
iface.footprint_library = MagicMock()
iface.project_commands = MagicMock()
iface.board_commands = MagicMock()
iface.component_commands = MagicMock()
iface.routing_commands = MagicMock()
# Build routes only (avoid full __init__ side-effects)
# The routes dict is built in __init__; we call it directly.
KiCADInterface.__init__(iface)
assert "get_wire_connections" in iface.command_routes
assert callable(iface.command_routes["get_wire_connections"])
# ---------------------------------------------------------------------------
# TestHandlerParamValidation
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestHandlerParamValidation:
"""Handler returns error responses for bad or missing parameters."""
def _make_handler(self) -> Any:
"""Return a bound _handle_get_wire_connections without full init."""
with patch("kicad_interface.USE_IPC_BACKEND", False):
from kicad_interface import KiCADInterface
iface = KiCADInterface.__new__(KiCADInterface)
return iface._handle_get_wire_connections
def test_missing_schematic_path(self) -> None:
handler = self._make_handler()
result = handler({"x": 1.0, "y": 2.0})
assert result["success"] is False
assert "schematicPath" in result["message"] or "Missing" in result["message"]
def test_missing_x(self) -> None:
handler = self._make_handler()
result = handler({"schematicPath": "/tmp/test.kicad_sch", "y": 2.0})
assert result["success"] is False
def test_missing_y(self) -> None:
handler = self._make_handler()
result = handler({"schematicPath": "/tmp/test.kicad_sch", "x": 1.0})
assert result["success"] is False
def test_non_numeric_x(self) -> None:
handler = self._make_handler()
result = handler({"schematicPath": "/tmp/test.kicad_sch", "x": "bad", "y": 2.0})
assert result["success"] is False
assert "numeric" in result["message"].lower() or "x" in result["message"]
def test_non_numeric_y(self) -> None:
handler = self._make_handler()
result = handler({"schematicPath": "/tmp/test.kicad_sch", "x": 1.0, "y": "bad"})
assert result["success"] is False
# ---------------------------------------------------------------------------
# TestCoreLogic
# ---------------------------------------------------------------------------
_IU = 10_000 # IU per mm
@pytest.mark.unit
class TestCoreLogic:
"""Unit tests for the pure-logic functions in wire_connectivity."""
# --- _to_iu ---
def test_to_iu_integer_mm(self) -> None:
assert _to_iu(1.0, 2.0) == (10_000, 20_000)
def test_to_iu_fractional_mm(self) -> None:
assert _to_iu(0.5, 0.25) == (5_000, 2_500)
def test_to_iu_zero(self) -> None:
assert _to_iu(0.0, 0.0) == (0, 0)
def test_to_iu_negative(self) -> None:
assert _to_iu(-1.0, -2.0) == (-10_000, -20_000)
# --- _parse_wires ---
def test_parse_wires_single_wire(self) -> None:
sch = _make_schematic(_make_wire(0.0, 0.0, 1.0, 0.0))
result = _parse_wires(sch)
assert len(result) == 1
assert result[0] == [(0, 0), (10_000, 0)]
def test_parse_wires_empty_schematic(self) -> None:
sch = MagicMock()
sch.wire = []
assert _parse_wires(sch) == []
def test_parse_wires_multiple_wires(self) -> None:
sch = _make_schematic(
_make_wire(0.0, 0.0, 1.0, 0.0),
_make_wire(1.0, 0.0, 2.0, 0.0),
)
assert len(_parse_wires(sch)) == 2
def test_parse_wires_skips_wire_without_pts(self) -> None:
bad_wire = MagicMock(spec=[]) # no `pts` attribute
sch = MagicMock()
sch.wire = [bad_wire]
assert _parse_wires(sch) == []
# --- _build_adjacency ---
def test_build_adjacency_two_connected_wires(self) -> None:
# wire0: (0,0)-(1,0), wire1: (1,0)-(2,0) — share endpoint (1,0)
wires = [
[(0, 0), (10_000, 0)],
[(10_000, 0), (20_000, 0)],
]
adjacency, iu_to_wires = _build_adjacency(wires)
assert 1 in adjacency[0]
assert 0 in adjacency[1]
def test_build_adjacency_two_disconnected_wires(self) -> None:
wires = [
[(0, 0), (10_000, 0)],
[(20_000, 0), (30_000, 0)],
]
adjacency, _ = _build_adjacency(wires)
assert adjacency[0] == set()
assert adjacency[1] == set()
def test_build_adjacency_iu_to_wires_maps_correctly(self) -> None:
wires = [
[(0, 0), (10_000, 0)],
[(10_000, 0), (20_000, 0)],
]
_, iu_to_wires = _build_adjacency(wires)
assert iu_to_wires[(10_000, 0)] == {0, 1}
assert iu_to_wires[(0, 0)] == {0}
def test_build_adjacency_three_wires_at_junction(self) -> None:
# All three wires meet at (10,000, 0)
wires = [
[(0, 0), (10_000, 0)],
[(10_000, 0), (20_000, 0)],
[(10_000, 0), (10_000, 10_000)],
]
adjacency, _ = _build_adjacency(wires)
assert adjacency[0] == {1, 2}
assert adjacency[1] == {0, 2}
assert adjacency[2] == {0, 1}
# --- _find_connected_wires ---
def test_find_connected_wires_no_wire_at_point(self) -> None:
wires = [[(0, 0), (10_000, 0)]]
adjacency, iu_to_wires = _build_adjacency(wires)
visited, net_points = _find_connected_wires(5.0, 0.0, wires, iu_to_wires, adjacency)
assert visited is None
assert net_points is None
def test_find_connected_wires_single_wire(self) -> None:
wires = [[(0, 0), (10_000, 0)]]
adjacency, iu_to_wires = _build_adjacency(wires)
visited, net_points = _find_connected_wires(0.0, 0.0, wires, iu_to_wires, adjacency)
assert visited == {0}
assert (0, 0) in net_points
assert (10_000, 0) in net_points
def test_find_connected_wires_flood_fills_chain(self) -> None:
# Three wires in a chain: A-B-C-D
wires = [
[(0, 0), (10_000, 0)],
[(10_000, 0), (20_000, 0)],
[(20_000, 0), (30_000, 0)],
]
adjacency, iu_to_wires = _build_adjacency(wires)
visited, net_points = _find_connected_wires(0.0, 0.0, wires, iu_to_wires, adjacency)
assert visited == {0, 1, 2}
def test_find_connected_wires_does_not_cross_gap(self) -> None:
# Two disconnected segments; query on segment 0 should not reach segment 1
wires = [
[(0, 0), (10_000, 0)],
[(20_000, 0), (30_000, 0)],
]
adjacency, iu_to_wires = _build_adjacency(wires)
visited, _ = _find_connected_wires(0.0, 0.0, wires, iu_to_wires, adjacency)
assert visited == {0}
# --- get_wire_connections (integration of internal functions) ---
def test_get_wire_connections_no_wires(self) -> None:
sch = MagicMock()
sch.wire = []
result = get_wire_connections(sch, "/fake/path.kicad_sch", 0.0, 0.0)
assert result == {"pins": [], "wires": []}
def test_get_wire_connections_no_wire_at_point_returns_none(self) -> None:
sch = _make_schematic(_make_wire(0.0, 0.0, 1.0, 0.0))
result = get_wire_connections(sch, "/fake/path.kicad_sch", 5.0, 0.0)
assert result is None
def test_get_wire_connections_returns_wire_data(self) -> None:
sch = _make_schematic(_make_wire(0.0, 0.0, 1.0, 0.0))
# Prevent _find_pins_on_net from iterating symbols
result = get_wire_connections(sch, "/fake/path.kicad_sch", 0.0, 0.0)
assert result is not None
assert result["pins"] == []
assert len(result["wires"]) == 1
wire = result["wires"][0]
assert wire["start"] == {"x": 0.0, "y": 0.0}
assert wire["end"] == {"x": 1.0, "y": 0.0}
def test_get_wire_connections_chain_returns_all_wires(self) -> None:
sch = _make_schematic(
_make_wire(0.0, 0.0, 1.0, 0.0),
_make_wire(1.0, 0.0, 2.0, 0.0),
)
result = get_wire_connections(sch, "/fake/path.kicad_sch", 0.0, 0.0)
assert result is not None
assert len(result["wires"]) == 2