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
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>
331 lines
13 KiB
Python
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
|