diff --git a/src/mckicad/tools/analysis.py b/src/mckicad/tools/analysis.py index 1c8e370..6d22ed1 100644 --- a/src/mckicad/tools/analysis.py +++ b/src/mckicad/tools/analysis.py @@ -6,6 +6,7 @@ detail retrieval into a single module. Uses KiCad IPC when available for live data, falling back to file-based checks otherwise. """ +import glob import json import logging import os @@ -188,15 +189,13 @@ def validate_project(project_path: str) -> dict[str, Any]: except Exception as e: issues.append(f"Error reading project file: {e}") - # Validate schematic sexp integrity (all .kicad_sch files in project dir) + # Validate schematic sexp integrity (all .kicad_sch files, including sub-sheets) if "schematic" in files: project_dir = os.path.dirname(project_path) - sch_files = [ - os.path.join(project_dir, f) - for f in os.listdir(project_dir) - if f.endswith(".kicad_sch") - ] - for sch_path in sorted(sch_files): + sch_files = sorted( + glob.glob(os.path.join(project_dir, "**", "*.kicad_sch"), recursive=True) + ) + for sch_path in sch_files: issues.extend(_validate_schematic_sexp(sch_path)) # Optional live analysis via KiCad IPC diff --git a/tests/test_analysis.py b/tests/test_analysis.py index b955798..aad210a 100644 --- a/tests/test_analysis.py +++ b/tests/test_analysis.py @@ -1,5 +1,6 @@ """Tests for project analysis and validation tools.""" +import json import os import textwrap @@ -134,3 +135,46 @@ class TestValidateSchematicSexp: 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]