Skip to content

Commit 512c5b1

Browse files
adiaybguomerdor001
authored andcommitted
Unit Tests for wtinylfu_cache.py and arc_cache.py
Co-authored-by: omerdor001 <omerdo@post.bgu.ac.il> Co-authored-by: adiaybgu <adiay@post.bgu.ac.il>
1 parent a37f92e commit 512c5b1

File tree

5 files changed

+457
-2
lines changed

5 files changed

+457
-2
lines changed

modelcache/manager/eviction/wtinylfu_cache.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,13 +117,21 @@ def _put(self, key):
117117
def _admit_to_main(self, key):
118118
if key in self.protected or key in self.probation:
119119
return
120+
if self.probation_size == 0:
121+
if self.on_evict:
122+
self.on_evict(key)
123+
self.data.pop(key, None)
124+
return
120125
if len(self.probation) < self.probation_size:
121126
self.probation[key] = True
122-
else:
127+
elif self.probation:
123128
evicted = next(iter(self.probation))
124129
self.probation.pop(evicted)
125130
self.probation[key] = True
126-
# this eviction removes it entirely
127131
if self.on_evict:
128132
self.on_evict(evicted)
129133
self.data.pop(evicted, None)
134+
else:
135+
if self.on_evict:
136+
self.on_evict(key)
137+
self.data.pop(key, None)

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,5 @@ snowflake-id==1.0.2
2020
flagembedding==1.3.4
2121
cryptography==45.0.2
2222
sentence-transformers==4.1.0
23+
pytest>=8.0
24+

tests/__init__.py

Whitespace-only changes.

