Skip to content

Commit befb594

Browse files
congzhangzhclaude
andcommitted
gh-145342: Expand asyncio guest mode tests
Follow test_asyncio conventions (threading_helper, policy reset) and cover the new behavior: non-daemon I/O thread joined after the run, running-loop semantics, signal-handler RuntimeError, restored asyncgen hooks, asyncio.run()-equivalent cleanup, wakeup fd preservation, and clean interpreter exit with an unfinished run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent abc03ec commit befb594

1 file changed

Lines changed: 239 additions & 17 deletions

File tree

Lib/test/test_asyncio/test_guest.py

Lines changed: 239 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,20 @@
22

33
import asyncio
44
import queue
5+
import signal
6+
import socket
7+
import sys
58
import threading
69
import time
710
import 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

1021
class 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

Comments
 (0)