diff --git a/tests/test_pin_locator_y_flip.py b/tests/test_pin_locator_y_flip.py index e734171..243903b 100644 --- a/tests/test_pin_locator_y_flip.py +++ b/tests/test_pin_locator_y_flip.py @@ -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]}"