Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mocked socket interfering with pytest-asyncio's teardown #425

Closed
twisteroidambassador opened this issue Apr 19, 2024 · 1 comment
Closed

Comments

@twisteroidambassador
Copy link

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

@nicoddemus
Copy link
Member

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.

@nicoddemus nicoddemus closed this as not planned Won't fix, can't repro, duplicate, stale Apr 19, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants