Merge pull request #196 from gcolonese/feat-schematic-justify
feat(schematic): expose justify directive on field labels via MCP
This commit is contained in:
295
tests/test_schematic_field_justify.py
Normal file
295
tests/test_schematic_field_justify.py
Normal file
@@ -0,0 +1,295 @@
|
||||
"""
|
||||
Tests for the justify directive support in edit_schematic_component fieldPositions
|
||||
and set_schematic_component_property.
|
||||
|
||||
Covers:
|
||||
- _set_justify_on_property() helper (unit tests, no file I/O)
|
||||
- fieldPositions[field].justify via edit_schematic_component (integration)
|
||||
- justify forwarding via set_schematic_component_property (integration)
|
||||
- Backward-compat: existing calls without justify must be unaffected
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
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"
|
||||
|
||||
# Minimal placed-symbol block used across integration tests
|
||||
PLACED_RESISTOR_BLOCK = """\
|
||||
(symbol (lib_id "Device:R") (at 50 50 0) (unit 1)
|
||||
(in_bom yes) (on_board yes) (dnp no)
|
||||
(uuid "aaaaaaaa-bbbb-cccc-dddd-ffffffffffff")
|
||||
(property "Reference" "R2" (at 51.27 47.46 0)
|
||||
(effects (font (size 1.27 1.27)))
|
||||
)
|
||||
(property "Value" "4k7" (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)
|
||||
)
|
||||
)
|
||||
"""
|
||||
|
||||
|
||||
def _make_test_schematic(tmp_dir: Path, extra_block: str = "") -> Path:
|
||||
dest = tmp_dir / "justify_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 — exercise _set_justify_on_property() directly, no file I/O
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestSetJustifyOnPropertyHelper:
|
||||
"""Unit tests for the _set_justify_on_property helper."""
|
||||
|
||||
def _get_interface(self) -> Any:
|
||||
from kicad_interface import KiCADInterface
|
||||
|
||||
return KiCADInterface()
|
||||
|
||||
def test_adds_justify_left(self) -> None:
|
||||
iface = self._get_interface()
|
||||
block = PLACED_RESISTOR_BLOCK
|
||||
result = iface._set_justify_on_property(block, "Reference", "left")
|
||||
assert "(justify left)" in result
|
||||
|
||||
def test_adds_justify_right(self) -> None:
|
||||
iface = self._get_interface()
|
||||
block = PLACED_RESISTOR_BLOCK
|
||||
result = iface._set_justify_on_property(block, "Reference", "right")
|
||||
assert "(justify right)" in result
|
||||
|
||||
def test_adds_combined_justify(self) -> None:
|
||||
iface = self._get_interface()
|
||||
block = PLACED_RESISTOR_BLOCK
|
||||
result = iface._set_justify_on_property(block, "Reference", "right top")
|
||||
assert "(justify right top)" in result
|
||||
|
||||
def test_center_removes_directive(self) -> None:
|
||||
"""Passing 'center' should remove any (justify ...) token (KiCad default)."""
|
||||
iface = self._get_interface()
|
||||
block = PLACED_RESISTOR_BLOCK
|
||||
# First set a non-center justify so there is something to remove
|
||||
block = iface._set_justify_on_property(block, "Reference", "left")
|
||||
assert "(justify left)" in block
|
||||
# Now reset to center
|
||||
block = iface._set_justify_on_property(block, "Reference", "center")
|
||||
assert "(justify" not in block
|
||||
|
||||
def test_replaces_existing_justify(self) -> None:
|
||||
iface = self._get_interface()
|
||||
block = PLACED_RESISTOR_BLOCK
|
||||
block = iface._set_justify_on_property(block, "Reference", "left")
|
||||
block = iface._set_justify_on_property(block, "Reference", "right")
|
||||
# Should have exactly one justify token and it should be 'right'
|
||||
matches = re.findall(r"\(justify\s+[^)]+\)", block)
|
||||
assert len(matches) == 1
|
||||
assert "right" in matches[0]
|
||||
|
||||
def test_does_not_affect_other_fields(self) -> None:
|
||||
iface = self._get_interface()
|
||||
block = PLACED_RESISTOR_BLOCK
|
||||
result = iface._set_justify_on_property(block, "Reference", "left")
|
||||
# Value field must not have acquired a justify token
|
||||
value_section = result[result.find('(property "Value"') :]
|
||||
ref_section = result[result.find('(property "Reference"') :]
|
||||
# The justify token should appear before the Value property section
|
||||
justify_pos = result.find("(justify left)")
|
||||
value_prop_pos = result.find('(property "Value"')
|
||||
assert justify_pos < value_prop_pos
|
||||
|
||||
def test_unknown_field_returns_block_unchanged(self) -> None:
|
||||
iface = self._get_interface()
|
||||
block = PLACED_RESISTOR_BLOCK
|
||||
result = iface._set_justify_on_property(block, "NoSuchField", "left")
|
||||
assert result == block
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Integration tests — real file I/O through edit_schematic_component
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestFieldPositionsJustify:
|
||||
"""Integration tests for justify via fieldPositions in edit_schematic_component."""
|
||||
|
||||
@pytest.fixture
|
||||
def sch_with_r2(self, tmp_path: Any) -> Any:
|
||||
return _make_test_schematic(tmp_path, PLACED_RESISTOR_BLOCK)
|
||||
|
||||
def _get_interface(self) -> Any:
|
||||
from kicad_interface import KiCADInterface
|
||||
|
||||
return KiCADInterface()
|
||||
|
||||
def test_set_justify_left_on_reference(self, sch_with_r2: Any) -> None:
|
||||
iface = self._get_interface()
|
||||
result = iface.handle_command(
|
||||
"edit_schematic_component",
|
||||
{
|
||||
"schematicPath": str(sch_with_r2),
|
||||
"reference": "R2",
|
||||
"fieldPositions": {"Reference": {"x": 51.27, "y": 47.46, "justify": "left"}},
|
||||
},
|
||||
)
|
||||
assert result["success"] is True
|
||||
content = sch_with_r2.read_text(encoding="utf-8")
|
||||
assert "(justify left)" in content
|
||||
|
||||
def test_set_justify_right_on_value(self, sch_with_r2: Any) -> None:
|
||||
iface = self._get_interface()
|
||||
result = iface.handle_command(
|
||||
"edit_schematic_component",
|
||||
{
|
||||
"schematicPath": str(sch_with_r2),
|
||||
"reference": "R2",
|
||||
"fieldPositions": {"Value": {"x": 51.27, "y": 52.54, "justify": "right"}},
|
||||
},
|
||||
)
|
||||
assert result["success"] is True
|
||||
content = sch_with_r2.read_text(encoding="utf-8")
|
||||
assert "(justify right)" in content
|
||||
|
||||
def test_set_justify_center_removes_directive(self, sch_with_r2: Any) -> None:
|
||||
iface = self._get_interface()
|
||||
# First add a left justify so there is something to remove
|
||||
iface.handle_command(
|
||||
"edit_schematic_component",
|
||||
{
|
||||
"schematicPath": str(sch_with_r2),
|
||||
"reference": "R2",
|
||||
"fieldPositions": {"Reference": {"x": 51.27, "y": 47.46, "justify": "left"}},
|
||||
},
|
||||
)
|
||||
# Now reset to center (default)
|
||||
result = iface.handle_command(
|
||||
"edit_schematic_component",
|
||||
{
|
||||
"schematicPath": str(sch_with_r2),
|
||||
"reference": "R2",
|
||||
"fieldPositions": {"Reference": {"x": 51.27, "y": 47.46, "justify": "center"}},
|
||||
},
|
||||
)
|
||||
assert result["success"] is True
|
||||
content = sch_with_r2.read_text(encoding="utf-8")
|
||||
# Check that the Reference property block itself has no (justify ...) token.
|
||||
# (The lib_symbols section may legitimately contain justify directives.)
|
||||
ref_prop_start = content.find('(property "Reference" "R2"')
|
||||
assert ref_prop_start >= 0, "Reference property block not found"
|
||||
ref_prop_end = content.find("\n )", ref_prop_start) + len("\n )")
|
||||
ref_prop_block = content[ref_prop_start:ref_prop_end]
|
||||
assert "(justify" not in ref_prop_block
|
||||
|
||||
def test_fieldpositions_without_justify_unchanged(self, sch_with_r2: Any) -> None:
|
||||
"""Omitting justify must not alter any existing justify on the field."""
|
||||
iface = self._get_interface()
|
||||
# Set a justify first
|
||||
iface.handle_command(
|
||||
"edit_schematic_component",
|
||||
{
|
||||
"schematicPath": str(sch_with_r2),
|
||||
"reference": "R2",
|
||||
"fieldPositions": {"Reference": {"x": 51.27, "y": 47.46, "justify": "left"}},
|
||||
},
|
||||
)
|
||||
# Reposition without passing justify — the left directive must survive
|
||||
iface.handle_command(
|
||||
"edit_schematic_component",
|
||||
{
|
||||
"schematicPath": str(sch_with_r2),
|
||||
"reference": "R2",
|
||||
"fieldPositions": {"Reference": {"x": 60.0, "y": 47.46}},
|
||||
},
|
||||
)
|
||||
content = sch_with_r2.read_text(encoding="utf-8")
|
||||
assert "(justify left)" in content
|
||||
|
||||
def test_position_and_justify_applied_together(self, sch_with_r2: Any) -> None:
|
||||
"""Both position and justify should be applied in a single call."""
|
||||
iface = self._get_interface()
|
||||
result = iface.handle_command(
|
||||
"edit_schematic_component",
|
||||
{
|
||||
"schematicPath": str(sch_with_r2),
|
||||
"reference": "R2",
|
||||
"fieldPositions": {"Reference": {"x": 99.0, "y": 88.0, "justify": "right"}},
|
||||
},
|
||||
)
|
||||
assert result["success"] is True
|
||||
content = sch_with_r2.read_text(encoding="utf-8")
|
||||
assert "(justify right)" in content
|
||||
assert "99.0 88.0" in content
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Integration tests — justify via set_schematic_component_property
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestSetPropertyJustify:
|
||||
"""Integration tests for justify via set_schematic_component_property."""
|
||||
|
||||
@pytest.fixture
|
||||
def sch_with_r2(self, tmp_path: Any) -> Any:
|
||||
return _make_test_schematic(tmp_path, PLACED_RESISTOR_BLOCK)
|
||||
|
||||
def _get_interface(self) -> Any:
|
||||
from kicad_interface import KiCADInterface
|
||||
|
||||
return KiCADInterface()
|
||||
|
||||
def test_set_property_with_justify(self, sch_with_r2: Any) -> None:
|
||||
iface = self._get_interface()
|
||||
result = iface.handle_command(
|
||||
"set_schematic_component_property",
|
||||
{
|
||||
"schematicPath": str(sch_with_r2),
|
||||
"reference": "R2",
|
||||
"name": "Tolerance",
|
||||
"value": "1%",
|
||||
"justify": "left",
|
||||
},
|
||||
)
|
||||
assert result["success"] is True
|
||||
content = sch_with_r2.read_text(encoding="utf-8")
|
||||
assert "(justify left)" in content
|
||||
|
||||
def test_set_property_without_justify_no_regression(self, sch_with_r2: Any) -> None:
|
||||
"""Omitting justify on set_property must not add any justify token."""
|
||||
iface = self._get_interface()
|
||||
result = iface.handle_command(
|
||||
"set_schematic_component_property",
|
||||
{
|
||||
"schematicPath": str(sch_with_r2),
|
||||
"reference": "R2",
|
||||
"name": "Rating",
|
||||
"value": "100mW",
|
||||
},
|
||||
)
|
||||
assert result["success"] is True
|
||||
content = sch_with_r2.read_text(encoding="utf-8")
|
||||
# No justify should appear in the newly added Rating property
|
||||
rating_start = content.find('(property "Rating"')
|
||||
rating_end = content.find(")", rating_start)
|
||||
assert "(justify" not in content[rating_start:rating_end]
|
||||
Reference in New Issue
Block a user