""" Tests for connected_pin_count in list_schematic_nets and the list_floating_labels tool. Covers: - Schema registration for list_floating_labels (TestListFloatingLabelsSchema) - Handler dispatch registration (TestListFloatingLabelsDispatch) - Parameter validation (TestListFloatingLabelsParamValidation) - Core logic: list_floating_labels (TestListFloatingLabelsCoreLogic) - Core logic: count_pins_on_net (TestCountPinsOnNet) - connected_pin_count field in list_schematic_nets handler (TestListSchematicNetsConnectedPinCount) - Integration: floating labels in a real schematic file (TestListFloatingLabelsIntegration) """ import shutil import sys import tempfile from pathlib import Path from typing import Any from unittest.mock import MagicMock, patch import pytest sys.path.insert(0, str(Path(__file__).parent.parent / "python")) from commands.wire_connectivity import ( _build_adjacency, _parse_virtual_connections, _parse_wires, count_pins_on_net, list_floating_labels, ) # --------------------------------------------------------------------------- # Shared mock helpers # --------------------------------------------------------------------------- TEMPLATE_SCH = Path(__file__).parent.parent / "python" / "templates" / "empty.kicad_sch" 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_label(name: str, x: float, y: float) -> MagicMock: label = MagicMock() label.value = name label.at = MagicMock() label.at.value = [x, y, 0] return label def _make_schematic_no_labels_no_symbols(*wires: Any) -> MagicMock: sch = MagicMock() sch.wire = list(wires) del sch.label del sch.symbol return sch # --------------------------------------------------------------------------- # TestListFloatingLabelsSchema # --------------------------------------------------------------------------- @pytest.mark.unit class TestListFloatingLabelsSchema: """Verify the list_floating_labels schema is registered and well-formed.""" def test_schema_registered(self) -> None: from schemas.tool_schemas import TOOL_SCHEMAS assert "list_floating_labels" in TOOL_SCHEMAS def test_schema_required_fields(self) -> None: from schemas.tool_schemas import TOOL_SCHEMAS required = TOOL_SCHEMAS["list_floating_labels"]["inputSchema"]["required"] assert required == ["schematicPath"] def test_schema_has_title_and_description(self) -> None: from schemas.tool_schemas import TOOL_SCHEMAS schema = TOOL_SCHEMAS["list_floating_labels"] assert schema.get("title") assert schema.get("description") # --------------------------------------------------------------------------- # TestListFloatingLabelsDispatch # --------------------------------------------------------------------------- @pytest.mark.unit class TestListFloatingLabelsDispatch: """Verify the handler is wired into KiCadInterface.command_routes.""" def test_list_floating_labels_in_routes(self) -> None: 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() KiCADInterface.__init__(iface) assert "list_floating_labels" in iface.command_routes assert callable(iface.command_routes["list_floating_labels"]) # --------------------------------------------------------------------------- # TestListFloatingLabelsParamValidation # --------------------------------------------------------------------------- @pytest.mark.unit class TestListFloatingLabelsParamValidation: """Handler returns error for missing schematicPath.""" def _make_handler(self) -> Any: with patch("kicad_interface.USE_IPC_BACKEND", False): from kicad_interface import KiCADInterface iface = KiCADInterface.__new__(KiCADInterface) return iface._handle_list_floating_labels def test_missing_schematic_path(self) -> None: handler = self._make_handler() result = handler({}) assert result["success"] is False assert "schematicPath" in result["message"] def test_bad_schematic_path_returns_error(self) -> None: handler = self._make_handler() result = handler({"schematicPath": "/nonexistent/path/test.kicad_sch"}) assert result["success"] is False # --------------------------------------------------------------------------- # TestListFloatingLabelsCoreLogic # --------------------------------------------------------------------------- @pytest.mark.unit class TestListFloatingLabelsCoreLogic: """Unit tests for the list_floating_labels function.""" def test_no_labels_returns_empty(self) -> None: sch = _make_schematic_no_labels_no_symbols() result = list_floating_labels(sch, "/tmp/test.kicad_sch") assert result == [] def test_label_with_no_wires_and_no_pins_is_floating(self) -> None: label = _make_label("SDA", 10.0, 5.0) sch = MagicMock() sch.wire = [] sch.label = [label] del sch.symbol result = list_floating_labels(sch, "/tmp/test.kicad_sch") assert len(result) == 1 assert result[0]["name"] == "SDA" assert result[0]["x"] == pytest.approx(10.0) assert result[0]["y"] == pytest.approx(5.0) assert result[0]["type"] == "label" def test_label_connected_to_pin_not_floating(self) -> None: """Label at (0,0) connected to a pin at (2,0) via wire should NOT be floating.""" wire = _make_wire(0.0, 0.0, 2.0, 0.0) label = _make_label("SCL", 0.0, 0.0) sch = MagicMock() sch.wire = [wire] sch.label = [label] # Mock a symbol whose pin is at (2, 0) symbol = MagicMock() symbol.property = MagicMock() symbol.property.Reference = MagicMock() symbol.property.Reference.value = "U1" sch.symbol = [symbol] with patch( "commands.pin_locator.PinLocator.get_all_symbol_pins", return_value={"1": (2.0, 0.0)}, ): result = list_floating_labels(sch, "/tmp/test.kicad_sch") assert result == [] def test_label_not_connected_to_any_pin_is_floating(self) -> None: """Label at (0,0) with no wires to any pin should be floating.""" label = _make_label("MOSI", 0.0, 0.0) wire = _make_wire(0.0, 0.0, 1.0, 0.0) sch = MagicMock() sch.wire = [wire] sch.label = [label] # A symbol whose pin is at a completely different location symbol = MagicMock() symbol.property = MagicMock() symbol.property.Reference = MagicMock() symbol.property.Reference.value = "U2" sch.symbol = [symbol] with patch( "commands.pin_locator.PinLocator.get_all_symbol_pins", return_value={"1": (99.0, 99.0)}, ): result = list_floating_labels(sch, "/tmp/test.kicad_sch") assert len(result) == 1 assert result[0]["name"] == "MOSI" def test_label_directly_on_pin_not_floating(self) -> None: """Label placed directly at a pin position (no wire needed) should NOT be floating.""" label = _make_label("PWR", 5.0, 3.0) sch = MagicMock() sch.wire = [] sch.label = [label] symbol = MagicMock() symbol.property = MagicMock() symbol.property.Reference = MagicMock() symbol.property.Reference.value = "R1" sch.symbol = [symbol] # Pin is exactly at the label position with patch( "commands.pin_locator.PinLocator.get_all_symbol_pins", return_value={"1": (5.0, 3.0)}, ): result = list_floating_labels(sch, "/tmp/test.kicad_sch") assert result == [] def test_multiple_labels_mixed_floating_and_connected(self) -> None: """Two labels: one connected, one floating.""" label_connected = _make_label("NET_A", 0.0, 0.0) label_floating = _make_label("NET_B", 20.0, 20.0) wire = _make_wire(0.0, 0.0, 2.0, 0.0) sch = MagicMock() sch.wire = [wire] sch.label = [label_connected, label_floating] symbol = MagicMock() symbol.property = MagicMock() symbol.property.Reference = MagicMock() symbol.property.Reference.value = "C1" sch.symbol = [symbol] with patch( "commands.pin_locator.PinLocator.get_all_symbol_pins", return_value={"1": (2.0, 0.0)}, ): result = list_floating_labels(sch, "/tmp/test.kicad_sch") assert len(result) == 1 assert result[0]["name"] == "NET_B" def test_template_symbols_skipped(self) -> None: """Symbols with _TEMPLATE references should be skipped, not crash.""" label = _make_label("VBUS", 0.0, 0.0) sch = MagicMock() sch.wire = [] sch.label = [label] template_sym = MagicMock() template_sym.property = MagicMock() template_sym.property.Reference = MagicMock() template_sym.property.Reference.value = "_TEMPLATE_R" sch.symbol = [template_sym] with patch( "commands.pin_locator.PinLocator.get_all_symbol_pins", return_value={"1": (0.0, 0.0)}, ) as mock_pins: result = list_floating_labels(sch, "/tmp/test.kicad_sch") # _TEMPLATE_ symbols are skipped; mock_pins should not have been called mock_pins.assert_not_called() assert len(result) == 1 # --------------------------------------------------------------------------- # TestCountPinsOnNet # --------------------------------------------------------------------------- @pytest.mark.unit class TestCountPinsOnNet: """Unit tests for count_pins_on_net.""" def _build_graph(self, sch: Any, schematic_path: str): # type: ignore[return] all_wires = _parse_wires(sch) if all_wires: adjacency, iu_to_wires = _build_adjacency(all_wires) else: adjacency, iu_to_wires = [], {} point_to_label, label_to_points = _parse_virtual_connections(sch, schematic_path) return all_wires, iu_to_wires, adjacency, point_to_label, label_to_points def test_no_labels_returns_zero(self) -> None: sch = _make_schematic_no_labels_no_symbols() all_wires, iu_to_wires, adj, p2l, l2p = self._build_graph(sch, "/tmp/t.kicad_sch") count = count_pins_on_net( sch, "/tmp/t.kicad_sch", "VCC", all_wires, iu_to_wires, adj, p2l, l2p ) assert count == 0 def test_unknown_net_returns_zero(self) -> None: wire = _make_wire(0.0, 0.0, 1.0, 0.0) label = _make_label("SDA", 0.0, 0.0) sch = MagicMock() sch.wire = [wire] sch.label = [label] del sch.symbol all_wires, iu_to_wires, adj, p2l, l2p = self._build_graph(sch, "/tmp/t.kicad_sch") count = count_pins_on_net( sch, "/tmp/t.kicad_sch", "UNKNOWN_NET", all_wires, iu_to_wires, adj, p2l, l2p ) assert count == 0 def test_counts_pin_via_wire(self) -> None: """Label at (0,0), wire to (2,0), pin at (2,0) → count == 1.""" wire = _make_wire(0.0, 0.0, 2.0, 0.0) label = _make_label("SCL", 0.0, 0.0) sch = MagicMock() sch.wire = [wire] sch.label = [label] symbol = MagicMock() symbol.property = MagicMock() symbol.property.Reference = MagicMock() symbol.property.Reference.value = "U1" sch.symbol = [symbol] all_wires, iu_to_wires, adj, p2l, l2p = self._build_graph(sch, "/tmp/t.kicad_sch") with patch( "commands.pin_locator.PinLocator.get_all_symbol_pins", return_value={"3": (2.0, 0.0)}, ): count = count_pins_on_net( sch, "/tmp/t.kicad_sch", "SCL", all_wires, iu_to_wires, adj, p2l, l2p ) assert count == 1 def test_no_symbol_attribute_returns_zero(self) -> None: wire = _make_wire(0.0, 0.0, 2.0, 0.0) label = _make_label("SDA", 0.0, 0.0) sch = MagicMock() sch.wire = [wire] sch.label = [label] del sch.symbol all_wires, iu_to_wires, adj, p2l, l2p = self._build_graph(sch, "/tmp/t.kicad_sch") count = count_pins_on_net( sch, "/tmp/t.kicad_sch", "SDA", all_wires, iu_to_wires, adj, p2l, l2p ) assert count == 0 # --------------------------------------------------------------------------- # TestListSchematicNetsConnectedPinCount # --------------------------------------------------------------------------- @pytest.mark.unit class TestListSchematicNetsConnectedPinCount: """Verify connected_pin_count is present in list_schematic_nets response.""" def _make_handler(self) -> Any: with patch("kicad_interface.USE_IPC_BACKEND", False): from kicad_interface import KiCADInterface iface = KiCADInterface.__new__(KiCADInterface) return iface._handle_list_schematic_nets def test_connected_pin_count_present_in_response(self) -> None: handler = self._make_handler() label = _make_label("NET1", 0.0, 0.0) mock_sch = MagicMock() mock_sch.wire = [] mock_sch.label = [label] del mock_sch.global_label del mock_sch.symbol with ( patch("kicad_interface.SchematicManager.load_schematic", return_value=mock_sch), patch( "kicad_interface.ConnectionManager.get_net_connections", return_value=[], ), ): result = handler({"schematicPath": "/tmp/test.kicad_sch"}) assert result["success"] is True assert len(result["nets"]) == 1 net = result["nets"][0] assert "connected_pin_count" in net assert isinstance(net["connected_pin_count"], int) def test_connected_pin_count_is_zero_when_no_pins(self) -> None: handler = self._make_handler() label = _make_label("ORPHAN_NET", 50.0, 50.0) mock_sch = MagicMock() mock_sch.wire = [] mock_sch.label = [label] del mock_sch.global_label del mock_sch.symbol with ( patch("kicad_interface.SchematicManager.load_schematic", return_value=mock_sch), patch( "kicad_interface.ConnectionManager.get_net_connections", return_value=[], ), ): result = handler({"schematicPath": "/tmp/test.kicad_sch"}) assert result["success"] is True assert result["nets"][0]["connected_pin_count"] == 0 # --------------------------------------------------------------------------- # TestListFloatingLabelsIntegration # --------------------------------------------------------------------------- @pytest.mark.integration class TestListFloatingLabelsIntegration: """Integration tests using a real .kicad_sch file.""" def _make_sch_with_floating_label(self, tmp_path: Path) -> Path: """Copy the empty template and append a floating label.""" sch_path = tmp_path / "test.kicad_sch" shutil.copy(TEMPLATE_SCH, sch_path) content = sch_path.read_text(encoding="utf-8") floating_label = ( ' (label "FLOATING_NET" (at 100 100 0)\n' " (effects (font (size 1.27 1.27)))\n" " (uuid 11111111-0000-0000-0000-000000000001)\n" " )" ) idx = content.rfind(")") content = content[:idx] + "\n" + floating_label + "\n)" sch_path.write_text(content, encoding="utf-8") return sch_path def test_empty_schematic_has_no_floating_labels(self) -> None: with tempfile.TemporaryDirectory() as tmp: sch_path = Path(tmp) / "empty.kicad_sch" shutil.copy(TEMPLATE_SCH, sch_path) with patch("kicad_interface.USE_IPC_BACKEND", False): from kicad_interface import KiCADInterface iface = KiCADInterface.__new__(KiCADInterface) result = iface._handle_list_floating_labels({"schematicPath": str(sch_path)}) assert result["success"] is True assert result["count"] == 0 assert result["floating_labels"] == [] def test_schematic_with_floating_label_detected(self) -> None: with tempfile.TemporaryDirectory() as tmp: sch_path = self._make_sch_with_floating_label(Path(tmp)) with patch("kicad_interface.USE_IPC_BACKEND", False): from kicad_interface import KiCADInterface iface = KiCADInterface.__new__(KiCADInterface) result = iface._handle_list_floating_labels({"schematicPath": str(sch_path)}) assert result["success"] is True assert result["count"] == 1 label = result["floating_labels"][0] assert label["name"] == "FLOATING_NET" assert label["x"] == pytest.approx(100.0) assert label["y"] == pytest.approx(100.0) assert label["type"] == "label" def test_list_schematic_nets_has_connected_pin_count(self) -> None: with tempfile.TemporaryDirectory() as tmp: sch_path = self._make_sch_with_floating_label(Path(tmp)) with patch("kicad_interface.USE_IPC_BACKEND", False): from kicad_interface import KiCADInterface iface = KiCADInterface.__new__(KiCADInterface) result = iface._handle_list_schematic_nets({"schematicPath": str(sch_path)}) assert result["success"] is True assert result["count"] == 1 net = result["nets"][0] assert net["name"] == "FLOATING_NET" assert "connected_pin_count" in net assert net["connected_pin_count"] == 0