Add [tool.black] config to pyproject.toml and Black hook to .pre-commit-config.yaml (rev 26.3.1), then auto-format all Python source and test files with line-length=100, target-version=py310. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
208 lines
7.5 KiB
Python
208 lines
7.5 KiB
Python
"""
|
||
Regression tests for delete_schematic_component.
|
||
|
||
Key regression: the handler previously used a line-by-line regex that required
|
||
`(symbol` and `(lib_id` to appear on the *same* line. KiCAD's file writer puts
|
||
them on *separate* lines, so every real-world delete returned "not found".
|
||
"""
|
||
|
||
import re
|
||
import sys
|
||
import tempfile
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||
|
||
TEMPLATE_SCH = Path(__file__).parent.parent / "templates" / "empty.kicad_sch"
|
||
|
||
# Inline format (single line) – matches what tests previously used
|
||
PLACED_RESISTOR_INLINE = """\
|
||
(symbol (lib_id "Device:R") (at 50 50 0) (unit 1)
|
||
(in_bom yes) (on_board yes) (dnp no)
|
||
(uuid "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee")
|
||
(property "Reference" "R1" (at 51.27 47.46 0)
|
||
(effects (font (size 1.27 1.27)))
|
||
)
|
||
(property "Value" "10k" (at 51.27 52.54 0)
|
||
(effects (font (size 1.27 1.27)))
|
||
)
|
||
(property "Footprint" "Resistor_SMD:R_0603_1608Metric" (at 50 50 0)
|
||
(effects (font (size 1.27 1.27)) hide)
|
||
)
|
||
(property "Datasheet" "~" (at 50 50 0)
|
||
(effects (font (size 1.27 1.27)) hide)
|
||
)
|
||
)
|
||
"""
|
||
|
||
# Multi-line format – as KiCAD's own file writer produces it.
|
||
# (symbol and (lib_id are on separate lines, which broke the old regex.
|
||
PLACED_RESISTOR_MULTILINE = """\
|
||
\t(symbol
|
||
\t\t(lib_id "Device:R")
|
||
\t\t(at 50 50 0)
|
||
\t\t(unit 1)
|
||
\t\t(in_bom yes)
|
||
\t\t(on_board yes)
|
||
\t\t(dnp no)
|
||
\t\t(uuid "bbbbbbbb-cccc-dddd-eeee-ffffffffffff")
|
||
\t\t(property "Reference" "R2"
|
||
\t\t\t(at 51.27 47.46 0)
|
||
\t\t\t(effects
|
||
\t\t\t\t(font
|
||
\t\t\t\t\t(size 1.27 1.27)
|
||
\t\t\t\t)
|
||
\t\t\t)
|
||
\t\t)
|
||
\t\t(property "Value" "4.7k"
|
||
\t\t\t(at 51.27 52.54 0)
|
||
\t\t\t(effects
|
||
\t\t\t\t(font
|
||
\t\t\t\t\t(size 1.27 1.27)
|
||
\t\t\t\t)
|
||
\t\t\t)
|
||
\t\t)
|
||
\t)
|
||
"""
|
||
|
||
# Multi-line power symbol – the exact scenario that was reported as broken.
|
||
PLACED_POWER_SYMBOL_MULTILINE = """\
|
||
\t(symbol
|
||
\t\t(lib_id "power:VCC")
|
||
\t\t(at 365.6 38.1 0)
|
||
\t\t(unit 1)
|
||
\t\t(in_bom yes)
|
||
\t\t(on_board yes)
|
||
\t\t(dnp no)
|
||
\t\t(uuid "cccccccc-dddd-eeee-ffff-000000000030")
|
||
\t\t(property "Reference" "#PWR030"
|
||
\t\t\t(at 365.6 41.91 0)
|
||
\t\t\t(effects
|
||
\t\t\t\t(font
|
||
\t\t\t\t\t(size 1.27 1.27)
|
||
\t\t\t\t)
|
||
\t\t\t\t(hide yes)
|
||
\t\t\t)
|
||
\t\t)
|
||
\t\t(property "Value" "VCC"
|
||
\t\t\t(at 365.6 35.56 0)
|
||
\t\t\t(effects
|
||
\t\t\t\t(font
|
||
\t\t\t\t\t(size 1.27 1.27)
|
||
\t\t\t\t)
|
||
\t\t\t)
|
||
\t\t)
|
||
\t)
|
||
"""
|
||
|
||
|
||
def _make_test_schematic(tmp_path: Path, extra_block: str = "") -> Path:
|
||
dest = tmp_path / "test.kicad_sch"
|
||
src_content = TEMPLATE_SCH.read_text(encoding="utf-8")
|
||
if extra_block:
|
||
src_content = src_content.rstrip()
|
||
if src_content.endswith(")"):
|
||
src_content = src_content[:-1] + "\n" + extra_block + ")\n"
|
||
dest.write_text(src_content, encoding="utf-8")
|
||
return dest
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Unit tests – regression proof for the old regex vs the new approach
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestDeleteDetectionRegex:
|
||
"""Verify that the new content-string pattern finds blocks in both formats."""
|
||
|
||
OLD_PATTERN = re.compile(r"^\s*\(symbol\s+\(lib_id\s+\"", re.MULTILINE)
|
||
NEW_PATTERN = re.compile(r'\(symbol\s+\(lib_id\s+"')
|
||
|
||
def test_old_regex_fails_on_multiline_format(self):
|
||
"""Regression: old line-by-line regex must NOT match the multi-line format."""
|
||
# The old code used re.match on individual lines; simulate that here.
|
||
lines = PLACED_RESISTOR_MULTILINE.split("\n")
|
||
matches = [l for l in lines if re.match(r"\s*\(symbol\s+\(lib_id\s+\"", l)]
|
||
assert matches == [], "Old regex should not match multi-line KiCAD format"
|
||
|
||
def test_old_regex_matches_inline_format(self):
|
||
"""Old regex did work on single-line (inline) format."""
|
||
lines = PLACED_RESISTOR_INLINE.split("\n")
|
||
matches = [l for l in lines if re.match(r"\s*\(symbol\s+\(lib_id\s+\"", l)]
|
||
assert len(matches) == 1
|
||
|
||
def test_new_pattern_matches_multiline_format(self):
|
||
"""New content-string pattern must find blocks in multi-line format."""
|
||
assert self.NEW_PATTERN.search(PLACED_RESISTOR_MULTILINE) is not None
|
||
|
||
def test_new_pattern_matches_inline_format(self):
|
||
"""New content-string pattern also works on inline format."""
|
||
assert self.NEW_PATTERN.search(PLACED_RESISTOR_INLINE) is not None
|
||
|
||
def test_new_pattern_matches_power_symbol_multiline(self):
|
||
"""New pattern must find #PWR030 power symbol in multi-line format."""
|
||
assert self.NEW_PATTERN.search(PLACED_POWER_SYMBOL_MULTILINE) is not None
|
||
|
||
def test_reference_extraction_from_multiline_block(self):
|
||
"""Reference property can be found inside a multi-line block."""
|
||
ref_pattern = re.compile(r'\(property\s+"Reference"\s+"#PWR030"')
|
||
assert ref_pattern.search(PLACED_POWER_SYMBOL_MULTILINE) is not None
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Integration tests – real file I/O using the handler
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
@pytest.mark.integration
|
||
class TestDeleteSchematicComponentIntegration:
|
||
def _get_handler(self):
|
||
from kicad_interface import KiCADInterface
|
||
|
||
iface = KiCADInterface.__new__(KiCADInterface)
|
||
return iface._handle_delete_schematic_component
|
||
|
||
def test_delete_inline_format_succeeds(self, tmp_path):
|
||
sch = _make_test_schematic(tmp_path, PLACED_RESISTOR_INLINE)
|
||
result = self._get_handler()({"schematicPath": str(sch), "reference": "R1"})
|
||
assert result["success"] is True
|
||
assert result["deleted_count"] == 1
|
||
|
||
def test_delete_multiline_format_succeeds(self, tmp_path):
|
||
"""Regression: must succeed when KiCAD writes (symbol and (lib_id on separate lines."""
|
||
sch = _make_test_schematic(tmp_path, PLACED_RESISTOR_MULTILINE)
|
||
result = self._get_handler()({"schematicPath": str(sch), "reference": "R2"})
|
||
assert result["success"] is True
|
||
assert result["deleted_count"] == 1
|
||
|
||
def test_delete_power_symbol_multiline_succeeds(self, tmp_path):
|
||
"""Regression: #PWR030 multi-line power symbol must be deletable."""
|
||
sch = _make_test_schematic(tmp_path, PLACED_POWER_SYMBOL_MULTILINE)
|
||
result = self._get_handler()({"schematicPath": str(sch), "reference": "#PWR030"})
|
||
assert result["success"] is True
|
||
assert result["deleted_count"] == 1
|
||
|
||
def test_component_absent_after_delete(self, tmp_path):
|
||
sch = _make_test_schematic(tmp_path, PLACED_POWER_SYMBOL_MULTILINE)
|
||
self._get_handler()({"schematicPath": str(sch), "reference": "#PWR030"})
|
||
remaining = sch.read_text(encoding="utf-8")
|
||
assert '"#PWR030"' not in remaining
|
||
|
||
def test_unknown_reference_returns_failure(self, tmp_path):
|
||
sch = _make_test_schematic(tmp_path, PLACED_RESISTOR_INLINE)
|
||
result = self._get_handler()({"schematicPath": str(sch), "reference": "U99"})
|
||
assert result["success"] is False
|
||
assert "not found" in result["message"]
|
||
|
||
def test_missing_schematic_path_returns_failure(self, tmp_path):
|
||
result = self._get_handler()({"reference": "R1"})
|
||
assert result["success"] is False
|
||
|
||
def test_missing_reference_returns_failure(self, tmp_path):
|
||
sch = _make_test_schematic(tmp_path)
|
||
result = self._get_handler()({"schematicPath": str(sch)})
|
||
assert result["success"] is False
|