# retoor import random from devplacepy.services.nodes.cache import ResultCache, cache_key from devplacepy.services.nodes.spec import SPEC LIMITS = SPEC.limits class Clock: def __init__(self): self.now = 100.0 def __call__(self): return self.now def make(**kwargs): clock = Clock() return ResultCache(clock=clock, **kwargs), clock def test_hit_and_miss_counters(): cache, _ = make() assert cache.get("GET", "u") is None assert cache.put("GET", "u", 200, {"a": "b"}, b"xyz", False) hit = cache.get("GET", "u") assert hit.body == b"xyz" and hit.headers == {"a": "b"} assert (cache.hits, cache.misses) == (1, 1) def test_method_is_part_of_the_key(): assert cache_key("GET", "u") != cache_key("HEAD", "u") assert cache_key("get", "u") == cache_key("GET", "u") def test_entries_expire_after_the_ttl(): cache, clock = make(ttl_seconds=10) cache.put("GET", "u", 200, {}, b"x", False) clock.now += 9.9 assert cache.get("GET", "u") is not None clock.now += 0.2 assert cache.get("GET", "u") is None assert len(cache) == 0 and cache.size_bytes == 0 def test_ttl_is_clamped_to_the_spec_ceiling_and_zero_disables(): assert ResultCache(ttl_seconds=10**9).ttl_seconds == LIMITS.cache_ttl_seconds off, _ = make(ttl_seconds=0) assert off.put("GET", "u", 200, {}, b"x", False) is False def test_errors_and_truncated_bodies_are_never_cached(): cache, _ = make() assert not cache.put("GET", "u", 500, {}, b"x", False) assert not cache.put("GET", "u", 503, {}, b"x", False) assert not cache.put("GET", "u", 200, {}, b"x", True) assert cache.put("GET", "u", 404, {}, b"x", False) def test_an_oversized_entry_is_refused(): cache, _ = make(entry_max_bytes=10, max_bytes=100) assert not cache.put("GET", "u", 200, {}, b"x" * 11, False) assert cache.put("GET", "u", 200, {}, b"x" * 10, False) def test_the_entry_cap_cannot_exceed_the_spec_ceiling(): assert ResultCache(entry_max_bytes=10**12).entry_max_bytes == LIMITS.cache_entry_max_bytes def test_least_recently_used_goes_first(): cache, _ = make(max_bytes=30, entry_max_bytes=30) for name in "abc": cache.put("GET", name, 200, {}, b"x" * 10, False) cache.get("GET", "a") cache.put("GET", "d", 200, {}, b"x" * 10, False) assert cache.get("GET", "b") is None assert cache.get("GET", "a") is not None assert cache.get("GET", "c") is not None assert cache.get("GET", "d") is not None def test_replacing_an_entry_does_not_leak_bytes(): cache, _ = make() cache.put("GET", "u", 200, {}, b"x" * 100, False) cache.put("GET", "u", 200, {}, b"y" * 40, False) assert cache.size_bytes == 40 and len(cache) == 1 def test_size_never_exceeds_the_cap_under_a_random_workload(): cache, clock = make(max_bytes=1000, entry_max_bytes=400, ttl_seconds=50) rng = random.Random(7) for step in range(5000): clock.now += rng.random() key = f"k{rng.randint(0, 60)}" if rng.random() < 0.7: cache.put("GET", key, rng.choice([200, 404, 500]), {}, b"x" * rng.randint(0, 500), False) else: cache.get("GET", key) assert 0 <= cache.size_bytes <= 1000 assert cache.size_bytes == sum(len(e.body) for e in cache._entries.values()) def test_clear_empties_everything(): cache, _ = make() cache.put("GET", "u", 200, {}, b"x", False) cache.clear() assert len(cache) == 0 and cache.size_bytes == 0 def test_age_moves_the_expiry_forward(): cache, clock = make(ttl_seconds=100) cache.put("GET", "https://r/x", 200, {}, b"x", False, age_seconds=90) clock.now = 105 assert cache.get("GET", "https://r/x") is not None clock.now = 111 assert cache.get("GET", "https://r/x") is None def shared(local_db, **kwargs): from devplacepy.database import nodes as store from devplacepy.services.nodes.cache import SharedResultCache store.cache_clear() clock = Clock() return SharedResultCache(clock=clock, **kwargs), clock, store def test_a_second_process_sees_what_the_first_stored(local_db): first, _, store = shared(local_db, ttl_seconds=60) assert first.put("GET", "https://r/a", 200, {"content-type": "text/plain"}, b"payload", False) from devplacepy.services.nodes.cache import SharedResultCache second = SharedResultCache(ttl_seconds=60) hit = second.get("GET", "https://r/a") assert hit is not None and hit.body == b"payload" and hit.headers == {"content-type": "text/plain"} assert (second.hits, second.misses) == (1, 0) again = second.get("GET", "https://r/a") assert again is not None and second.hits == 2 store.cache_clear() def test_the_shared_layer_honours_ttl_truncation_and_server_errors(local_db): cache, _, store = shared(local_db, ttl_seconds=60) assert not cache.put("GET", "https://r/b", 500, {}, b"x", False) assert not cache.put("GET", "https://r/b", 200, {}, b"x", True) assert store.cache_stats()["entries"] == 0 cache.put("GET", "https://r/c", 200, {}, b"x", False) from datetime import datetime, timedelta, timezone assert store.cache_get("k-not-there") is None key = cache_key("GET", "https://r/c") assert store.cache_get(key) is not None assert store.cache_get(key, datetime.now(timezone.utc) + timedelta(seconds=120)) is None store.cache_clear() def test_clear_empties_both_layers(local_db): cache, _, store = shared(local_db, ttl_seconds=60) cache.put("GET", "https://r/d", 200, {}, b"x", False) cache.clear() assert len(cache) == 0 and store.cache_stats()["entries"] == 0 def test_the_shared_layer_prunes_expired_rows_and_evicts_oldest_over_the_cap(local_db): from datetime import datetime, timedelta, timezone cache, _, store = shared(local_db, ttl_seconds=60) now = datetime.now(timezone.utc) for index in range(4): store.cache_put(f"k{index}", "GET", f"https://r/{index}", 200, "{}", "AAAA", 1000, 60, now + timedelta(seconds=index)) store.cache_put("old", "GET", "https://r/old", 200, "{}", "AAAA", 10, 60, now - timedelta(seconds=300)) removed = store.cache_prune(2500) assert removed["expired"] == 1 and removed["evicted"] == 2 and removed["bytes"] == 2000 left = store.cache_stats() assert left["entries"] == 2 assert store.cache_get("k0") is None and store.cache_get("k3") is not None store.cache_clear()