From d1aea98c4f552d04458e49f1484b9c018f2a3ca8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Wed, 11 Sep 2024 09:57:01 +0300 Subject: [PATCH] Fixed KeyboardInterrupt hanging the asyncio test runner (#779) --- docs/versionhistory.rst | 1 + src/anyio/_backends/_asyncio.py | 10 ++++++++++ tests/test_pytest_plugin.py | 27 +++++++++++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/docs/versionhistory.rst b/docs/versionhistory.rst index d99d30a2..4ce4d4c7 100644 --- a/docs/versionhistory.rst +++ b/docs/versionhistory.rst @@ -42,6 +42,7 @@ This library adheres to `Semantic Versioning 2.0 `_. arrives in an exception group) - Fixed support for Linux abstract namespaces in UNIX sockets that was broken in v4.2 (#781 _; PR by @tapetersen) +- Fixed ``KeyboardInterrupt`` (ctrl+c) hanging the asyncio pytest runner **4.4.0** diff --git a/src/anyio/_backends/_asyncio.py b/src/anyio/_backends/_asyncio.py index 97edb8ad..0d4cdf65 100644 --- a/src/anyio/_backends/_asyncio.py +++ b/src/anyio/_backends/_asyncio.py @@ -2082,13 +2082,23 @@ async def _run_tests_and_fixtures( tuple[Awaitable[T_Retval], asyncio.Future[T_Retval]] ], ) -> None: + from _pytest.outcomes import OutcomeException + with receive_stream, self._send_stream: async for coro, future in receive_stream: try: retval = await coro + except CancelledError as exc: + if not future.cancelled(): + future.cancel(*exc.args) + + raise except BaseException as exc: if not future.cancelled(): future.set_exception(exc) + + if not isinstance(exc, (Exception, OutcomeException)): + raise else: if not future.cancelled(): future.set_result(retval) diff --git a/tests/test_pytest_plugin.py b/tests/test_pytest_plugin.py index 2b7d9ca4..1aa8911e 100644 --- a/tests/test_pytest_plugin.py +++ b/tests/test_pytest_plugin.py @@ -441,3 +441,30 @@ async def test_debugger_exit(): result = testdir.runpytest(*pytest_args) result.assert_outcomes() + + +@pytest.mark.parametrize("anyio_backend", get_all_backends(), indirect=True) +def test_keyboardinterrupt_during_test( + testdir: Pytester, anyio_backend_name: str +) -> None: + testdir.makepyfile( + f""" + import pytest + from anyio import create_task_group, sleep + + @pytest.fixture + def anyio_backend(): + return {anyio_backend_name!r} + + async def send_keyboardinterrupt(): + raise KeyboardInterrupt + + @pytest.mark.anyio + async def test_anyio_mark_first(): + async with create_task_group() as tg: + tg.start_soon(send_keyboardinterrupt) + await sleep(10) + """ + ) + + testdir.runpytest_subprocess(*pytest_args, timeout=3)