""" 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 from typing import Any import pytest sys.path.insert(0, str(Path(__file__).parent.parent / "python")) TEMPLATE_SCH = Path(__file__).parent.parent / "python" / "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) -> None: """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) -> None: """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) -> None: """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) -> None: """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) -> None: """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) -> None: """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) -> Any: from kicad_interface import KiCADInterface iface = KiCADInterface.__new__(KiCADInterface) return iface._handle_delete_schematic_component def test_delete_inline_format_succeeds(self, tmp_path: Any) -> None: 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: Any) -> None: """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: Any) -> None: """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: Any) -> None: 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: Any) -> None: 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: Any) -> None: result = self._get_handler()({"reference": "R1"}) assert result["success"] is False def test_missing_reference_returns_failure(self, tmp_path: Any) -> None: sch = _make_test_schematic(tmp_path) result = self._get_handler()({"schematicPath": str(sch)}) assert result["success"] is False