From 43e41cdf9474462c151b480acec15154e25fc19b Mon Sep 17 00:00:00 2001 From: Sergey Vasilyev Date: Sun, 24 Jul 2022 15:14:58 +0200 Subject: [PATCH 1/2] Configure the "auto" mode explicitly for pytest-asyncio>=0.19 Signed-off-by: Sergey Vasilyev --- pytest.ini | 1 + tests/conftest.py | 9 --------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/pytest.ini b/pytest.ini index a68aafec..afd6b82b 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,3 +1,4 @@ [pytest] +asyncio_mode = auto addopts = --strict-markers diff --git a/tests/conftest.py b/tests/conftest.py index b498be51..02095105 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -43,15 +43,6 @@ def pytest_addoption(parser): parser.addoption("--with-e2e", action="store_true", help="Include end-to-end tests.") -# Make all tests in this directory and below asyncio-compatible by default. -# Due to how pytest-async checks for these markers, they should be added as early as possible. -@pytest.hookimpl(hookwrapper=True) -def pytest_pycollect_makeitem(collector, name, obj): - if collector.funcnamefilter(name) and asyncio.iscoroutinefunction(obj): - pytest.mark.asyncio(obj) - yield - - # This logic is not applied if pytest is started explicitly on ./examples/. # In that case, regular pytest behaviour applies -- this is intended. def pytest_collection_modifyitems(config, items): From 6b8c1b36fba73468021373c44fdf5e4bca158989 Mon Sep 17 00:00:00 2001 From: Sergey Vasilyev Date: Sun, 24 Jul 2022 15:47:11 +0200 Subject: [PATCH 2/2] Fix the asyncio guards to expect changes during the task lising MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The WeakSet can change while it is iterated and being converted to a list (this seems fast, but it is not always the case and tends to be reproducible in the same setup). This fix guards against that and tries to convert the WeakSet to a list 1000 times before giving up — the same as Python's asyncio does. Signed-off-by: Sergey Vasilyev --- tests/conftest.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 02095105..0534586b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,6 +7,7 @@ import re import sys import time +from typing import Set from unittest.mock import Mock import aiohttp.web @@ -700,9 +701,7 @@ def _no_asyncio_pending_tasks(event_loop): collection after every test, and check messages from `asyncio.Task.__del__`. This, however, requires intercepting all event-loop creation in the code. """ - - # See `asyncio.all_tasks()` implementation for reference. - before = {t for t in list(asyncio.tasks._all_tasks) if not t.done()} + before = _get_all_tasks() # Run the test. yield @@ -712,7 +711,22 @@ def _no_asyncio_pending_tasks(event_loop): event_loop.run_until_complete(asyncio.sleep(0)) # Detect all leftover tasks. - after = {t for t in list(asyncio.tasks._all_tasks) if not t.done()} + after = _get_all_tasks() remains = after - before if remains: pytest.fail(f"Unattended asyncio tasks detected: {remains!r}") + + +def _get_all_tasks() -> Set[asyncio.Task]: + """Similar to `asyncio.all_tasks`, but for all event loops at once.""" + i = 0 + while True: + try: + tasks = list(asyncio.tasks._all_tasks) + except RuntimeError: + i += 1 + if i >= 1000: + raise # we are truly unlucky today; try again tomorrow + else: + break + return {t for t in tasks if not t.done()}