Tests: patch waitress to reduce test flakiness

Waitress is a WSGI server that Anki starts to serve css etc to
its web views. It seems to have a race condition issue;
the main loop thread is trying to `select.select` the sockets
which a worker thread is closing because of a dead connection.

This makes waitress skip actually closing the sockets.
This commit is contained in:
oakkitten 2022-04-11 19:39:07 +01:00
parent c688895c0e
commit 6643dca83e

View File

@ -6,7 +6,9 @@ from dataclasses import dataclass
import aqt.operations.note import aqt.operations.note
import pytest import pytest
from PyQt5 import QtTest from PyQt5 import QtTest
from _pytest.monkeypatch import MonkeyPatch # noqa
from pytest_anki._launch import anki_running, temporary_user # noqa from pytest_anki._launch import anki_running, temporary_user # noqa
from waitress import wasyncore
from plugin import AnkiConnect from plugin import AnkiConnect
from plugin.edit import Edit from plugin.edit import Edit
@ -48,8 +50,36 @@ def get_dialog_instance(name):
return aqt.dialogs._dialogs[name][1] # noqa return aqt.dialogs._dialogs[name][1] # noqa
# waitress is a WSGI server that Anki starts to serve css etc to its web views.
# it seems to have a race condition issue;
# the main loop thread is trying to `select.select` the sockets
# which a worker thread is closing because of a dead connection.
# this is especially pronounced in tests,
# as we open and close windows rapidly--and so web views and their connections.
# this small patch makes waitress skip actually closing the sockets
# (unless the server is shutting down--if it is, loop exceptions are ignored).
# while the unclosed sockets might accumulate,
# this should not pose an issue in test environment.
# see https://github.com/Pylons/waitress/issues/374
@contextmanager
def waitress_patched_to_prevent_it_from_dying():
original_close = wasyncore.dispatcher.close
sockets_that_must_not_be_garbage_collected = [] # lists are thread-safe
def close(self):
if not aqt.mw.mediaServer.is_shutdown:
sockets_that_must_not_be_garbage_collected.append(self.socket)
self.socket = None
original_close(self)
with MonkeyPatch().context() as monkey:
monkey.setattr(wasyncore.dispatcher, "close", close)
yield
@contextmanager @contextmanager
def empty_anki_session_started(): def empty_anki_session_started():
with waitress_patched_to_prevent_it_from_dying():
with anki_running( with anki_running(
qtbot=None, # noqa qtbot=None, # noqa
enable_web_debugging=False, enable_web_debugging=False,
@ -187,8 +217,7 @@ def run_background_tasks_on_main_thread(request, monkeypatch): # noqa
if on_done is not None: if on_done is not None:
on_done(future) on_done(future)
monkeypatch.setattr(aqt.mw.taskman, "run_in_background", monkeypatch.setattr(aqt.mw.taskman, "run_in_background", run_in_background)
run_in_background)
# don't use run_background_tasks_on_main_thread for tests that don't run Anki # don't use run_background_tasks_on_main_thread for tests that don't run Anki