test(rotate_schematic_mirror): isolate sys.modules stubs to fix test pollution
`test_rotate_handler_no_crash` permanently replaced `sys.modules["schemas.tool_schemas"].TOOL_SCHEMAS` with `[]`, leaking into later tests. When test_wire_connectivity (or any test) ran after this one and did `from schemas.tool_schemas import TOOL_SCHEMAS`, it got the empty list and `TOOL_SCHEMAS["get_wire_connections"]` raised `TypeError: list indices must be integers or slices, not str`. Save the original sys.modules entries and restore them in a `finally` block so the stubs are scoped to the test body. Whole suite now passes (678 tests, previously 4 failed in TestSchema when run in suite order). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -6,13 +6,13 @@ Tests are split into two layers:
|
|||||||
2. Handler integration smoke test — patches SchematicManager away.
|
2. Handler integration smoke test — patches SchematicManager away.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
import math
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import math
|
|
||||||
import textwrap
|
|
||||||
import tempfile
|
import tempfile
|
||||||
import importlib.util
|
import textwrap
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import sexpdata
|
import sexpdata
|
||||||
from sexpdata import Symbol
|
from sexpdata import Symbol
|
||||||
@@ -40,6 +40,7 @@ WireDragger = _wd_mod.WireDragger
|
|||||||
# Helpers
|
# Helpers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def _parse(text: str) -> list:
|
def _parse(text: str) -> list:
|
||||||
return sexpdata.loads(text)
|
return sexpdata.loads(text)
|
||||||
|
|
||||||
@@ -80,6 +81,7 @@ def _make_sch(sym_extra: str = "", wires: str = "") -> list:
|
|||||||
# Tests: update_symbol_rotation_mirror
|
# Tests: update_symbol_rotation_mirror
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def test_update_rotation_sets_angle():
|
def test_update_rotation_sets_angle():
|
||||||
sch = _make_sch()
|
sch = _make_sch()
|
||||||
result = WireDragger.update_symbol_rotation_mirror(sch, "Q1", 90.0, None)
|
result = WireDragger.update_symbol_rotation_mirror(sch, "Q1", 90.0, None)
|
||||||
@@ -130,6 +132,7 @@ def test_update_unknown_reference_returns_false():
|
|||||||
# Tests: compute_pin_positions_for_rotation
|
# Tests: compute_pin_positions_for_rotation
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def test_pin_positions_change_on_rotation():
|
def test_pin_positions_change_on_rotation():
|
||||||
"""Pins at non-zero local offsets should move when the symbol rotates."""
|
"""Pins at non-zero local offsets should move when the symbol rotates."""
|
||||||
sch = _make_sch()
|
sch = _make_sch()
|
||||||
@@ -179,6 +182,7 @@ def test_pin_positions_mirror_x_flips_x():
|
|||||||
# Integration smoke test: handler uses sexpdata, not kicad-skip
|
# Integration smoke test: handler uses sexpdata, not kicad-skip
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def test_rotate_handler_no_crash(tmp_path):
|
def test_rotate_handler_no_crash(tmp_path):
|
||||||
"""_handle_rotate_schematic_component should succeed without kicad-skip."""
|
"""_handle_rotate_schematic_component should succeed without kicad-skip."""
|
||||||
# Ensure python/ is on sys.path so commands.* imports resolve
|
# Ensure python/ is on sys.path so commands.* imports resolve
|
||||||
@@ -186,26 +190,44 @@ def test_rotate_handler_no_crash(tmp_path):
|
|||||||
if _python_dir not in sys.path:
|
if _python_dir not in sys.path:
|
||||||
sys.path.insert(0, _python_dir)
|
sys.path.insert(0, _python_dir)
|
||||||
|
|
||||||
# Stub heavy imports before loading kicad_interface
|
# Stub heavy imports before loading kicad_interface. Save and restore
|
||||||
for modname in ("pcbnew", "skip", "resources", "schemas",
|
# sys.modules state so we don't pollute already-imported real modules
|
||||||
"resources.resource_definitions", "schemas.tool_schemas",
|
# (notably schemas.tool_schemas, whose TOOL_SCHEMAS dict is shared across
|
||||||
"annotations"):
|
# the test session).
|
||||||
sys.modules.setdefault(modname, MagicMock())
|
_stub_modnames = (
|
||||||
sys.modules["resources.resource_definitions"].RESOURCE_DEFINITIONS = {}
|
"pcbnew",
|
||||||
sys.modules["resources.resource_definitions"].handle_resource_read = MagicMock()
|
"skip",
|
||||||
sys.modules["schemas.tool_schemas"].TOOL_SCHEMAS = []
|
"resources",
|
||||||
|
"schemas",
|
||||||
_pcbnew = sys.modules["pcbnew"]
|
"resources.resource_definitions",
|
||||||
_pcbnew.__file__ = "/fake/pcbnew.so"
|
"schemas.tool_schemas",
|
||||||
_pcbnew.GetBuildVersion.return_value = "9.0.0"
|
"annotations",
|
||||||
|
|
||||||
ki_spec = importlib.util.spec_from_file_location(
|
|
||||||
"kicad_interface_smoke",
|
|
||||||
os.path.join(os.path.dirname(__file__), "..", "python", "kicad_interface.py"),
|
|
||||||
)
|
)
|
||||||
ki_mod = importlib.util.module_from_spec(ki_spec)
|
_saved_modules = {n: sys.modules.get(n) for n in _stub_modnames}
|
||||||
ki_spec.loader.exec_module(ki_mod)
|
try:
|
||||||
KiCADInterface = ki_mod.KiCADInterface
|
for modname in _stub_modnames:
|
||||||
|
sys.modules[modname] = MagicMock()
|
||||||
|
sys.modules["resources.resource_definitions"].RESOURCE_DEFINITIONS = {}
|
||||||
|
sys.modules["resources.resource_definitions"].handle_resource_read = MagicMock()
|
||||||
|
sys.modules["schemas.tool_schemas"].TOOL_SCHEMAS = []
|
||||||
|
|
||||||
|
_pcbnew = sys.modules["pcbnew"]
|
||||||
|
_pcbnew.__file__ = "/fake/pcbnew.so"
|
||||||
|
_pcbnew.GetBuildVersion.return_value = "9.0.0"
|
||||||
|
|
||||||
|
ki_spec = importlib.util.spec_from_file_location(
|
||||||
|
"kicad_interface_smoke",
|
||||||
|
os.path.join(os.path.dirname(__file__), "..", "python", "kicad_interface.py"),
|
||||||
|
)
|
||||||
|
ki_mod = importlib.util.module_from_spec(ki_spec)
|
||||||
|
ki_spec.loader.exec_module(ki_mod)
|
||||||
|
KiCADInterface = ki_mod.KiCADInterface
|
||||||
|
finally:
|
||||||
|
for modname, mod in _saved_modules.items():
|
||||||
|
if mod is None:
|
||||||
|
sys.modules.pop(modname, None)
|
||||||
|
else:
|
||||||
|
sys.modules[modname] = mod
|
||||||
|
|
||||||
# Write a minimal schematic file
|
# Write a minimal schematic file
|
||||||
sch_path = str(tmp_path / "test.kicad_sch")
|
sch_path = str(tmp_path / "test.kicad_sch")
|
||||||
@@ -233,11 +255,13 @@ def test_rotate_handler_no_crash(tmp_path):
|
|||||||
f.write(sch_content)
|
f.write(sch_content)
|
||||||
|
|
||||||
iface = KiCADInterface.__new__(KiCADInterface)
|
iface = KiCADInterface.__new__(KiCADInterface)
|
||||||
result = iface._handle_rotate_schematic_component({
|
result = iface._handle_rotate_schematic_component(
|
||||||
"schematicPath": sch_path,
|
{
|
||||||
"reference": "R1",
|
"schematicPath": sch_path,
|
||||||
"angle": 90,
|
"reference": "R1",
|
||||||
})
|
"angle": 90,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
assert result["success"] is True
|
assert result["success"] is True
|
||||||
assert result["angle"] == 90
|
assert result["angle"] == 90
|
||||||
|
|||||||
Reference in New Issue
Block a user