kicad-mcp/tests/test_analysis.py
Ryan Malloy 56705cf345 Add schematic sexp validation and export_pdf working_dir fix
validate_project now scans all .kicad_sch files for two classes of
silent rendering failure: quoted (property "private" ...) that should
be a bare keyword in KiCad 9, and lib_id references with no matching
lib_symbols entry. Both cause kicad-cli to blank entire pages during
export without any error message.

Also pass working_dir to run_kicad_command in export_pdf for robust
library path resolution.

Addresses feedback from ESP32-P4 project (agent thread message 021).
2026-03-05 10:46:47 -07:00

137 lines
5.0 KiB
Python

"""Tests for project analysis and validation tools."""
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 == []