tests/test_arc_cache.py

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
import pytest
2+
from modelcache.manager.eviction.arc_cache import ARC
3+
4+
@pytest.fixture()
5+
def empty_arc():
6+
return ARC(maxsize=4)
7+
8+
@pytest.fixture()
9+
def arc_with_data():
10+
c = ARC(maxsize=4)
11+
c['a'] = 1
12+
c['b'] = 2
13+
return c
14+
15+
def test_setitem_adds_to_arc(empty_arc):
16+
"""Test __setitem__ adds a key-value pair to ARC."""
17+
empty_arc['x'] = 123
18+
assert 'x' in empty_arc
19+
assert empty_arc['x'] == 123
20+
21+
def test_setitem_overwrites_value(empty_arc):
22+
"""Test that __setitem__ overwrites existing value in ARC."""
23+
empty_arc['y'] = 1
24+
empty_arc['y'] = 55
25+
assert empty_arc['y'] == 55
26+
27+
def test_getitem_returns_value(arc_with_data):
28+
"""Test __getitem__ returns the correct value if present."""
29+
assert arc_with_data['a'] == 1
30+
31+
def test_getitem_raises_keyerror_on_missing(empty_arc):
32+
"""Test __getitem__ raises KeyError if key is missing."""
33+
with pytest.raises(KeyError):
34+
_ = empty_arc['nope']
35+
36+
def test_contains_true_for_present(arc_with_data):
37+
"""Test __contains__ returns True for a present key."""
38+
assert 'b' in arc_with_data
39+
40+
def test_contains_false_for_missing(arc_with_data):
41+
"""Test __contains__ returns False for missing key."""
42+
assert 'notfound' not in arc_with_data
43+
44+
def test_len_reports_active_cache_size(arc_with_data):
45+
"""Test __len__ reports only active items (T1 + T2)."""
46+
assert len(arc_with_data) == 2
47+
arc_with_data['c'] = 3
48+
assert len(arc_with_data) == 3
49+
50+
def test_pop_removes_key_from_one_list(arc_with_data):
51+
"""Test pop removes key from the first ARC list where it is found."""
52+
arc_with_data['ghost'] = 9
53+
arc_with_data.b1['ghost'] = 9
54+
arc_with_data.pop('ghost')
55+
assert 'ghost' not in arc_with_data.t1
56+
assert 'ghost' in arc_with_data.b1
57+
58+
def test_clear_removes_all_keys(empty_arc):
59+
"""Test clear() empties all lists and resets p."""
60+
empty_arc['a'] = 1
61+
empty_arc['b'] = 2
62+
empty_arc.clear()
63+
assert len(empty_arc.t1) == 0
64+
assert len(empty_arc.t2) == 0
65+
assert len(empty_arc.b1) == 0
66+
assert len(empty_arc.b2) == 0
67+
assert empty_arc.p == 0
68+
69+
def test_evict_internal_evicts_when_over_capacity():
70+
"""Test _evict_internal evicts oldest when ARC is full."""
71+
evicted = []
72+
c = ARC(maxsize=2, on_evict=lambda keys: evicted.extend(keys))
73+
c['a'] = 1
74+
c['b'] = 2
75+
c['c'] = 3
76+
assert len(c) == 2
77+
assert len(evicted) >= 1
78+
for k in evicted:
79+
assert k in ['a', 'b']
80+
81+
def test_eviction_callback_is_called():
82+
"""Test on_evict callback is called on eviction."""
83+
evicted = []
84+
c = ARC(maxsize=2, on_evict=lambda keys: evicted.extend(keys))
85+
c['x'] = 1
86+
c['y'] = 2
87+
c['z'] = 3
88+
assert len(evicted) > 0
89+
assert all(isinstance(k, str) for k in evicted)
90+
91+
def test_promote_from_t1_to_t2(empty_arc):
92+
"""Test that accessing key in T1 promotes it to T2."""
93+
empty_arc['foo'] = 10
94+
assert 'foo' in empty_arc.t1
95+
_ = empty_arc['foo']
96+
assert 'foo' in empty_arc.t2
97+
98+
def test_refresh_in_t2_updates_order(empty_arc):
99+
"""Test that repeated access in T2 keeps item in T2."""
100+
empty_arc['x'] = 1
101+
_ = empty_arc['x'] # promote to T2
102+
empty_arc['y'] = 2
103+
_ = empty_arc['x'] # refresh x in T2
104+
assert 'x' in empty_arc.t2
105+
assert empty_arc.t2.popitem(last=True)[0] == 'x'
106+
107+
def test_hit_in_ghost_lists_promotes_to_t2(empty_arc):
108+
"""Test access in ghost list B1 promotes key to T2."""
109+
empty_arc['a'] = 1
110+
empty_arc['b'] = 2
111+
empty_arc['c'] = 3 # triggers eviction to ghost B1
112+
if empty_arc.b1:
113+
ghost_key = next(iter(empty_arc.b1))
114+
empty_arc.__missing__ = lambda key: 999
115+
_ = empty_arc[ghost_key]
116+
assert ghost_key in empty_arc.t2
117+
118+
def test_iter_lists_keys_in_order(arc_with_data):
119+
"""Test __iter__ yields keys from T1 and then T2."""
120+
arc_with_data['c'] = 3
121+
keys = list(iter(arc_with_data))
122+
expected = list(arc_with_data.t1.keys()) + list(arc_with_data.t2.keys())
123+
assert keys == expected
124+
125+
def test_repr_outputs_status(empty_arc):
126+
"""Test __repr__ returns a string with cache stats."""
127+
r = repr(empty_arc)
128+
assert r.startswith("ARC(")
129+
assert "maxsize" in r
130+
131+
# ----------- Additional/Edge case tests -----------
132+
133+
def test_pop_missing_key_returns_default(empty_arc):
134+
"""Test that pop() returns default if key not found in any list."""
135+
assert empty_arc.pop('missing', default='sentinel') == 'sentinel'
136+
137+
def test_setitem_multiple_evictions():
138+
"""Test multiple evictions in sequence do not corrupt the cache."""
139+
evicted = []
140+
c = ARC(maxsize=2, on_evict=lambda keys: evicted.extend(keys))
141+
c['a'] = 1
142+
c['b'] = 2
143+
c['c'] = 3
144+
c['d'] = 4
145+
assert len(c) == 2
146+
assert len(evicted) >= 2
147+
assert all(isinstance(k, str) for k in evicted)
148+
149+
def test_access_promotes_b1_and_b2(empty_arc):
150+
"""Test accessing a key in B1 or B2 increases/decreases p appropriately."""
151+
empty_arc['a'] = 1
152+
empty_arc['b'] = 2
153+
empty_arc['c'] = 3
154+
if empty_arc.b1:
155+
ghost_key = next(iter(empty_arc.b1))
156+
empty_arc.__missing__ = lambda key: 555
157+
p_before = empty_arc.p
158+
_ = empty_arc[ghost_key]
159+
assert empty_arc.p > p_before or empty_arc.p == empty_arc.maxsize
160+
161+
def test_active_and_ghost_lists_dont_exceed_maxsize():
162+
"""Test ghost lists (B1/B2) and active lists (T1/T2) don't exceed maxsize."""
163+
c = ARC(maxsize=3)
164+
for k in ['a', 'b', 'c', 'd', 'e']:
165+
c[k] = ord(k)
166+
# B1 + B2 should never be larger than maxsize
167+
assert len(c.b1) + len(c.b2) <= c.maxsize
168+
assert len(c.t1) + len(c.t2) <= c.maxsize
169+
170+
def test_clear_resets_all_lists_and_p(empty_arc):
171+
"""Test that clear() resets all lists and p after many ops."""
172+
for k in 'abcd':
173+
empty_arc[k] = ord(k)
174+
empty_arc.clear()
175+
assert not any([empty_arc.t1, empty_arc.t2, empty_arc.b1, empty_arc.b2])
176+
assert empty_arc.p == 0
177+
178+
def test_repr_is_informative(empty_arc):
179+
"""Test that __repr__ outputs all important stats."""
180+
empty_arc['q'] = 9
181+
r = repr(empty_arc)
182+
assert "t1_len" in r and "b1_len" in r and "p=" in r
183+
184+
def test_setitem_duplicate_key_resets_position(empty_arc):
185+
"""Test that setting the same key again resets its position."""
186+
empty_arc['x'] = 10
187+
empty_arc['y'] = 11
188+
empty_arc['x'] = 99
189+
# x should be last in t1
190+
assert list(empty_arc.t1.keys())[-1] == 'x'
191+
assert empty_arc['x'] == 99
192+
193+
def test_eviction_of_promoted_key():
194+
"""Test that a key promoted to T2 can still be evicted if capacity is exceeded."""
195+
evicted = []
196+
c = ARC(maxsize=2, on_evict=lambda keys: evicted.extend(keys))
197+
c['a'] = 1
198+
c['b'] = 2
199+
_ = c['a'] # Promote 'a' to T2
200+
c['c'] = 3 # Evict one of the keys
201+
assert len(c) == 2
202+
assert len(evicted) >= 1
203+
204+
def test_keyerror_on_missing_and_no_default(empty_arc):
205+
"""Test pop() raises KeyError if no default is given and key is missing."""
206+
with pytest.raises(KeyError):
207+
empty_arc.pop('never-there')
208+

0 commit comments

Comments
 (0)