""" Unit tests for batch schematic authoring commands (commands/schematic_batch.py). Pure helpers are tested directly; handlers are tested for parameter validation and orchestration with the heavy dependencies (DynamicSymbolLoader / PinLocator / WireManager / interface handlers) stubbed, so no real KiCad install is needed. """ import sys import types from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent / "python")) from commands import schematic_batch as sb # noqa: E402 from commands.schematic_batch import ( # noqa: E402 SchematicBatchCommands, _bbox_from_pins, _field_positions_for_pins, _find_facing_label, _find_project_root, _snap, ) class TestHelpers: def test_snap(self): # 1.27mm (50-mil) grid assert _snap(1.27) == 1.27 assert _snap(2.54) == 2.54 assert _snap(0.6) == 0.0 # rounds down to 0 assert _snap(2.0) == 2.54 # nearest grid point def test_find_project_root(self, tmp_path): (tmp_path / "proj.kicad_pro").write_text("{}") sub = tmp_path / "a" / "b" sub.mkdir(parents=True) assert _find_project_root(sub) == tmp_path.resolve() def test_find_project_root_fallback(self, tmp_path): sub = tmp_path / "x" sub.mkdir() assert _find_project_root(sub) == sub # no .kicad_pro anywhere -> start dir def test_find_facing_label(self, tmp_path): f = tmp_path / "s.kicad_sch" # A label "VBUS" at (10,20) angle 0; a pin at (24,20) facing it needs orientation 180. f.write_text('(kicad_sch (label "VBUS" (at 10 20 0)))') assert _find_facing_label(f, "VBUS", [12, 20], orientation=180) == [10.0, 20.0] # Wrong net -> None assert _find_facing_label(f, "GND", [12, 20], orientation=180) is None # Too far -> None assert _find_facing_label(f, "VBUS", [100, 200], orientation=180) is None def test_field_positions_two_pin_vertical(self): # pins stacked vertically -> labels go left/right pins = {"1": [0, 5], "2": [0, -5]} fp = _field_positions_for_pins(0, 0, pins) names = {n for n, *_ in fp} assert names == {"Reference", "Value"} ref = next(p for p in fp if p[0] == "Reference") assert ref[1] != 0 # x offset (left/right), y unchanged def test_field_positions_two_pin_horizontal(self): pins = {"1": [-5, 0], "2": [5, 0]} fp = _field_positions_for_pins(0, 0, pins) ref = next(p for p in fp if p[0] == "Reference") assert ref[1] == 0 and ref[2] != 0 # above/below def test_bbox_from_pins(self): bb = _bbox_from_pins({"1": [0, 0], "2": [10, 4]}, 5, 2) assert bb["x_min"] == -1.27 and bb["x_max"] == 11.27 def test_bbox_from_pins_fallback(self): bb = _bbox_from_pins({}, 5, 5) assert bb == {"x_min": 2.46, "y_min": 2.46, "x_max": 7.54, "y_max": 7.54} class TestParamValidation: def setup_method(self): self.c = SchematicBatchCommands(types.SimpleNamespace()) def test_validation(self): assert self.c.batch_add_components({})["success"] is False assert ( self.c.batch_add_components({"schematicPath": "/x"})["success"] is False ) # no components assert self.c.batch_edit_schematic_components({"schematicPath": "/x"})["success"] is False assert ( self.c.replace_schematic_component({"schematicPath": "/x", "reference": "R1"})[ "success" ] is False ) assert self.c.batch_add_no_connects({"schematicPath": "/x"})["success"] is False assert self.c.batch_connect({"schematicPath": "/x"})["success"] is False assert self.c.batch_add_and_connect({"schematicPath": "/x"})["success"] is False class TestBatchEdit: def test_aggregates_results(self): calls = [] def fake_edit(params): calls.append(params) ref = params["reference"] if ref == "BAD": return {"success": False, "message": "nope"} return {"success": True, "updated": {"value": params.get("value")}} iface = types.SimpleNamespace(_handle_edit_schematic_component=fake_edit) c = SchematicBatchCommands(iface) r = c.batch_edit_schematic_components( { "schematicPath": "/x.kicad_sch", "components": {"R1": {"value": "10k"}, "BAD": {"value": "x"}}, } ) assert r["updated_count"] == 1 assert r["error_count"] == 1 assert r["success"] is False # sub-handler received merged params assert any(c_["reference"] == "R1" and c_["value"] == "10k" for c_ in calls) class TestBatchAddNoConnects: def test_happy_and_fallback(self, monkeypatch): fake_loc = types.SimpleNamespace( get_pin_location=lambda p, ref, pin: [10.0, 20.0] if pin == "1" else None, get_all_symbol_pins=lambda p, ref: {"1": [10.0, 20.0]}, # single-pin fallback ) added = [] fake_wm = types.SimpleNamespace(add_no_connect=lambda p, loc: added.append(loc) or True) monkeypatch.setattr(sb, "PinLocator", lambda: fake_loc) monkeypatch.setattr(sb, "WireManager", fake_wm) c = SchematicBatchCommands(types.SimpleNamespace()) r = c.batch_add_no_connects( { "schematicPath": "/x.kicad_sch", "pins": [ {"componentRef": "U1", "pinName": "1"}, {"componentRef": "TP1", "pinName": "9"}, ], } ) assert r["success"] is True # both succeed (2nd via single-pin fallback) assert len(r["placed"]) == 2 assert len(added) == 2 class TestBatchConnect: def test_places_label(self, monkeypatch, tmp_path): f = tmp_path / "x.kicad_sch" f.write_text("(kicad_sch)") fake_loc = types.SimpleNamespace( get_pin_location=lambda p, ref, pin: [10.0, 20.0], get_pin_angle=lambda p, ref, pin: 0, get_all_symbol_pins=lambda p, ref: {"1": [10.0, 20.0]}, ) labels = [] fake_wm = types.SimpleNamespace( add_label=lambda p, net, pos, label_type="label", orientation=0: labels.append( (net, orientation) ) or True, add_wire=lambda *a: True, delete_label=lambda *a, **k: True, ) monkeypatch.setattr(sb, "PinLocator", lambda: fake_loc) monkeypatch.setattr(sb, "WireManager", fake_wm) monkeypatch.setattr(sb, "_find_facing_label", lambda *a, **k: None) c = SchematicBatchCommands(types.SimpleNamespace()) r = c.batch_connect({"schematicPath": str(f), "connections": {"R1": {"1": "SDA"}}}) assert r["success"] is True assert len(r["placed"]) == 1 assert labels == [("SDA", 180)] # pin angle 0 -> label orientation 180 class TestBatchAddAndConnect: def test_splits_nets_and_orchestrates(self): c = SchematicBatchCommands(types.SimpleNamespace()) add_args = {} conn_args = {} def fake_add(params): add_args.update(params) return { "added_count": 1, "added": [{"reference": "R1"}], "errors": [], "placement_bbox": None, } def fake_connect(params): conn_args.update(params) return {"placed": [{"ref": "R1", "pin": "1", "net": "VCC"}], "failed": []} c.batch_add_components = fake_add c.batch_connect = fake_connect r = c.batch_add_and_connect( { "schematicPath": "/x.kicad_sch", "components": [{"symbol": "Device:R", "reference": "R1", "nets": {"1": "VCC"}}], } ) assert r["success"] is True assert r["added_count"] == 1 assert r["connected_count"] == 1 # 'nets' stripped from the components passed to batch_add_components assert "nets" not in add_args["components"][0] # nets routed to batch_connect keyed by reference assert conn_args["connections"] == {"R1": {"1": "VCC"}}