diff --git a/Lib/test/test_asyncio/functional.py b/Lib/test/test_asyncio/functional.py index 96dc9ab44010670..47a3ab8c306edd9 100644 --- a/Lib/test/test_asyncio/functional.py +++ b/Lib/test/test_asyncio/functional.py @@ -28,6 +28,7 @@ def setUp(self): self.loop.set_exception_handler(self.loop_exception_handler) self.__unhandled_exceptions = [] + self.__abort_exception = None def tearDown(self): try: @@ -38,9 +39,13 @@ def tearDown(self): pprint.pprint(self.__unhandled_exceptions) self.fail('unexpected calls to loop.call_exception_handler()') + if self.__abort_exception is not None: + raise self.__abort_exception + finally: asyncio.set_event_loop(None) self.loop = None + self.__abort_exception = None def tcp_server(self, server_prog, *, family=socket.AF_INET, @@ -104,10 +109,18 @@ def unix_sock_name(self): pass def _abort_socket_test(self, ex): + # This runs in the client/server thread, not the main thread, so + # it must not call self.fail(): the AssertionError would escape + # Thread.run() without failing the test. Stash the exception and + # let tearDown() re-raise it on the main thread. try: - self.loop.stop() + self.loop.call_soon_threadsafe(self.loop.stop) + except RuntimeError: + # The loop is already closed; nothing left to stop. + pass finally: - self.fail(ex) + if self.__abort_exception is None: + self.__abort_exception = ex ############################################################################## diff --git a/Misc/NEWS.d/next/Tests/2026-08-01-10-30-00.gh-issue-155027.Kq7Wm2.rst b/Misc/NEWS.d/next/Tests/2026-08-01-10-30-00.gh-issue-155027.Kq7Wm2.rst new file mode 100644 index 000000000000000..f23b541d9e3c2bc --- /dev/null +++ b/Misc/NEWS.d/next/Tests/2026-08-01-10-30-00.gh-issue-155027.Kq7Wm2.rst @@ -0,0 +1,7 @@ +Fix ``test_asyncio``'s socket test harness so that a failure in the client +or server thread actually fails the test. ``_abort_socket_test()`` called +``self.fail()`` from a worker thread, where the resulting +:exc:`AssertionError` cannot fail the test; it now records the exception and +re-raises it on the main thread. It also stops the event loop with +:meth:`~asyncio.loop.call_soon_threadsafe` rather than calling +:meth:`~asyncio.loop.stop` directly from a non-main thread.