Skip to main content
Glama
test_cache_manager.py20.4 kB
"""Unit tests for cache manager functionality.""" import json import tempfile from datetime import datetime from pathlib import Path import pytest from src.autodoc_mcp.core.cache_manager import FileCacheManager from src.autodoc_mcp.exceptions import CacheError from src.autodoc_mcp.models import CacheEntry, PackageInfo @pytest.fixture def temp_cache_dir(): """Create temporary cache directory for tests.""" with tempfile.TemporaryDirectory() as temp_dir: yield Path(temp_dir) @pytest.fixture def cache_manager(temp_cache_dir): """Create cache manager instance with temp directory.""" return FileCacheManager(temp_cache_dir) @pytest.fixture def sample_package_info(): """Sample PackageInfo for tests.""" return PackageInfo( name="requests", version="2.28.0", summary="Python HTTP for Humans.", description="Requests is a simple HTTP library.", author="Kenneth Reitz", license="Apache 2.0", home_page="https://requests.readthedocs.io", project_urls={ "Documentation": "https://requests.readthedocs.io", "Source": "https://github.com/psf/requests", }, classifiers=["Development Status :: 5 - Production/Stable"], keywords=["http", "requests", "web"], ) @pytest.fixture def cache_entry(sample_package_info): """Sample CacheEntry for tests.""" return CacheEntry( data=sample_package_info, timestamp=datetime.now(), version="2.28.0", ) class TestFileCacheManagerInitialization: """Test cache manager initialization.""" def test_cache_dir_creation_on_init(self, temp_cache_dir): """Test cache directory is created during initialization.""" cache_dir = temp_cache_dir / "new_cache" assert not cache_dir.exists() FileCacheManager(cache_dir) assert cache_dir.exists() @pytest.mark.asyncio async def test_initialize_creates_directory(self, temp_cache_dir): """Test initialize method creates cache directory.""" cache_dir = temp_cache_dir / "init_test" cache_manager = FileCacheManager(cache_dir) # Remove directory to test initialization cache_dir.rmdir() assert not cache_dir.exists() await cache_manager.initialize() assert cache_dir.exists() @pytest.mark.asyncio async def test_initialize_existing_directory(self, cache_manager): """Test initialize method with existing directory.""" # Should not raise error with existing directory await cache_manager.initialize() assert cache_manager.cache_dir.exists() class TestCacheOperations: """Test basic cache operations.""" @pytest.mark.asyncio async def test_get_cache_miss(self, cache_manager): """Test cache miss returns None.""" result = await cache_manager.get("nonexistent-key") assert result is None @pytest.mark.asyncio async def test_set_and_get_cache_success(self, cache_manager, sample_package_info): """Test successful cache set and get operations.""" cache_key = "requests-2.28.0" # Set cache entry await cache_manager.set(cache_key, sample_package_info) # Verify file was created cache_file = cache_manager.cache_dir / f"{cache_key}.json" assert cache_file.exists() # Get cache entry result = await cache_manager.get(cache_key) assert result is not None assert result.data.name == "requests" assert result.data.version == "2.28.0" assert result.version == "2.28.0" @pytest.mark.asyncio async def test_get_corrupted_cache_file_json_error(self, cache_manager): """Test handling of corrupted JSON cache files.""" cache_key = "corrupted-key" cache_file = cache_manager.cache_dir / f"{cache_key}.json" # Create corrupted JSON file cache_file.write_text("invalid json content") # Should return None and remove corrupted file result = await cache_manager.get(cache_key) assert result is None assert not cache_file.exists() @pytest.mark.asyncio async def test_get_corrupted_cache_file_key_error(self, cache_manager): """Test handling of cache files with missing keys.""" cache_key = "missing-keys" cache_file = cache_manager.cache_dir / f"{cache_key}.json" # Create JSON with missing required keys cache_file.write_text(json.dumps({"invalid": "structure"})) # Should return None and remove corrupted file result = await cache_manager.get(cache_key) assert result is None assert not cache_file.exists() @pytest.mark.asyncio async def test_set_cache_io_error(self, cache_manager, sample_package_info): """Test cache set with I/O error.""" # Mock cache directory to be a file (causing OSError) cache_manager.cache_dir = Path("/dev/null/invalid") with pytest.raises(CacheError, match="Failed to cache"): await cache_manager.set("test-key", sample_package_info) @pytest.mark.asyncio async def test_set_cache_type_error(self, cache_manager, mocker): """Test cache set with serialization error.""" # Create a package info that can't be serialized bad_package = mocker.Mock(spec=PackageInfo) bad_package.model_dump.side_effect = TypeError("Cannot serialize") bad_package.name = "test" bad_package.version = "1.0.0" with pytest.raises(CacheError, match="Failed to cache"): await cache_manager.set("test-key", bad_package) class TestCacheManagement: """Test cache management operations.""" @pytest.mark.asyncio async def test_invalidate_specific_key_existing( self, cache_manager, sample_package_info ): """Test invalidating specific existing cache key.""" cache_key = "requests-2.28.0" # Set cache entry await cache_manager.set(cache_key, sample_package_info) cache_file = cache_manager.cache_dir / f"{cache_key}.json" assert cache_file.exists() # Invalidate specific key await cache_manager.invalidate(cache_key) # File should be removed assert not cache_file.exists() @pytest.mark.asyncio async def test_invalidate_specific_key_nonexistent(self, cache_manager): """Test invalidating non-existent cache key.""" # Should not raise error await cache_manager.invalidate("nonexistent-key") @pytest.mark.asyncio async def test_invalidate_specific_key_os_error( self, cache_manager, sample_package_info, mocker ): """Test invalidating with OS error during deletion.""" cache_key = "requests-2.28.0" # Set cache entry await cache_manager.set(cache_key, sample_package_info) # Mock unlink to raise OSError mocker.patch.object(Path, "unlink", side_effect=OSError("Permission denied")) # Should not raise error, just log warning await cache_manager.invalidate(cache_key) @pytest.mark.asyncio async def test_invalidate_entire_cache(self, cache_manager, sample_package_info): """Test invalidating entire cache.""" # Set multiple cache entries await cache_manager.set("requests-2.28.0", sample_package_info) await cache_manager.set("httpx-0.24.0", sample_package_info) # Verify files exist assert len(list(cache_manager.cache_dir.glob("*.json"))) == 2 # Clear entire cache await cache_manager.invalidate() # All files should be removed assert len(list(cache_manager.cache_dir.glob("*.json"))) == 0 @pytest.mark.asyncio async def test_invalidate_entire_cache_os_error( self, cache_manager, sample_package_info, mocker ): """Test clearing entire cache with OS error.""" await cache_manager.set("test-key", sample_package_info) # Mock glob to raise OSError mocker.patch.object(Path, "glob", side_effect=OSError("Permission denied")) with pytest.raises(CacheError, match="Failed to clear cache"): await cache_manager.invalidate() @pytest.mark.asyncio async def test_list_cached_packages_success( self, cache_manager, sample_package_info ): """Test listing cached packages successfully.""" # Add some cache entries await cache_manager.set("requests-2.28.0", sample_package_info) await cache_manager.set("httpx-0.24.0", sample_package_info) # List cached packages packages = await cache_manager.list_cached_packages() assert len(packages) == 2 assert "requests-2.28.0" in packages assert "httpx-0.24.0" in packages @pytest.mark.asyncio async def test_list_cached_packages_empty_cache(self, cache_manager): """Test listing cached packages with empty cache.""" packages = await cache_manager.list_cached_packages() assert packages == [] @pytest.mark.asyncio async def test_list_cached_packages_os_error(self, cache_manager, mocker): """Test listing cached packages with OS error.""" # Mock glob to raise OSError mock_glob = mocker.patch.object( Path, "glob", side_effect=OSError("Permission denied") ) packages = await cache_manager.list_cached_packages() assert packages == [] mock_glob.assert_called_once_with("*.json") @pytest.mark.asyncio async def test_get_cache_stats_success(self, cache_manager, sample_package_info): """Test getting cache statistics successfully.""" # Add some cache entries await cache_manager.set("requests-2.28.0", sample_package_info) await cache_manager.set("httpx-0.24.0", sample_package_info) stats = await cache_manager.get_cache_stats() assert stats["total_entries"] == 2 assert stats["total_size_bytes"] > 0 assert stats["cache_dir"] == str(cache_manager.cache_dir) @pytest.mark.asyncio async def test_get_cache_stats_empty_cache(self, cache_manager): """Test getting cache statistics with empty cache.""" stats = await cache_manager.get_cache_stats() assert stats["total_entries"] == 0 assert stats["total_size_bytes"] == 0 assert stats["cache_dir"] == str(cache_manager.cache_dir) @pytest.mark.asyncio async def test_get_cache_stats_os_error(self, cache_manager, mocker): """Test getting cache statistics with OS error.""" # Mock glob to raise OSError mock_glob = mocker.patch.object( Path, "glob", side_effect=OSError("Permission denied") ) stats = await cache_manager.get_cache_stats() assert "error" in stats mock_glob.assert_called_once_with("*.json") class TestSafeCacheOperations: """Test safe cache operations with error handling.""" def test_get_cached_entry_safe_cache_miss(self, cache_manager): """Test safe cache retrieval with cache miss.""" entry, errors = cache_manager.get_cached_entry_safe("nonexistent-key") assert entry is None assert len(errors) == 0 def test_get_cached_entry_safe_success(self, cache_manager, sample_package_info): """Test safe cache retrieval with successful hit.""" cache_key = "requests-2.28.0" cache_file = cache_manager.cache_dir / f"{cache_key}.json" # Create valid cache file cache_data = { "data": sample_package_info.model_dump(), "timestamp": datetime.now().isoformat(), "version": "2.28.0", } cache_file.write_text(json.dumps(cache_data)) entry, errors = cache_manager.get_cached_entry_safe(cache_key) assert entry is not None assert entry.data.name == "requests" assert len(errors) == 0 def test_get_cached_entry_safe_empty_file(self, cache_manager): """Test safe cache retrieval with empty cache file.""" cache_key = "empty-file" cache_file = cache_manager.cache_dir / f"{cache_key}.json" # Create empty cache file cache_file.write_text("") entry, errors = cache_manager.get_cached_entry_safe(cache_key) assert entry is None assert len(errors) == 1 assert errors[0].error_code == "cache_corruption_fixed" assert not cache_file.exists() # File should be removed def test_get_cached_entry_safe_json_decode_error(self, cache_manager): """Test safe cache retrieval with JSON decode error.""" cache_key = "corrupted-json" cache_file = cache_manager.cache_dir / f"{cache_key}.json" # Create corrupted JSON file cache_file.write_text("invalid json content") entry, errors = cache_manager.get_cached_entry_safe(cache_key) assert entry is None assert len(errors) == 1 assert errors[0].error_code == "cache_corruption_fixed" assert not cache_file.exists() # File should be removed def test_get_cached_entry_safe_generic_exception(self, cache_manager, mocker): """Test safe cache retrieval with generic exception.""" cache_key = "error-key" # Mock sanitize_cache_key to raise exception mock_sanitize = mocker.patch( "src.autodoc_mcp.core.cache_manager.sanitize_cache_key", side_effect=ValueError("Test error"), ) entry, errors = cache_manager.get_cached_entry_safe(cache_key) assert entry is None assert len(errors) == 1 assert errors[0].error_code is not None mock_sanitize.assert_called_once_with(cache_key) class TestResolveAndCache: """Test resolve and cache functionality.""" @pytest.mark.asyncio async def test_resolve_and_cache_cache_hit( self, cache_manager, sample_package_info, mocker ): """Test resolve and cache with cache hit.""" # Pre-populate cache cache_key = "requests-2.28.0" await cache_manager.set(cache_key, sample_package_info) mock_resolver_class = mocker.patch( "src.autodoc_mcp.core.version_resolver.VersionResolver" ) mock_resolver = mocker.Mock() mock_resolver.resolve_version = mocker.AsyncMock(return_value="2.28.0") mock_resolver.generate_cache_key.return_value = cache_key mock_resolver_class.return_value = mock_resolver result, from_cache = await cache_manager.resolve_and_cache( "requests", ">=2.0.0" ) assert from_cache is True assert result["name"] == "requests" assert result["version"] == "2.28.0" @pytest.mark.asyncio async def test_resolve_and_cache_cache_miss( self, cache_manager, sample_package_info, mocker ): """Test resolve and cache with cache miss and fresh fetch.""" cache_key = "requests-2.28.0" # Mock version resolver mock_resolver_class = mocker.patch( "src.autodoc_mcp.core.version_resolver.VersionResolver" ) mock_resolver = mocker.Mock() mock_resolver.resolve_version = mocker.AsyncMock(return_value="2.28.0") mock_resolver.generate_cache_key.return_value = cache_key mock_resolver_class.return_value = mock_resolver # Mock fetcher mock_fetcher_class = mocker.patch( "src.autodoc_mcp.core.doc_fetcher.PyPIDocumentationFetcher" ) mock_fetcher = mocker.AsyncMock() mock_fetcher.fetch_package_info = mocker.AsyncMock( return_value=sample_package_info ) mock_fetcher.__aenter__ = mocker.AsyncMock(return_value=mock_fetcher) mock_fetcher.__aexit__ = mocker.AsyncMock(return_value=None) mock_fetcher_class.return_value = mock_fetcher result, from_cache = await cache_manager.resolve_and_cache( "requests", ">=2.0.0" ) assert from_cache is False assert result["name"] == "requests" assert result["version"] == "2.28.0" # Verify entry was cached cached_entry = await cache_manager.get(cache_key) assert cached_entry is not None @pytest.mark.asyncio async def test_resolve_and_cache_version_resolution_error( self, cache_manager, mocker ): """Test resolve and cache with version resolution error.""" mock_resolver_class = mocker.patch( "src.autodoc_mcp.core.version_resolver.VersionResolver" ) mock_resolver = mocker.Mock() mock_resolver.resolve_version = mocker.AsyncMock( side_effect=ValueError("Version resolution failed") ) mock_resolver_class.return_value = mock_resolver with pytest.raises(ValueError, match="Version resolution failed"): await cache_manager.resolve_and_cache("nonexistent-package", ">=1.0.0") @pytest.mark.asyncio async def test_resolve_and_cache_fetch_error(self, cache_manager, mocker): """Test resolve and cache with fetch error.""" cache_key = "requests-2.28.0" # Mock version resolver mock_resolver_class = mocker.patch( "src.autodoc_mcp.core.version_resolver.VersionResolver" ) mock_resolver = mocker.Mock() mock_resolver.resolve_version = mocker.AsyncMock(return_value="2.28.0") mock_resolver.generate_cache_key.return_value = cache_key mock_resolver_class.return_value = mock_resolver # Mock fetcher to raise error mock_fetcher_class = mocker.patch( "src.autodoc_mcp.core.doc_fetcher.PyPIDocumentationFetcher" ) mock_fetcher = mocker.AsyncMock() mock_fetcher.fetch_package_info = mocker.AsyncMock( side_effect=ValueError("Fetch failed") ) mock_fetcher.__aenter__ = mocker.AsyncMock(return_value=mock_fetcher) mock_fetcher.__aexit__ = mocker.AsyncMock(return_value=None) mock_fetcher_class.return_value = mock_fetcher with pytest.raises(ValueError, match="Fetch failed"): await cache_manager.resolve_and_cache("requests", ">=2.0.0") class TestEdgeCases: """Test edge cases and error scenarios.""" def test_cache_key_sanitization(self, cache_manager, sample_package_info, mocker): """Test that cache keys are properly sanitized.""" # This test verifies that sanitize_cache_key is called mock_sanitize = mocker.patch( "src.autodoc_mcp.core.cache_manager.sanitize_cache_key", return_value="sanitized-key", ) cache_manager.cache_dir / "sanitized-key.json" # The function should be called during cache operations cache_manager.get_cached_entry_safe("unsafe/key\\with:special*chars") mock_sanitize.assert_called_with("unsafe/key\\with:special*chars") @pytest.mark.asyncio async def test_concurrent_cache_operations( self, cache_manager, sample_package_info ): """Test handling of concurrent cache operations.""" import asyncio # Test concurrent set operations tasks = [ cache_manager.set(f"package-{i}", sample_package_info) for i in range(5) ] await asyncio.gather(*tasks) # All entries should be cached stats = await cache_manager.get_cache_stats() assert stats["total_entries"] == 5 @pytest.mark.asyncio async def test_large_cache_data(self, cache_manager): """Test handling of large cache data.""" # Create package with large description large_package = PackageInfo( name="large-package", version="1.0.0", summary="Test package", description="Large description. " * 10000, # ~200KB description author="Test Author", license="MIT", home_page="https://example.com", project_urls={}, classifiers=[], keywords=[], ) # Should handle large data without issues await cache_manager.set("large-package-1.0.0", large_package) result = await cache_manager.get("large-package-1.0.0") assert result is not None assert result.data.name == "large-package" assert len(result.data.description) > 100000

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