返回 douyin-downloader
test_rate_limiter.py
根目录 / tests / test_rate_limiter.py
1 import asyncio
2 import time
3
4 import pytest
5
6 from control.rate_limiter import RateLimiter
7
8
9 @pytest.mark.asyncio
10 async def test_rate_limiter_enforces_interval():
11 limiter = RateLimiter(max_per_second=10)
12 start = time.time()
13 for _ in range(5):
14 await limiter.acquire()
15 elapsed = time.time() - start
16 assert elapsed >= 0.4
17
18
19 @pytest.mark.asyncio
20 async def test_rate_limiter_invalid_value_uses_default():
21 limiter = RateLimiter(max_per_second=0)
22 assert limiter.max_per_second == 2
23
24 limiter_neg = RateLimiter(max_per_second=-5)
25 assert limiter_neg.max_per_second == 2
26
27
28 @pytest.mark.asyncio
29 async def test_rate_limiter_caps_concurrent_acquire_rate():
30 # 10 concurrent acquires at 2/s must take at least 4.5s.
31 limiter = RateLimiter(max_per_second=2)
32 start = time.time()
33 await asyncio.gather(*[limiter.acquire() for _ in range(10)])
34 elapsed = time.time() - start
35 assert elapsed >= 4.5, f"10 concurrent acquires finished in {elapsed:.2f}s, expected >= 4.5s"
36
37
38 @pytest.mark.asyncio
39 async def test_rate_limiter_spaces_consecutive_fires(monkeypatch):
40 # Each fire (acquire() returning) must be at least min_interval after the
41 # previous fire. The buggy implementation released the lock before its jitter
42 # sleep — with alternating max/min jitter, consecutive fires would cluster
43 # within the same millisecond, busting the per-second rate cap.
44 import control.rate_limiter as rl_mod
45
46 jitters = iter([0.5, 0.0] * 10)
47 monkeypatch.setattr(rl_mod.random, "uniform", lambda *_a, **_kw: next(jitters))
48
49 limiter = rl_mod.RateLimiter(max_per_second=2)
50 fire_times: list[float] = []
51
52 async def acquire_and_record():
53 await limiter.acquire()
54 fire_times.append(time.time())
55
56 await asyncio.gather(*[acquire_and_record() for _ in range(6)])
57
58 fire_times.sort()
59 intervals = [fire_times[i + 1] - fire_times[i] for i in range(len(fire_times) - 1)]
60 min_interval = 1.0 / 2
61 slack = 0.05
62 too_close = [round(i, 3) for i in intervals if i < min_interval - slack]
63 assert not too_close, (
64 f"fire intervals violate min_interval={min_interval}s: "
65 f"too_close={too_close} all={[round(i, 3) for i in intervals]}"
66 )
67
67 lines PYTHON