Skip to main content
Glama
test_main_server.py25.3 kB
"""Comprehensive tests for main.py MCP server functionality.""" import asyncio from datetime import datetime from pathlib import Path import pytest from autodoc_mcp.exceptions import ( PackageNotFoundError, ProjectParsingError, ) from autodoc_mcp.models import CacheEntry, PackageInfo, ScanResult class TestMCPTools: """Comprehensive MCP tool testing.""" @pytest.mark.asyncio async def test_scan_dependencies_success(self, mocker): """Test successful dependency scanning.""" # Setup mock parser response mock_result = ScanResult( project_path=Path("/test/project"), dependencies=[], project_name="test-project", scan_timestamp=datetime.now(), successful_deps=3, failed_deps=[], ) # Use mocker instead of unittest.mock mock_parser = mocker.AsyncMock() mock_parser.parse_project.return_value = mock_result mock_formatter = mocker.patch("autodoc_mcp.main.ResponseFormatter") mock_validator = mocker.patch( "autodoc_mcp.main.InputValidator.validate_project_path" ) mocker.patch("autodoc_mcp.main.parser", mock_parser) mock_formatter.format_scan_response.return_value = { "success": True, "dependency_count": 3, } mock_validator.return_value = Path("/test/project") # Import and call the function directly from autodoc_mcp.main import scan_dependencies result = await scan_dependencies.fn("/test/project") assert result["success"] is True assert result["dependency_count"] == 3 mock_parser.parse_project.assert_called_once() @pytest.mark.asyncio async def test_scan_dependencies_parser_not_initialized(self, mocker): """Test scan_dependencies when parser is None.""" from autodoc_mcp.main import scan_dependencies mocker.patch("autodoc_mcp.main.parser", None) result = await scan_dependencies.fn("/test/project") assert result["success"] is False assert result["error"]["code"] == "service_not_initialized" assert "Parser not initialized" in result["error"]["message"] assert result["error"]["recoverable"] is False @pytest.mark.asyncio async def test_scan_dependencies_project_parsing_error(self, mock_services, mocker): """Test scan_dependencies with ProjectParsingError.""" from autodoc_mcp.main import scan_dependencies mock_services["parser"].parse_project.side_effect = ProjectParsingError( "Invalid project", Path("/test") ) # Need to mock InputValidator as well mocker.patch( "autodoc_mcp.main.InputValidator.validate_project_path", return_value=Path("/test/project"), ) mock_formatter = mocker.patch("autodoc_mcp.main.ErrorFormatter") mock_error = mocker.MagicMock() mock_error.message = "Invalid project structure" mock_error.suggestion = "Check pyproject.toml" mock_error.severity.value = "critical" mock_error.error_code = "project_parse_error" mock_error.recoverable = False mock_formatter.format_exception.return_value = mock_error result = await scan_dependencies.fn("/test/project") assert result["success"] is False assert "Invalid project structure" in result["error"]["message"] @pytest.mark.asyncio async def test_scan_dependencies_generic_error(self, mock_services, mocker): """Test scan_dependencies with generic exception.""" from autodoc_mcp.main import scan_dependencies mock_services["parser"].parse_project.side_effect = ValueError("Generic error") mock_formatter = mocker.patch("autodoc_mcp.main.ErrorFormatter") mock_error = mocker.MagicMock() mock_error.message = "Unexpected error occurred" mock_error.suggestion = "Try again" mock_error.severity.value = "error" mock_error.error_code = "unexpected_error" mock_error.recoverable = True mock_formatter.format_exception.return_value = mock_error result = await scan_dependencies.fn("/test/project") assert result["success"] is False assert result["error"]["code"] == "unexpected_error" @pytest.mark.asyncio async def test_get_package_docs_success(self, mock_services, mocker): """Test successful package documentation retrieval.""" from autodoc_mcp.main import get_package_docs # Mock services mock_services["version_resolver"].resolve_version.return_value = "2.28.2" mock_services[ "version_resolver" ].generate_cache_key.return_value = "requests-2.28.2" # Mock cache miss, fresh fetch mock_services["cache_manager"].get.return_value = None mock_services["cache_manager"].set.return_value = None # Mock package info mock_package_info = PackageInfo( name="requests", version="2.28.2", summary="HTTP library", description="Python HTTP library", author="Kenneth Reitz", author_email="me@kennethreitz.org", home_page="https://requests.readthedocs.io", package_url="https://pypi.org/project/requests/", project_urls={}, classifiers=[], keywords=[], license="Apache 2.0", ) mock_fetcher_class = mocker.patch("autodoc_mcp.main.PyPIDocumentationFetcher") mock_validator = mocker.patch("autodoc_mcp.main.InputValidator") # Create a mock fetcher that works for both context manager uses mock_fetcher = mocker.MagicMock() mock_fetcher.fetch_package_info = mocker.AsyncMock( return_value=mock_package_info ) mock_fetcher.format_documentation.return_value = "# requests v2.28.2\n..." # Mock the context manager async_context_manager = mocker.MagicMock() async_context_manager.__aenter__ = mocker.AsyncMock(return_value=mock_fetcher) async_context_manager.__aexit__ = mocker.AsyncMock(return_value=None) mock_fetcher_class.return_value = async_context_manager mock_validator.validate_package_name.return_value = "requests" mock_validator.validate_version_constraint.return_value = ">=2.0.0" result = await get_package_docs.fn("requests", ">=2.0.0", None) assert result["success"] is True assert result["package_name"] == "requests" assert result["version"] == "2.28.2" assert result["from_cache"] is False assert "documentation" in result @pytest.mark.asyncio async def test_get_package_docs_cache_hit(self, mock_services, mocker): """Test package docs retrieval with cache hit.""" from autodoc_mcp.main import get_package_docs mock_services["version_resolver"].resolve_version.return_value = "2.28.2" mock_services[ "version_resolver" ].generate_cache_key.return_value = "requests-2.28.2" # Mock cache hit mock_package_info = PackageInfo( name="requests", version="2.28.2", summary="HTTP library", description="Python HTTP library", author="Kenneth Reitz", author_email="me@kennethreitz.org", home_page="https://requests.readthedocs.io", package_url="https://pypi.org/project/requests/", project_urls={}, classifiers=[], keywords=[], license="Apache 2.0", ) mock_cache_entry = CacheEntry( data=mock_package_info, timestamp=datetime.fromtimestamp(1234567890.0), version="2.28.2", ) mock_services["cache_manager"].get.return_value = mock_cache_entry mock_fetcher_class = mocker.patch("autodoc_mcp.main.PyPIDocumentationFetcher") mock_validator = mocker.patch("autodoc_mcp.main.InputValidator") # Create a mock fetcher that works for context manager mock_fetcher = mocker.MagicMock() mock_fetcher.format_documentation.return_value = "# requests v2.28.2\n..." # Mock the context manager async_context_manager = mocker.MagicMock() async_context_manager.__aenter__ = mocker.AsyncMock(return_value=mock_fetcher) async_context_manager.__aexit__ = mocker.AsyncMock(return_value=None) mock_fetcher_class.return_value = async_context_manager mock_validator.validate_package_name.return_value = "requests" result = await get_package_docs.fn("requests", None, None) assert result["success"] is True assert result["from_cache"] is True # Should not call set when cache hit mock_services["cache_manager"].set.assert_not_called() @pytest.mark.asyncio async def test_get_package_docs_services_not_initialized(self, mocker): """Test get_package_docs when services are None.""" from autodoc_mcp.main import get_package_docs mocker.patch("autodoc_mcp.main.cache_manager", None) mocker.patch("autodoc_mcp.main.version_resolver", None) result = await get_package_docs.fn("requests") assert result["success"] is False assert result["error"]["code"] == "service_not_initialized" @pytest.mark.asyncio async def test_get_package_docs_package_not_found(self, mock_services, mocker): """Test get_package_docs with PackageNotFoundError.""" from autodoc_mcp.main import get_package_docs mock_services[ "version_resolver" ].resolve_version.side_effect = PackageNotFoundError("Package not found") mock_validator = mocker.patch("autodoc_mcp.main.InputValidator") mock_formatter = mocker.patch("autodoc_mcp.main.ErrorFormatter") mock_validator.validate_package_name.return_value = "nonexistent" mock_error = mocker.MagicMock() mock_error.message = "Package 'nonexistent' not found on PyPI" mock_error.suggestion = "Check package name spelling" mock_error.severity.value = "error" mock_error.error_code = "package_not_found" mock_error.recoverable = True mock_formatter.format_exception.return_value = mock_error result = await get_package_docs.fn("nonexistent") assert result["success"] is False assert result["error"]["code"] == "package_not_found" @pytest.mark.asyncio async def test_get_package_docs_with_context_success(self, mock_services, mocker): """Test successful context documentation retrieval.""" from autodoc_mcp.main import get_package_docs_with_context # Mock context fetcher mock_context = mocker.MagicMock() mock_context.primary_package.model_dump.return_value = { "name": "requests", "version": "2.28.2", } mock_context.runtime_dependencies = [] mock_context.dev_dependencies = [] mock_context.context_scope = "smart" mock_context.total_packages = 1 mock_context.truncated_packages = 0 mock_context.token_estimate = 1000 mock_performance = {"fetch_time": 1.5, "cache_hits": 0} mock_services["context_fetcher"].fetch_package_context.return_value = ( mock_context, mock_performance, ) mock_validator = mocker.patch("autodoc_mcp.main.InputValidator") mock_validator.validate_package_name.return_value = "requests" result = await get_package_docs_with_context.fn("requests") assert result["success"] is True assert result["context"]["primary_package"]["name"] == "requests" assert result["performance"]["fetch_time"] == 1.5 @pytest.mark.asyncio async def test_get_package_docs_with_context_not_initialized(self, mocker): """Test get_package_docs_with_context when context_fetcher is None.""" from autodoc_mcp.main import get_package_docs_with_context mocker.patch("autodoc_mcp.main.context_fetcher", None) result = await get_package_docs_with_context.fn("requests") assert result["success"] is False assert "Context fetcher not initialized" in result["error"]["message"] @pytest.mark.asyncio async def test_refresh_cache_success(self, mock_services): """Test successful cache refresh.""" from autodoc_mcp.main import refresh_cache # Mock cache stats initial_stats = {"total_entries": 5, "total_size_bytes": 50000} final_stats = {"total_entries": 0, "total_size_bytes": 0} mock_services["cache_manager"].get_cache_stats.side_effect = [ initial_stats, final_stats, ] mock_services["cache_manager"].invalidate.return_value = None result = await refresh_cache.fn() assert result["success"] is True assert result["cleared_entries"] == 5 assert result["freed_bytes"] == 50000 assert result["final_entries"] == 0 @pytest.mark.asyncio async def test_refresh_cache_not_initialized(self, mocker): """Test refresh_cache when cache_manager is None.""" from autodoc_mcp.main import refresh_cache mocker.patch("autodoc_mcp.main.cache_manager", None) result = await refresh_cache.fn() assert result["success"] is False assert result["error"]["code"] == "service_not_initialized" @pytest.mark.asyncio async def test_get_cache_stats_success(self, mock_services): """Test successful cache statistics retrieval.""" from autodoc_mcp.main import get_cache_stats mock_stats = { "total_entries": 10, "total_size_bytes": 100000, "cache_hit_rate": 85.5, } mock_packages = ["requests-2.28.2", "pydantic-1.10.0", "fastapi-0.95.0"] mock_services["cache_manager"].get_cache_stats.return_value = mock_stats mock_services["cache_manager"].list_cached_packages.return_value = mock_packages result = await get_cache_stats.fn() assert result["success"] is True assert result["cache_stats"] == mock_stats assert result["cached_packages"] == mock_packages assert result["total_packages"] == 3 @pytest.mark.asyncio async def test_get_cache_stats_not_initialized(self, mocker): """Test get_cache_stats when cache_manager is None.""" from autodoc_mcp.main import get_cache_stats mocker.patch("autodoc_mcp.main.cache_manager", None) result = await get_cache_stats.fn() assert result["success"] is False assert result["error"]["code"] == "service_not_initialized" @pytest.mark.asyncio async def test_get_cache_stats_error_handling(self, mock_services, mocker): """Test get_cache_stats with exception handling.""" from autodoc_mcp.main import get_cache_stats mock_services["cache_manager"].get_cache_stats.side_effect = Exception( "Cache error" ) mock_formatter = mocker.patch("autodoc_mcp.main.ErrorFormatter") mock_error = mocker.MagicMock() mock_error.message = "Failed to retrieve cache statistics" mock_error.suggestion = "Check cache directory permissions" mock_error.severity.value = "error" mock_error.error_code = "cache_stats_error" mock_error.recoverable = True mock_formatter.format_exception.return_value = mock_error result = await get_cache_stats.fn() assert result["success"] is False assert result["error"]["code"] == "cache_stats_error" class TestServiceInitialization: """Test service initialization and error handling.""" @pytest.mark.asyncio async def test_initialize_services_success(self, mocker): """Test successful service initialization.""" from autodoc_mcp.main import initialize_services mock_config = mocker.patch("autodoc_mcp.main.get_config") mocker.patch("autodoc_mcp.main.PyProjectParser") mock_cache_class = mocker.patch("autodoc_mcp.main.FileCacheManager") mocker.patch("autodoc_mcp.main.VersionResolver") mock_create_context = mocker.patch("autodoc_mcp.main.create_context_fetcher") mock_config.return_value.cache_dir = Path("/tmp/cache") mock_cache_manager = mocker.AsyncMock() mock_cache_manager.initialize.return_value = None mock_cache_class.return_value = mock_cache_manager mock_context_fetcher = mocker.AsyncMock() mock_create_context.return_value = mock_context_fetcher await initialize_services() mock_cache_manager.initialize.assert_called_once() mock_create_context.assert_called_once_with(mock_cache_manager) @pytest.mark.asyncio async def test_initialize_services_cache_failure(self, mocker): """Test service initialization with cache manager failure.""" from autodoc_mcp.main import initialize_services mock_config = mocker.patch("autodoc_mcp.main.get_config") mocker.patch("autodoc_mcp.main.PyProjectParser") mock_cache_class = mocker.patch("autodoc_mcp.main.FileCacheManager") mocker.patch("autodoc_mcp.main.VersionResolver") mock_config.return_value.cache_dir = Path("/tmp/cache") mock_cache_manager = mocker.AsyncMock() mock_cache_manager.initialize.side_effect = Exception("Cache init failed") mock_cache_class.return_value = mock_cache_manager with pytest.raises(Exception, match="Cache init failed"): await initialize_services() class TestGracefulShutdown: """Test graceful shutdown functionality.""" @pytest.mark.asyncio async def test_graceful_shutdown_no_active_requests(self, mocker): """Test graceful shutdown with no active requests.""" from autodoc_mcp.main import GracefulShutdown shutdown_handler = GracefulShutdown() shutdown_handler.active_requests = 0 mock_cleanup = mocker.patch.object(shutdown_handler, "_cleanup_resources") mock_cleanup.return_value = None shutdown_handler.shutdown_event.set() await shutdown_handler.wait_for_shutdown() mock_cleanup.assert_called_once() @pytest.mark.asyncio async def test_graceful_shutdown_with_active_requests(self, mocker): """Test graceful shutdown waits for active requests.""" from autodoc_mcp.main import GracefulShutdown shutdown_handler = GracefulShutdown() shutdown_handler.active_requests = 2 async def mock_requests_complete(): await asyncio.sleep(0.1) # Simulate request completion shutdown_handler.active_requests = 0 mock_cleanup = mocker.patch.object( shutdown_handler, "_cleanup_resources", return_value=None ) shutdown_handler.shutdown_event.set() # Start background task to simulate requests completing asyncio.create_task(mock_requests_complete()) await shutdown_handler.wait_for_shutdown() mock_cleanup.assert_called_once() @pytest.mark.asyncio async def test_graceful_shutdown_timeout(self, mocker): """Test graceful shutdown with timeout.""" from autodoc_mcp.main import GracefulShutdown shutdown_handler = GracefulShutdown() shutdown_handler.active_requests = 5 shutdown_handler.max_shutdown_wait = 0.1 # Very short timeout mock_cleanup = mocker.patch.object(shutdown_handler, "_cleanup_resources") mock_cleanup.return_value = None shutdown_handler.shutdown_event.set() await shutdown_handler.wait_for_shutdown() # Should force shutdown after timeout mock_cleanup.assert_called_once() @pytest.mark.asyncio async def test_cleanup_resources(self, mocker): """Test resource cleanup functionality.""" from autodoc_mcp.main import GracefulShutdown shutdown_handler = GracefulShutdown() mock_pool_class = mocker.patch( "autodoc_mcp.core.network_resilience.ConnectionPoolManager" ) mocker.patch("autodoc_mcp.main.cache_manager") mock_pool_manager = mocker.AsyncMock() mock_pool_manager.close_all.return_value = None mock_pool_class.return_value = mock_pool_manager await shutdown_handler._cleanup_resources() mock_pool_manager.close_all.assert_called_once() def test_signal_handler(self): """Test signal handler registration and handling.""" from autodoc_mcp.main import GracefulShutdown shutdown_handler = GracefulShutdown() # Test signal handler shutdown_handler._signal_handler(15, None) # SIGTERM assert shutdown_handler.shutdown_event.is_set() @pytest.mark.asyncio async def test_request_context_manager(self): """Test request context manager tracking.""" from autodoc_mcp.main import GracefulShutdown shutdown_handler = GracefulShutdown() assert shutdown_handler.active_requests == 0 async with shutdown_handler.request_context(): assert shutdown_handler.active_requests == 1 assert shutdown_handler.active_requests == 0 @pytest.mark.asyncio async def test_request_context_manager_with_exception(self): """Test request context manager with exception.""" from autodoc_mcp.main import GracefulShutdown shutdown_handler = GracefulShutdown() assert shutdown_handler.active_requests == 0 with pytest.raises(ValueError): async with shutdown_handler.request_context(): assert shutdown_handler.active_requests == 1 raise ValueError("Test exception") # Should decrement even with exception assert shutdown_handler.active_requests == 0 class TestMainFunctions: """Test main function and server startup.""" @pytest.mark.asyncio async def test_async_main_success(self, mocker): """Test successful async_main execution.""" from autodoc_mcp.main import async_main mock_init = mocker.patch("autodoc_mcp.main.initialize_services") mocker.patch("autodoc_mcp.main.run_mcp_server") mock_shutdown_class = mocker.patch("autodoc_mcp.main.GracefulShutdown") mock_init.return_value = None mock_server_task = mocker.MagicMock() mock_shutdown_task = mocker.MagicMock() mock_shutdown_handler = mocker.MagicMock() mock_shutdown_handler.register_signals.return_value = None mock_shutdown_handler.wait_for_shutdown = mocker.AsyncMock(return_value=None) mock_shutdown_handler._cleanup_resources = mocker.AsyncMock(return_value=None) mock_shutdown_class.return_value = mock_shutdown_handler mock_create_task = mocker.patch("asyncio.create_task") mock_create_task.side_effect = [mock_server_task, mock_shutdown_task] mock_wait = mocker.patch("asyncio.wait") mock_wait.return_value = ({mock_shutdown_task}, {mock_server_task}) await async_main() mock_init.assert_called_once() mock_shutdown_handler.register_signals.assert_called_once() @pytest.mark.asyncio async def test_async_main_initialization_failure(self, mocker): """Test async_main with service initialization failure.""" from autodoc_mcp.main import async_main mock_init = mocker.patch("autodoc_mcp.main.initialize_services") mock_shutdown_class = mocker.patch("autodoc_mcp.main.GracefulShutdown") mock_init.side_effect = Exception("Initialization failed") mock_shutdown_handler = mocker.MagicMock() mock_shutdown_handler.register_signals.return_value = None mock_shutdown_handler._cleanup_resources = mocker.AsyncMock(return_value=None) mock_shutdown_class.return_value = mock_shutdown_handler with pytest.raises(Exception, match="Initialization failed"): await async_main() # Should still cleanup resources mock_shutdown_handler._cleanup_resources.assert_called_once() def test_main_function_keyboard_interrupt(self, mocker): """Test main function handling KeyboardInterrupt.""" from autodoc_mcp.main import main mock_run = mocker.patch("autodoc_mcp.main.asyncio.run") mock_logger = mocker.patch("autodoc_mcp.main.logger") mock_run.side_effect = KeyboardInterrupt() main() # Should not raise mock_logger.info.assert_called_with("Server shutdown requested") def test_main_function_generic_exception(self, mocker): """Test main function handling generic exception.""" from autodoc_mcp.main import main mock_run = mocker.patch("autodoc_mcp.main.asyncio.run") mock_logger = mocker.patch("autodoc_mcp.main.logger") mock_run.side_effect = Exception("Server error") with pytest.raises(Exception, match="Server error"): main() mock_logger.error.assert_called_with("Server failed", error="Server error")

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/bradleyfay/autodoc-mcp'

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