import threading import unittest from rate_limiter import RateLimiter class TestRateLimiter(unittest.TestCase): def test_basic_admit_and_reject(self): t = [0.0] rl = RateLimiter(limit=2, window=10.0, clock=lambda: t[0]) self.assertTrue(rl.allow()) self.assertTrue(rl.allow()) self.assertFalse(rl.allow()) def test_window_expiry(self): t = [0.0] rl = RateLimiter(limit=1, window=5.0, clock=lambda: t[0]) self.assertTrue(rl.allow()) t[0] = 4.999 self.assertFalse(rl.allow()) t[0] = 5.0 # exactly window old -> expired self.assertTrue(rl.allow()) def test_boundary_half_open(self): t = [0.0] rl = RateLimiter(limit=2, window=1.0, clock=lambda: t[0]) self.assertTrue(rl.allow()) # ts 0 t[0] = 0.5 self.assertTrue(rl.allow()) # ts 0.5 t[0] = 1.0 # ts 0 expired (1.0-0>=1), ts 0.5 still live (1.0-0.5=0.5<1) self.assertTrue(rl.allow()) # admits, now 2 live? ts0.5 + new # window now has 0.5 and 1.0 -> full self.assertFalse(rl.allow()) def test_limit_zero_rejects(self): rl = RateLimiter(limit=0, window=10.0, clock=lambda: 0.0) self.assertFalse(rl.allow()) def test_window_zero_rejects(self): rl = RateLimiter(limit=5, window=0.0, clock=lambda: 0.0) self.assertFalse(rl.allow()) def test_negative_args(self): with self.assertRaises(ValueError): RateLimiter(limit=-1, window=1.0) with self.assertRaises(ValueError): RateLimiter(limit=1, window=-1.0) def test_reset(self): t = [0.0] rl = RateLimiter(limit=1, window=10.0, clock=lambda: t[0]) self.assertTrue(rl.allow()) self.assertFalse(rl.allow()) rl.reset() self.assertTrue(rl.allow()) def test_count(self): t = [0.0] rl = RateLimiter(limit=5, window=10.0, clock=lambda: t[0]) rl.allow(); rl.allow() self.assertEqual(rl.count(), 2) t[0] = 20.0 self.assertEqual(rl.count(), 0) def test_thread_safety(self): t = [0.0] rl = RateLimiter(limit=1000, window=10.0, clock=lambda: t[0]) admitted = [0] lock = threading.Lock() def worker(): for _ in range(500): if rl.allow(): with lock: admitted[0] += 1 threads = [threading.Thread(target=worker) for _ in range(4)] for th in threads: th.start() for th in threads: th.join() self.assertEqual(admitted[0], 1000) def test_concurrent_never_exceeds_limit(self): t = [0.0] rl = RateLimiter(limit=50, window=10.0, clock=lambda: t[0]) admitted = [0] lock = threading.Lock() def worker(): for _ in range(200): if rl.allow(): with lock: admitted[0] += 1 threads = [threading.Thread(target=worker) for _ in range(8)] for th in threads: th.start() for th in threads: th.join() self.assertLessEqual(admitted[0], 50) if __name__ == "__main__": unittest.main()