kicad-mcp/tests/unit/utils/test_file_utils.py
Ryan Malloy bc0f3db97c
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
Implement comprehensive AI/LLM integration for KiCad MCP server
Add intelligent analysis and recommendation tools for KiCad designs:

## New AI Tools (kicad_mcp/tools/ai_tools.py)
- suggest_components_for_circuit: Smart component suggestions based on circuit analysis
- recommend_design_rules: Automated design rule recommendations for different technologies
- optimize_pcb_layout: PCB layout optimization for signal integrity, thermal, and cost
- analyze_design_completeness: Comprehensive design completeness analysis

## Enhanced Utilities
- component_utils.py: Add ComponentType enum and component classification functions
- pattern_recognition.py: Enhanced circuit pattern analysis and recommendations
- netlist_parser.py: Implement missing parse_netlist_file function for AI tools

## Key Features
- Circuit pattern recognition for power supplies, amplifiers, microcontrollers
- Technology-specific design rules (standard, HDI, RF, automotive)
- Layout optimization suggestions with implementation steps
- Component suggestion system with standard values and examples
- Design completeness scoring with actionable recommendations

## Server Integration
- Register AI tools in FastMCP server
- Integrate with existing KiCad utilities and file parsers
- Error handling and graceful fallbacks for missing data

Fixes ImportError that prevented server startup and enables advanced
AI-powered design assistance for KiCad projects.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-11 16:15:58 -06:00

331 lines
13 KiB
Python

