Skip to main content
Glama
johannhartmann

MCP Code Analysis Server

test_dependency_tools.py20.8 kB
"""Tests for dependency analysis tools.""" from __future__ import annotations from unittest.mock import AsyncMock, MagicMock, patch import pytest from fastmcp import FastMCP from sqlalchemy.ext.asyncio import AsyncSession from src.database.models import File, Import, Module from src.mcp_server.tools.analysis_tools import AnalysisTools @pytest.fixture def mock_db_session() -> AsyncSession: """Create mock database session.""" return AsyncMock(spec=AsyncSession) @pytest.fixture def mock_mcp() -> FastMCP: """Create mock FastMCP instance.""" mcp = MagicMock(spec=FastMCP) mcp.tool = MagicMock(side_effect=lambda **kwargs: lambda func: func) return mcp @pytest.fixture def analysis_tools(mock_db_session: AsyncSession, mock_mcp: FastMCP) -> AnalysisTools: """Create analysis tools fixture.""" with patch("src.mcp_server.tools.analysis_tools.settings") as mock_settings: mock_settings.openai_api_key.get_secret_value.return_value = "test-key" return AnalysisTools(mock_db_session, mock_mcp) class TestDependencyTools: """Tests for dependency analysis tools.""" @pytest.mark.asyncio async def test_analyze_dependencies_file_not_found( self, analysis_tools: AnalysisTools, mock_db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test analyzing dependencies when file is not found.""" mock_result = MagicMock() mock_result.scalar_one_or_none.return_value = None monkeypatch.setattr( mock_db_session, "execute", AsyncMock(return_value=mock_result), ) result = await analysis_tools.analyze_dependencies("nonexistent.py") assert result["error"] == "File not found: nonexistent.py" @pytest.mark.asyncio async def test_analyze_dependencies_with_imports( self, analysis_tools: AnalysisTools, mock_db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test analyzing dependencies with various import types.""" # Mock file mock_file = MagicMock(spec=File) mock_file.id = 1 mock_file.path = "/src/utils/helpers.py" mock_file.repository_id = 10 file_result = MagicMock() file_result.scalar_one_or_none.return_value = mock_file # Mock imports imports = [] import_data = [ ("os", None, "os", True, False), ("json", None, "json", True, False), ("typing", "List, Dict", "typing", True, False), ("numpy", None, "np", False, False), ("src.models", "User, Product", "src.models", False, True), ("src.database", None, "db", False, True), ("..common", "utils", "..common", False, True), ] for _i, (module, names, alias, is_stdlib, is_local) in enumerate(import_data): imp = MagicMock(spec=Import) imp.module_name = module imp.imported_names = names imp.alias = alias if alias != module else None imp.is_stdlib = is_stdlib imp.is_local = is_local imports.append(imp) imports_result = MagicMock() imports_result.scalars.return_value.all.return_value = imports # Mock module resolution for local imports mock_module1 = MagicMock(spec=Module) mock_module1.name = "src.models" mock_module1.file_id = 20 mock_module2 = MagicMock(spec=Module) mock_module2.name = "src.database" mock_module2.file_id = 30 module_results = [ MagicMock(scalar_one_or_none=lambda: mock_module1), MagicMock(scalar_one_or_none=lambda: mock_module2), MagicMock(scalar_one_or_none=lambda: None), # ..common not found ] # Mock files for resolved modules mock_file1 = MagicMock(spec=File) mock_file1.path = "/src/models.py" mock_file2 = MagicMock(spec=File) mock_file2.path = "/src/database.py" file_results = [ MagicMock(scalar_one_or_none=lambda: mock_file1), MagicMock(scalar_one_or_none=lambda: mock_file2), ] # Setup mock sequence monkeypatch.setattr( mock_db_session, "execute", AsyncMock( side_effect=[ file_result, imports_result, *module_results, *file_results, ] ), ) result = await analysis_tools.analyze_dependencies("/src/utils/helpers.py") assert result["file"] == "/src/utils/helpers.py" assert result["total_imports"] == 7 assert result["stdlib_imports"] == 3 assert result["third_party_imports"] == 1 assert result["local_imports"] == 3 # Check categorized imports assert len(result["imports"]["stdlib"]) == 3 assert "os" in result["imports"]["stdlib"] assert "typing (List, Dict)" in result["imports"]["stdlib"] assert len(result["imports"]["third_party"]) == 1 assert "numpy as np" in result["imports"]["third_party"] assert len(result["imports"]["local"]) == 3 # Check resolved dependencies assert len(result["resolved_dependencies"]) == 2 assert any(d["module"] == "src.models" for d in result["resolved_dependencies"]) assert any( d["file"] == "/src/database.py" for d in result["resolved_dependencies"] ) # Check unresolved dependencies assert len(result["unresolved_dependencies"]) == 1 assert result["unresolved_dependencies"][0] == "..common (utils)" @pytest.mark.asyncio async def test_analyze_dependencies_module_only( self, analysis_tools: AnalysisTools, mock_db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test analyzing dependencies for a module file.""" # Mock module file mock_file = MagicMock(spec=File) mock_file.id = 1 mock_file.path = "/src/services/__init__.py" mock_file.repository_id = 10 file_result = MagicMock() file_result.scalar_one_or_none.return_value = mock_file # Mock module mock_module = MagicMock(spec=Module) mock_module.name = "src.services" mock_module.file_id = 1 module_result = MagicMock() module_result.scalar_one_or_none.return_value = mock_module # Mock imports from all files in the module module_imports = [] for _i, (imp_module, is_local) in enumerate( [ ("logging", False), ("src.models", True), ("src.utils", True), ("requests", False), ] ): imp = MagicMock(spec=Import) imp.module_name = imp_module imp.imported_names = None imp.alias = None imp.is_stdlib = imp_module == "logging" imp.is_local = is_local module_imports.append(imp) imports_result = MagicMock() imports_result.scalars.return_value.all.return_value = module_imports # Mock resolved modules mock_models = MagicMock(spec=Module) mock_models.name = "src.models" mock_models.file_id = 20 mock_utils = MagicMock(spec=Module) mock_utils.name = "src.utils" mock_utils.file_id = 30 module_resolutions = [ MagicMock(scalar_one_or_none=lambda: mock_models), MagicMock(scalar_one_or_none=lambda: mock_utils), ] # Mock files for resolved modules mock_models_file = MagicMock(spec=File) mock_models_file.path = "/src/models.py" mock_utils_file = MagicMock(spec=File) mock_utils_file.path = "/src/utils.py" file_resolutions = [ MagicMock(scalar_one_or_none=lambda: mock_models_file), MagicMock(scalar_one_or_none=lambda: mock_utils_file), ] monkeypatch.setattr( mock_db_session, "execute", AsyncMock( side_effect=[ file_result, module_result, imports_result, *module_resolutions, *file_resolutions, ] ), ) result = await analysis_tools.analyze_dependencies("/src/services/__init__.py") assert result["file"] == "/src/services/__init__.py" assert result["module"] == "src.services" assert result["total_imports"] == 4 assert result["stdlib_imports"] == 1 assert result["third_party_imports"] == 1 assert result["local_imports"] == 2 @pytest.mark.asyncio async def test_find_circular_dependencies_none_found( self, analysis_tools: AnalysisTools, mock_db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test finding circular dependencies when none exist.""" # Mock files in repository files = [] for i, path in enumerate( ["/src/models.py", "/src/views.py", "/src/controllers.py"] ): f = MagicMock(spec=File) f.id = i + 1 f.path = path files.append(f) files_result = MagicMock() files_result.scalars.return_value.all.return_value = files # Mock imports (no cycles) # models.py imports nothing # views.py imports models # controllers.py imports models and views import_results = [ MagicMock(scalars=lambda: MagicMock(all=list)), # models imports MagicMock( scalars=lambda: MagicMock( all=lambda: [ MagicMock( module_name="src.models", is_local=True, imported_file_id=1 ) ] ) ), # views imports MagicMock( scalars=lambda: MagicMock( all=lambda: [ MagicMock( module_name="src.models", is_local=True, imported_file_id=1 ), MagicMock( module_name="src.views", is_local=True, imported_file_id=2 ), ] ) ), # controllers imports ] monkeypatch.setattr( mock_db_session, "execute", AsyncMock(side_effect=[files_result, *import_results]), ) result = await analysis_tools.find_circular_dependencies(repository_id=10) assert result["repository_id"] == 10 assert result["circular_dependencies"] == [] assert result["files_analyzed"] == 3 @pytest.mark.asyncio async def test_find_circular_dependencies_with_cycles( self, analysis_tools: AnalysisTools, mock_db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test finding circular dependencies with multiple cycles.""" # Mock files files = [] file_paths = [ "/src/auth/user.py", "/src/auth/permissions.py", "/src/models/order.py", "/src/models/product.py", "/src/services/pricing.py", ] for i, path in enumerate(file_paths): f = MagicMock(spec=File) f.id = i + 1 f.path = path files.append(f) files_result = MagicMock() files_result.scalars.return_value.all.return_value = files # Create circular dependencies: # Cycle 1: user.py -> permissions.py -> user.py # Cycle 2: order.py -> product.py -> pricing.py -> order.py def create_import(module_name: str, file_id: int) -> Import: imp = MagicMock(spec=Import) imp.module_name = module_name imp.is_local = True imp.imported_file_id = file_id return imp import_results = [ # user.py imports permissions MagicMock( scalars=lambda: MagicMock( all=lambda: [create_import("src.auth.permissions", 2)] ) ), # permissions.py imports user (cycle!) MagicMock( scalars=lambda: MagicMock( all=lambda: [create_import("src.auth.user", 1)] ) ), # order.py imports product MagicMock( scalars=lambda: MagicMock( all=lambda: [create_import("src.models.product", 4)] ) ), # product.py imports pricing MagicMock( scalars=lambda: MagicMock( all=lambda: [create_import("src.services.pricing", 5)] ) ), # pricing.py imports order (cycle!) MagicMock( scalars=lambda: MagicMock( all=lambda: [create_import("src.models.order", 3)] ) ), ] monkeypatch.setattr( mock_db_session, "execute", AsyncMock(side_effect=[files_result, *import_results]), ) result = await analysis_tools.find_circular_dependencies(repository_id=10) assert result["repository_id"] == 10 assert len(result["circular_dependencies"]) == 2 assert result["files_analyzed"] == 5 # Check first cycle cycle1 = next( c for c in result["circular_dependencies"] if "/src/auth/user.py" in c["cycle"] ) assert len(cycle1["cycle"]) == 3 # user -> permissions -> user assert cycle1["cycle"][0] == cycle1["cycle"][-1] # Cycle closes # Check second cycle cycle2 = next( c for c in result["circular_dependencies"] if "/src/models/order.py" in c["cycle"] ) assert len(cycle2["cycle"]) == 4 # order -> product -> pricing -> order assert "/src/services/pricing.py" in cycle2["cycle"] @pytest.mark.asyncio async def test_analyze_import_graph_complex( self, analysis_tools: AnalysisTools, mock_db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test analyzing complex import graph with metrics.""" # Mock repository files files = [] for i in range(10): f = MagicMock(spec=File) f.id = i + 1 f.path = f"/src/module{i}.py" files.append(f) files_result = MagicMock() files_result.scalars.return_value.all.return_value = files # Create import graph: # module0 is imported by everyone (core module) # module1-3 import module0 and each other # module4-6 form isolated group # module7-9 have no imports import_patterns = { 0: [], # Core module, imports nothing 1: [0, 2], # Imports core and module2 2: [0, 1, 3], # Imports core, module1, and module3 3: [0, 2], # Imports core and module2 4: [5, 6], # Isolated group 5: [4, 6], 6: [4, 5], 7: [], # No imports 8: [], 9: [], } import_results = [] for imported_ids in import_patterns.values(): imports = [ MagicMock( spec=Import, module_name=f"src.module{imp_id}", is_local=True, imported_file_id=imp_id + 1, ) for imp_id in imported_ids ] import_results.append( MagicMock( scalars=lambda imports=imports: MagicMock(all=lambda: imports) ) ) def create_import(module_name: str, file_id: int) -> Import: imp = MagicMock(spec=Import) imp.module_name = module_name imp.is_local = True imp.imported_file_id = file_id return imp monkeypatch.setattr( mock_db_session, "execute", AsyncMock(side_effect=[files_result, *import_results]), ) result = await analysis_tools.analyze_import_graph(repository_id=10) assert result["repository_id"] == 10 assert result["total_files"] == 10 assert result["total_local_imports"] > 0 # Check most imported (should be module0) assert len(result["most_imported_files"]) > 0 assert "/src/module0.py" in [f["file"] for f in result["most_imported_files"]] # Check most importing (should include module2 with 3 imports) assert len(result["most_importing_files"]) > 0 # Check isolated files (module7-9) assert result["isolated_files"] >= 3 @pytest.mark.asyncio async def test_analyze_dependencies_with_relative_imports( self, analysis_tools: AnalysisTools, mock_db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test analyzing dependencies with complex relative imports.""" # Mock file in nested structure mock_file = MagicMock(spec=File) mock_file.id = 1 mock_file.path = "/src/services/auth/handlers.py" mock_file.repository_id = 10 file_result = MagicMock() file_result.scalar_one_or_none.return_value = mock_file # Mock various relative imports imports = [] relative_imports = [ (".", "validators", "current package import"), ("..", "utils", "parent package import"), ("..models", "User, Role", "cousin package import"), ("...common", "constants", "grandparent package import"), ] for module, names, _desc in relative_imports: imp = MagicMock(spec=Import) imp.module_name = module imp.imported_names = names imp.alias = None imp.is_stdlib = False imp.is_local = True imports.append(imp) imports_result = MagicMock() imports_result.scalars.return_value.all.return_value = imports # Mock resolved modules (some found, some not) mock_validators = MagicMock(spec=Module) mock_validators.name = "src.services.auth.validators" mock_validators.file_id = 20 mock_utils = MagicMock(spec=Module) mock_utils.name = "src.services.utils" mock_utils.file_id = 30 module_results = [ MagicMock(scalar_one_or_none=lambda: mock_validators), MagicMock(scalar_one_or_none=lambda: mock_utils), MagicMock(scalar_one_or_none=lambda: None), # ..models not found MagicMock(scalar_one_or_none=lambda: None), # ...common not found ] # Mock files for resolved modules mock_validators_file = MagicMock(spec=File) mock_validators_file.path = "/src/services/auth/validators.py" mock_utils_file = MagicMock(spec=File) mock_utils_file.path = "/src/services/utils.py" file_results = [ MagicMock(scalar_one_or_none=lambda: mock_validators_file), MagicMock(scalar_one_or_none=lambda: mock_utils_file), ] monkeypatch.setattr( mock_db_session, "execute", AsyncMock( side_effect=[ file_result, imports_result, *module_results, *file_results, ] ), ) result = await analysis_tools.analyze_dependencies( "/src/services/auth/handlers.py" ) assert result["total_imports"] == 4 assert result["local_imports"] == 4 # Check resolved relative imports assert len(result["resolved_dependencies"]) == 2 resolved_modules = [d["module"] for d in result["resolved_dependencies"]] assert "src.services.auth.validators" in resolved_modules assert "src.services.utils" in resolved_modules # Check unresolved relative imports assert len(result["unresolved_dependencies"]) == 2 assert any("..models" in dep for dep in result["unresolved_dependencies"]) assert any("...common" in dep for dep in result["unresolved_dependencies"])

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/johannhartmann/mcpcodeanalysis'

If you have feedback or need assistance with the MCP directory API, please join our Discord server