Coverage for agentos/tests/test_rate_limiter.py: 0%
236 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-09 07:12 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-09 07:12 +0800
1"""Comprehensive tests for agentos/tools/rate_limiter.py."""
3import time
5import pytest
7from agentos.tools.rate_limiter import (
8 RateLimitExceeded,
9 SlidingWindow,
10 TokenBucket,
11)
14# ============================================================================
15# RateLimitExceeded
16# ============================================================================
19class TestRateLimitExceeded:
20 def test_constructor_and_attributes(self):
21 exc = RateLimitExceeded(key="api:test", limit=10.0, window=60.0)
22 assert exc.key == "api:test"
23 assert exc.limit == 10.0
24 assert exc.window == 60.0
26 def test_string_message(self):
27 exc = RateLimitExceeded(key="user:42", limit=5.0, window=1.0)
28 msg = str(exc)
29 assert "user:42" in msg
30 assert "5.0" in msg
31 assert "1.0" in msg
34# ============================================================================
35# TokenBucket
36# ============================================================================
39class TestTokenBucketInit:
40 def test_default_burst_equals_rate(self):
41 tb = TokenBucket(rate=10.0)
42 assert tb._rate == 10.0
43 assert tb._burst == 10.0
45 def test_custom_burst(self):
46 tb = TokenBucket(rate=5.0, burst=20.0)
47 assert tb._rate == 5.0
48 assert tb._burst == 20.0
50 def test_rate_zero_raises(self):
51 with pytest.raises(ValueError, match="positive"):
52 TokenBucket(rate=0)
54 def test_rate_negative_raises(self):
55 with pytest.raises(ValueError, match="positive"):
56 TokenBucket(rate=-1.0)
58 def test_rate_property(self):
59 tb = TokenBucket(rate=7.5)
60 assert tb.rate == 7.5
63class TestTokenBucketTryAcquire:
64 def test_first_acquire_succeeds(self):
65 tb = TokenBucket(rate=10.0, burst=5.0)
66 assert tb.try_acquire("key1") is True
68 def test_acquire_within_burst(self):
69 tb = TokenBucket(rate=10.0, burst=5.0)
70 for _ in range(5):
71 assert tb.try_acquire("key1") is True
72 # 6th should fail
73 assert tb.try_acquire("key1") is False
75 def test_acquire_multi_tokens(self):
76 tb = TokenBucket(rate=10.0, burst=10.0)
77 assert tb.try_acquire("key1", tokens=5.0) is True
78 assert tb.try_acquire("key1", tokens=5.0) is True
79 assert tb.try_acquire("key1", tokens=5.0) is False
81 def test_different_keys_independent(self):
82 tb = TokenBucket(rate=10.0, burst=2.0)
83 assert tb.try_acquire("key1") is True
84 assert tb.try_acquire("key1") is True
85 assert tb.try_acquire("key1") is False
86 # key2 should be unaffected
87 assert tb.try_acquire("key2") is True
89 def test_refill_after_wait(self):
90 tb = TokenBucket(rate=100.0, burst=2.0) # 100 tokens/sec
91 assert tb.try_acquire("key1") is True
92 assert tb.try_acquire("key1") is True
93 assert tb.try_acquire("key1") is False
94 time.sleep(0.02) # ~2 tokens refilled
95 assert tb.try_acquire("key1") is True
97 def test_refill_capped_at_burst(self):
98 tb = TokenBucket(rate=1000.0, burst=3.0)
99 for _ in range(3):
100 assert tb.try_acquire("key1") is True
101 assert tb.try_acquire("key1") is False
102 time.sleep(0.1) # would refill 100, but burst caps at 3
103 # should have exactly 3 tokens, not 100
104 for _ in range(3):
105 assert tb.try_acquire("key1") is True
106 assert tb.try_acquire("key1") is False
108 def test_acquire_exact_tokens(self):
109 tb = TokenBucket(rate=10.0, burst=10.0)
110 assert tb.try_acquire("key1", tokens=10.0) is True
111 assert tb.try_acquire("key1") is False
114class TestTokenBucketAcquireOrWait:
115 def test_immediate_success(self):
116 tb = TokenBucket(rate=10.0, burst=5.0)
117 assert tb.acquire_or_wait("key1", timeout=1.0) is True
119 def test_blocks_and_waits_for_refill(self):
120 tb = TokenBucket(rate=200.0, burst=2.0) # 200 tokens/sec
121 for _ in range(2):
122 tb.try_acquire("key1")
123 # Should block briefly then succeed
124 assert tb.acquire_or_wait("key1", timeout=1.0) is True
126 def test_timeout_expires(self):
127 tb = TokenBucket(rate=0.1, burst=1.0) # very slow refill
128 tb.try_acquire("key1")
129 assert tb.acquire_or_wait("key1", timeout=0.01) is False
131 def test_no_timeout_blocks_indefinitely(self):
132 tb = TokenBucket(rate=500.0, burst=1.0)
133 tb.try_acquire("key1")
134 # Should succeed within a very short time without explicit timeout
135 assert tb.acquire_or_wait("key1", timeout=None) is True
137 def test_multi_token_acquire_or_wait(self):
138 tb = TokenBucket(rate=500.0, burst=5.0)
139 assert tb.acquire_or_wait("key1", timeout=1.0, tokens=5.0) is True
140 assert tb.try_acquire("key1") is False
143class TestTokenBucketReset:
144 def test_reset_removes_key(self):
145 tb = TokenBucket(rate=10.0, burst=3.0)
146 for _ in range(3):
147 tb.try_acquire("key1")
148 assert tb.try_acquire("key1") is False
149 tb.reset("key1")
150 assert tb.try_acquire("key1") is True
152 def test_reset_nonexistent_key_no_error(self):
153 tb = TokenBucket(rate=10.0)
154 tb.reset("no-such-key")
156 def test_reset_all_clears_everything(self):
157 tb = TokenBucket(rate=10.0, burst=2.0)
158 tb.try_acquire("key1")
159 tb.try_acquire("key2")
160 tb.reset_all()
161 assert len(tb._buckets) == 0
162 assert tb.try_acquire("key1") is True
165class TestTokenBucketStats:
166 def test_initial_stats(self):
167 tb = TokenBucket(rate=10.0, burst=20.0)
168 s = tb.stats()
169 assert s["rate"] == 10.0
170 assert s["burst"] == 20.0
171 assert s["active_keys"] == 0
172 assert s["total_acquired"] == 0
173 assert s["total_rejected"] == 0
175 def test_stats_after_acquire(self):
176 tb = TokenBucket(rate=10.0, burst=5.0)
177 for _ in range(3):
178 tb.try_acquire("key1")
179 s = tb.stats()
180 assert s["total_acquired"] == 3
181 assert s["active_keys"] == 1
183 def test_stats_after_reject(self):
184 tb = TokenBucket(rate=10.0, burst=1.0)
185 tb.try_acquire("key1")
186 tb.try_acquire("key1") # rejected — same key, bucket exhausted
187 s = tb.stats()
188 assert s["total_acquired"] == 1
189 assert s["total_rejected"] == 1
192# ============================================================================
193# SlidingWindow
194# ============================================================================
197class TestSlidingWindowInit:
198 def test_default_window(self):
199 sw = SlidingWindow(limit=100)
200 assert sw._limit == 100
201 assert sw._window == 60.0
203 def test_custom_window(self):
204 sw = SlidingWindow(limit=50, window=30.0)
205 assert sw._limit == 50
206 assert sw._window == 30.0
208 def test_limit_zero_raises(self):
209 with pytest.raises(ValueError, match="positive"):
210 SlidingWindow(limit=0)
212 def test_limit_negative_raises(self):
213 with pytest.raises(ValueError, match="positive"):
214 SlidingWindow(limit=-5)
216 def test_limit_property(self):
217 sw = SlidingWindow(limit=42)
218 assert sw.limit == 42
221class TestSlidingWindowTryAcquire:
222 def test_within_limit(self):
223 sw = SlidingWindow(limit=5, window=60.0)
224 for _ in range(5):
225 assert sw.try_acquire("key1") is True
227 def test_exceeds_limit(self):
228 sw = SlidingWindow(limit=3, window=60.0)
229 for _ in range(3):
230 assert sw.try_acquire("key1") is True
231 assert sw.try_acquire("key1") is False
233 def test_different_keys_independent(self):
234 sw = SlidingWindow(limit=2, window=60.0)
235 assert sw.try_acquire("key1") is True
236 assert sw.try_acquire("key1") is True
237 assert sw.try_acquire("key1") is False
238 assert sw.try_acquire("key2") is True
240 def test_eviction_of_old_entries(self):
241 sw = SlidingWindow(limit=2, window=0.05) # 50ms window
242 assert sw.try_acquire("key1") is True
243 assert sw.try_acquire("key1") is True
244 assert sw.try_acquire("key1") is False
245 time.sleep(0.06)
246 assert sw.try_acquire("key1") is True
249class TestSlidingWindowAcquireOrWait:
250 def test_immediate_success(self):
251 sw = SlidingWindow(limit=5, window=60.0)
252 assert sw.acquire_or_wait("key1", timeout=1.0) is True
254 def test_blocks_and_waits(self):
255 sw = SlidingWindow(limit=2, window=0.05)
256 for _ in range(2):
257 sw.try_acquire("key1")
258 assert sw.acquire_or_wait("key1", timeout=1.0) is True
260 def test_timeout_expires(self):
261 sw = SlidingWindow(limit=1, window=60.0)
262 sw.try_acquire("key1")
263 assert sw.acquire_or_wait("key1", timeout=0.01) is False
265 def test_no_timeout_succeeds(self):
266 sw = SlidingWindow(limit=1, window=0.02)
267 sw.try_acquire("key1")
268 assert sw.acquire_or_wait("key1", timeout=None) is True
271class TestSlidingWindowReset:
272 def test_reset_removes_key(self):
273 sw = SlidingWindow(limit=1, window=60.0)
274 sw.try_acquire("key1")
275 assert sw.try_acquire("key1") is False
276 sw.reset("key1")
277 assert sw.try_acquire("key1") is True
279 def test_reset_nonexistent_no_error(self):
280 sw = SlidingWindow(limit=10)
281 sw.reset("ghost")
283 def test_reset_all(self):
284 sw = SlidingWindow(limit=2, window=60.0)
285 sw.try_acquire("key1")
286 sw.try_acquire("key2")
287 sw.reset_all()
288 assert len(sw._windows) == 0
289 assert sw.try_acquire("key1") is True
292class TestSlidingWindowStats:
293 def test_initial_stats(self):
294 sw = SlidingWindow(limit=100, window=60.0)
295 s = sw.stats()
296 assert s["limit"] == 100
297 assert s["window"] == 60.0
298 assert s["active_keys"] == 0
299 assert s["total_acquired"] == 0
300 assert s["total_rejected"] == 0
302 def test_stats_after_use(self):
303 sw = SlidingWindow(limit=5, window=60.0)
304 for _ in range(3):
305 sw.try_acquire("key1")
306 sw.try_acquire("key2")
307 s = sw.stats()
308 assert s["total_acquired"] == 4
309 assert s["active_keys"] >= 1
311 def test_stats_after_reject(self):
312 sw = SlidingWindow(limit=2, window=60.0)
313 for _ in range(2):
314 sw.try_acquire("key1")
315 sw.try_acquire("key1") # rejected
316 sw.try_acquire("key1") # rejected
317 s = sw.stats()
318 assert s["total_acquired"] == 2
319 assert s["total_rejected"] == 2