"""Tests for shared rate limiter.""" from __future__ import annotations import threading import time import pytest from src.orchestration.rate_limiter import DummyRateLimiter, SharedRateLimiter class TestSharedRateLimiter: """Tests for SharedRateLimiter.""" def test_default_values(self) -> None: """Test default configuration values.""" limiter = SharedRateLimiter() assert limiter.max_concurrent == 5 assert limiter.delay == 1.2 limiter.shutdown() def test_custom_values(self) -> None: """Test custom configuration values.""" limiter = SharedRateLimiter(max_concurrent=3, delay=0.5) assert limiter.max_concurrent == 3 assert limiter.delay == 0.5 limiter.shutdown() def test_acquire_release(self) -> None: """Test basic acquire and release.""" limiter = SharedRateLimiter(max_concurrent=2, delay=0.01) try: limiter.acquire() limiter.release() finally: limiter.shutdown() def test_context_manager_call(self) -> None: """Test using limiter as context manager via __call__.""" limiter = SharedRateLimiter(max_concurrent=2, delay=0.01) try: with limiter(): pass finally: limiter.shutdown() def test_context_manager_direct(self) -> None: """Test using limiter as context manager directly.""" limiter = SharedRateLimiter(max_concurrent=2, delay=0.01) try: with limiter: pass finally: limiter.shutdown() def test_delay_enforcement(self) -> None: """Test that delay is enforced between calls.""" limiter = SharedRateLimiter(max_concurrent=2, delay=0.1) try: start = time.time() with limiter(): pass with limiter(): pass elapsed = time.time() - start # Should take at least delay seconds for second call assert elapsed >= 0.1 finally: limiter.shutdown() def test_max_concurrent_limit(self) -> None: """Test that max concurrent limit is enforced.""" limiter = SharedRateLimiter(max_concurrent=2, delay=0.01) lock = threading.Lock() concurrent_count = 0 max_observed = 0 def worker() -> None: nonlocal concurrent_count, max_observed with limiter: with lock: concurrent_count += 1 if concurrent_count > max_observed: max_observed = concurrent_count time.sleep(0.05) with lock: concurrent_count -= 1 try: threads = [threading.Thread(target=worker) for _ in range(5)] for t in threads: t.start() for t in threads: t.join() # Max concurrent should not exceed limit assert max_observed <= 2 finally: limiter.shutdown() def test_shutdown_idempotent(self) -> None: """Test that shutdown can be called multiple times.""" limiter = SharedRateLimiter(max_concurrent=2, delay=0.01) limiter.shutdown() limiter.shutdown() # Should not raise class TestDummyRateLimiter: """Tests for DummyRateLimiter.""" def test_default_values(self) -> None: """Test default configuration values.""" limiter = DummyRateLimiter() assert limiter.max_concurrent == 5 assert limiter.delay == 1.2 def test_acquire_release(self) -> None: """Test basic acquire and release.""" limiter = DummyRateLimiter(delay=0.01) limiter.acquire() limiter.release() def test_context_manager_call(self) -> None: """Test using limiter as context manager via __call__.""" limiter = DummyRateLimiter(delay=0.01) with limiter(): pass def test_context_manager_direct(self) -> None: """Test using limiter as context manager directly.""" limiter = DummyRateLimiter(delay=0.01) with limiter: pass def test_delay_enforcement(self) -> None: """Test that delay is enforced.""" limiter = DummyRateLimiter(delay=0.1) start = time.time() with limiter(): pass with limiter(): pass elapsed = time.time() - start assert elapsed >= 0.1 def test_no_concurrency_limit(self) -> None: """Test that there's no actual concurrency limit.""" limiter = DummyRateLimiter(delay=0.001) concurrent_count = 0 max_concurrent = 0 lock = threading.Lock() def worker() -> None: nonlocal concurrent_count, max_concurrent with limiter: with lock: concurrent_count += 1 max_concurrent = max(max_concurrent, concurrent_count) time.sleep(0.01) with lock: concurrent_count -= 1 threads = [threading.Thread(target=worker) for _ in range(10)] for t in threads: t.start() for t in threads: t.join() # With dummy limiter, all threads can run concurrently # (they still have delay but no semaphore) assert max_concurrent >= 1 def test_shutdown_noop(self) -> None: """Test that shutdown is a no-op.""" limiter = DummyRateLimiter() limiter.shutdown() limiter.shutdown() # Should not raise class TestLimiterInteroperability: """Tests for using different limiters interchangeably.""" def test_same_interface(self) -> None: """Test that both limiters have the same interface.""" for limiter_class in [SharedRateLimiter, DummyRateLimiter]: limiter = limiter_class(max_concurrent=2, delay=0.01) # Should have same methods assert hasattr(limiter, "acquire") assert hasattr(limiter, "release") assert hasattr(limiter, "shutdown") assert hasattr(limiter, "__enter__") assert hasattr(limiter, "__exit__") assert hasattr(limiter, "__call__") # Should have same attributes assert hasattr(limiter, "max_concurrent") assert hasattr(limiter, "delay") limiter.shutdown() def test_function_works_with_either(self) -> None: """Test that a function can use either limiter type.""" def do_work(limiter: SharedRateLimiter | DummyRateLimiter) -> str: with limiter: return "done" shared = SharedRateLimiter(max_concurrent=2, delay=0.01) dummy = DummyRateLimiter(delay=0.01) try: assert do_work(shared) == "done" assert do_work(dummy) == "done" finally: shared.shutdown() dummy.shutdown()