add_label bypasses kicad-sch-api serializer entirely — generates s-expression strings and inserts them directly into the .kicad_sch file via atomic write. Fixes two upstream bugs: global labels silently dropped on save (serializer never iterates "global_label" key), and local labels raising TypeError (parameter signature mismatch in LabelCollection.add()). add_power_symbol now falls back to sexp pin parsing when the API returns None for custom library symbols (e.g. SMF5.0CA). Extracts shared resolve_pin_position() utility used by both add_power_symbol and batch operations. Batch labels also fixed — collected as sexp strings during the batch loop and inserted after sch.save() so the serializer can't overwrite them.
252 lines
8.8 KiB
Python
252 lines
8.8 KiB
Python
"""Tests for schematic tools (kicad-sch-api integration)."""
|
|
|
|
import os
|
|
|
|
import pytest
|
|
|
|
from tests.conftest import requires_sch_api
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_create_schematic(tmp_output_dir):
|
|
"""create_schematic should produce a .kicad_sch file."""
|
|
from mckicad.tools.schematic import create_schematic
|
|
|
|
output_path = os.path.join(tmp_output_dir, "test.kicad_sch")
|
|
result = create_schematic(name="test_circuit", output_path=output_path)
|
|
assert result["success"] is True
|
|
assert os.path.exists(output_path)
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_create_schematic_invalid_path():
|
|
"""create_schematic should fail gracefully for invalid paths."""
|
|
from mckicad.tools.schematic import create_schematic
|
|
|
|
result = create_schematic(name="x", output_path="/nonexistent/dir/test.kicad_sch")
|
|
assert result["success"] is False
|
|
assert "error" in result
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_search_components():
|
|
"""search_components should return results for common queries."""
|
|
from mckicad.tools.schematic import search_components
|
|
|
|
result = search_components(query="resistor")
|
|
# Should succeed even if no libs installed (returns empty results)
|
|
assert "success" in result
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_list_components_empty_schematic(tmp_output_dir):
|
|
"""list_components on new empty schematic should return empty list."""
|
|
from mckicad.tools.schematic import create_schematic, list_components
|
|
|
|
path = os.path.join(tmp_output_dir, "empty.kicad_sch")
|
|
create_schematic(name="empty", output_path=path)
|
|
result = list_components(schematic_path=path)
|
|
if result["success"]:
|
|
assert result.get("count", 0) == 0
|
|
|
|
|
|
@requires_sch_api
|
|
@pytest.mark.unit
|
|
def test_list_components_single_lookup(populated_schematic):
|
|
"""list_components with reference param should return one component."""
|
|
from mckicad.tools.schematic import list_components
|
|
|
|
result = list_components(schematic_path=populated_schematic, reference="R1")
|
|
assert result["success"] is True
|
|
assert result["count"] == 1
|
|
assert result["components"][0]["reference"] == "R1"
|
|
|
|
|
|
@requires_sch_api
|
|
@pytest.mark.unit
|
|
def test_list_components_single_lookup_not_found(populated_schematic):
|
|
"""list_components with nonexistent reference should fail."""
|
|
from mckicad.tools.schematic import list_components
|
|
|
|
result = list_components(schematic_path=populated_schematic, reference="Z99")
|
|
assert result["success"] is False
|
|
|
|
|
|
@requires_sch_api
|
|
@pytest.mark.unit
|
|
def test_get_schematic_info_compact(populated_schematic):
|
|
"""get_schematic_info should return compact output."""
|
|
from mckicad.tools.schematic import get_schematic_info
|
|
|
|
result = get_schematic_info(schematic_path=populated_schematic)
|
|
assert result["success"] is True
|
|
assert "statistics" in result
|
|
assert "validation" in result
|
|
assert "unique_symbol_count" in result
|
|
|
|
|
|
@requires_sch_api
|
|
@pytest.mark.unit
|
|
def test_get_component_detail(populated_schematic):
|
|
"""get_component_detail should return full info for one component."""
|
|
from mckicad.tools.schematic import get_component_detail
|
|
|
|
result = get_component_detail(schematic_path=populated_schematic, reference="R1")
|
|
assert result["success"] is True
|
|
assert result["component"]["reference"] == "R1"
|
|
assert result["component"]["lib_id"] == "Device:R"
|
|
|
|
|
|
@requires_sch_api
|
|
@pytest.mark.unit
|
|
def test_get_component_detail_not_found(populated_schematic):
|
|
"""get_component_detail for missing component should fail."""
|
|
from mckicad.tools.schematic import get_component_detail
|
|
|
|
result = get_component_detail(schematic_path=populated_schematic, reference="Z99")
|
|
assert result["success"] is False
|
|
|
|
|
|
@requires_sch_api
|
|
@pytest.mark.unit
|
|
def test_get_schematic_hierarchy(populated_schematic):
|
|
"""get_schematic_hierarchy should return at least the root sheet."""
|
|
from mckicad.tools.schematic import get_schematic_hierarchy
|
|
|
|
result = get_schematic_hierarchy(schematic_path=populated_schematic)
|
|
assert result["success"] is True
|
|
assert result["hierarchy"]["name"] == "root"
|
|
assert result["hierarchy"]["total_sheets"] >= 1
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_file_output_infrastructure(tmp_output_dir):
|
|
"""write_detail_file should create .mckicad/{stem}/ sidecar directory and file."""
|
|
from mckicad.utils.file_utils import write_detail_file
|
|
|
|
fake_sch = os.path.join(tmp_output_dir, "test.kicad_sch")
|
|
# File doesn't need to exist for write_detail_file
|
|
open(fake_sch, "w").close()
|
|
|
|
data = {"test": "data", "items": [1, 2, 3]}
|
|
path = write_detail_file(fake_sch, "test_output.json", data)
|
|
|
|
assert os.path.isfile(path)
|
|
assert ".mckicad" in path
|
|
assert os.path.join(".mckicad", "test", "test_output.json") in path
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_file_output_cwd_fallback(tmp_output_dir, monkeypatch):
|
|
"""write_detail_file with None path should use flat CWD/.mckicad/."""
|
|
from mckicad.utils.file_utils import write_detail_file
|
|
|
|
monkeypatch.chdir(tmp_output_dir)
|
|
path = write_detail_file(None, "test_cwd.json", {"test": True})
|
|
|
|
assert os.path.isfile(path)
|
|
assert ".mckicad" in path
|
|
# None path stays flat — no stem subdirectory
|
|
assert path.endswith(os.path.join(".mckicad", "test_cwd.json"))
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_sidecar_per_schematic_isolation(tmp_output_dir):
|
|
"""Detail files for different schematics land in separate subdirectories."""
|
|
from mckicad.utils.file_utils import write_detail_file
|
|
|
|
sch_a = os.path.join(tmp_output_dir, "power.kicad_sch")
|
|
sch_b = os.path.join(tmp_output_dir, "esp32_p4_core.kicad_sch")
|
|
open(sch_a, "w").close()
|
|
open(sch_b, "w").close()
|
|
|
|
path_a = write_detail_file(sch_a, "connectivity.json", {"nets": {}})
|
|
path_b = write_detail_file(sch_b, "connectivity.json", {"nets": {}})
|
|
|
|
assert os.path.isfile(path_a)
|
|
assert os.path.isfile(path_b)
|
|
assert path_a != path_b
|
|
assert os.path.join(".mckicad", "power", "connectivity.json") in path_a
|
|
assert os.path.join(".mckicad", "esp32_p4_core", "connectivity.json") in path_b
|
|
|
|
|
|
class TestAddLabelPersistence:
|
|
"""Labels added via add_label must actually appear in the saved file."""
|
|
|
|
@pytest.mark.unit
|
|
def test_local_label_persists(self, tmp_output_dir):
|
|
from mckicad.tools.schematic import add_label
|
|
|
|
path = os.path.join(tmp_output_dir, "label_test.kicad_sch")
|
|
with open(path, "w") as f:
|
|
f.write("(kicad_sch\n (version 20231120)\n (uuid \"abc\")\n)\n")
|
|
|
|
result = add_label(schematic_path=path, text="SPI_CLK", x=100.0, y=200.0)
|
|
assert result["success"] is True
|
|
assert result["label_type"] == "local"
|
|
assert result["engine"] == "sexp-direct"
|
|
|
|
with open(path) as f:
|
|
content = f.read()
|
|
assert '(label "SPI_CLK"' in content
|
|
assert "(at 100 200 0)" in content
|
|
|
|
@pytest.mark.unit
|
|
def test_global_label_persists(self, tmp_output_dir):
|
|
from mckicad.tools.schematic import add_label
|
|
|
|
path = os.path.join(tmp_output_dir, "glabel_test.kicad_sch")
|
|
with open(path, "w") as f:
|
|
f.write("(kicad_sch\n (version 20231120)\n (uuid \"abc\")\n)\n")
|
|
|
|
result = add_label(
|
|
schematic_path=path, text="VBUS_OUT", x=187.96, y=114.3,
|
|
global_label=True,
|
|
)
|
|
assert result["success"] is True
|
|
assert result["label_type"] == "global"
|
|
|
|
with open(path) as f:
|
|
content = f.read()
|
|
assert '(global_label "VBUS_OUT"' in content
|
|
assert "(shape bidirectional)" in content
|
|
assert "Intersheetrefs" in content
|
|
|
|
@pytest.mark.unit
|
|
def test_multiple_labels_all_persist(self, tmp_output_dir):
|
|
from mckicad.tools.schematic import add_label
|
|
|
|
path = os.path.join(tmp_output_dir, "multi_label.kicad_sch")
|
|
with open(path, "w") as f:
|
|
f.write("(kicad_sch\n (version 20231120)\n (uuid \"abc\")\n)\n")
|
|
|
|
labels = ["NET_A", "NET_B", "NET_C", "GLOBAL_D"]
|
|
for i, name in enumerate(labels):
|
|
is_global = name.startswith("GLOBAL_")
|
|
result = add_label(
|
|
schematic_path=path, text=name,
|
|
x=100.0 + i * 10, y=200.0,
|
|
global_label=is_global,
|
|
)
|
|
assert result["success"] is True
|
|
|
|
with open(path) as f:
|
|
content = f.read()
|
|
|
|
for name in labels:
|
|
if name.startswith("GLOBAL_"):
|
|
assert f'(global_label "{name}"' in content
|
|
else:
|
|
assert f'(label "{name}"' in content
|
|
|
|
@pytest.mark.unit
|
|
def test_empty_text_rejected(self, tmp_output_dir):
|
|
from mckicad.tools.schematic import add_label
|
|
|
|
path = os.path.join(tmp_output_dir, "empty_label.kicad_sch")
|
|
with open(path, "w") as f:
|
|
f.write("(kicad_sch\n (version 20231120)\n (uuid \"abc\")\n)\n")
|
|
|
|
result = add_label(schematic_path=path, text="", x=0, y=0)
|
|
assert result["success"] is False
|