117 lines
4.0 KiB
Python
117 lines
4.0 KiB
Python
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import asyncio
|
||
|
|
import time
|
||
|
|
|
||
|
|
from yuxi.channel.routing.cache import RouteCache, get_default_route_cache
|
||
|
|
|
||
|
|
|
||
|
|
class TestRouteCache:
|
||
|
|
async def test_get_returns_none_for_missing_key(self):
|
||
|
|
cache = RouteCache()
|
||
|
|
assert await cache.get("missing") is None
|
||
|
|
|
||
|
|
async def test_set_and_get(self):
|
||
|
|
cache = RouteCache()
|
||
|
|
await cache.set("key", "value")
|
||
|
|
assert await cache.get("key") == "value"
|
||
|
|
|
||
|
|
async def test_get_moves_entry_to_end(self):
|
||
|
|
cache = RouteCache(maxsize=2)
|
||
|
|
await cache.set("a", 1)
|
||
|
|
await cache.set("b", 2)
|
||
|
|
await cache.get("a")
|
||
|
|
await cache.set("c", 3)
|
||
|
|
assert await cache.get("a") == 1
|
||
|
|
assert await cache.get("b") is None
|
||
|
|
assert await cache.get("c") == 3
|
||
|
|
|
||
|
|
async def test_expired_entry_is_removed(self):
|
||
|
|
cache = RouteCache(ttl_seconds=0.01)
|
||
|
|
await cache.set("key", "value")
|
||
|
|
await asyncio.sleep(0.02)
|
||
|
|
assert await cache.get("key") is None
|
||
|
|
|
||
|
|
async def test_set_updates_existing_key(self):
|
||
|
|
cache = RouteCache()
|
||
|
|
await cache.set("key", "old")
|
||
|
|
await cache.set("key", "new")
|
||
|
|
assert await cache.get("key") == "new"
|
||
|
|
|
||
|
|
async def test_maxsize_evicts_oldest(self):
|
||
|
|
cache = RouteCache(maxsize=2)
|
||
|
|
await cache.set("a", 1)
|
||
|
|
await cache.set("b", 2)
|
||
|
|
await cache.set("c", 3)
|
||
|
|
assert await cache.get("a") is None
|
||
|
|
assert await cache.get("b") == 2
|
||
|
|
assert await cache.get("c") == 3
|
||
|
|
|
||
|
|
async def test_maxsize_at_least_one(self):
|
||
|
|
cache = RouteCache(maxsize=0)
|
||
|
|
await cache.set("a", 1)
|
||
|
|
await cache.set("b", 2)
|
||
|
|
assert await cache.get("a") is None
|
||
|
|
assert await cache.get("b") == 2
|
||
|
|
|
||
|
|
async def test_clear_removes_all(self):
|
||
|
|
cache = RouteCache()
|
||
|
|
await cache.set("a", 1)
|
||
|
|
await cache.set("b", 2)
|
||
|
|
await cache.clear()
|
||
|
|
assert await cache.get("a") is None
|
||
|
|
assert await cache.get("b") is None
|
||
|
|
|
||
|
|
async def test_invalidate_by_channel(self):
|
||
|
|
cache = RouteCache()
|
||
|
|
await cache.set("feishu:acc1:dm:user1", 1)
|
||
|
|
await cache.set("feishu:acc1:dm:user2", 2)
|
||
|
|
await cache.set("slack:acc1:dm:user1", 3)
|
||
|
|
removed = await cache.invalidate_by_channel("feishu")
|
||
|
|
assert removed == 2
|
||
|
|
assert await cache.get("feishu:acc1:dm:user1") is None
|
||
|
|
assert await cache.get("slack:acc1:dm:user1") == 3
|
||
|
|
|
||
|
|
async def test_invalidate_by_account(self):
|
||
|
|
cache = RouteCache()
|
||
|
|
await cache.set("feishu:acc1:dm:user1", 1)
|
||
|
|
await cache.set("feishu:acc2:dm:user1", 2)
|
||
|
|
await cache.set("feishu:acc1:group:g1", 3)
|
||
|
|
removed = await cache.invalidate_by_account("feishu", "acc1")
|
||
|
|
assert removed == 2
|
||
|
|
assert await cache.get("feishu:acc1:dm:user1") is None
|
||
|
|
assert await cache.get("feishu:acc2:dm:user1") == 2
|
||
|
|
|
||
|
|
async def test_concurrent_access_does_not_raise(self):
|
||
|
|
cache = RouteCache()
|
||
|
|
|
||
|
|
async def worker(idx: int) -> None:
|
||
|
|
for i in range(20):
|
||
|
|
await cache.set(f"key-{idx}-{i}", i)
|
||
|
|
await cache.get(f"key-{idx}-{i}")
|
||
|
|
|
||
|
|
await asyncio.gather(*[worker(i) for i in range(5)])
|
||
|
|
|
||
|
|
async def test_custom_ttl_is_used(self, monkeypatch):
|
||
|
|
cache = RouteCache(ttl_seconds=10)
|
||
|
|
now = time.time()
|
||
|
|
monkeypatch.setattr(time, "time", lambda: now)
|
||
|
|
await cache.set("key", "value")
|
||
|
|
monkeypatch.setattr(time, "time", lambda: now + 5)
|
||
|
|
assert await cache.get("key") == "value"
|
||
|
|
monkeypatch.setattr(time, "time", lambda: now + 11)
|
||
|
|
assert await cache.get("key") is None
|
||
|
|
|
||
|
|
|
||
|
|
class TestDefaultRouteCache:
|
||
|
|
def test_returns_same_instance(self):
|
||
|
|
first = get_default_route_cache()
|
||
|
|
second = get_default_route_cache()
|
||
|
|
assert first is second
|
||
|
|
assert isinstance(first, RouteCache)
|
||
|
|
|
||
|
|
def test_global_instance_has_expected_defaults(self):
|
||
|
|
cache = get_default_route_cache()
|
||
|
|
assert cache.maxsize == 4000
|
||
|
|
assert cache.ttl == 60
|