Small fixes
This commit is contained in:
parent
54fbcb1ea6
commit
083f8a982e
24 changed files with 1505 additions and 79 deletions
90
finder/test_http_client.py
Normal file
90
finder/test_http_client.py
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
"""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
|
||||
Loading…
Add table
Add a link
Reference in a new issue