22
33import asyncio
44import queue
5+ import signal
6+ import socket
7+ import sys
58import threading
69import time
710import unittest
11+ from test .support import threading_helper
12+ from test .support .script_helper import assert_python_ok
13+
14+ threading_helper .requires_working_threading (module = True )
15+
16+
17+ def tearDownModule ():
18+ asyncio .events ._set_event_loop_policy (None )
819
920
1021class MockHost :
@@ -48,8 +59,13 @@ def run(self, timeout=10.0):
4859 return self ._task
4960
5061
51- class TestGuestRun (unittest .TestCase ):
52- """Test asyncio.start_guest_run with a mock host loop."""
62+ class GuestTestCase (unittest .TestCase ):
63+
64+ def setUp (self ):
65+ self ._thread_key = threading_helper .threading_setup ()
66+
67+ def tearDown (self ):
68+ threading_helper .threading_cleanup (* self ._thread_key )
5369
5470 def _run_guest (self , async_fn , * args , timeout = 10.0 ):
5571 """Helper: run *async_fn* in guest mode and return the completed task."""
@@ -61,6 +77,10 @@ def _run_guest(self, async_fn, *args, timeout=10.0):
6177 )
6278 return host .run (timeout = timeout )
6379
80+
81+ class TestGuestRun (GuestTestCase ):
82+ """Test asyncio.start_guest_run with a mock host loop."""
83+
6484 # -- basic lifecycle -----------------------------------------------
6585
6686 def test_simple_return (self ):
@@ -85,6 +105,20 @@ async def add(a, b):
85105 task = self ._run_guest (add , 3 , 7 )
86106 self .assertEqual (task .result (), 10 )
87107
108+ def test_early_sync_completion (self ):
109+ # The task can already be done when the I/O thread starts.
110+ async def coro ():
111+ return 'early'
112+
113+ host = MockHost ()
114+ task = asyncio .start_guest_run (
115+ coro ,
116+ run_sync_soon_threadsafe = host .run_sync_soon_threadsafe ,
117+ done_callback = host .done_callback ,
118+ )
119+ self .assertIs (host .run (), task )
120+ self .assertEqual (task .result (), 'early' )
121+
88122 # -- exception propagation -----------------------------------------
89123
90124 def test_exception (self ):
@@ -124,14 +158,13 @@ async def coro():
124158
125159 def test_sleep (self ):
126160 async def coro ():
127- t0 = asyncio .get_event_loop ().time ()
161+ loop = asyncio .get_running_loop ()
162+ t0 = loop .time ()
128163 await asyncio .sleep (0.1 )
129- elapsed = asyncio .get_event_loop ().time () - t0
130- return elapsed
164+ return loop .time () - t0
131165
132166 task = self ._run_guest (coro )
133- elapsed = task .result ()
134- self .assertGreaterEqual (elapsed , 0.05 )
167+ self .assertGreaterEqual (task .result (), 0.05 )
135168
136169 def test_create_task (self ):
137170 async def helper ():
@@ -140,8 +173,7 @@ async def helper():
140173
141174 async def coro ():
142175 t = asyncio .ensure_future (helper ())
143- result = await t
144- return result
176+ return await t
145177
146178 task = self ._run_guest (coro )
147179 self .assertEqual (task .result (), "helper" )
@@ -152,17 +184,14 @@ async def sleeper(n):
152184 return n
153185
154186 async def coro ():
155- results = await asyncio .gather (
156- sleeper (1 ), sleeper (2 ), sleeper (3 )
157- )
158- return results
187+ return await asyncio .gather (sleeper (1 ), sleeper (2 ), sleeper (3 ))
159188
160189 task = self ._run_guest (coro )
161190 self .assertEqual (task .result (), [1 , 2 , 3 ])
162191
163192 def test_call_later (self ):
164193 async def coro ():
165- loop = asyncio .get_event_loop ()
194+ loop = asyncio .get_running_loop ()
166195 fut = loop .create_future ()
167196 loop .call_later (0.05 , fut .set_result , "later" )
168197 return await fut
@@ -171,20 +200,213 @@ async def coro():
171200 self .assertEqual (task .result (), "later" )
172201
173202 def test_call_soon_threadsafe (self ):
203+ timer = None
204+
174205 async def coro ():
175- loop = asyncio .get_event_loop ()
206+ nonlocal timer
207+ loop = asyncio .get_running_loop ()
176208 fut = loop .create_future ()
177209
178210 def setter ():
179211 loop .call_soon_threadsafe (fut .set_result , "safe" )
180- threading .Timer (0.05 , setter ).start ()
212+ timer = threading .Timer (0.05 , setter )
213+ timer .start ()
181214 return await fut
182215
183216 task = self ._run_guest (coro )
184217 self .assertEqual (task .result (), "safe" )
218+ timer .join ()
219+
220+ # -- thread lifecycle ----------------------------------------------
221+
222+ def test_io_thread_nondaemon_and_joined (self ):
223+ seen = {}
224+
225+ async def coro ():
226+ # The I/O thread is started after the initial batch; a sleep
227+ # guarantees it is up and polling by the time we look.
228+ await asyncio .sleep (0.01 )
229+ for thread in threading .enumerate ():
230+ if thread .name == 'asyncio-guest-io' :
231+ seen ['thread' ] = thread
232+
233+ task = self ._run_guest (coro )
234+ self .assertIsNone (task .exception ())
235+ self .assertIn ('thread' , seen )
236+ self .assertFalse (seen ['thread' ].daemon )
237+ self .assertFalse (seen ['thread' ].is_alive ())
238+
239+ def test_interpreter_exit_with_pending_run (self ):
240+ # Exiting with an unfinished guest run must not hang: the atexit
241+ # hook wakes the non-daemon I/O thread out of its selector wait
242+ # and joins it.
243+ code = (
244+ 'import asyncio, collections\n '
245+ 'q = collections.deque()\n '
246+ 'async def coro():\n '
247+ ' await asyncio.sleep(3600)\n '
248+ 'asyncio.start_guest_run(\n '
249+ ' coro,\n '
250+ ' run_sync_soon_threadsafe=q.append,\n '
251+ ' done_callback=lambda task: None,\n '
252+ ')\n '
253+ )
254+ assert_python_ok ('-c' , code )
255+
256+ # -- running-loop semantics ----------------------------------------
257+
258+ def test_is_running_inside (self ):
259+ async def coro ():
260+ return asyncio .get_running_loop ().is_running ()
261+
262+ task = self ._run_guest (coro )
263+ self .assertTrue (task .result ())
264+
265+ def test_nested_run_raises (self ):
266+ test = self
267+
268+ async def coro ():
269+ loop = asyncio .get_running_loop ()
270+ inner = asyncio .sleep (0 )
271+ try :
272+ with test .assertRaises (RuntimeError ):
273+ loop .run_until_complete (inner )
274+ finally :
275+ inner .close ()
276+ inner = asyncio .sleep (0 )
277+ try :
278+ with test .assertRaises (RuntimeError ):
279+ asyncio .run (inner )
280+ finally :
281+ inner .close ()
282+
283+ task = self ._run_guest (coro )
284+ self .assertIsNone (task .exception ())
285+
286+ def test_state_restored_after_run (self ):
287+ old_hooks = sys .get_asyncgen_hooks ()
288+
289+ async def coro ():
290+ pass
291+
292+ task = self ._run_guest (coro )
293+ self .assertEqual (sys .get_asyncgen_hooks (), old_hooks )
294+ self .assertIsNone (asyncio ._get_running_loop ())
295+ self .assertFalse (task .get_loop ().is_running ())
296+
297+ # -- signal handling -----------------------------------------------
298+
299+ @unittest .skipUnless (hasattr (signal , 'SIGUSR1' ),
300+ 'requires UNIX signal handling' )
301+ def test_add_signal_handler_raises (self ):
302+ async def coro ():
303+ loop = asyncio .get_running_loop ()
304+ loop .add_signal_handler (signal .SIGUSR1 , lambda : None )
305+
306+ task = self ._run_guest (coro )
307+ with self .assertRaisesRegex (RuntimeError , 'guest mode' ):
308+ task .result ()
309+
310+ @unittest .skipUnless (hasattr (signal , 'SIGUSR1' ),
311+ 'requires UNIX signal handling' )
312+ def test_remove_signal_handler_raises (self ):
313+ async def coro ():
314+ loop = asyncio .get_running_loop ()
315+ loop .remove_signal_handler (signal .SIGUSR1 )
316+
317+ task = self ._run_guest (coro )
318+ with self .assertRaisesRegex (RuntimeError , 'guest mode' ):
319+ task .result ()
320+
321+ @unittest .skipUnless (hasattr (signal , 'set_wakeup_fd' ),
322+ 'requires signal.set_wakeup_fd' )
323+ def test_wakeup_fd_preserved (self ):
324+ if threading .current_thread () is not threading .main_thread ():
325+ self .skipTest ('requires the main thread' )
326+ rsock , wsock = socket .socketpair ()
327+ self .addCleanup (rsock .close )
328+ self .addCleanup (wsock .close )
329+ wsock .setblocking (False )
330+ old_fd = signal .set_wakeup_fd (wsock .fileno ())
331+ self .addCleanup (signal .set_wakeup_fd , old_fd )
332+
333+ async def coro ():
334+ await asyncio .sleep (0.01 )
335+
336+ self ._run_guest (coro )
337+
338+ fd = signal .set_wakeup_fd (- 1 )
339+ if fd != - 1 :
340+ signal .set_wakeup_fd (fd )
341+ self .assertEqual (fd , wsock .fileno ())
342+
343+ # -- final cleanup matches asyncio.run() ---------------------------
344+
345+ def test_background_task_cancelled_on_finish (self ):
346+ state = {}
347+
348+ async def background ():
349+ await asyncio .sleep (3600 )
350+
351+ async def coro ():
352+ state ['bg' ] = asyncio .get_running_loop ().create_task (background ())
353+ await asyncio .sleep (0.01 )
354+
355+ task = self ._run_guest (coro )
356+ self .assertIsNone (task .exception ())
357+ self .assertTrue (state ['bg' ].cancelled ())
358+
359+ def test_abandoned_asyncgen_finalized (self ):
360+ finalized = False
361+ holder = []
362+
363+ async def agen ():
364+ nonlocal finalized
365+ try :
366+ yield 1
367+ finally :
368+ finalized = True
369+
370+ async def coro ():
371+ it = agen ()
372+ holder .append (it ) # keep it alive until shutdown_asyncgens()
373+ await anext (it )
374+
375+ task = self ._run_guest (coro )
376+ self .assertIsNone (task .exception ())
377+ self .assertTrue (finalized )
378+
379+ def test_loop_closed_in_done_callback (self ):
380+ # Cleanup (cancel remaining tasks, close the loop) happens
381+ # before done_callback, like asyncio.run().
382+ seen = {}
383+ host = MockHost ()
384+ original = host .done_callback
385+
386+ def done_callback (task ):
387+ seen ['closed' ] = task .get_loop ().is_closed ()
388+ original (task )
389+
390+ async def coro ():
391+ pass
392+
393+ asyncio .start_guest_run (
394+ coro ,
395+ run_sync_soon_threadsafe = host .run_sync_soon_threadsafe ,
396+ done_callback = done_callback ,
397+ )
398+ host .run ()
399+ self .assertTrue (seen ['closed' ])
400+
401+ def test_loop_closed_after_run (self ):
402+ async def coro ():
403+ pass
404+
405+ task = self ._run_guest (coro )
406+ self .assertTrue (task .get_loop ().is_closed ())
185407
186408
187- class TestBaseEventLoopDecomposition (unittest . TestCase ):
409+ class TestBaseEventLoopDecomposition (GuestTestCase ):
188410 """Verify that poll_events / process_events / process_ready exist
189411 and compose correctly (i.e. _run_once still works)."""
190412
0 commit comments