New modules: - patterns/ library: decoupling bank, pull resistor, crystal oscillator placement with power symbol attachment and grid math helpers - tools/batch.py: atomic file-based batch operations with dry_run - tools/power_symbols.py: add_power_symbol with auto #PWR refs - tools/schematic_patterns.py: MCP wrappers for pattern library - tools/schematic_edit.py: modify/remove components, title blocks, annotations - resources/schematic.py: schematic data resources 43 new tests (99 total), lint clean.
189 lines
6.1 KiB
Python
189 lines
6.1 KiB
Python
"""Tests for the batch operations tool."""
|
|
|
|
import json
|
|
import os
|
|
|
|
from tests.conftest import requires_sch_api
|
|
|
|
|
|
class TestBatchValidation:
|
|
"""Tests for batch JSON validation (no kicad-sch-api needed for some)."""
|
|
|
|
def test_bad_json_file(self, tmp_output_dir):
|
|
from mckicad.tools.batch import apply_batch
|
|
|
|
# Create a schematic file (minimal)
|
|
sch_path = os.path.join(tmp_output_dir, "test.kicad_sch")
|
|
with open(sch_path, "w") as f:
|
|
f.write("(kicad_sch (version 20230121))")
|
|
|
|
# Create a bad JSON file
|
|
bad_json = os.path.join(tmp_output_dir, "bad.json")
|
|
with open(bad_json, "w") as f:
|
|
f.write("{invalid json}")
|
|
|
|
result = apply_batch(
|
|
schematic_path=sch_path,
|
|
batch_file=bad_json,
|
|
)
|
|
|
|
assert result["success"] is False
|
|
assert "json" in result["error"].lower() or "JSON" in result["error"]
|
|
|
|
def test_nonexistent_batch_file(self, tmp_output_dir):
|
|
from mckicad.tools.batch import apply_batch
|
|
|
|
sch_path = os.path.join(tmp_output_dir, "test.kicad_sch")
|
|
with open(sch_path, "w") as f:
|
|
f.write("(kicad_sch (version 20230121))")
|
|
|
|
result = apply_batch(
|
|
schematic_path=sch_path,
|
|
batch_file="/nonexistent/batch.json",
|
|
)
|
|
|
|
assert result["success"] is False
|
|
assert "not found" in result["error"].lower()
|
|
|
|
def test_bad_schematic_path(self, batch_json_file):
|
|
from mckicad.tools.batch import apply_batch
|
|
|
|
result = apply_batch(
|
|
schematic_path="/nonexistent/path.kicad_sch",
|
|
batch_file=batch_json_file,
|
|
)
|
|
|
|
assert result["success"] is False
|
|
|
|
|
|
@requires_sch_api
|
|
class TestBatchDryRun:
|
|
"""Tests for batch dry_run mode."""
|
|
|
|
def test_dry_run_returns_preview(self, populated_schematic_with_ic, batch_json_file):
|
|
from mckicad.tools.batch import apply_batch
|
|
|
|
result = apply_batch(
|
|
schematic_path=populated_schematic_with_ic,
|
|
batch_file=batch_json_file,
|
|
dry_run=True,
|
|
)
|
|
|
|
assert result["success"] is True
|
|
assert result["dry_run"] is True
|
|
assert "preview" in result
|
|
assert result["preview"]["components"] == 2
|
|
assert result["preview"]["wires"] == 1
|
|
assert result["preview"]["labels"] == 1
|
|
assert result["preview"]["no_connects"] == 1
|
|
assert result["validation"] == "passed"
|
|
|
|
def test_dry_run_catches_missing_fields(self, populated_schematic_with_ic, tmp_output_dir):
|
|
from mckicad.tools.batch import apply_batch
|
|
|
|
bad_data = {
|
|
"components": [{"lib_id": "Device:R"}], # missing x, y
|
|
}
|
|
batch_path = os.path.join(tmp_output_dir, "bad_batch.json")
|
|
with open(batch_path, "w") as f:
|
|
json.dump(bad_data, f)
|
|
|
|
result = apply_batch(
|
|
schematic_path=populated_schematic_with_ic,
|
|
batch_file=batch_path,
|
|
dry_run=True,
|
|
)
|
|
|
|
assert result["success"] is False
|
|
assert "validation_errors" in result
|
|
|
|
def test_empty_batch_rejected(self, populated_schematic_with_ic, tmp_output_dir):
|
|
from mckicad.tools.batch import apply_batch
|
|
|
|
batch_path = os.path.join(tmp_output_dir, "empty_batch.json")
|
|
with open(batch_path, "w") as f:
|
|
json.dump({}, f)
|
|
|
|
result = apply_batch(
|
|
schematic_path=populated_schematic_with_ic,
|
|
batch_file=batch_path,
|
|
)
|
|
|
|
assert result["success"] is False
|
|
|
|
|
|
@requires_sch_api
|
|
class TestBatchApply:
|
|
"""Integration tests for applying batch operations."""
|
|
|
|
def test_apply_components_and_wires(self, populated_schematic_with_ic, batch_json_file):
|
|
from kicad_sch_api import load_schematic
|
|
|
|
from mckicad.tools.batch import apply_batch
|
|
|
|
result = apply_batch(
|
|
schematic_path=populated_schematic_with_ic,
|
|
batch_file=batch_json_file,
|
|
)
|
|
|
|
assert result["success"] is True
|
|
assert result["components_placed"] == 2
|
|
assert result["wires_placed"] == 1
|
|
assert result["labels_placed"] == 1
|
|
assert result["no_connects_placed"] == 1
|
|
|
|
# Verify components exist in saved schematic
|
|
sch = load_schematic(populated_schematic_with_ic)
|
|
r10 = sch.components.get("R10")
|
|
assert r10 is not None
|
|
assert r10.value == "1k"
|
|
|
|
def test_apply_with_power_symbols(self, populated_schematic_with_ic, tmp_output_dir):
|
|
from mckicad.tools.batch import apply_batch
|
|
|
|
data = {
|
|
"components": [
|
|
{"lib_id": "Device:C", "reference": "C20", "value": "100nF", "x": 300, "y": 100},
|
|
],
|
|
"power_symbols": [
|
|
{"net": "GND", "pin_ref": "C20", "pin_number": "2"},
|
|
],
|
|
}
|
|
batch_path = os.path.join(tmp_output_dir, "pwr_batch.json")
|
|
with open(batch_path, "w") as f:
|
|
json.dump(data, f)
|
|
|
|
result = apply_batch(
|
|
schematic_path=populated_schematic_with_ic,
|
|
batch_file=batch_path,
|
|
)
|
|
|
|
assert result["success"] is True
|
|
assert result["components_placed"] == 1
|
|
assert result["power_symbols_placed"] == 1
|
|
|
|
def test_mckicad_sidecar_lookup(self, populated_schematic_with_ic, tmp_output_dir):
|
|
"""Test that relative paths resolve to .mckicad/ directory."""
|
|
from mckicad.tools.batch import apply_batch
|
|
|
|
# Create .mckicad/ sidecar next to schematic
|
|
sch_dir = os.path.dirname(populated_schematic_with_ic)
|
|
mckicad_dir = os.path.join(sch_dir, ".mckicad")
|
|
os.makedirs(mckicad_dir, exist_ok=True)
|
|
|
|
data = {
|
|
"labels": [{"text": "SIDECAR_TEST", "x": 100, "y": 100}],
|
|
}
|
|
sidecar_path = os.path.join(mckicad_dir, "sidecar_batch.json")
|
|
with open(sidecar_path, "w") as f:
|
|
json.dump(data, f)
|
|
|
|
# Use relative path — should find it in .mckicad/
|
|
result = apply_batch(
|
|
schematic_path=populated_schematic_with_ic,
|
|
batch_file="sidecar_batch.json",
|
|
)
|
|
|
|
assert result["success"] is True
|
|
assert result["labels_placed"] == 1
|