"""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 == []