kicad-mcp/tests/test_schematic_analysis.py
Ryan Malloy 700ad29bdd Redesign audit_wiring output for large ICs
Group results by net name instead of per-pin, keeping the summary
compact enough to stay inline even for 100+ pin components. Add
anomaly detection (unconnected pins, high-fanout nets, auto-named
nets) and optional pin/net filters. Wire coordinates are now opt-in
via include_wires flag to avoid flooding the calling LLM with
coordinate noise.
2026-03-05 08:19:06 -07:00

331 lines
11 KiB
Python

"""Tests for schematic analysis tools."""
import pytest
from tests.conftest import requires_sch_api
@requires_sch_api
@pytest.mark.unit
class TestRunSchematicErc:
"""Tests for the run_schematic_erc tool."""
def test_erc_on_populated_schematic(self, populated_schematic):
from mckicad.tools.schematic_analysis import run_schematic_erc
result = run_schematic_erc(schematic_path=populated_schematic)
assert result["success"] is True
assert "violation_count" in result or "error" not in result
def test_erc_invalid_path(self):
from mckicad.tools.schematic_analysis import run_schematic_erc
result = run_schematic_erc(schematic_path="/tmp/nonexistent.kicad_sch")
assert result["success"] is False
@requires_sch_api
@pytest.mark.unit
class TestAnalyzeConnectivity:
"""Tests for the analyze_connectivity tool."""
def test_connectivity_on_populated(self, populated_schematic):
from mckicad.tools.schematic_analysis import analyze_connectivity
result = analyze_connectivity(schematic_path=populated_schematic)
assert result["success"] is True
assert "net_count" in result or "error" not in result
def test_connectivity_invalid_path(self):
from mckicad.tools.schematic_analysis import analyze_connectivity
result = analyze_connectivity(schematic_path="/tmp/nonexistent.kicad_sch")
assert result["success"] is False
@requires_sch_api
@pytest.mark.unit
class TestCheckPinConnection:
"""Tests for the check_pin_connection tool."""
def test_check_existing_pin(self, populated_schematic):
from mckicad.tools.schematic_analysis import check_pin_connection
result = check_pin_connection(
schematic_path=populated_schematic,
reference="R1",
pin="1",
)
# May succeed or fail depending on kicad-sch-api version
assert "success" in result
def test_check_nonexistent_pin(self, populated_schematic):
from mckicad.tools.schematic_analysis import check_pin_connection
result = check_pin_connection(
schematic_path=populated_schematic,
reference="Z99",
pin="1",
)
assert "success" in result
@requires_sch_api
@pytest.mark.unit
class TestVerifyPinsConnected:
"""Tests for the verify_pins_connected tool."""
def test_verify_two_pins(self, populated_schematic):
from mckicad.tools.schematic_analysis import verify_pins_connected
result = verify_pins_connected(
schematic_path=populated_schematic,
ref1="R1",
pin1="1",
ref2="R2",
pin2="1",
)
# May succeed or fail depending on kicad-sch-api version
assert "success" in result
@requires_sch_api
@pytest.mark.unit
class TestGetComponentPins:
"""Tests for the get_component_pins tool."""
def test_get_pins(self, populated_schematic):
from mckicad.tools.schematic_analysis import get_component_pins
result = get_component_pins(
schematic_path=populated_schematic,
reference="R1",
)
assert "success" in result
def test_get_pins_nonexistent(self, populated_schematic):
from mckicad.tools.schematic_analysis import get_component_pins
result = get_component_pins(
schematic_path=populated_schematic,
reference="Z99",
)
assert result["success"] is False
@pytest.mark.unit
class TestExportValidation:
"""Tests for input validation in export tools."""
def test_export_netlist_invalid_path(self):
from mckicad.tools.schematic_analysis import export_netlist
result = export_netlist(schematic_path="/tmp/nonexistent.kicad_sch")
assert result["success"] is False
def test_export_pdf_invalid_path(self):
from mckicad.tools.schematic_analysis import export_schematic_pdf
result = export_schematic_pdf(schematic_path="/tmp/nonexistent.kicad_sch")
assert result["success"] is False
def test_export_netlist_bad_format(self):
from mckicad.tools.schematic_analysis import export_netlist
result = export_netlist(
schematic_path="/tmp/test.kicad_sch",
format="invalid_format",
)
assert result["success"] is False
assert "Unsupported" in result.get("error", "")
@requires_sch_api
@pytest.mark.unit
class TestAuditWiring:
"""Tests for the audit_wiring tool."""
def test_audit_existing_component(self, populated_schematic):
from mckicad.tools.schematic_analysis import audit_wiring
result = audit_wiring(
schematic_path=populated_schematic,
reference="R1",
)
assert result["success"] is True
assert result["reference"] == "R1"
assert "net_summary" in result
assert result.get("pin_count", 0) > 0
assert "net_count" in result
def test_audit_nonexistent_component(self, populated_schematic):
from mckicad.tools.schematic_analysis import audit_wiring
result = audit_wiring(
schematic_path=populated_schematic,
reference="Z99",
)
assert result["success"] is False
def test_audit_invalid_path(self):
from mckicad.tools.schematic_analysis import audit_wiring
result = audit_wiring(
schematic_path="/tmp/nonexistent.kicad_sch",
reference="R1",
)
assert result["success"] is False
def test_audit_empty_reference(self):
from mckicad.tools.schematic_analysis import audit_wiring
result = audit_wiring(
schematic_path="/tmp/nonexistent.kicad_sch",
reference="",
)
assert result["success"] is False
def test_audit_net_summary_structure(self, populated_schematic):
"""Each net entry has pins, wire_count, connected_to — no wires by default."""
from mckicad.tools.schematic_analysis import audit_wiring
result = audit_wiring(
schematic_path=populated_schematic,
reference="R1",
)
if result["success"]:
for _net_name, entry in result["net_summary"].items():
assert "pins" in entry
assert isinstance(entry["pins"], list)
assert "wire_count" in entry
assert "connected_to" in entry
assert isinstance(entry["connected_to"], list)
# No wire coords by default
assert "wires" not in entry
def test_audit_include_wires(self, populated_schematic):
"""When include_wires=True, each net entry contains a wires list."""
from mckicad.tools.schematic_analysis import audit_wiring
result = audit_wiring(
schematic_path=populated_schematic,
reference="R1",
include_wires=True,
)
if result["success"]:
for _net_name, entry in result["net_summary"].items():
assert "wires" in entry
assert isinstance(entry["wires"], list)
def test_audit_pin_filter(self, populated_schematic):
"""Filtering by pin number limits the net_summary to matching nets."""
from mckicad.tools.schematic_analysis import audit_wiring
result = audit_wiring(
schematic_path=populated_schematic,
reference="R1",
pins=["1"],
)
if result["success"]:
# Only nets containing pin "1" should appear
for entry in result["net_summary"].values():
assert "1" in entry["pins"]
def test_audit_net_filter(self, populated_schematic):
"""Filtering by net name limits the summary to that net only."""
from mckicad.tools.schematic_analysis import audit_wiring
# First get all nets to pick one
full = audit_wiring(
schematic_path=populated_schematic,
reference="R1",
)
if full["success"] and full["net_summary"]:
target_net = next(iter(full["net_summary"]))
filtered = audit_wiring(
schematic_path=populated_schematic,
reference="R1",
net=target_net,
)
assert filtered["success"] is True
assert list(filtered["net_summary"].keys()) == [target_net]
def test_audit_anomalies_structure(self, populated_schematic):
"""Anomalies dict always present with expected keys."""
from mckicad.tools.schematic_analysis import audit_wiring
result = audit_wiring(
schematic_path=populated_schematic,
reference="R1",
)
if result["success"]:
assert "anomalies" in result
anomalies = result["anomalies"]
assert "unconnected_pins" in anomalies
assert isinstance(anomalies["unconnected_pins"], list)
assert "high_fanout_nets" in anomalies
assert isinstance(anomalies["high_fanout_nets"], list)
assert "auto_named_nets" in anomalies
assert isinstance(anomalies["auto_named_nets"], list)
@requires_sch_api
@pytest.mark.unit
class TestVerifyConnectivity:
"""Tests for the verify_connectivity tool."""
def test_verify_with_matching_net(self, populated_schematic):
from mckicad.tools.schematic_analysis import (
analyze_connectivity,
verify_connectivity,
)
# First get actual connectivity to build a valid expected map
conn = analyze_connectivity(schematic_path=populated_schematic)
assert conn["success"] is True
# Try to verify with an empty expected — should fail validation
result = verify_connectivity(
schematic_path=populated_schematic,
expected={},
)
assert result["success"] is False
def test_verify_missing_net(self, populated_schematic):
from mckicad.tools.schematic_analysis import verify_connectivity
result = verify_connectivity(
schematic_path=populated_schematic,
expected={"NONEXISTENT_NET": [["U99", "1"]]},
)
assert result["success"] is True
assert result["failed"] >= 1
# Should report as missing_net or missing pin
statuses = {r["status"] for r in result["results"]}
assert statuses & {"missing_net", "mismatch"}
def test_verify_invalid_path(self):
from mckicad.tools.schematic_analysis import verify_connectivity
result = verify_connectivity(
schematic_path="/tmp/nonexistent.kicad_sch",
expected={"NET": [["R1", "1"]]},
)
assert result["success"] is False
def test_verify_result_structure(self, populated_schematic):
from mckicad.tools.schematic_analysis import verify_connectivity
result = verify_connectivity(
schematic_path=populated_schematic,
expected={"TEST_NET": [["R1", "1"]]},
)
assert result["success"] is True
assert "verified" in result
assert "failed" in result
assert "total" in result
assert "results" in result
for r in result["results"]:
assert "net" in r
assert "status" in r