diff --git a/.github/codecov.yml b/.github/codecov.yml index 4be01859..7293e6e8 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -1,7 +1,7 @@ comment: require_changes: false # Always show messages if codecov is triggered layout: "flags" # Only show coverage by flags - after_n_builds: 8 # nb_tested_python_version(==2) * (nb_tested_platforms(==3) + (nb_unit_test_runs==1)) + after_n_builds: 8 # nb_tested_python_version(==2) * (nb_tested_platforms(==3) + nb_unit_test_runs(==1)) coverage: status: project: diff --git a/src/easynetwork/lowlevel/asyncio/datagram/endpoint.py b/src/easynetwork/lowlevel/asyncio/datagram/endpoint.py index 708b7448..b58eb604 100644 --- a/src/easynetwork/lowlevel/asyncio/datagram/endpoint.py +++ b/src/easynetwork/lowlevel/asyncio/datagram/endpoint.py @@ -89,15 +89,18 @@ def __init__( self.__transport: asyncio.DatagramTransport = transport self.__protocol: DatagramEndpointProtocol = protocol - def close(self) -> None: - self.__transport.close() - - async def wait_closed(self) -> None: - await self.__protocol._get_close_waiter() - async def aclose(self) -> None: - self.close() - await self.wait_closed() + if self.__transport.is_closing(): + # Only wait for it. + await self.__protocol._get_close_waiter() + return + + self.__transport.close() + try: + await self.__protocol._get_close_waiter() + except asyncio.CancelledError: + self.__transport.abort() + raise def is_closing(self) -> bool: return self.__transport.is_closing() diff --git a/src/easynetwork/lowlevel/asyncio/datagram/listener.py b/src/easynetwork/lowlevel/asyncio/datagram/listener.py index b1246661..2bef5526 100644 --- a/src/easynetwork/lowlevel/asyncio/datagram/listener.py +++ b/src/easynetwork/lowlevel/asyncio/datagram/listener.py @@ -61,12 +61,7 @@ def is_closing(self) -> bool: async def aclose(self) -> None: self.__closing = True - self.__endpoint.close() - try: - await self.__endpoint.wait_closed() - except asyncio.CancelledError: - self.__endpoint.transport.abort() - raise + await self.__endpoint.aclose() async def recv_from(self) -> tuple[bytes, tuple[Any, ...]]: return await self.__endpoint.recvfrom() diff --git a/src/easynetwork/lowlevel/asyncio/datagram/socket.py b/src/easynetwork/lowlevel/asyncio/datagram/socket.py index 5e2ec142..04ebcac4 100644 --- a/src/easynetwork/lowlevel/asyncio/datagram/socket.py +++ b/src/easynetwork/lowlevel/asyncio/datagram/socket.py @@ -57,12 +57,7 @@ def __init__(self, endpoint: DatagramEndpoint) -> None: async def aclose(self) -> None: self.__closing = True - self.__endpoint.close() - try: - return await self.__endpoint.wait_closed() - except asyncio.CancelledError: - self.__endpoint.transport.abort() - raise + await self.__endpoint.aclose() def is_closing(self) -> bool: return self.__closing diff --git a/src/easynetwork/lowlevel/asyncio/stream/socket.py b/src/easynetwork/lowlevel/asyncio/stream/socket.py index a5af0928..3b328c19 100644 --- a/src/easynetwork/lowlevel/asyncio/stream/socket.py +++ b/src/easynetwork/lowlevel/asyncio/stream/socket.py @@ -64,14 +64,21 @@ def __init__( async def aclose(self) -> None: self.__closing = True - if not self.__writer.is_closing(): + if self.__writer.is_closing(): + # Only wait for it. try: - if self.__writer.can_write_eof(): - self.__writer.write_eof() + await self.__writer.wait_closed() except OSError: pass - finally: - self.__writer.close() + return + + try: + if self.__writer.can_write_eof(): + self.__writer.write_eof() + except OSError: + pass + finally: + self.__writer.close() try: await self.__writer.wait_closed() except OSError: diff --git a/tests/functional_test/test_communication/test_async/test_client/test_udp.py b/tests/functional_test/test_communication/test_async/test_client/test_udp.py index 4b68e7f8..b6c2b073 100644 --- a/tests/functional_test/test_communication/test_async/test_client/test_udp.py +++ b/tests/functional_test/test_communication/test_async/test_client/test_udp.py @@ -48,8 +48,7 @@ async def factory() -> DatagramEndpoint: family=socket_family, local_addr=(localhost_ip, 0), ) - stack.push_async_callback(lambda: asyncio.wait_for(endpoint.wait_closed(), 3)) - stack.callback(endpoint.close) + stack.push_async_callback(lambda: asyncio.wait_for(endpoint.aclose(), 3)) return endpoint yield factory diff --git a/tests/functional_test/test_communication/test_async/test_server/test_udp.py b/tests/functional_test/test_communication/test_async/test_server/test_udp.py index 5bcbdd64..5892f0a6 100644 --- a/tests/functional_test/test_communication/test_async/test_server/test_udp.py +++ b/tests/functional_test/test_communication/test_async/test_server/test_udp.py @@ -258,8 +258,7 @@ async def factory() -> DatagramEndpoint: local_addr=(localhost_ip, 0), remote_addr=server_address, ) - stack.push_async_callback(endpoint.wait_closed) - stack.callback(endpoint.close) + stack.push_async_callback(endpoint.aclose) return endpoint yield factory diff --git a/tests/unit_test/test_async/test_asyncio_backend/test_datagram.py b/tests/unit_test/test_async/test_asyncio_backend/test_datagram.py index 48750003..b0918d72 100644 --- a/tests/unit_test/test_async/test_asyncio_backend/test_datagram.py +++ b/tests/unit_test/test_async/test_asyncio_backend/test_datagram.py @@ -150,48 +150,55 @@ async def test____transport____property( # Act & Assert assert endpoint.transport is mock_asyncio_transport - async def test____close____close_transport( + @pytest.mark.parametrize("transport_is_closing", [False, True], ids=lambda p: f"transport_is_closing=={p}") + async def test____aclose____close_transport_and_wait( self, + transport_is_closing: bool, endpoint: DatagramEndpoint, mock_asyncio_transport: MagicMock, - ) -> None: - # Arrange - - # Act - endpoint.close() - - # Assert - mock_asyncio_transport.close.assert_called_once_with() - mock_asyncio_transport.abort.assert_not_called() - - async def test____wait_closed____wait_for_protocol_to_close_connection( - self, - endpoint: DatagramEndpoint, mock_asyncio_protocol: MagicMock, ) -> None: # Arrange + mock_asyncio_transport.is_closing.return_value = transport_is_closing # Act - await endpoint.wait_closed() + await endpoint.aclose() # Assert - mock_asyncio_protocol._get_close_waiter.assert_awaited_once_with() + if transport_is_closing: + mock_asyncio_transport.close.assert_not_called() + mock_asyncio_protocol._get_close_waiter.assert_awaited_once_with() + mock_asyncio_transport.abort.assert_not_called() + else: + mock_asyncio_transport.close.assert_called_once_with() + mock_asyncio_protocol._get_close_waiter.assert_awaited_once_with() + mock_asyncio_transport.abort.assert_not_called() - async def test____aclose____close_transport_and_wait( + @pytest.mark.parametrize("transport_is_closing", [False, True], ids=lambda p: f"transport_is_closing=={p}") + async def test____aclose____abort_transport_if_cancelled( self, + transport_is_closing: bool, endpoint: DatagramEndpoint, mock_asyncio_transport: MagicMock, mock_asyncio_protocol: MagicMock, ) -> None: # Arrange + mock_asyncio_transport.is_closing.return_value = transport_is_closing + mock_asyncio_protocol._get_close_waiter.side_effect = asyncio.CancelledError # Act - await endpoint.aclose() + with pytest.raises(asyncio.CancelledError): + await endpoint.aclose() # Assert - mock_asyncio_transport.close.assert_called_once_with() - mock_asyncio_transport.abort.assert_not_called() - mock_asyncio_protocol._get_close_waiter.assert_awaited_once_with() + if transport_is_closing: + mock_asyncio_transport.close.assert_not_called() + mock_asyncio_protocol._get_close_waiter.assert_awaited_once_with() + mock_asyncio_transport.abort.assert_not_called() + else: + mock_asyncio_transport.close.assert_called_once_with() + mock_asyncio_protocol._get_close_waiter.assert_awaited_once_with() + mock_asyncio_transport.abort.assert_called_once_with() async def test____is_closing____return_transport_state( self, @@ -712,26 +719,7 @@ async def test____aclose____close_transport_and_wait( await socket.aclose() # Assert - mock_endpoint.close.assert_called_once_with() - mock_endpoint.wait_closed.assert_awaited_once_with() - mock_endpoint.transport.abort.assert_not_called() - - async def test____aclose____abort_transport_if_cancelled( - self, - socket: AsyncBaseTransport, - mock_endpoint: MagicMock, - ) -> None: - # Arrange - mock_endpoint.wait_closed.side_effect = asyncio.CancelledError - - # Act - with pytest.raises(asyncio.CancelledError): - await socket.aclose() - - # Assert - mock_endpoint.close.assert_called_once_with() - mock_endpoint.wait_closed.assert_awaited_once_with() - mock_endpoint.transport.abort.assert_called_once_with() + mock_endpoint.aclose.assert_awaited_once_with() @pytest.mark.parametrize("transport_closed", [False, True], ids=lambda p: f"transport_closed=={p}") async def test____is_closing____return_internal_flag( diff --git a/tests/unit_test/test_async/test_asyncio_backend/test_stream.py b/tests/unit_test/test_async/test_asyncio_backend/test_stream.py index 663c7fe9..bd1430f0 100644 --- a/tests/unit_test/test_async/test_asyncio_backend/test_stream.py +++ b/tests/unit_test/test_async/test_asyncio_backend/test_stream.py @@ -87,58 +87,53 @@ def socket(mock_asyncio_reader: MagicMock, mock_asyncio_writer: MagicMock) -> As mock_asyncio_writer.can_write_eof.return_value = True return AsyncioTransportStreamSocketAdapter(mock_asyncio_reader, mock_asyncio_writer) + @pytest.mark.parametrize("transport_is_closing", [False, True], ids=lambda p: f"transport_is_closing=={p}") @pytest.mark.parametrize("can_write_eof", [False, True], ids=lambda p: f"can_write_eof=={p}") @pytest.mark.parametrize("wait_close_raise_error", [False, True], ids=lambda p: f"wait_close_raise_error=={p}") - @pytest.mark.parametrize("write_eof_rais_error", [False, True], ids=lambda p: f"write_eof_rais_error=={p}") + @pytest.mark.parametrize("write_eof_raise_error", [False, True], ids=lambda p: f"write_eof_raise_error=={p}") async def test____aclose____close_transport_and_wait( self, + transport_is_closing: bool, can_write_eof: bool, wait_close_raise_error: bool, - write_eof_rais_error: bool, + write_eof_raise_error: bool, socket: AsyncioTransportStreamSocketAdapter, mock_asyncio_writer: MagicMock, ) -> None: # Arrange + mock_asyncio_writer.is_closing.return_value = transport_is_closing mock_asyncio_writer.can_write_eof.return_value = can_write_eof if wait_close_raise_error: mock_asyncio_writer.wait_closed.side_effect = OSError - if write_eof_rais_error: + if write_eof_raise_error: mock_asyncio_writer.write_eof.side_effect = OSError # Act await socket.aclose() # Assert - if can_write_eof: - mock_asyncio_writer.write_eof.assert_called_once_with() + if transport_is_closing: + mock_asyncio_writer.close.assert_not_called() + mock_asyncio_writer.wait_closed.assert_awaited_once_with() + mock_asyncio_writer.transport.abort.assert_not_called() else: - mock_asyncio_writer.write_eof.assert_not_called() - mock_asyncio_writer.close.assert_called_once_with() - mock_asyncio_writer.wait_closed.assert_awaited_once_with() - mock_asyncio_writer.transport.abort.assert_not_called() - - async def test____aclose____wait_only_if_already_closing( - self, - socket: AsyncioTransportStreamSocketAdapter, - mock_asyncio_writer: MagicMock, - ) -> None: - # Arrange - mock_asyncio_writer.is_closing.return_value = True - - # Act - await socket.aclose() - - # Assert - mock_asyncio_writer.close.assert_not_called() - mock_asyncio_writer.wait_closed.assert_awaited_once_with() - mock_asyncio_writer.transport.abort.assert_not_called() - + if can_write_eof: + mock_asyncio_writer.write_eof.assert_called_once_with() + else: + mock_asyncio_writer.write_eof.assert_not_called() + mock_asyncio_writer.close.assert_called_once_with() + mock_asyncio_writer.wait_closed.assert_awaited_once_with() + mock_asyncio_writer.transport.abort.assert_not_called() + + @pytest.mark.parametrize("transport_is_closing", [False, True], ids=lambda p: f"transport_is_closing=={p}") async def test____aclose____abort_transport_if_cancelled( self, + transport_is_closing: bool, socket: AsyncioTransportStreamSocketAdapter, mock_asyncio_writer: MagicMock, ) -> None: # Arrange + mock_asyncio_writer.is_closing.return_value = transport_is_closing mock_asyncio_writer.wait_closed.side_effect = asyncio.CancelledError # Act @@ -146,9 +141,14 @@ async def test____aclose____abort_transport_if_cancelled( await socket.aclose() # Assert - mock_asyncio_writer.close.assert_called_once_with() - mock_asyncio_writer.wait_closed.assert_awaited_once_with() - mock_asyncio_writer.transport.abort.assert_called_once_with() + if transport_is_closing: + mock_asyncio_writer.close.assert_not_called() + mock_asyncio_writer.wait_closed.assert_awaited_once_with() + mock_asyncio_writer.transport.abort.assert_not_called() + else: + mock_asyncio_writer.close.assert_called_once_with() + mock_asyncio_writer.wait_closed.assert_awaited_once_with() + mock_asyncio_writer.transport.abort.assert_called_once_with() @pytest.mark.parametrize("transport_closed", [False, True], ids=lambda p: f"transport_closed=={p}") async def test____is_closing____return_internal_flag(