test: close mirror-fixture gap exposed by post-fix audit

The eeschema-ground-truth fixture (_build_mirror_case) passed
'mirror': 'x' to ComponentManager.add_component, which silently drops
the kwarg — so the resulting .kicad_sch had no (mirror x|y) token,
eeschema rendered an unmirrored symbol, and our pin coords (also
unmirrored) tautologically matched. The mirror tests were GREEN both
before and after the rotation/mirror fix in 7e67cb9, providing zero
regression coverage for the mirror semantics.

Fix:
  - _build_mirror_case now applies the mirror via the same low-level
    helper (WireDragger.update_symbol_rotation_mirror) that
    rotate_schematic_component uses, with a guard assertion that the
    written file actually contains (mirror x|y).
  - Two new pin-down unit tests in test_add_schematic_component.py
    document and lock down ComponentManager.add_component's silent-drop
    behavior for mirror, so the next person to touch that path knows to
    update the eeschema-truth fixture if they grow real mirror support.

Verified: with the production fix at 7e67cb9 reverted, the kicad-cli
mirror tests now go RED (previously they stayed GREEN regardless).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Eugene Mikhantyev
2026-05-03 22:40:34 +01:00
parent 7e67cb91c4
commit 496be317e9
2 changed files with 119 additions and 38 deletions

View File

@@ -106,6 +106,20 @@ def _build_diode_case(tmp: Path, rotation: int) -> tuple[Path, dict]:
return sch_path, {("D1", "1"): "D1_K", ("D1", "2"): "D1_A"}
def _apply_mirror_to_file(sch_path: Path, reference: str, axis: str) -> None:
"""Apply (mirror x|y) to a placed symbol via direct sexpr mutation.
ComponentManager.add_component silently drops a 'mirror' kwarg, so this
fixture goes around it via the same low-level helper rotate_schematic_component
uses (WireDragger.update_symbol_rotation_mirror)."""
import sexpdata
sch_data = sexpdata.loads(sch_path.read_text())
if not WireDragger.update_symbol_rotation_mirror(sch_data, reference, 0, axis):
raise RuntimeError(f"Failed to apply mirror={axis} to {reference}")
sch_path.write_text(sexpdata.dumps(sch_data))
def _build_mirror_case(tmp: Path, axis: str) -> tuple[Path, dict]:
sch_path = tmp / f"resistor_mirror_{axis}.kicad_sch"
template = PYTHON_DIR / "templates" / "template_with_symbols.kicad_sch"
@@ -114,23 +128,23 @@ def _build_mirror_case(tmp: Path, axis: str) -> tuple[Path, dict]:
sch = SchematicManager.load_schematic(str(sch_path))
ComponentManager.add_component(
sch,
{
"type": "R",
"reference": "R1",
"value": "10k",
"x": 100.0,
"y": 100.0,
"rotation": 0,
"mirror": axis,
},
{"type": "R", "reference": "R1", "value": "10k", "x": 100.0, "y": 100.0, "rotation": 0},
sch_path,
)
SchematicManager.save_schematic(sch, str(sch_path))
_apply_mirror_to_file(sch_path, "R1", axis)
if f"(mirror {axis})" not in sch_path.read_text():
raise RuntimeError(
f"Fixture failed to write (mirror {axis}) — the kicad-cli oracle would "
f"silently match our pin coords for an unmirrored symbol."
)
locator = PinLocator()
p1 = locator.get_pin_location(sch_path, "R1", "1")
p2 = locator.get_pin_location(sch_path, "R1", "2")
assert p1 is not None and p2 is not None
if p1 is None or p2 is None:
raise RuntimeError(f"PinLocator returned None for R1 mirror={axis}")
_add_labels_to_file(sch_path, [("R1_PIN1", p1[0], p1[1]), ("R1_PIN2", p2[0], p2[1])])