test(pin_locator): add asymmetric multi-pin Y-flip regression (#188)
The existing y_flip tests cover Device:R (symmetric two-pin) and Device:C rotated 90° (still electrically symmetric). The original bug — a double Y-flip in get_pin_location — was invisible on symmetric passives because pin 1 and pin 2 are interchangeable; it only showed up on asymmetric multi-pin ICs like RF_Module:ESP32-WROOM-32, where labels meant for pin 3 (EN) silently landed on pin 35 (TXD0). This adds a third test using an inline 6-pin asymmetric symbol with pins at both positive and negative library Y on both sides. It asserts every pin lands at the formula-predicted (symbol_x + lib_px, symbol_y - lib_py) position, with an explicit cross-check that lib +Y pins resolve *above* the placement centre in schematic Y-down space. No system-library dependency — the symbol is constructed inline so the test runs anywhere pytest does. Fixes #135
This commit is contained in:
@@ -111,3 +111,132 @@ def test_rotated_capacitor_pin_x_matches_render_convention():
|
||||
# World: (150-3.81, 100) = (146.19, 100). Verified vs kicad-cli netlist.
|
||||
assert p1[0] == pytest.approx(146.19), f"rotated pin 1 X wrong: {p1[0]}"
|
||||
assert p1[1] == pytest.approx(100.0), f"rotated pin 1 Y wrong: {p1[1]}"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Asymmetric multi-pin regression (the case where the original bug hit hardest)
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# The symmetric-symbol tests above pass even with the pre-fix code because
|
||||
# Device:R and Device:C are pin-equivalent — swap pin 1 and pin 2 in the
|
||||
# netlist and the circuit still works. The bug only became visible on
|
||||
# asymmetric, multi-pin ICs like RF_Module:ESP32-WROOM-32, where pin 3 (EN)
|
||||
# and pin 35 (TXD0/IO1) are not interchangeable. With the original code,
|
||||
# pins at library y=+30.48 (top of the symbol) were reported at world
|
||||
# y=symbol_y+30.48 (bottom of the schematic), so labels meant for EN landed
|
||||
# on TXD0, and ERC reported either a dangling label or a wrong connection.
|
||||
#
|
||||
# Issue #135 specifically asked for an asymmetric-symbol regression. Rather
|
||||
# than depend on system libraries (RF_Module is not bundled with the repo),
|
||||
# this fixture builds a minimal 6-pin asymmetric symbol inline and asserts
|
||||
# every pin lands at the formula-predicted absolute position.
|
||||
|
||||
_ASYMMETRIC_FIXTURE = """(kicad_sch
|
||||
(version 20231120) (generator "test_pin_locator_y_flip")
|
||||
(uuid "00000000-0000-0000-0000-000000000001")
|
||||
(paper "A4")
|
||||
(lib_symbols
|
||||
(symbol "Test:Asym6"
|
||||
(pin_numbers hide) (pin_names (offset 0.508))
|
||||
(in_bom yes) (on_board yes)
|
||||
(property "Reference" "U" (at 0 15.24 0)
|
||||
(effects (font (size 1.27 1.27))))
|
||||
(property "Value" "Asym6" (at 0 -15.24 0)
|
||||
(effects (font (size 1.27 1.27))))
|
||||
(property "Footprint" "" (at 0 0 0)
|
||||
(effects (font (size 1.27 1.27)) hide))
|
||||
(property "Datasheet" "" (at 0 0 0)
|
||||
(effects (font (size 1.27 1.27)) hide))
|
||||
(symbol "Asym6_0_1"
|
||||
(rectangle (start -7.62 12.7) (end 7.62 -12.7)
|
||||
(stroke (width 0.254) (type default))
|
||||
(fill (type none))))
|
||||
(symbol "Asym6_1_1"
|
||||
(pin input line (at -10.16 10.16 0) (length 2.54)
|
||||
(name "TOP_LEFT" (effects (font (size 1.27 1.27))))
|
||||
(number "1" (effects (font (size 1.27 1.27)))))
|
||||
(pin input line (at -10.16 5.08 0) (length 2.54)
|
||||
(name "MID_HIGH_L" (effects (font (size 1.27 1.27))))
|
||||
(number "2" (effects (font (size 1.27 1.27)))))
|
||||
(pin input line (at -10.16 -5.08 0) (length 2.54)
|
||||
(name "MID_LOW_L" (effects (font (size 1.27 1.27))))
|
||||
(number "3" (effects (font (size 1.27 1.27)))))
|
||||
(pin input line (at -10.16 -10.16 0) (length 2.54)
|
||||
(name "BOT_LEFT" (effects (font (size 1.27 1.27))))
|
||||
(number "4" (effects (font (size 1.27 1.27)))))
|
||||
(pin output line (at 10.16 10.16 180) (length 2.54)
|
||||
(name "TOP_RIGHT" (effects (font (size 1.27 1.27))))
|
||||
(number "5" (effects (font (size 1.27 1.27)))))
|
||||
(pin output line (at 10.16 -10.16 180) (length 2.54)
|
||||
(name "BOT_RIGHT" (effects (font (size 1.27 1.27))))
|
||||
(number "6" (effects (font (size 1.27 1.27))))))))
|
||||
(symbol
|
||||
(lib_id "Test:Asym6") (at 100 100 0) (unit 1)
|
||||
(in_bom yes) (on_board yes) (dnp no)
|
||||
(uuid "11111111-1111-1111-1111-111111111111")
|
||||
(property "Reference" "U1" (at 100 84.76 0)
|
||||
(effects (font (size 1.27 1.27))))
|
||||
(property "Value" "Asym6" (at 100 115.24 0)
|
||||
(effects (font (size 1.27 1.27))))
|
||||
(property "Footprint" "" (at 100 100 0)
|
||||
(effects (font (size 1.27 1.27)) hide))
|
||||
(property "Datasheet" "" (at 100 100 0)
|
||||
(effects (font (size 1.27 1.27)) hide))
|
||||
(instances
|
||||
(project "test"
|
||||
(path "/00000000-0000-0000-0000-000000000001"
|
||||
(reference "U1") (unit 1))))))
|
||||
"""
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_asymmetric_multi_pin_symbol_y_flip():
|
||||
"""A 6-pin asymmetric IC must report every pin at the correct side of centre.
|
||||
|
||||
Pre-#135 fix, library y=+10.16 pins were reported at schematic y=+110.16
|
||||
(below the placement centre at y=100). The render-correct answer is
|
||||
schematic y=89.84 (above the centre, smaller numerical y in Y-down).
|
||||
|
||||
Each side (left at lib x=-10.16, right at lib x=+10.16) carries pins at
|
||||
both positive and negative library Y — so a Y-flip bug or a left/right
|
||||
swap would visibly mis-place at least one pin. This is the asymmetric
|
||||
multi-pin regression requested in issue #135 (originally hit on
|
||||
RF_Module:ESP32-WROOM-32 in the wild; same arithmetic, smaller fixture).
|
||||
"""
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
sch_path = Path(tmp) / "asym6_regression.kicad_sch"
|
||||
sch_path.write_text(_ASYMMETRIC_FIXTURE, encoding="utf-8")
|
||||
|
||||
locator = PinLocator()
|
||||
# Placement: U1 at (100, 100) rotation 0, no mirror.
|
||||
# Formula in screen Y-down (after the Y-flip fix):
|
||||
# world_x = symbol_x + lib_px
|
||||
# world_y = symbol_y + (-lib_py)
|
||||
# so the lib +Y pins (top of symbol) end up at smaller world_y, i.e.
|
||||
# *above* the placement centre.
|
||||
expected = {
|
||||
"1": (100 - 10.16, 100 - 10.16), # 89.84, 89.84
|
||||
"2": (100 - 10.16, 100 - 5.08), # 89.84, 94.92
|
||||
"3": (100 - 10.16, 100 + 5.08), # 89.84, 105.08
|
||||
"4": (100 - 10.16, 100 + 10.16), # 89.84, 110.16
|
||||
"5": (100 + 10.16, 100 - 10.16), # 110.16, 89.84
|
||||
"6": (100 + 10.16, 100 + 10.16), # 110.16, 110.16
|
||||
}
|
||||
|
||||
for pin_num, (exp_x, exp_y) in expected.items():
|
||||
loc = locator.get_pin_location(sch_path, "U1", pin_num)
|
||||
assert loc is not None, f"PinLocator returned None for pin {pin_num}"
|
||||
assert loc[0] == pytest.approx(exp_x), (
|
||||
f"pin {pin_num} X wrong: got {loc[0]}, expected {exp_x}"
|
||||
)
|
||||
assert loc[1] == pytest.approx(exp_y), (
|
||||
f"pin {pin_num} Y wrong: got {loc[1]}, expected {exp_y}"
|
||||
)
|
||||
|
||||
# Cross-check the side-asymmetry directly: top-left (lib y=+10.16) must
|
||||
# be ABOVE centre, bottom-left (lib y=-10.16) BELOW centre. Pre-fix
|
||||
# these would be swapped because of the double-Y-flip.
|
||||
p1 = locator.get_pin_location(sch_path, "U1", "1")
|
||||
p4 = locator.get_pin_location(sch_path, "U1", "4")
|
||||
assert p1[1] < 100, f"top pin must be above centre, got y={p1[1]}"
|
||||
assert p4[1] > 100, f"bottom pin must be below centre, got y={p4[1]}"
|
||||
|
||||
Reference in New Issue
Block a user