"""Tests for the shared request rate limiter (http_client.RateLimiter). Uses a fake clock so the spacing logic is asserted deterministically rather than by sleeping for real.""" import threading import pytest import http_client import shutdown from http_client import RateLimiter class _FakeClock: """A monotonic clock that only advances when sleep() is called.""" def __init__(self, start=1000.0): self.t = start self.sleeps = [] def monotonic(self): return self.t def sleep(self, seconds): self.sleeps.append(seconds) self.t += seconds def _install(monkeypatch): clock = _FakeClock() monkeypatch.setattr(http_client.time, "monotonic", clock.monotonic) # The limiter waits via the interruptible shutdown.sleep so a Ctrl+C wakes a # worker parked on a reserved rate-limit slot instead of stalling the exit. monkeypatch.setattr(shutdown, "sleep", clock.sleep) return clock def test_first_acquire_does_not_sleep(monkeypatch): clock = _install(monkeypatch) RateLimiter(10).acquire() assert clock.sleeps == [] def test_subsequent_acquires_are_spaced_by_the_interval(monkeypatch): clock = _install(monkeypatch) rl = RateLimiter(10) # 0.1s minimum interval rl.acquire() # no wait rl.acquire() # waits ~0.1 rl.acquire() # waits ~0.1 assert clock.sleeps == pytest.approx([0.1, 0.1]) def test_acquire_after_idle_gap_does_not_sleep(monkeypatch): clock = _install(monkeypatch) rl = RateLimiter(10) rl.acquire() clock.t += 5.0 # plenty of idle time passes rl.acquire() assert clock.sleeps == [] # the slot was already free def test_zero_rate_disables_limiting(monkeypatch): def _boom(_seconds): # pragma: no cover - must never be called raise AssertionError("disabled limiter must not sleep") monkeypatch.setattr(shutdown, "sleep", _boom) rl = RateLimiter(0) for _ in range(100): rl.acquire() def test_concurrent_acquires_are_all_spaced(monkeypatch): # Real clock, tiny rate: N threads hitting acquire() at once must be # serialised so the total wall time is at least (N-1) * interval. rl = RateLimiter(200) # 5ms interval, fast but measurable barrier = threading.Barrier(8) def worker(): barrier.wait() rl.acquire() start = http_client.time.monotonic() threads = [threading.Thread(target=worker) for _ in range(8)] for t in threads: t.start() for t in threads: t.join() elapsed = http_client.time.monotonic() - start assert elapsed >= 7 * 0.005 * 0.5 # allow scheduling slack, but not ~0