Skip to content

Commit e2ab67c

Browse files
authored
Use freezegun to speed up rate limit tests (#14)
1 parent 1e737dd commit e2ab67c

File tree

2 files changed

+71
-60
lines changed

2 files changed

+71
-60
lines changed

pyproject.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,14 +95,20 @@ dependencies = [
9595
"httpx==0.28.1",
9696
"ms_cv==0.1.1",
9797
"pydantic==2.12.3",
98+
"pytest==8.4.2",
99+
"pytest-asyncio==1.2.0",
100+
"pytest-cov==7.0.0",
101+
"freezegun==1.5.5",
102+
"respx~=0.22",
98103
]
99104

100105
[tool.hatch.envs.hatch-test]
101106
parallel = true
102107
extra-dependencies = [
103108
"pytest-cov~=6.2",
104109
"pytest-asyncio~=1.1",
105-
"respx~=0.22"
110+
"respx~=0.22",
111+
"freezegun==1.5.5"
106112
]
107113
extra-args = ["--cov=xbox/webapi/", "--cov-report=term-missing", "--cov-report=xml", "-vv"]
108114

tests/test_ratelimits.py

Lines changed: 64 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
import asyncio
21
from datetime import datetime, timedelta
32

3+
from freezegun import freeze_time
4+
from freezegun.api import FrozenDateTimeFactory
45
from httpx import Response
56
import pytest
67

8+
from tests.common import get_response_json
79
from xbox.webapi.api.provider.ratelimitedprovider import RateLimitedProvider
810
from xbox.webapi.common.exceptions import RateLimitExceededException, XboxException
911
from xbox.webapi.common.ratelimits import CombinedRateLimit
1012
from xbox.webapi.common.ratelimits.models import TimePeriod
1113

12-
from tests.common import get_response_json
13-
1414

1515
def helper_test_combinedratelimit(
1616
crl: CombinedRateLimit, burstLimit: int, sustainLimit: int
@@ -143,7 +143,11 @@ async def make_request():
143143

144144

145145
async def helper_reach_and_wait_for_burst(
146-
make_request, start_time, burst_limit: int, expected_counter: int
146+
make_request,
147+
start_time,
148+
burst_limit: int,
149+
expected_counter: int,
150+
frozen_datetime: FrozenDateTimeFactory
147151
):
148152
# Make as many requests as possible without exceeding the BURST limit.
149153
for _ in range(burst_limit):
@@ -164,79 +168,80 @@ async def helper_reach_and_wait_for_burst(
164168
burst_resets_after = ex.rate_limit.get_reset_after()
165169

166170
# Wait for the burst limit timeout to elapse.
167-
await asyncio.sleep(TimePeriod.BURST.value) # 15 seconds
171+
frozen_datetime.tick(timedelta(seconds=TimePeriod.BURST.value))
168172

169173
# Assert that the reset_after value has passed.
170-
assert burst_resets_after < datetime.now()
174+
assert burst_resets_after == datetime.now()
175+
frozen_datetime.tick(timedelta(seconds=1))
171176

172177

173178
@pytest.mark.asyncio
174179
async def test_ratelimits_exceeded_sustain_only(respx_mock, xbl_client):
175-
async def make_request():
176-
route = respx_mock.get("https://social.xboxlive.com").mock(
177-
return_value=Response(200, json=get_response_json("people_summary_own"))
178-
)
179-
await xbl_client.people.get_friends_summary_own()
180-
181-
assert route.called
180+
with freeze_time("2025-10-30T00:00:00-00:00") as frozen_datetime:
181+
async def make_request():
182+
route = respx_mock.get("https://social.xboxlive.com").mock(
183+
return_value=Response(200, json=get_response_json("people_summary_own"))
184+
)
185+
await xbl_client.people.get_friends_summary_own()
182186

183-
# Record the start time to ensure that the timeouts are the correct length
184-
start_time = datetime.now()
187+
assert route.called
185188

186-
# Get the max requests for this route.
187-
max_request_num = xbl_client.people.RATE_LIMITS["sustain"] # 30
188-
burst_max_request_num = xbl_client.people.RATE_LIMITS["burst"] # 10
189+
# Record the start time to ensure that the timeouts are the correct length
190+
start_time = datetime.now()
189191

190-
# In this case, the BURST limit is three times that of SUSTAIN, so we need to exceed the burst limit three times.
192+
# Get the max requests for this route.
193+
max_request_num = xbl_client.people.RATE_LIMITS["sustain"] # 30
194+
burst_max_request_num = xbl_client.people.RATE_LIMITS["burst"] # 10
191195

192-
# Exceed the burst limit and wait for it to reset (10 requests)
193-
await helper_reach_and_wait_for_burst(
194-
make_request, start_time, burst_limit=burst_max_request_num, expected_counter=10
195-
)
196+
# In this case, the BURST limit is three times that of SUSTAIN, so we need to exceed the burst limit three times.
196197

197-
# Repeat: Exceed the burst limit and wait for it to reset (10 requests)
198-
# Counter (the sustain one will be returned)
199-
# For (CombinedRateLimit).get_counter(), the highest counter is returned. (sustain in this case)
200-
await helper_reach_and_wait_for_burst(
201-
make_request, start_time, burst_limit=burst_max_request_num, expected_counter=20
202-
)
198+
# Exceed the burst limit and wait for it to reset (10 requests)
199+
await helper_reach_and_wait_for_burst(
200+
make_request, start_time, burst_limit=burst_max_request_num, expected_counter=10, frozen_datetime=frozen_datetime
201+
)
203202

204-
# Now, make the rest of the requests (10 left, 20/30 done!)
205-
for _ in range(10):
206-
await make_request()
203+
# Repeat: Exceed the burst limit and wait for it to reset (10 requests)
204+
# Counter (the sustain one will be returned)
205+
# For (CombinedRateLimit).get_counter(), the highest counter is returned. (sustain in this case)
206+
await helper_reach_and_wait_for_burst(
207+
make_request, start_time, burst_limit=burst_max_request_num, expected_counter=20, frozen_datetime=frozen_datetime
208+
)
207209

208-
# Wait for the burst limit to 'reset'.
209-
await asyncio.sleep(TimePeriod.BURST.value) # 15 seconds
210+
# Now, make the rest of the requests (10 left, 20/30 done!)
211+
for _ in range(10):
212+
await make_request()
210213

211-
# Now, we have made 30 requests.
212-
# The counters should be as follows:
213-
# - BURST: 0* (will reset on next check)
214-
# - SUSTAIN: 30
215-
# The next request we make should exceed the SUSTAIN rate limit.
214+
# Wait for the burst limit to 'reset'.
215+
frozen_datetime.tick(timedelta(seconds=TimePeriod.BURST.value+1))
216+
# Now, we have made 30 requests.
217+
# The counters should be as follows:
218+
# - BURST: 0* (will reset on next check)
219+
# - SUSTAIN: 30
220+
# The next request we make should exceed the SUSTAIN rate limit.
216221

217-
# Make another request, ensure that it raises the exception.
218-
with pytest.raises(RateLimitExceededException) as exception:
219-
await make_request()
222+
# Make another request, ensure that it raises the exception.
223+
with pytest.raises(RateLimitExceededException) as exception:
224+
await make_request()
220225

221-
# Get the error instance from pytest
222-
ex: RateLimitExceededException = exception.value
226+
# Get the error instance from pytest
227+
ex: RateLimitExceededException = exception.value
223228

224-
# Get the SingleRateLimit objects from the exception
225-
rl: CombinedRateLimit = ex.rate_limit
226-
burst = rl.get_limits_by_period(TimePeriod.BURST)[0]
227-
sustain = rl.get_limits_by_period(TimePeriod.SUSTAIN)[0]
229+
# Get the SingleRateLimit objects from the exception
230+
rl: CombinedRateLimit = ex.rate_limit
231+
burst = rl.get_limits_by_period(TimePeriod.BURST)[0]
232+
sustain = rl.get_limits_by_period(TimePeriod.SUSTAIN)[0]
228233

229-
# Assert that we have only exceeded the sustain limit.
230-
assert not burst.is_exceeded()
231-
assert sustain.is_exceeded()
234+
# Assert that we have only exceeded the sustain limit.
235+
assert not burst.is_exceeded()
236+
assert sustain.is_exceeded()
232237

233-
# Assert that the counter matches the max request num (should not have incremented above max value)
234-
assert ex.rate_limit.get_counter() == max_request_num
238+
# Assert that the counter matches the max request num (should not have incremented above max value)
239+
assert ex.rate_limit.get_counter() == max_request_num
235240

236-
# Get the timeout we were issued
237-
try_again_in = ex.rate_limit.get_reset_after()
241+
# Get the timeout we were issued
242+
try_again_in = ex.rate_limit.get_reset_after()
238243

239-
# Assert that the timeout is the correct length
240-
# The SUSTAIN counter has not been reset during this test, so the try again in should be 300 seconds since we started this test.
241-
delta: timedelta = try_again_in - start_time
242-
assert delta.seconds == TimePeriod.SUSTAIN.value # 300 seconds (5 minutes)
244+
# Assert that the timeout is the correct length
245+
# The SUSTAIN counter has not been reset during this test, so the try again in should be 300 seconds since we started this test.
246+
delta: timedelta = try_again_in - start_time
247+
assert delta.seconds == TimePeriod.SUSTAIN.value # 300 seconds (5 minutes)

0 commit comments

Comments
 (0)