1
1
import asyncio
2
2
import sys
3
3
import time
4
+ from functools import wraps
5
+ from typing import Any , Callable , List , TypeVar
4
6
5
7
import pytest
6
8
7
9
from async_timeout import Timeout , timeout , timeout_at
8
10
9
11
12
+ _Func = TypeVar ("_Func" , bound = Callable [..., Any ])
13
+
14
+
15
+ def log_func (func : _Func , msg : str , call_order : List [str ]) -> _Func :
16
+ """Simple wrapper to add a log to call_order when the function is called."""
17
+
18
+ @wraps (func )
19
+ def wrapper (* args : Any , ** kwargs : Any ) -> Any : # type: ignore[misc]
20
+ call_order .append (msg )
21
+ return func (* args , ** kwargs )
22
+
23
+ return wrapper # type: ignore[return-value]
24
+
25
+
10
26
@pytest .mark .asyncio
11
27
async def test_timeout () -> None :
12
28
canceled_raised = False
@@ -347,15 +363,8 @@ async def test_deprecated_with() -> None:
347
363
await asyncio .sleep (0 )
348
364
349
365
350
- @pytest .mark .skipif (sys .version_info < (3 , 7 ), reason = "Not supported in 3.6" )
351
- @pytest .mark .asyncio
352
- async def test_race_condition_cancel_before () -> None :
353
- """Test race condition when cancelling before timeout.
354
-
355
- If cancel happens immediately before the timeout, then
356
- the timeout may overrule the cancellation, making it
357
- impossible to cancel some tasks.
358
- """
366
+ async def race_condition (offset : float = 0 ) -> List [str ]:
367
+ """Common code for below race condition tests."""
359
368
360
369
async def test_task (deadline : float , loop : asyncio .AbstractEventLoop ) -> None :
361
370
# We need the internal Timeout class to specify the deadline (not delay).
@@ -364,39 +373,51 @@ async def test_task(deadline: float, loop: asyncio.AbstractEventLoop) -> None:
364
373
with Timeout (deadline , loop ):
365
374
await asyncio .sleep (10 )
366
375
376
+ call_order : List [str ] = []
377
+ f_exit = log_func (Timeout ._do_exit , "exit" , call_order )
378
+ Timeout ._do_exit = f_exit # type: ignore[assignment]
379
+ f_timeout = log_func (Timeout ._on_timeout , "timeout" , call_order )
380
+ Timeout ._on_timeout = f_timeout # type: ignore[assignment]
381
+
367
382
loop = asyncio .get_running_loop ()
368
383
deadline = loop .time () + 1
369
384
t = asyncio .create_task (test_task (deadline , loop ))
370
- loop .call_at (deadline , t .cancel )
385
+ loop .call_at (deadline + offset , log_func ( t .cancel , "cancel" , call_order ) )
371
386
# If we get a TimeoutError, then the code is broken.
372
387
with pytest .raises (asyncio .CancelledError ):
373
388
await t
374
389
390
+ return call_order
391
+
375
392
376
- @pytest .mark .xfail (
377
- reason = "The test is CPU performance sensitive, might fail on slow CI box"
378
- )
379
393
@pytest .mark .skipif (sys .version_info < (3 , 7 ), reason = "Not supported in 3.6" )
380
394
@pytest .mark .asyncio
395
+ async def test_race_condition_cancel_before () -> None :
396
+ """Test race condition when cancelling before timeout.
397
+
398
+ If cancel happens immediately before the timeout, then
399
+ the timeout may overrule the cancellation, making it
400
+ impossible to cancel some tasks.
401
+ """
402
+ call_order = await race_condition ()
403
+
404
+ # This test is very timing dependant, so we check the order that calls
405
+ # happened to be sure the test itself ran correctly.
406
+ assert call_order == ["cancel" , "timeout" , "exit" ]
407
+
408
+
409
+ @pytest .mark .xfail (reason = "Can't see a way to fix this currently." )
410
+ @pytest .mark .skipif (sys .version_info < (3 , 9 ), reason = "Can't be fixed in <3.9." )
411
+ @pytest .mark .asyncio
381
412
async def test_race_condition_cancel_after () -> None :
382
413
"""Test race condition when cancelling after timeout.
383
414
384
415
Similarly to the previous test, if a cancel happens
385
416
immediately after the timeout (but before the __exit__),
386
417
then the explicit cancel can get overruled again.
387
418
"""
419
+ call_order = await race_condition (0.000001 )
388
420
389
- async def test_task (deadline : float , loop : asyncio .AbstractEventLoop ) -> None :
390
- # We need the internal Timeout class to specify the deadline (not delay).
391
- # This is needed to create the precise timing to reproduce the race condition.
392
- with pytest .warns (DeprecationWarning ):
393
- with Timeout (deadline , loop ):
394
- await asyncio .sleep (10 )
395
-
396
- loop = asyncio .get_running_loop ()
397
- deadline = loop .time () + 1
398
- t = asyncio .create_task (test_task (deadline , loop ))
399
- loop .call_at (deadline + 0.0000000000001 , t .cancel )
400
- # If we get a TimeoutError, then the code is broken.
401
- with pytest .raises (asyncio .CancelledError ):
402
- await t
421
+ # This test is very timing dependant, so we check the order that calls
422
+ # happened to be sure the test itself ran correctly.
423
+ assert call_order == ["timeout" , "cancel" , "exit" ]
0 commit comments