diff --git a/newsfragments/3509.bugfix.rst b/newsfragments/3509.bugfix.rst new file mode 100644 index 0000000000..2966f4706f --- /dev/null +++ b/newsfragments/3509.bugfix.rst @@ -0,0 +1,4 @@ +On Windows, :func:`trio.lowlevel.wait_readable`, :func:`trio.lowlevel.wait_writable` +and :func:`trio.lowlevel.notify_closing` raised ``BusyResourceError`` and +``ClosedResourceError`` without any message. They now explain themselves, as they +already did on the epoll and kqueue backends. diff --git a/src/trio/_core/_io_windows.py b/src/trio/_core/_io_windows.py index 7b789c3dec..99f31aea0a 100644 --- a/src/trio/_core/_io_windows.py +++ b/src/trio/_core/_io_windows.py @@ -740,7 +740,9 @@ async def _afd_poll(self, sock: _HasFileNo | int, mode: str) -> None: waiters = AFDWaiters() self._afd_waiters[base_handle] = waiters if getattr(waiters, mode) is not None: - raise _core.BusyResourceError + raise _core.BusyResourceError( + "another task is already reading / writing this socket", + ) setattr(waiters, mode, _core.current_task()) # Could potentially raise if the handle is somehow invalid; that's OK, # we let it escape. @@ -822,7 +824,10 @@ def notify_closing(self, handle: Handle | int | _HasFileNo) -> None: handle = _get_base_socket(handle) waiters = self._afd_waiters.get(handle) if waiters is not None: - wake_all(waiters, _core.ClosedResourceError()) + wake_all( + waiters, + _core.ClosedResourceError("another task closed this socket"), + ) self._refresh_afd(handle) ################################################################ diff --git a/src/trio/_core/_tests/test_io.py b/src/trio/_core/_tests/test_io.py index 379daa025e..b71e8b616d 100644 --- a/src/trio/_core/_tests/test_io.py +++ b/src/trio/_core/_tests/test_io.py @@ -162,6 +162,12 @@ async def block_on_write() -> None: assert record == ["cancelled"] +# The two real BusyResourceError texts from the three backends: epoll and +# windows word it one way, kqueue another. Matching both asserts the message is +# one of them rather than merely non-empty. +BUSY_MESSAGE = r"already reading / writing this|multiple listeners for same ident" + + @read_socket_test async def test_double_read(socketpair: SocketPair, wait_readable: WaitSocket) -> None: a, _b = socketpair @@ -170,7 +176,8 @@ async def test_double_read(socketpair: SocketPair, wait_readable: WaitSocket) -> async with _core.open_nursery() as nursery: nursery.start_soon(wait_readable, a) await wait_all_tasks_blocked() - with pytest.raises(_core.BusyResourceError): + # epoll and windows say one thing here, kqueue another, so match both. + with pytest.raises(_core.BusyResourceError, match=BUSY_MESSAGE): await wait_readable(a) nursery.cancel_scope.cancel() @@ -184,7 +191,8 @@ async def test_double_write(socketpair: SocketPair, wait_writable: WaitSocket) - async with _core.open_nursery() as nursery: nursery.start_soon(wait_writable, a) await wait_all_tasks_blocked() - with pytest.raises(_core.BusyResourceError): + # epoll and windows say one thing here, kqueue another, so match both. + with pytest.raises(_core.BusyResourceError, match=BUSY_MESSAGE): await wait_writable(a) nursery.cancel_scope.cancel() @@ -201,11 +209,15 @@ async def test_interrupted_by_close( a, _b = socketpair async def reader() -> None: - with pytest.raises(_core.ClosedResourceError): + with pytest.raises( + _core.ClosedResourceError, match="another task closed this " + ): await wait_readable(a) async def writer() -> None: - with pytest.raises(_core.ClosedResourceError): + with pytest.raises( + _core.ClosedResourceError, match="another task closed this " + ): await wait_writable(a) fill_socket(a)