1. add_schematic_net_label failed on schematics with no existing labels. The clone-based path required a pre-existing label to copy from; the documented "fallback to sexpdata" was a misleading log line — the RuntimeError was caught and the call silently returned False. Restore hand-built sexpdata construction (without the buggy fields_autoplaced token, with orientation-aware justify). 2. get_pin_angle returned the wrong angle for every mirrored symbol (off by exactly 180°, all rotations, both mirror axes). The mirror_x and mirror_y formulas were swapped relative to the pin_world_xy convention — pin_world_xy mirrors a position by flipping its local axis component, so the matching angle transform is (180 - θ) for mirror_x and -θ for mirror_y. Add regression tests: - test_add_label_empty_schematic.py — first label on empty schematic, orientation-aware justify. - test_get_pin_angle.py — full 24-case matrix (4 rotations × 3 mirror states × 2 pins).
106 lines
3.2 KiB
Python
106 lines
3.2 KiB
Python
"""
|
|
Regression tests for WireManager.add_label on schematics with zero existing labels.
|
|
|
|
Bug: PR #88 rewrote add_label to clone an existing label via kicad-skip; on a
|
|
fresh schematic with no labels this returns False. The fix restores a hand-built
|
|
sexpdata path (without the spurious fields_autoplaced token) as the primary path.
|
|
"""
|
|
|
|
import importlib.util
|
|
import os
|
|
import sys
|
|
from unittest.mock import MagicMock
|
|
|
|
import sexpdata
|
|
from sexpdata import Symbol
|
|
|
|
# Stub heavy / optional deps before loading wire_manager
|
|
for modname in ("pcbnew", "skip"):
|
|
sys.modules.setdefault(modname, MagicMock())
|
|
|
|
_wm_spec = importlib.util.spec_from_file_location(
|
|
"wire_manager",
|
|
os.path.join(os.path.dirname(__file__), "..", "python", "commands", "wire_manager.py"),
|
|
)
|
|
_wm_mod = importlib.util.module_from_spec(_wm_spec)
|
|
_wm_spec.loader.exec_module(_wm_mod)
|
|
WireManager = _wm_mod.WireManager
|
|
|
|
|
|
_EMPTY_SCH = """\
|
|
(kicad_sch (version 20250114) (generator "test")
|
|
(lib_symbols)
|
|
(sheet_instances
|
|
(path "/" (page "1"))
|
|
)
|
|
)
|
|
"""
|
|
|
|
|
|
def _find_label(sch_data, text):
|
|
for item in sch_data:
|
|
if (
|
|
isinstance(item, list)
|
|
and len(item) >= 2
|
|
and isinstance(item[0], Symbol)
|
|
and str(item[0]) == "label"
|
|
and item[1] == text
|
|
):
|
|
return item
|
|
return None
|
|
|
|
|
|
def _has_token(label_sexp, name):
|
|
for part in label_sexp[2:]:
|
|
if isinstance(part, list) and part and isinstance(part[0], Symbol) and str(part[0]) == name:
|
|
return True
|
|
return False
|
|
|
|
|
|
def _find_subtoken(label_sexp, name):
|
|
for part in label_sexp[2:]:
|
|
if isinstance(part, list) and part and isinstance(part[0], Symbol) and str(part[0]) == name:
|
|
return part
|
|
return None
|
|
|
|
|
|
def test_add_label_to_empty_schematic_succeeds(tmp_path):
|
|
sch_path = tmp_path / "empty.kicad_sch"
|
|
sch_path.write_text(_EMPTY_SCH)
|
|
|
|
ok = WireManager.add_label(sch_path, "TEST", [50.0, 50.0], "label", 0)
|
|
assert ok is True
|
|
|
|
sch_data = sexpdata.loads(sch_path.read_text())
|
|
label = _find_label(sch_data, "TEST")
|
|
assert label is not None, 'Expected a (label "TEST" ...) token in the file'
|
|
|
|
# The fields_autoplaced token must NOT be present (regression guard for PR #88's bug).
|
|
assert not _has_token(
|
|
label, "fields_autoplaced"
|
|
), "fields_autoplaced should not be emitted by add_label"
|
|
|
|
|
|
def test_add_label_orientation_180_uses_right_bottom_justify(tmp_path):
|
|
sch_path = tmp_path / "empty180.kicad_sch"
|
|
sch_path.write_text(_EMPTY_SCH)
|
|
|
|
ok = WireManager.add_label(sch_path, "NETR", [60.0, 60.0], "label", 180)
|
|
assert ok is True
|
|
|
|
sch_data = sexpdata.loads(sch_path.read_text())
|
|
label = _find_label(sch_data, "NETR")
|
|
assert label is not None
|
|
|
|
effects = _find_subtoken(label, "effects")
|
|
assert effects is not None, "label must carry an (effects ...) token"
|
|
|
|
justify = _find_subtoken(effects, "justify")
|
|
assert justify is not None, "effects must carry a (justify ...) token"
|
|
|
|
justify_vals = [str(t) for t in justify[1:] if isinstance(t, Symbol)]
|
|
assert justify_vals == [
|
|
"right",
|
|
"bottom",
|
|
], f"orientation=180 should produce 'right bottom' justify, got {justify_vals}"
|