Some checks are pending
CI / Lint and Format (push) Waiting to run
CI / Test Python 3.11 on macos-latest (push) Waiting to run
CI / Test Python 3.12 on macos-latest (push) Waiting to run
CI / Test Python 3.13 on macos-latest (push) Waiting to run
CI / Test Python 3.10 on ubuntu-latest (push) Waiting to run
CI / Test Python 3.11 on ubuntu-latest (push) Waiting to run
CI / Test Python 3.12 on ubuntu-latest (push) Waiting to run
CI / Test Python 3.13 on ubuntu-latest (push) Waiting to run
CI / Security Scan (push) Waiting to run
CI / Build Package (push) Blocked by required conditions
add_hierarchical_sheet now returns sheet_uuid and parent_uuid. apply_batch accepts these as optional params to call set_hierarchy_context() before placing components, fixing kicad-cli netlist export for hierarchical designs.
281 lines
9.8 KiB
Python
281 lines
9.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
|
|
|
|
|
|
@requires_sch_api
|
|
@pytest.mark.unit
|
|
def test_add_hierarchical_sheet_returns_uuids(tmp_output_dir):
|
|
"""add_hierarchical_sheet should return sheet_uuid and parent_uuid."""
|
|
from mckicad.tools.schematic import add_hierarchical_sheet, create_schematic
|
|
|
|
root_path = os.path.join(tmp_output_dir, "root.kicad_sch")
|
|
create_schematic(name="root_project", output_path=root_path)
|
|
|
|
# Create an empty child schematic
|
|
child_path = os.path.join(tmp_output_dir, "child.kicad_sch")
|
|
create_schematic(name="root_project", output_path=child_path)
|
|
|
|
result = add_hierarchical_sheet(
|
|
schematic_path=root_path,
|
|
name="Power Supply",
|
|
filename="child.kicad_sch",
|
|
x=50, y=50, width=100, height=80,
|
|
)
|
|
|
|
assert result["success"] is True
|
|
assert "sheet_uuid" in result
|
|
assert "parent_uuid" in result
|
|
assert isinstance(result["sheet_uuid"], str)
|
|
assert isinstance(result["parent_uuid"], str)
|
|
assert len(result["sheet_uuid"]) > 0
|
|
assert len(result["parent_uuid"]) > 0
|
|
|
|
|
|
@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
|