11import asyncio
2+ import time
23from concurrent .futures import ThreadPoolExecutor
4+ from datetime import timedelta
35from typing import Any
46
57import pytest
6- from common_library .async_tools import make_async , maybe_await
8+ from common_library .async_tools import (
9+ cancel_wait_task ,
10+ delayed_start ,
11+ make_async ,
12+ maybe_await ,
13+ )
714
815
916@make_async ()
@@ -13,7 +20,8 @@ def sync_function(x: int, y: int) -> int:
1320
1421@make_async ()
1522def sync_function_with_exception () -> None :
16- raise ValueError ("This is an error!" )
23+ msg = "This is an error!"
24+ raise ValueError (msg )
1725
1826
1927@pytest .mark .asyncio
@@ -93,3 +101,118 @@ def fetchone(self) -> Any: # pylint: disable=no-self-use
93101
94102 sync_result = await maybe_await (SyncResultProxy ().fetchone ())
95103 assert sync_result == {"id" : 2 , "name" : "test2" }
104+
105+
106+ async def test_cancel_and_wait ():
107+ state = {"started" : False , "cancelled" : False , "cleaned_up" : False }
108+ SLEEP_TIME = 5 # seconds
109+
110+ async def coro ():
111+ try :
112+ state ["started" ] = True
113+ await asyncio .sleep (SLEEP_TIME )
114+ except asyncio .CancelledError :
115+ state ["cancelled" ] = True
116+ raise
117+ finally :
118+ state ["cleaned_up" ] = True
119+
120+ task = asyncio .create_task (coro ())
121+ await asyncio .sleep (0.1 ) # Let coro start
122+
123+ start = time .time ()
124+ await cancel_wait_task (task )
125+
126+ elapsed = time .time () - start
127+ assert elapsed < SLEEP_TIME , "Task should be cancelled quickly"
128+ assert task .done ()
129+ assert task .cancelled ()
130+ assert state ["started" ]
131+ assert state ["cancelled" ]
132+ assert state ["cleaned_up" ]
133+
134+
135+ async def test_cancel_and_wait_propagates_external_cancel ():
136+ """
137+ This test ensures that if the caller of cancel_and_wait is cancelled,
138+ the CancelledError is not swallowed.
139+ """
140+
141+ async def coro ():
142+ try :
143+ await asyncio .sleep (4 )
144+ except asyncio .CancelledError :
145+ await asyncio .sleep (1 ) # simulate cleanup
146+ raise
147+
148+ inner_task = asyncio .create_task (coro ())
149+
150+ async def outer_coro ():
151+ try :
152+ await cancel_wait_task (inner_task )
153+ except asyncio .CancelledError :
154+ assert (
155+ not inner_task .cancelled ()
156+ ), "Internal Task DOES NOT RAISE CancelledError"
157+ raise
158+
159+ # Cancel the wrapper after a short delay
160+ outer_task = asyncio .create_task (outer_coro ())
161+ await asyncio .sleep (0.1 )
162+ outer_task .cancel ()
163+
164+ with pytest .raises (asyncio .CancelledError ):
165+ await outer_task
166+
167+ # Ensure the task was cancelled
168+ assert inner_task .cancelled () is False , "Task should not be cancelled initially"
169+
170+ done_event = asyncio .Event ()
171+
172+ def on_done (_ ):
173+ done_event .set ()
174+
175+ inner_task .add_done_callback (on_done )
176+ await done_event .wait ()
177+
178+
179+ async def test_cancel_and_wait_timeout_on_slow_cleanup ():
180+ """Test that cancel_and_wait raises TimeoutError when cleanup takes longer than max_delay"""
181+
182+ CLEANUP_TIME = 2 # seconds
183+
184+ async def slow_cleanup_coro ():
185+ try :
186+ await asyncio .sleep (10 ) # Long running task
187+ except asyncio .CancelledError :
188+ # Simulate slow cleanup that exceeds max_delay!
189+ await asyncio .sleep (CLEANUP_TIME )
190+ raise
191+
192+ task = asyncio .create_task (slow_cleanup_coro ())
193+ await asyncio .sleep (0.1 ) # Let the task start
194+
195+ # Cancel with a max_delay shorter than cleanup time
196+ with pytest .raises (TimeoutError ):
197+ await cancel_wait_task (
198+ task , max_delay = CLEANUP_TIME / 10
199+ ) # 0.2 seconds < 2 seconds cleanup
200+
201+ assert task .cancelled ()
202+
203+
204+ async def test_with_delay ():
205+ @delayed_start (timedelta (seconds = 0.2 ))
206+ async def decorated_awaitable () -> int :
207+ return 42
208+
209+ assert await decorated_awaitable () == 42
210+
211+ async def another_awaitable () -> int :
212+ return 42
213+
214+ decorated_another_awaitable = delayed_start (timedelta (seconds = 0.2 ))(
215+ another_awaitable
216+ )
217+
218+ assert await decorated_another_awaitable () == 42
0 commit comments