"""
Tests for the kicad_mcp.utils.file_utils module.
"""
import json
import os
import tempfile
from unittest.mock import mock_open, patch
from kicad_mcp.utils.file_utils import get_project_files, load_project_json
class TestGetProjectFiles:
"""Test get_project_files function."""
@patch('kicad_mcp.utils.file_utils.get_project_name_from_path')
@patch('os.path.dirname')
@patch('os.path.exists')
@patch('os.listdir')
def test_get_project_files_basic(self, mock_listdir, mock_exists, mock_dirname, mock_get_name):
"""Test basic project file discovery."""
mock_dirname.return_value = "/test/project"
mock_get_name.return_value = "myproject"
mock_exists.side_effect = lambda x: x.endswith(('.kicad_pcb', '.kicad_sch'))
mock_listdir.return_value = ["myproject-bom.csv", "myproject-pos.pos"]
result = get_project_files("/test/project/myproject.kicad_pro")
# Should include project file and detected files
assert result["project"] == "/test/project/myproject.kicad_pro"
assert "pcb" in result or "schematic" in result
assert "bom" in result
assert result["bom"] == "/test/project/myproject-bom.csv"
@patch('kicad_mcp.utils.file_utils.get_project_name_from_path')
@patch('os.path.dirname')
@patch('os.path.exists')
@patch('os.listdir')
def test_get_project_files_with_kicad_extensions(self, mock_listdir, mock_exists, mock_dirname, mock_get_name):
"""Test project file discovery with KiCad extensions."""
mock_dirname.return_value = "/test/project"
mock_get_name.return_value = "test_project"
mock_listdir.return_value = []
# Mock all KiCad extensions as existing
def mock_exists_func(path):
return any(ext in path for ext in ['.kicad_pcb', '.kicad_sch', '.kicad_mod'])
mock_exists.side_effect = mock_exists_func
result = get_project_files("/test/project/test_project.kicad_pro")
assert result["project"] == "/test/project/test_project.kicad_pro"
# Check that KiCad file types are included
expected_types = ["pcb", "schematic", "footprint"]
for file_type in expected_types:
if file_type in result:
assert result[file_type].startswith("/test/project/test_project")
@patch('kicad_mcp.utils.file_utils.get_project_name_from_path')
@patch('os.path.dirname')
@patch('os.path.exists')
@patch('os.listdir')
def test_get_project_files_data_extensions(self, mock_listdir, mock_exists, mock_dirname, mock_get_name):
"""Test discovery of data files with various extensions."""
mock_dirname.return_value = "/test/project"
mock_get_name.return_value = "project"
mock_exists.return_value = False # No KiCad files
mock_listdir.return_value = [
"project-bom.csv",
"project_positions.pos",
"project.net",
"project-gerbers.zip",
"project.drl"
]
result = get_project_files("/test/project/project.kicad_pro")
# Should have project file and data files
assert result["project"] == "/test/project/project.kicad_pro"
assert "bom" in result
assert "positions" in result
assert "net" in result
# Check paths are correct
assert result["bom"] == "/test/project/project-bom.csv"
assert result["positions"] == "/test/project/project_positions.pos"
@patch('kicad_mcp.utils.file_utils.get_project_name_from_path')
@patch('os.path.dirname')
@patch('os.path.exists')
@patch('os.listdir')
def test_get_project_files_directory_access_error(self, mock_listdir, mock_exists, mock_dirname, mock_get_name):
"""Test handling of directory access errors."""
mock_dirname.return_value = "/test/project"
mock_get_name.return_value = "project"
mock_exists.return_value = False
mock_listdir.side_effect = OSError("Permission denied")
result = get_project_files("/test/project/project.kicad_pro")
# Should still return project file
assert result["project"] == "/test/project/project.kicad_pro"
# Should not crash and return basic result
assert len(result) >= 1
@patch('kicad_mcp.utils.file_utils.get_project_name_from_path')
@patch('os.path.dirname')
@patch('os.path.exists')
@patch('os.listdir')
def test_get_project_files_no_matching_files(self, mock_listdir, mock_exists, mock_dirname, mock_get_name):
"""Test when no additional files are found."""
mock_dirname.return_value = "/test/project"
mock_get_name.return_value = "project"
mock_exists.return_value = False
mock_listdir.return_value = ["other_file.txt", "unrelated.csv"]
result = get_project_files("/test/project/project.kicad_pro")
# Should only have the project file
assert result["project"] == "/test/project/project.kicad_pro"
assert len(result) == 1
@patch('kicad_mcp.utils.file_utils.get_project_name_from_path')
@patch('os.path.dirname')
@patch('os.path.exists')
@patch('os.listdir')
def test_get_project_files_filename_parsing(self, mock_listdir, mock_exists, mock_dirname, mock_get_name):
"""Test parsing of different filename patterns."""
mock_dirname.return_value = "/test/project"
mock_get_name.return_value = "myproject"
mock_exists.return_value = False
mock_listdir.return_value = [
"myproject-bom.csv", # dash separator
"myproject_positions.pos", # underscore separator
"myproject.net", # no separator
"myprojectdata.zip" # no separator, should use extension
]
result = get_project_files("/test/project/myproject.kicad_pro")
# Check different parsing results
assert "bom" in result
assert "positions" in result
assert "net" in result
assert "data" in result # "projectdata.zip" becomes "data"
def test_get_project_files_real_directories(self):
"""Test with real temporary directory structure."""
with tempfile.TemporaryDirectory() as temp_dir:
# Create test files
project_path = os.path.join(temp_dir, "test.kicad_pro")
pcb_path = os.path.join(temp_dir, "test.kicad_pcb")
sch_path = os.path.join(temp_dir, "test.kicad_sch")
bom_path = os.path.join(temp_dir, "test-bom.csv")
# Create actual files
for path in [project_path, pcb_path, sch_path, bom_path]:
with open(path, 'w') as f:
f.write("test content")
result = get_project_files(project_path)
# Should find all files
assert result["project"] == project_path
assert result["pcb"] == pcb_path
assert result["schematic"] == sch_path
assert result["bom"] == bom_path
class TestLoadProjectJson:
"""Test load_project_json function."""
def test_load_project_json_success(self):
"""Test successful JSON loading."""
test_data = {"version": 1, "board": {"thickness": 1.6}}
json_content = json.dumps(test_data)
with patch('builtins.open', mock_open(read_data=json_content)):
result = load_project_json("/test/project.kicad_pro")
assert result == test_data
assert result["version"] == 1
assert result["board"]["thickness"] == 1.6
def test_load_project_json_file_not_found(self):
"""Test handling of missing file."""
with patch('builtins.open', side_effect=FileNotFoundError("File not found")):
result = load_project_json("/nonexistent/project.kicad_pro")
assert result is None
def test_load_project_json_invalid_json(self):
"""Test handling of invalid JSON."""
invalid_json = '{"version": 1, "incomplete":'
with patch('builtins.open', mock_open(read_data=invalid_json)):
result = load_project_json("/test/project.kicad_pro")
assert result is None
def test_load_project_json_empty_file(self):
"""Test handling of empty file."""
with patch('builtins.open', mock_open(read_data="")):
result = load_project_json("/test/project.kicad_pro")
assert result is None
def test_load_project_json_permission_error(self):
"""Test handling of permission errors."""
with patch('builtins.open', side_effect=PermissionError("Permission denied")):
result = load_project_json("/test/project.kicad_pro")
assert result is None
def test_load_project_json_complex_data(self):
"""Test loading complex JSON data."""
complex_data = {
"version": 1,
"board": {
"thickness": 1.6,
"layers": [
{"name": "F.Cu", "type": "copper"},
{"name": "B.Cu", "type": "copper"}
]
},
"nets": [
{"name": "GND", "priority": 1},
{"name": "VCC", "priority": 2}
],
"rules": {
"trace_width": 0.25,
"via_drill": 0.4
}
}
json_content = json.dumps(complex_data)
with patch('builtins.open', mock_open(read_data=json_content)):
result = load_project_json("/test/project.kicad_pro")
assert result == complex_data
assert len(result["board"]["layers"]) == 2
assert len(result["nets"]) == 2
assert result["rules"]["trace_width"] == 0.25
def test_load_project_json_unicode_content(self):
"""Test loading JSON with Unicode content."""
unicode_data = {
"version": 1,
"title": "测试项目", # Chinese characters
"author": "José María" # Accented characters
}
json_content = json.dumps(unicode_data, ensure_ascii=False)
with patch('builtins.open', mock_open(read_data=json_content)) as mock_file:
mock_file.return_value.__enter__.return_value.read.return_value = json_content
result = load_project_json("/test/project.kicad_pro")
assert result == unicode_data
assert result["title"] == "测试项目"
assert result["author"] == "José María"
def test_load_project_json_real_file(self):
"""Test with real temporary file."""
test_data = {"version": 1, "test": True}
with tempfile.NamedTemporaryFile(mode='w', suffix='.kicad_pro', delete=False) as temp_file:
json.dump(test_data, temp_file)
temp_file.flush()
try:
result = load_project_json(temp_file.name)
assert result == test_data
finally:
os.unlink(temp_file.name)
class TestIntegration:
"""Integration tests combining both functions."""
def test_project_files_and_json_loading(self):
"""Test combining project file discovery and JSON loading."""
with tempfile.TemporaryDirectory() as temp_dir:
# Create project structure
project_path = os.path.join(temp_dir, "integration_test.kicad_pro")
pcb_path = os.path.join(temp_dir, "integration_test.kicad_pcb")
# Create project JSON file
project_data = {
"version": 1,
"board": {"thickness": 1.6},
"nets": []
}
with open(project_path, 'w') as f:
json.dump(project_data, f)
# Create PCB file
with open(pcb_path, 'w') as f:
f.write("PCB content")
# Test file discovery
files = get_project_files(project_path)
assert files["project"] == project_path
assert files["pcb"] == pcb_path
# Test JSON loading
json_data = load_project_json(project_path)
assert json_data == project_data
assert json_data["board"]["thickness"] == 1.6
@patch('kicad_mcp.utils.file_utils.get_project_name_from_path')
def test_project_name_integration(self, mock_get_name):
"""Test integration with get_project_name_from_path function."""
mock_get_name.return_value = "custom_name"
with tempfile.TemporaryDirectory() as temp_dir:
project_path = os.path.join(temp_dir, "actual_file.kicad_pro")
custom_pcb = os.path.join(temp_dir, "custom_name.kicad_pcb")
# Create files with custom naming
with open(project_path, 'w') as f:
f.write('{"version": 1}')
with open(custom_pcb, 'w') as f:
f.write("PCB content")
files = get_project_files(project_path)
# Should use the mocked project name
mock_get_name.assert_called_once_with(project_path)
assert files["project"] == project_path
assert files["pcb"] == custom_pcb