maestroai / tests /test_cache.py
gabrielpamplonapg
feat: MaestroAI v1.0 β€” instrument identification SaaS
d4d0bc7
Raw
History Blame Contribute Delete
5.97 kB
"""Tests for backend/cache.py β€” TTLCache and module-level singletons."""
import time
import threading
import pytest
from backend.cache import TTLCache, plans_cache, stats_cache
# ── Basic get/set ─────────────────────────────────────────────────────────────
def test_set_and_get_basic():
cache = TTLCache(ttl=60)
cache.set("key1", "value1")
assert cache.get("key1") == "value1"
def test_get_missing_key_returns_none():
cache = TTLCache(ttl=60)
assert cache.get("nonexistent") is None
def test_set_various_value_types():
cache = TTLCache(ttl=60)
cache.set("int_key", 42)
cache.set("dict_key", {"a": 1, "b": [1, 2, 3]})
cache.set("list_key", [1, 2, 3])
assert cache.get("int_key") == 42
assert cache.get("dict_key") == {"a": 1, "b": [1, 2, 3]}
assert cache.get("list_key") == [1, 2, 3]
# ── TTL expiry ────────────────────────────────────────────────────────────────
def test_expired_entry_returns_none():
cache = TTLCache(ttl=1)
cache.set("expiring", "will_expire", ttl=1)
time.sleep(1.1)
assert cache.get("expiring") is None
def test_ttl_not_yet_expired():
cache = TTLCache(ttl=60)
cache.set("fresh", "still_alive", ttl=60)
assert cache.get("fresh") == "still_alive"
def test_per_entry_ttl_overrides_default():
cache = TTLCache(ttl=60)
# Use a very short TTL for this specific entry
cache.set("short_lived", "gone_soon", ttl=1)
assert cache.get("short_lived") == "gone_soon"
time.sleep(1.1)
assert cache.get("short_lived") is None
def test_default_ttl_is_used_when_not_specified():
"""Setting an entry without explicit TTL uses the cache's default TTL."""
cache = TTLCache(ttl=300)
cache.set("default_ttl_key", "hello")
# Should be present because 300s haven't passed
assert cache.get("default_ttl_key") == "hello"
# ── delete() ──────────────────────────────────────────────────────────────────
def test_delete_removes_entry():
cache = TTLCache(ttl=60)
cache.set("to_delete", "present")
assert cache.get("to_delete") == "present"
cache.delete("to_delete")
assert cache.get("to_delete") is None
def test_delete_nonexistent_key_does_not_raise():
cache = TTLCache(ttl=60)
cache.delete("ghost_key") # should not raise
# ── clear() ───────────────────────────────────────────────────────────────────
def test_clear_empties_cache():
cache = TTLCache(ttl=60)
cache.set("a", 1)
cache.set("b", 2)
cache.set("c", 3)
cache.clear()
assert cache.get("a") is None
assert cache.get("b") is None
assert cache.get("c") is None
def test_clear_on_empty_cache_does_not_raise():
cache = TTLCache(ttl=60)
cache.clear() # should not raise
# ── size() ────────────────────────────────────────────────────────────────────
def test_size_returns_correct_count():
cache = TTLCache(ttl=60)
assert cache.size() == 0
cache.set("x", 1)
assert cache.size() == 1
cache.set("y", 2)
assert cache.size() == 2
cache.delete("x")
assert cache.size() == 1
cache.clear()
assert cache.size() == 0
def test_size_does_not_count_expired_entries():
"""Expired entries that have been accessed are removed, shrinking size."""
cache = TTLCache(ttl=1)
cache.set("expires", "soon", ttl=1)
assert cache.size() == 1
time.sleep(1.1)
# Access triggers eviction
cache.get("expires")
assert cache.size() == 0
# ── Thread safety ─────────────────────────────────────────────────────────────
def test_concurrent_reads_writes_do_not_crash():
"""Multiple threads reading and writing should not raise or corrupt data."""
cache = TTLCache(ttl=60)
errors = []
def writer(n):
try:
for i in range(50):
cache.set(f"key_{n}_{i}", f"val_{n}_{i}")
except Exception as e:
errors.append(e)
def reader(n):
try:
for i in range(50):
cache.get(f"key_{n}_{i}")
except Exception as e:
errors.append(e)
threads = []
for t in range(5):
threads.append(threading.Thread(target=writer, args=(t,)))
threads.append(threading.Thread(target=reader, args=(t,)))
for th in threads:
th.start()
for th in threads:
th.join()
assert errors == [], f"Thread errors: {errors}"
# ── Module-level singletons ───────────────────────────────────────────────────
def test_plans_cache_is_ttlcache_instance():
assert isinstance(plans_cache, TTLCache)
def test_stats_cache_is_ttlcache_instance():
assert isinstance(stats_cache, TTLCache)
def test_plans_cache_ttl_is_600():
assert plans_cache.ttl == 600
def test_stats_cache_ttl_is_120():
assert stats_cache.ttl == 120
def test_plans_cache_and_stats_cache_are_different_objects():
assert plans_cache is not stats_cache
def test_singletons_are_same_object_on_reimport():
from backend.cache import plans_cache as pc2, stats_cache as sc2
assert plans_cache is pc2
assert stats_cache is sc2