Hierarchical KiCad projects store sub-sheets in subdirectories (e.g. sheets/). The flat os.listdir scan missed all of them. Use recursive glob to find .kicad_sch files at any depth under the project directory. Reported by ESP32-P4 project (agent thread message 025) — their 8 malformed property-private entries were all in sheets/ subdirectory.
181 lines
6.7 KiB
Python
181 lines
6.7 KiB
Python
"""Tests for project analysis and validation tools."""
|
|
|
|
import json
|
|
import os
|
|
import textwrap
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestValidateSchematicSexp:
|
|
"""Tests for the _validate_schematic_sexp helper."""
|
|
|
|
def _write_sch(self, tmp_path: str, name: str, content: str) -> str:
|
|
path = os.path.join(tmp_path, name)
|
|
with open(path, "w") as f:
|
|
f.write(content)
|
|
return path
|
|
|
|
def test_clean_schematic_no_issues(self, tmp_output_dir):
|
|
from mckicad.tools.analysis import _validate_schematic_sexp
|
|
|
|
content = textwrap.dedent("""\
|
|
(kicad_sch (version 20231120) (generator "eeschema")
|
|
(lib_symbols
|
|
(symbol "Device:R"
|
|
(property private "KLC_S3.3" "Valid bare keyword"
|
|
(at 0 0 0)
|
|
(effects (font (size 1.27 1.27)) (hide yes))
|
|
)
|
|
(pin passive line (at -3.81 0 0) (length 2.54) (name "1"))
|
|
)
|
|
)
|
|
(symbol (lib_id "Device:R") (at 100 100 0)
|
|
(property "Reference" "R1" (at 0 0 0))
|
|
)
|
|
)
|
|
""")
|
|
path = self._write_sch(tmp_output_dir, "clean.kicad_sch", content)
|
|
issues = _validate_schematic_sexp(path)
|
|
assert issues == []
|
|
|
|
def test_detects_quoted_property_private(self, tmp_output_dir):
|
|
from mckicad.tools.analysis import _validate_schematic_sexp
|
|
|
|
content = textwrap.dedent("""\
|
|
(kicad_sch (version 20231120) (generator "eeschema")
|
|
(lib_symbols
|
|
(symbol "Device:Crystal_GND24"
|
|
(property "private" "KLC_S3.3" The rectangle is not a symbol body
|
|
(at 0 -12.7 0)
|
|
(effects (font (size 1.27 1.27)) (hide yes))
|
|
)
|
|
(property "private" "KLC_S4.1" Some pins are on 50mil grid
|
|
(at 0 -15.24 0)
|
|
(effects (font (size 1.27 1.27)) (hide yes))
|
|
)
|
|
)
|
|
)
|
|
)
|
|
""")
|
|
path = self._write_sch(tmp_output_dir, "malformed.kicad_sch", content)
|
|
issues = _validate_schematic_sexp(path)
|
|
assert len(issues) == 1
|
|
assert 'property "private"' in issues[0]
|
|
assert "2 malformed" in issues[0]
|
|
|
|
def test_detects_missing_lib_symbols(self, tmp_output_dir):
|
|
from mckicad.tools.analysis import _validate_schematic_sexp
|
|
|
|
content = textwrap.dedent("""\
|
|
(kicad_sch (version 20231120) (generator "eeschema")
|
|
(lib_symbols
|
|
(symbol "Device:R"
|
|
(pin passive line (at -3.81 0 0) (length 2.54) (name "1"))
|
|
)
|
|
)
|
|
(symbol (lib_id "Device:R") (at 100 100 0)
|
|
(property "Reference" "R1" (at 0 0 0))
|
|
)
|
|
(symbol (lib_id "Device:C") (at 200 100 0)
|
|
(property "Reference" "C1" (at 0 0 0))
|
|
)
|
|
(symbol (lib_id "MyLib:CustomIC") (at 300 100 0)
|
|
(property "Reference" "U1" (at 0 0 0))
|
|
)
|
|
)
|
|
""")
|
|
path = self._write_sch(tmp_output_dir, "missing_syms.kicad_sch", content)
|
|
issues = _validate_schematic_sexp(path)
|
|
assert len(issues) == 1
|
|
assert "2 lib_id" in issues[0]
|
|
assert "Device:C" in issues[0]
|
|
assert "MyLib:CustomIC" in issues[0]
|
|
|
|
def test_detects_both_issues(self, tmp_output_dir):
|
|
from mckicad.tools.analysis import _validate_schematic_sexp
|
|
|
|
content = textwrap.dedent("""\
|
|
(kicad_sch (version 20231120) (generator "eeschema")
|
|
(lib_symbols
|
|
(symbol "Device:R"
|
|
(property "private" "KLC_S3.3" malformed
|
|
(at 0 0 0)
|
|
(effects (font (size 1.27 1.27)) (hide yes))
|
|
)
|
|
)
|
|
)
|
|
(symbol (lib_id "Device:R") (at 100 100 0))
|
|
(symbol (lib_id "Missing:Symbol") (at 200 100 0))
|
|
)
|
|
""")
|
|
path = self._write_sch(tmp_output_dir, "both.kicad_sch", content)
|
|
issues = _validate_schematic_sexp(path)
|
|
assert len(issues) == 2
|
|
# One for property private, one for missing lib_symbol
|
|
issue_text = " ".join(issues)
|
|
assert "private" in issue_text
|
|
assert "Missing:Symbol" in issue_text
|
|
|
|
def test_nonexistent_file(self):
|
|
from mckicad.tools.analysis import _validate_schematic_sexp
|
|
|
|
issues = _validate_schematic_sexp("/tmp/nonexistent_12345.kicad_sch")
|
|
assert len(issues) == 1
|
|
assert "could not read" in issues[0]
|
|
|
|
def test_no_lib_symbols_section(self, tmp_output_dir):
|
|
from mckicad.tools.analysis import _validate_schematic_sexp
|
|
|
|
content = textwrap.dedent("""\
|
|
(kicad_sch (version 20231120) (generator "eeschema")
|
|
)
|
|
""")
|
|
path = self._write_sch(tmp_output_dir, "empty.kicad_sch", content)
|
|
issues = _validate_schematic_sexp(path)
|
|
assert issues == []
|
|
|
|
def test_subdirectory_sheets_scanned(self, tmp_output_dir):
|
|
"""validate_project scans .kicad_sch files in subdirectories."""
|
|
from mckicad.tools.analysis import validate_project
|
|
|
|
# Create a minimal project structure with a sub-sheet in sheets/
|
|
pro_path = os.path.join(tmp_output_dir, "test.kicad_pro")
|
|
with open(pro_path, "w") as f:
|
|
json.dump({}, f)
|
|
|
|
# Root schematic — clean
|
|
root_sch = os.path.join(tmp_output_dir, "test.kicad_sch")
|
|
with open(root_sch, "w") as f:
|
|
f.write(textwrap.dedent("""\
|
|
(kicad_sch (version 20231120) (generator "eeschema")
|
|
(lib_symbols)
|
|
)
|
|
"""))
|
|
|
|
# Sub-sheet in sheets/ — has malformed property private
|
|
sheets_dir = os.path.join(tmp_output_dir, "sheets")
|
|
os.makedirs(sheets_dir)
|
|
sub_sch = os.path.join(sheets_dir, "sub.kicad_sch")
|
|
with open(sub_sch, "w") as f:
|
|
f.write(textwrap.dedent("""\
|
|
(kicad_sch (version 20231120) (generator "eeschema")
|
|
(lib_symbols
|
|
(symbol "Device:Crystal_GND24"
|
|
(property "private" "KLC_S3.3" malformed
|
|
(at 0 0 0)
|
|
(effects (font (size 1.27 1.27)) (hide yes))
|
|
)
|
|
)
|
|
)
|
|
)
|
|
"""))
|
|
|
|
result = validate_project(pro_path)
|
|
# Should find the malformation in the sub-sheet
|
|
issues = result.get("issues") or []
|
|
sexp_issues = [i for i in issues if "private" in i]
|
|
assert len(sexp_issues) == 1
|
|
assert "sub.kicad_sch" in sexp_issues[0]
|