""" Tests for the kicad_mcp.utils.file_utils module. """ import json import os import tempfile from unittest.mock import Mock, patch, mock_open import pytest 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