Some checks failed
CI/CD Pipeline / Code Quality Checks (push) Failing after 3m10s
CI/CD Pipeline / Run Tests (push) Successful in 3m35s
CI/CD Pipeline / Telegram Notify Success (push) Has been skipped
CI/CD Pipeline / Code Quality Checks (pull_request) Failing after 2m26s
CI/CD Pipeline / Run Tests (pull_request) Successful in 2m46s
CI/CD Pipeline / Telegram Notify Success (pull_request) Has been skipped
274 lines
8.3 KiB
Python
274 lines
8.3 KiB
Python
"""Tests for core cache utilities"""
|
|
|
|
from apps.core.cache import (
|
|
CacheManager,
|
|
_build_cache_key,
|
|
cache_method,
|
|
cache_result,
|
|
invalidate_cache,
|
|
)
|
|
from django.core.cache import cache
|
|
from django.test import TestCase
|
|
|
|
from tests.utils.fixtures import fake
|
|
|
|
|
|
class CacheResultDecoratorTest(TestCase):
|
|
"""Tests for @cache_result decorator"""
|
|
|
|
def setUp(self):
|
|
cache.clear()
|
|
self.call_count = 0
|
|
|
|
def test_result_is_cached(self):
|
|
"""Test that function result is cached"""
|
|
|
|
@cache_result(timeout=60, key_prefix="test")
|
|
def expensive_function(x):
|
|
self.call_count += 1
|
|
return x * 2
|
|
|
|
# First call - should execute
|
|
result1 = expensive_function(5)
|
|
self.assertEqual(result1, 10)
|
|
self.assertEqual(self.call_count, 1)
|
|
|
|
# Second call - should return cached result
|
|
result2 = expensive_function(5)
|
|
self.assertEqual(result2, 10)
|
|
self.assertEqual(self.call_count, 1) # Still 1, not called again
|
|
|
|
def test_key_builder_used(self):
|
|
"""Test custom key builder overrides default."""
|
|
called = {"count": 0}
|
|
|
|
def key_builder(*_args, **_kwargs):
|
|
return "custom-key"
|
|
|
|
@cache_result(timeout=60, key_builder=key_builder)
|
|
def expensive_function(x):
|
|
called["count"] += 1
|
|
return x * 3
|
|
|
|
result1 = expensive_function(2)
|
|
result2 = expensive_function(2)
|
|
|
|
self.assertEqual(result1, 6)
|
|
self.assertEqual(result2, 6)
|
|
self.assertEqual(called["count"], 1)
|
|
|
|
def test_invalidate_cache_fallback(self):
|
|
"""Test invalidate_cache fallback without delete_pattern."""
|
|
cache.set("cache-key", "value", timeout=60)
|
|
invalidate_cache("cache-key")
|
|
self.assertIsNone(cache.get("cache-key"))
|
|
|
|
def test_invalidate_wrapper_with_key_builder(self):
|
|
key_suffix = fake.pystr(min_chars=3, max_chars=8)
|
|
|
|
def key_builder(x):
|
|
return f"{key_suffix}:{x}"
|
|
|
|
@cache_result(timeout=60, key_builder=key_builder)
|
|
def expensive_function(x):
|
|
self.call_count += 1
|
|
return x * 2
|
|
|
|
value = fake.random_int(min=1, max=9)
|
|
result = expensive_function(value)
|
|
self.assertEqual(result, value * 2)
|
|
self.assertIsNotNone(cache.get(f"{key_suffix}:{value}"))
|
|
|
|
expensive_function.invalidate(value)
|
|
self.assertIsNone(cache.get(f"{key_suffix}:{value}"))
|
|
|
|
def test_invalidate_wrapper_default_builder(self):
|
|
value = fake.random_int(min=1, max=9)
|
|
|
|
@cache_result(timeout=60, key_prefix="default")
|
|
def expensive_function(x):
|
|
self.call_count += 1
|
|
return x * 2
|
|
|
|
_ = expensive_function(value)
|
|
key = _build_cache_key(expensive_function, "default", (value,), {})
|
|
self.assertIsNotNone(cache.get(key))
|
|
|
|
expensive_function.invalidate(value)
|
|
self.assertIsNone(cache.get(key))
|
|
|
|
def test_different_args_not_cached(self):
|
|
"""Test that different arguments create different cache entries"""
|
|
|
|
@cache_result(timeout=60, key_prefix="test")
|
|
def expensive_function(x):
|
|
self.call_count += 1
|
|
return x * 2
|
|
|
|
result1 = expensive_function(5)
|
|
result2 = expensive_function(10)
|
|
|
|
self.assertEqual(result1, 10)
|
|
self.assertEqual(result2, 20)
|
|
self.assertEqual(self.call_count, 2)
|
|
|
|
def test_kwargs_included_in_cache_key(self):
|
|
"""Test that kwargs are included in cache key"""
|
|
|
|
@cache_result(timeout=60, key_prefix="test")
|
|
def expensive_function(x, multiplier=2):
|
|
self.call_count += 1
|
|
return x * multiplier
|
|
|
|
result1 = expensive_function(5, multiplier=2)
|
|
result2 = expensive_function(5, multiplier=3)
|
|
|
|
self.assertEqual(result1, 10)
|
|
self.assertEqual(result2, 15)
|
|
self.assertEqual(self.call_count, 2)
|
|
|
|
|
|
class CacheMethodDecoratorTest(TestCase):
|
|
"""Tests for @cache_method decorator"""
|
|
|
|
def setUp(self):
|
|
cache.clear()
|
|
|
|
def test_classmethod_caching(self):
|
|
"""Test caching works with classmethod"""
|
|
call_count = {"value": 0}
|
|
|
|
class MyService:
|
|
@classmethod
|
|
@cache_method(timeout=60, key_prefix="service")
|
|
def get_data(cls, item_id):
|
|
call_count["value"] += 1
|
|
return {"id": item_id, "data": "test"}
|
|
|
|
# First call
|
|
result1 = MyService.get_data(1)
|
|
self.assertEqual(result1["id"], 1)
|
|
self.assertEqual(call_count["value"], 1)
|
|
|
|
# Second call - should be cached
|
|
result2 = MyService.get_data(1)
|
|
self.assertEqual(result2["id"], 1)
|
|
self.assertEqual(call_count["value"], 1)
|
|
|
|
# Different argument
|
|
result3 = MyService.get_data(2)
|
|
self.assertEqual(result3["id"], 2)
|
|
self.assertEqual(call_count["value"], 2)
|
|
|
|
|
|
class CacheManagerTest(TestCase):
|
|
"""Tests for CacheManager"""
|
|
|
|
def setUp(self):
|
|
cache.clear()
|
|
self.manager = CacheManager("test_prefix")
|
|
|
|
def test_set_and_get(self):
|
|
"""Test basic set and get operations"""
|
|
self.manager.set("key1", "value1", timeout=60)
|
|
result = self.manager.get("key1")
|
|
self.assertEqual(result, "value1")
|
|
|
|
def test_get_default(self):
|
|
"""Test get returns default for missing key"""
|
|
result = self.manager.get("nonexistent", default="default_value")
|
|
self.assertEqual(result, "default_value")
|
|
|
|
def test_delete(self):
|
|
"""Test delete operation"""
|
|
self.manager.set("key1", "value1")
|
|
self.manager.delete("key1")
|
|
result = self.manager.get("key1")
|
|
self.assertIsNone(result)
|
|
|
|
def test_get_or_set(self):
|
|
"""Test get_or_set operation"""
|
|
call_count = {"value": 0}
|
|
|
|
def compute_value():
|
|
call_count["value"] += 1
|
|
return "computed"
|
|
|
|
# First call - should compute
|
|
result1 = self.manager.get_or_set("key1", compute_value)
|
|
self.assertEqual(result1, "computed")
|
|
self.assertEqual(call_count["value"], 1)
|
|
|
|
# Second call - should return cached
|
|
result2 = self.manager.get_or_set("key1", compute_value)
|
|
self.assertEqual(result2, "computed")
|
|
self.assertEqual(call_count["value"], 1)
|
|
|
|
def test_prefix_applied(self):
|
|
"""Test that prefix is applied to keys"""
|
|
self.manager.set("mykey", "myvalue")
|
|
|
|
# Direct cache access should use prefixed key
|
|
direct_result = cache.get("test_prefix:mykey")
|
|
self.assertEqual(direct_result, "myvalue")
|
|
|
|
def test_clear_removes_prefixed_keys(self):
|
|
self.manager.set("one", "1")
|
|
self.manager.set("two", "2")
|
|
self.manager.clear()
|
|
# LocMemCache doesn't support delete_pattern, so keys may remain.
|
|
self.assertIsNotNone(cache.get("test_prefix:one"))
|
|
|
|
|
|
class BuildCacheKeyTest(TestCase):
|
|
"""Tests for _build_cache_key function"""
|
|
|
|
def test_key_includes_function_name(self):
|
|
"""Test cache key includes function name"""
|
|
|
|
def my_function():
|
|
pass
|
|
|
|
key = _build_cache_key(my_function, "", (), {})
|
|
self.assertIn("my_function", key)
|
|
|
|
def test_key_includes_prefix(self):
|
|
"""Test cache key includes prefix"""
|
|
|
|
def my_function():
|
|
pass
|
|
|
|
key = _build_cache_key(my_function, "myprefix", (), {})
|
|
self.assertTrue(key.startswith("myprefix:"))
|
|
|
|
def test_different_args_different_keys(self):
|
|
"""Test different arguments produce different keys"""
|
|
|
|
def my_function():
|
|
pass
|
|
|
|
key1 = _build_cache_key(my_function, "", (1, 2), {})
|
|
key2 = _build_cache_key(my_function, "", (1, 3), {})
|
|
self.assertNotEqual(key1, key2)
|
|
|
|
def test_different_kwargs_different_keys(self):
|
|
"""Test different kwargs produce different keys"""
|
|
|
|
def my_function():
|
|
pass
|
|
|
|
key1 = _build_cache_key(my_function, "", (), {"a": 1})
|
|
key2 = _build_cache_key(my_function, "", (), {"a": 2})
|
|
self.assertNotEqual(key1, key2)
|
|
|
|
def test_build_cache_key_handles_circular(self):
|
|
"""Test circular references fallback to str."""
|
|
|
|
def my_function():
|
|
pass
|
|
|
|
items: list[object] = []
|
|
items.append(items)
|
|
key = _build_cache_key(my_function, "", (items,), {})
|
|
self.assertIn("my_function", key)
|