Research-Stack/5-Applications/nodupe/tests/database/test_query_cache.py
2026-05-05 21:15:26 -05:00

446 lines
13 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2025 NoDupeLabs
"""Tests to achieve 100% coverage on query_cache.py module.
This test file targets the missing coverage in:
- query_cache.py: QueryCache class methods
"""
import time
from unittest.mock import MagicMock, patch
import pytest
from nodupe.tools.databases.query_cache import (
QueryCache,
QueryCacheError,
create_query_cache,
)
class TestQueryCacheInit:
"""Tests for QueryCache initialization."""
def test_init_default_values(self):
"""Test QueryCache with default values."""
cache = QueryCache()
assert cache.max_size == 1000
assert cache.ttl_seconds == 3600
def test_init_custom_values(self):
"""Test QueryCache with custom values."""
cache = QueryCache(max_size=500, ttl_seconds=1800)
assert cache.max_size == 500
assert cache.ttl_seconds == 1800
def test_init_stats_initialized(self):
"""Test stats are initialized correctly."""
cache = QueryCache()
stats = cache._stats
assert stats['hits'] == 0
assert stats['misses'] == 0
assert stats['evictions'] == 0
assert stats['insertions'] == 0
class TestGetResult:
"""Tests for get_result method."""
def test_get_result_miss(self):
"""Test get_result when not cached."""
cache = QueryCache()
result = cache.get_result("SELECT * FROM test")
assert result is None
def test_get_result_hit(self):
"""Test get_result when cached."""
cache = QueryCache()
cache.set_result("SELECT * FROM test", result={"data": "value"})
result = cache.get_result("SELECT * FROM test")
assert result == {"data": "value"}
def test_get_result_with_params(self):
"""Test get_result with parameters."""
cache = QueryCache()
cache.set_result("SELECT * FROM test WHERE id = ?", params={"id": 1}, result=[1])
result = cache.get_result("SELECT * FROM test WHERE id = ?", params={"id": 1})
assert result == [1]
def test_get_result_params_mismatch(self):
"""Test get_result with different parameters."""
cache = QueryCache()
cache.set_result("SELECT * FROM test", params={"id": 1}, result=[1])
result = cache.get_result("SELECT * FROM test", params={"id": 2})
assert result is None
def test_get_result_expired(self):
"""Test get_result when entry is expired."""
cache = QueryCache(ttl_seconds=1)
# Manually add with old timestamp
cache._cache["select * from test:none"] = ({"data": "value"}, time.monotonic() - 10)
result = cache.get_result("SELECT * FROM test")
assert result is None
class TestSetResult:
"""Tests for set_result method."""
def test_set_result_basic(self):
"""Test basic set_result."""
cache = QueryCache()
cache.set_result("SELECT * FROM test", result={"data": "value"})
assert "select * from test:none" in cache._cache
def test_set_result_with_params(self):
"""Test set_result with parameters."""
cache = QueryCache()
cache.set_result("SELECT * FROM test", params={"id": 1}, result=[1])
# Check that key is generated with params hash
keys = list(cache._cache.keys())
assert any("select * from test:" in k for k in keys)
def test_set_result_updates_stats(self):
"""Test set_result updates insertion stats."""
cache = QueryCache()
cache.set_result("SELECT * FROM test", result={"data": "value"})
assert cache._stats['insertions'] == 1
def test_set_result_eviction_when_full(self):
"""Test set_result evicts oldest when cache is full."""
cache = QueryCache(max_size=2)
cache.set_result("SELECT 1", result=1)
cache.set_result("SELECT 2", result=2)
cache.set_result("SELECT 3", result=3)
assert len(cache._cache) == 2
assert cache._stats['evictions'] == 1
class TestInvalidate:
"""Tests for invalidate method."""
def test_invalidate_existing(self):
"""Test invalidating existing entry."""
cache = QueryCache()
cache.set_result("SELECT * FROM test", result={"data": "value"})
result = cache.invalidate("SELECT * FROM test")
assert result is True
assert "select * from test:none" not in cache._cache
def test_invalidate_nonexisting(self):
"""Test invalidating non-existing entry."""
cache = QueryCache()
result = cache.invalidate("SELECT * FROM nonexistent")
assert result is False
class TestInvalidateAll:
"""Tests for invalidate_all method."""
def test_invalidate_all(self):
"""Test invalidating all entries."""
cache = QueryCache()
cache.set_result("SELECT 1", result=1)
cache.set_result("SELECT 2", result=2)
cache.invalidate_all()
assert len(cache._cache) == 0
assert cache._stats['evictions'] == 2
class TestInvalidateByPrefix:
"""Tests for invalidate_by_prefix method."""
def test_invalidate_by_prefix(self):
"""Test invalidating by prefix."""
cache = QueryCache()
cache.set_result("SELECT * FROM users", result=[1])
cache.set_result("SELECT * FROM posts", result=[2])
cache.set_result("SELECT * FROM comments", result=[3])
count = cache.invalidate_by_prefix("select * from users")
assert count == 1
def test_invalidate_by_prefix_no_match(self):
"""Test invalidating by prefix with no match."""
cache = QueryCache()
cache.set_result("SELECT * FROM users", result=[1])
count = cache.invalidate_by_prefix("nonexistent")
assert count == 0
class TestValidateCache:
"""Tests for validate_cache method."""
def test_validate_cache_removes_expired(self):
"""Test validate_cache removes expired entries."""
cache = QueryCache(ttl_seconds=1)
# Manually add with old timestamp
cache._cache["test:none"] = ({"data": "value"}, time.monotonic() - 10)
removed = cache.validate_cache()
assert removed == 1
assert "test:none" not in cache._cache
def test_validate_cache_none_expired(self):
"""Test validate_cache when no entries expired."""
cache = QueryCache()
cache.set_result("SELECT * FROM test", result={"data": "value"})
removed = cache.validate_cache()
assert removed == 0
class TestGetStats:
"""Tests for get_stats method."""
def test_get_stats_basic(self):
"""Test basic get_stats."""
cache = QueryCache()
stats = cache.get_stats()
assert 'hits' in stats
assert 'misses' in stats
assert 'evictions' in stats
assert 'insertions' in stats
assert 'size' in stats
assert 'capacity' in stats
assert 'hit_rate' in stats
def test_get_stats_with_hits(self):
"""Test get_stats with cache hits."""
cache = QueryCache()
cache.set_result("SELECT * FROM test", result={"data": "value"})
cache.get_result("SELECT * FROM test") # Hit
cache.get_result("SELECT * FROM test") # Hit
stats = cache.get_stats()
assert stats['hits'] == 2
assert stats['hit_rate'] == 1.0
def test_get_stats_hit_rate_no_activity(self):
"""Test hit rate calculation with no activity."""
cache = QueryCache()
stats = cache.get_stats()
assert stats['hit_rate'] == 0.0
class TestGetCacheSize:
"""Tests for get_cache_size method."""
def test_get_cache_size_empty(self):
"""Test get_cache_size when empty."""
cache = QueryCache()
assert cache.get_cache_size() == 0
def test_get_cache_size_with_entries(self):
"""Test get_cache_size with entries."""
cache = QueryCache()
cache.set_result("SELECT * FROM test", result={"data": "value"})
assert cache.get_cache_size() == 1
class TestIsCached:
"""Tests for is_cached method."""
def test_is_cached_true(self):
"""Test is_cached when cached."""
cache = QueryCache()
cache.set_result("SELECT * FROM test", result={"data": "value"})
assert cache.is_cached("SELECT * FROM test") is True
def test_is_cached_false(self):
"""Test is_cached when not cached."""
cache = QueryCache()
assert cache.is_cached("SELECT * FROM nonexistent") is False
class TestCleanupExpired:
"""Tests for cleanup_expired method."""
def test_cleanup_expired(self):
"""Test cleanup_expired."""
cache = QueryCache(ttl_seconds=1)
# Manually add with old timestamp
cache._cache["test:none"] = ({"data": "value"}, time.monotonic() - 10)
removed = cache.cleanup_expired()
assert removed == 1
class TestResize:
"""Tests for resize method."""
def test_resize_smaller(self):
"""Test resizing to smaller size."""
cache = QueryCache(max_size=10)
cache.set_result("SELECT 1", result=1)
cache.set_result("SELECT 2", result=2)
cache.resize(1)
assert cache.max_size == 1
assert len(cache._cache) == 1
def test_resize_larger(self):
"""Test resizing to larger size."""
cache = QueryCache(max_size=2)
cache.resize(10)
assert cache.max_size == 10
class TestGetMemoryUsage:
"""Tests for get_memory_usage method."""
def test_get_memory_usage_empty(self):
"""Test get_memory_usage when empty."""
cache = QueryCache()
usage = cache.get_memory_usage()
assert usage == 0
def test_get_memory_usage_with_entries(self):
"""Test get_memory_usage with entries."""
cache = QueryCache()
cache.set_result("SELECT * FROM test", result={"data": "value"})
usage = cache.get_memory_usage()
assert usage > 0
class TestGenerateKey:
"""Tests for _generate_key method."""
def test_generate_key_no_params(self):
"""Test _generate_key without params."""
cache = QueryCache()
key = cache._generate_key("SELECT * FROM test")
assert "select * from test" in key
assert "none" in key
def test_generate_key_with_params(self):
"""Test _generate_key with params."""
cache = QueryCache()
key = cache._generate_key("SELECT * FROM test", params={"id": 1})
assert "select * from test" in key
def test_generate_key_normalizes_query(self):
"""Test _generate_key normalizes query."""
cache = QueryCache()
key1 = cache._generate_key("SELECT * FROM test")
key2 = cache._generate_key("select * from test")
assert key1 == key2
def test_generate_key_with_different_params(self):
"""Test _generate_key with different params produces different keys."""
cache = QueryCache()
key1 = cache._generate_key("SELECT * FROM test", params={"id": 1})
key2 = cache._generate_key("SELECT * FROM test", params={"id": 2})
assert key1 != key2
class TestClearByQueryPattern:
"""Tests for clear_by_query_pattern method."""
def test_clear_by_query_pattern(self):
"""Test clearing by query pattern."""
cache = QueryCache()
cache.set_result("SELECT * FROM users", result=[1])
cache.set_result("SELECT * FROM posts", result=[2])
count = cache.clear_by_query_pattern("users")
assert count == 1
def test_clear_by_query_pattern_case_insensitive(self):
"""Test clearing by query pattern is case insensitive."""
cache = QueryCache()
cache.set_result("SELECT * FROM users", result=[1])
count = cache.clear_by_query_pattern("USERS")
assert count == 1
class TestGetCachedQueries:
"""Tests for get_cached_queries method."""
def test_get_cached_queries(self):
"""Test getting cached queries."""
cache = QueryCache()
cache.set_result("SELECT * FROM users", result=[1])
cache.set_result("SELECT * FROM posts", result=[2])
queries = cache.get_cached_queries()
assert len(queries) == 2
def test_get_cached_queries_deduplicates(self):
"""Test get_cached_queries deduplicates."""
cache = QueryCache()
cache.set_result("SELECT * FROM users", params={"id": 1}, result=[1])
cache.set_result("SELECT * FROM users", params={"id": 2}, result=[2])
queries = cache.get_cached_queries()
assert len(queries) == 1
class TestCreateQueryCache:
"""Tests for create_query_cache factory function."""
def test_create_query_cache_default(self):
"""Test create_query_cache with defaults."""
cache = create_query_cache()
assert isinstance(cache, QueryCache)
assert cache.max_size == 1000
assert cache.ttl_seconds == 3600
def test_create_query_cache_custom(self):
"""Test create_query_cache with custom values."""
cache = create_query_cache(max_size=500, ttl_seconds=1800)
assert cache.max_size == 500
assert cache.ttl_seconds == 1800
class TestQueryCacheError:
"""Tests for QueryCacheError exception."""
def test_query_cache_error_creation(self):
"""Test QueryCacheError can be created."""
error = QueryCacheError("Test error")
assert str(error) == "Test error"
def test_query_cache_error_inheritance(self):
"""Test QueryCacheError inherits from Exception."""
error = QueryCacheError("Test")
assert isinstance(error, Exception)