"""cache.py 本地层(不碰 Redis):TTL 过期、容量淘汰、熔断冷却恢复。""" from __future__ import annotations import pytest import app.cache as cache_mod from app import cache class _FakeClock: """替换 cache 命名空间里的 time(不影响全局 time 模块)。""" def __init__(self): self.now = 1000.0 def monotonic(self) -> float: return self.now def time(self) -> float: return self.now @pytest.fixture(autouse=True) def _reset_state(): """每个用例干净的本地缓存状态。""" cache._local_store.clear() cache._local_bytes = 0 cache._disabled_until = 0.0 yield cache._local_store.clear() cache._local_bytes = 0 cache._disabled_until = 0.0 def test_local_set_get_roundtrip(): cache.local_set("k", '{"a":1}', ttl=60) assert cache.local_get("k") == '{"a":1}' def test_local_get_miss(): assert cache.local_get("nope") is None def test_local_expiry(monkeypatch): clock = _FakeClock() monkeypatch.setattr(cache_mod, "time", clock) cache.local_set("k", "v", ttl=10) clock.now = 1005.0 assert cache.local_get("k") == "v" clock.now = 1101.0 # 过期 assert cache.local_get("k") is None assert "k" not in cache._local_store # 过期读取顺手清理 def test_local_ttl_capped_at_120s(): """本地层恒 ≤120s(多进程部署时最多比 Redis 多陈旧 120s 的约定)。""" base = cache.time.monotonic() cache.local_set("k", "v", ttl=99999) ent = cache._local_store["k"] assert ent[0] - base <= 120.0 + 5 # 相对当前 monotonic 的上限(留误差余量) def test_local_entries_cap_evicts_oldest(): for i in range(cache._LOCAL_MAX_ENTRIES + 5): cache.local_set(f"k{i}", "v", ttl=60) assert len(cache._local_store) <= cache._LOCAL_MAX_ENTRIES # 先插入的(最旧)被近似 LRU 淘汰 assert cache.local_get("k0") is None assert cache.local_get(f"k{cache._LOCAL_MAX_ENTRIES + 4}") == "v" def test_local_overwrite_releases_bytes(): cache.local_set("k", "x" * 1000, ttl=60) before = cache._local_bytes cache.local_set("k", "y", ttl=60) assert cache._local_bytes < before assert cache.local_get("k") == "y" def test_bail_cooldown_recovers(monkeypatch): """熔断 60s:期间 _client 为 None,到期自动放行(Redis 未配置时也返回 None,但不熔断)。""" clock = _FakeClock() monkeypatch.setattr(cache_mod, "time", clock) cache._bail() assert cache._disabled_until > clock.now clock.now += 59.0 assert clock.monotonic() < cache._disabled_until # 仍在熔断期 clock.now += 2.0 # 越过 60s 冷却 assert clock.monotonic() >= cache._disabled_until # 恢复探测资格 def test_digest_stable_and_distinct(): assert cache.digest("a", 1, None) == cache.digest("a", 1, None) assert cache.digest("a", 1) != cache.digest("a", 2)