Refactors _build_connectivity() into a two-layer state builder so the union-find internals (pin_at, label_at, wire_segments) are accessible to new analysis tools without duplicating the 200-line connectivity engine. New tools: - audit_wiring: trace all wires connected to a component, report per-pin net membership with wire segment coordinates and connected pins - remove_wires_by_criteria: bulk-remove wires by coordinate filters (y, x, min/max ranges, tolerance) with dry_run preview support - verify_connectivity: compare actual wiring against an expected net-to-pin mapping, report matches/mismatches/missing nets New sexp_parser utilities: - parse_wire_segments: extract (wire ...) blocks with start/end/uuid - remove_sexp_blocks_by_uuid: atomically remove blocks by UUID set
347 lines
10 KiB
Python
347 lines
10 KiB
Python
"""Tests for schematic editing tools."""
|
|
|
|
import os
|
|
import tempfile
|
|
|
|
import pytest
|
|
|
|
from tests.conftest import requires_sch_api
|
|
|
|
|
|
@requires_sch_api
|
|
@pytest.mark.unit
|
|
class TestModifyComponent:
|
|
"""Tests for the modify_component tool."""
|
|
|
|
def test_modify_value(self, populated_schematic):
|
|
from mckicad.tools.schematic_edit import modify_component
|
|
|
|
result = modify_component(
|
|
schematic_path=populated_schematic,
|
|
reference="R1",
|
|
value="22k",
|
|
)
|
|
assert result["success"] is True
|
|
assert any("value" in m for m in result["modified"])
|
|
|
|
def test_modify_nonexistent_component(self, populated_schematic):
|
|
from mckicad.tools.schematic_edit import modify_component
|
|
|
|
result = modify_component(
|
|
schematic_path=populated_schematic,
|
|
reference="Z99",
|
|
value="1k",
|
|
)
|
|
assert result["success"] is False
|
|
assert "error" in result
|
|
|
|
def test_modify_no_changes(self, populated_schematic):
|
|
from mckicad.tools.schematic_edit import modify_component
|
|
|
|
result = modify_component(
|
|
schematic_path=populated_schematic,
|
|
reference="R1",
|
|
)
|
|
assert result["success"] is True
|
|
assert result.get("modified") == []
|
|
|
|
|
|
@requires_sch_api
|
|
@pytest.mark.unit
|
|
class TestRemoveComponent:
|
|
"""Tests for the remove_component tool."""
|
|
|
|
def test_remove_existing(self, populated_schematic):
|
|
from mckicad.tools.schematic_edit import remove_component
|
|
|
|
result = remove_component(
|
|
schematic_path=populated_schematic,
|
|
reference="R2",
|
|
)
|
|
assert result["success"] is True
|
|
assert result["reference"] == "R2"
|
|
|
|
def test_remove_nonexistent(self, populated_schematic):
|
|
from mckicad.tools.schematic_edit import remove_component
|
|
|
|
result = remove_component(
|
|
schematic_path=populated_schematic,
|
|
reference="Z99",
|
|
)
|
|
assert result["success"] is False
|
|
|
|
|
|
@requires_sch_api
|
|
@pytest.mark.unit
|
|
class TestBackupSchematic:
|
|
"""Tests for the backup_schematic tool."""
|
|
|
|
def test_backup_creates_file(self, populated_schematic):
|
|
from mckicad.tools.schematic_edit import backup_schematic
|
|
|
|
result = backup_schematic(schematic_path=populated_schematic)
|
|
assert result["success"] is True
|
|
assert "backup_path" in result
|
|
assert os.path.isfile(result["backup_path"])
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestValidation:
|
|
"""Tests for input validation in edit tools."""
|
|
|
|
def test_invalid_path_extension(self):
|
|
from mckicad.tools.schematic_edit import modify_component
|
|
|
|
result = modify_component(
|
|
schematic_path="/tmp/test.txt",
|
|
reference="R1",
|
|
value="1k",
|
|
)
|
|
assert result["success"] is False
|
|
assert ".kicad_sch" in result["error"]
|
|
|
|
def test_empty_path(self):
|
|
from mckicad.tools.schematic_edit import remove_component
|
|
|
|
result = remove_component(schematic_path="", reference="R1")
|
|
assert result["success"] is False
|
|
|
|
def test_nonexistent_file(self):
|
|
from mckicad.tools.schematic_edit import set_title_block
|
|
|
|
result = set_title_block(
|
|
schematic_path="/tmp/nonexistent.kicad_sch",
|
|
title="Test",
|
|
)
|
|
assert result["success"] is False
|
|
|
|
|
|
@requires_sch_api
|
|
@pytest.mark.unit
|
|
class TestSetTitleBlock:
|
|
"""Tests for the set_title_block tool."""
|
|
|
|
def test_set_fields(self, populated_schematic):
|
|
from mckicad.tools.schematic_edit import set_title_block
|
|
|
|
result = set_title_block(
|
|
schematic_path=populated_schematic,
|
|
title="Test Circuit",
|
|
revision="1.0",
|
|
)
|
|
assert result["success"] is True
|
|
assert "title" in result.get("fields_set", [])
|
|
|
|
def test_set_author(self, populated_schematic):
|
|
from mckicad.tools.schematic_edit import set_title_block
|
|
|
|
result = set_title_block(
|
|
schematic_path=populated_schematic,
|
|
author="Test Author",
|
|
)
|
|
assert result["success"] is True
|
|
assert "author" in result.get("fields_set", [])
|
|
|
|
def test_no_fields_is_noop(self, populated_schematic):
|
|
from mckicad.tools.schematic_edit import set_title_block
|
|
|
|
result = set_title_block(schematic_path=populated_schematic)
|
|
assert result["success"] is True
|
|
assert result.get("fields_set") == []
|
|
|
|
|
|
@requires_sch_api
|
|
@pytest.mark.unit
|
|
class TestAnnotationTools:
|
|
"""Tests for add_text_annotation and add_no_connect."""
|
|
|
|
def test_add_text(self, populated_schematic):
|
|
from mckicad.tools.schematic_edit import add_text_annotation
|
|
|
|
result = add_text_annotation(
|
|
schematic_path=populated_schematic,
|
|
text="Test note",
|
|
x=50,
|
|
y=50,
|
|
)
|
|
assert result["success"] is True
|
|
|
|
def test_add_no_connect(self, populated_schematic):
|
|
from mckicad.tools.schematic_edit import add_no_connect
|
|
|
|
result = add_no_connect(
|
|
schematic_path=populated_schematic,
|
|
x=300,
|
|
y=300,
|
|
)
|
|
assert result["success"] is True
|
|
|
|
|
|
# Minimal schematic with wire segments for testing remove_wires_by_criteria.
|
|
# Does NOT require kicad-sch-api — works on raw s-expression files.
|
|
_WIRES_SCHEMATIC = """\
|
|
(kicad_sch
|
|
(version 20231120)
|
|
(generator "eeschema")
|
|
(uuid "test-root")
|
|
(paper "A4")
|
|
(lib_symbols
|
|
)
|
|
(wire (pts (xy 148.59 194.31) (xy 156.21 194.31))
|
|
(stroke (width 0) (type default))
|
|
(uuid "hw-1")
|
|
)
|
|
(wire (pts (xy 156.21 194.31) (xy 163.83 194.31))
|
|
(stroke (width 0) (type default))
|
|
(uuid "hw-2")
|
|
)
|
|
(wire (pts (xy 163.83 194.31) (xy 171.45 194.31))
|
|
(stroke (width 0) (type default))
|
|
(uuid "hw-3")
|
|
)
|
|
(wire (pts (xy 100.0 100.0) (xy 100.0 130.0))
|
|
(stroke (width 0) (type default))
|
|
(uuid "vw-1")
|
|
)
|
|
(wire (pts (xy 200.0 50.0) (xy 250.0 80.0))
|
|
(stroke (width 0) (type default))
|
|
(uuid "dw-1")
|
|
)
|
|
)
|
|
"""
|
|
|
|
|
|
@pytest.fixture
|
|
def wires_schematic():
|
|
"""Write a schematic with wires to a temp file for testing."""
|
|
with tempfile.NamedTemporaryFile(
|
|
mode="w", suffix=".kicad_sch", delete=False, encoding="utf-8",
|
|
) as f:
|
|
f.write(_WIRES_SCHEMATIC)
|
|
path = f.name
|
|
yield path
|
|
os.unlink(path)
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestRemoveWiresByCriteria:
|
|
"""Tests for the remove_wires_by_criteria tool."""
|
|
|
|
def test_dry_run_horizontal(self, wires_schematic):
|
|
from mckicad.tools.schematic_edit import remove_wires_by_criteria
|
|
|
|
result = remove_wires_by_criteria(
|
|
schematic_path=wires_schematic,
|
|
y=194.31,
|
|
dry_run=True,
|
|
)
|
|
assert result["success"] is True
|
|
assert result["dry_run"] is True
|
|
assert result["matched_count"] == 3
|
|
assert result["removed_count"] == 0
|
|
|
|
def test_remove_horizontal_wires(self, wires_schematic):
|
|
from mckicad.tools.schematic_edit import remove_wires_by_criteria
|
|
|
|
result = remove_wires_by_criteria(
|
|
schematic_path=wires_schematic,
|
|
y=194.31,
|
|
)
|
|
assert result["success"] is True
|
|
assert result["removed_count"] == 3
|
|
|
|
# Verify they're gone
|
|
from mckicad.utils.sexp_parser import parse_wire_segments
|
|
|
|
remaining = parse_wire_segments(wires_schematic)
|
|
remaining_uuids = {w["uuid"] for w in remaining}
|
|
assert "hw-1" not in remaining_uuids
|
|
assert "hw-2" not in remaining_uuids
|
|
assert "hw-3" not in remaining_uuids
|
|
assert "vw-1" in remaining_uuids
|
|
|
|
def test_remove_with_x_range(self, wires_schematic):
|
|
from mckicad.tools.schematic_edit import remove_wires_by_criteria
|
|
|
|
result = remove_wires_by_criteria(
|
|
schematic_path=wires_schematic,
|
|
y=194.31,
|
|
min_x=148.0,
|
|
max_x=164.0,
|
|
)
|
|
assert result["success"] is True
|
|
assert result["matched_count"] == 2 # hw-1 and hw-2 only
|
|
|
|
def test_remove_vertical_wire(self, wires_schematic):
|
|
from mckicad.tools.schematic_edit import remove_wires_by_criteria
|
|
|
|
result = remove_wires_by_criteria(
|
|
schematic_path=wires_schematic,
|
|
x=100.0,
|
|
)
|
|
assert result["success"] is True
|
|
assert result["matched_count"] == 1
|
|
assert result["removed_count"] == 1
|
|
|
|
def test_no_criteria_fails(self, wires_schematic):
|
|
from mckicad.tools.schematic_edit import remove_wires_by_criteria
|
|
|
|
result = remove_wires_by_criteria(
|
|
schematic_path=wires_schematic,
|
|
)
|
|
assert result["success"] is False
|
|
assert "criterion" in result["error"].lower()
|
|
|
|
def test_no_matches(self, wires_schematic):
|
|
from mckicad.tools.schematic_edit import remove_wires_by_criteria
|
|
|
|
result = remove_wires_by_criteria(
|
|
schematic_path=wires_schematic,
|
|
y=999.0,
|
|
)
|
|
assert result["success"] is True
|
|
assert result["matched_count"] == 0
|
|
|
|
def test_invalid_path(self):
|
|
from mckicad.tools.schematic_edit import remove_wires_by_criteria
|
|
|
|
result = remove_wires_by_criteria(
|
|
schematic_path="/tmp/nonexistent.kicad_sch",
|
|
y=100.0,
|
|
)
|
|
assert result["success"] is False
|
|
|
|
def test_min_max_y_range(self, wires_schematic):
|
|
from mckicad.tools.schematic_edit import remove_wires_by_criteria
|
|
|
|
result = remove_wires_by_criteria(
|
|
schematic_path=wires_schematic,
|
|
min_y=99.0,
|
|
max_y=131.0,
|
|
dry_run=True,
|
|
)
|
|
assert result["success"] is True
|
|
# vw-1 (100→130) fits, horizontal wires at 194.31 do not
|
|
assert result["matched_count"] == 1
|
|
|
|
def test_tolerance(self, wires_schematic):
|
|
from mckicad.tools.schematic_edit import remove_wires_by_criteria
|
|
|
|
# With tight tolerance, exact match
|
|
result = remove_wires_by_criteria(
|
|
schematic_path=wires_schematic,
|
|
y=194.31,
|
|
tolerance=0.001,
|
|
dry_run=True,
|
|
)
|
|
assert result["matched_count"] == 3
|
|
|
|
# Offset slightly beyond tolerance
|
|
result = remove_wires_by_criteria(
|
|
schematic_path=wires_schematic,
|
|
y=194.35,
|
|
tolerance=0.01,
|
|
dry_run=True,
|
|
)
|
|
assert result["matched_count"] == 0
|