@@ -0,0 +1,109 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user