pytest-dev/pytest-mock

Mocked socket interfering with pytest-asyncio's teardown

Closed this issue · 1 comments

Consider these two test cases:

@pytest.mark.asyncio
async def test_mocksocket_mocker(mocker):
    mocker.patch('socket.socket', wraps=MockSocket)
    pass


@pytest.mark.asyncio
async def test_mocksocket_none():
    unittest.mock.patch('socket.socket', wraps=MockSocket)
    pass

The top case reports ERROR at teardown of test_mocksocket_mocker, while the bottom one does not report any error.

The error message in full:

================================================================================================== ERRORS =================================================================================================== 
________________________________________________________________________________ ERROR at teardown of test_mocksocket_mocker ________________________________________________________________________________ 

    def _provide_clean_event_loop() -> None:
        # At this point, the event loop for the current thread is closed.
        # When a user calls asyncio.get_event_loop(), they will get a closed loop.
        # In order to avoid this side effect from pytest-asyncio, we need to replace
        # the current loop with a fresh one.
        # Note that we cannot set the loop to None, because get_event_loop only creates
        # a new loop, when set_event_loop has not been called.
        policy = asyncio.get_event_loop_policy()
>       new_loop = policy.new_event_loop()

.venv\Lib\site-packages\pytest_asyncio\plugin.py:850:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
..\..\..\AppData\Local\Programs\Python\Python311\Lib\asyncio\events.py:699: in new_event_loop
    return self._loop_factory()
..\..\..\AppData\Local\Programs\Python\Python311\Lib\asyncio\windows_events.py:315: in __init__
    super().__init__(proactor)
..\..\..\AppData\Local\Programs\Python\Python311\Lib\asyncio\proactor_events.py:639: in __init__
    self._make_self_pipe()
..\..\..\AppData\Local\Programs\Python\Python311\Lib\asyncio\proactor_events.py:784: in _make_self_pipe
    self._ssock, self._csock = socket.socketpair()
..\..\..\AppData\Local\Programs\Python\Python311\Lib\socket.py:631: in socketpair
    lsock = socket(family, type, proto)
..\..\..\AppData\Local\Programs\Python\Python311\Lib\unittest\mock.py:1124: in __call__
    return self._mock_call(*args, **kwargs)
..\..\..\AppData\Local\Programs\Python\Python311\Lib\unittest\mock.py:1128: in _mock_call
    return self._execute_mock_call(*args, **kwargs)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <MagicMock name='socket' id='2550221096912'>, args = (<AddressFamily.AF_INET: 2>, <SocketKind.SOCK_STREAM: 1>, 0), kwargs = {}, effect = None

    def _execute_mock_call(self, /, *args, **kwargs):
        # separate from _increment_mock_call so that awaited functions are
        # executed separately from their call, also AsyncMock overrides this method

        effect = self.side_effect
        if effect is not None:
            if _is_exception(effect):
                raise effect
            elif not _callable(effect):
                result = next(effect)
                if _is_exception(result):
                    raise result
            else:
                result = effect(*args, **kwargs)

            if result is not DEFAULT:
                return result

        if self._mock_return_value is not DEFAULT:
            return self.return_value

        if self._mock_delegate and self._mock_delegate.return_value is not DEFAULT:
            return self.return_value

        if self._mock_wraps is not None:
>           return self._mock_wraps(*args, **kwargs)
E           TypeError: MockSocket() takes no arguments

..\..\..\AppData\Local\Programs\Python\Python311\Lib\unittest\mock.py:1201: TypeError
--------------------------------------------------------------------------------------------- Captured log call --------------------------------------------------------------------------------------------- 
ERROR    asyncio:base_events.py:1785 Error on reading from the event loop self pipe
loop: <ProactorEventLoop running=True closed=False debug=False>
Traceback (most recent call last):
  File "C:\Users\username\AppData\Local\Programs\Python\Python311\Lib\asyncio\proactor_events.py", line 801, in _loop_self_reading
    f = self._proactor.recv(self._ssock, 4096)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\username\AppData\Local\Programs\Python\Python311\Lib\asyncio\windows_events.py", line 462, in recv
    if isinstance(conn, socket.socket):
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: isinstance() arg 2 must be a type, a tuple of types, or a union

It seems like the mocked socket survived while pytest-asyncio is closing the current event loop and creating a new one.

Also, there seems to be a weird interaction with pytest-asyncio. The error disappears if an event_loop fixture is requested, like so:

@pytest.mark.asyncio
async def test_mocksocket_loop_mocker(event_loop, mocker):
    mocker.patch('socket.socket', wraps=MockSocket)
    pass

Attached is the test file, full console output, and package versions. This is running on Windows 10.

https://github.com/pytest-dev/pytest-asyncio/files/15039206/pytest-asyncio-mock.zip

I have also reported this problem to pytest-asyncio: pytest-dev/pytest-asyncio#818

From the traceback, seems like the pytest-asyncio is trying to create a new event lopp before the pytest-mock has a chance to uninstall the mock.

I'm closing because I don't see what can be done on pytest-mock's side to prevent this: we remove the mocks during fixture teardown, which is the straightforward way to handle unmocking here.