Skip to content

Commit 90a5a39

Browse files
authored
test: add blockbuster to detect blocking calls in asyncio tests (#1761)
1 parent 3e5ac4f commit 90a5a39

3 files changed

Lines changed: 89 additions & 3 deletions

File tree

poetry.lock

Lines changed: 29 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ cython = "^3.2.4"
9191
setuptools = ">=65.6.3,<83.0.0"
9292
pytest-timeout = "^2.1.0"
9393
pytest-codspeed = ">=5.0.2,<6.0"
94+
blockbuster = ">=1.5.5,<2.0.0"
9495

9596
[tool.poetry.group.docs.dependencies]
9697
sphinx = "^7.4.7 || ^8.1.3"

tests/conftest.py

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from __future__ import annotations
44

55
import threading
6-
from collections.abc import AsyncGenerator, Generator
6+
from collections.abc import AsyncGenerator, Generator, Iterator
77
from unittest.mock import patch
88

99
import pytest
@@ -15,6 +15,64 @@
1515
from zeroconf._services import info as service_info
1616
from zeroconf.asyncio import AsyncZeroconf
1717

18+
try:
19+
from blockbuster import BlockBuster, blockbuster_ctx
20+
except ImportError: # platforms without blockbuster (e.g. PyPy under QEMU)
21+
BlockBuster = None # type: ignore[assignment,misc]
22+
blockbuster_ctx = None # type: ignore[assignment]
23+
24+
_BENCHMARKS_DIR = "tests/benchmarks"
25+
26+
# Tests that perform sync IO inside the asyncio event loop and trip
27+
# blockbuster. Marked xfail (strict=False) so CI stays green; pop
28+
# entries as the underlying blocking calls get fixed. Most of the
29+
# `test_async_service_registration*` and `test_async_tasks` entries
30+
# share a single root cause: `Zeroconf.async_close()` -> ... ->
31+
# `ServiceBrowser.cancel()` calls `Thread.join()` to drain the
32+
# dedicated browser thread, and on Python 3.10-3.12 the thread is
33+
# still alive when the join happens. `test_use_asyncio_false_*` is
34+
# by design (sync bootstrap when `use_asyncio=False` is requested from
35+
# inside a running loop); `test_run_coro_with_timeout` exercises the
36+
# sync-from-thread bridge intentionally. The strict=False marker keeps
37+
# the suite green on the Python versions where the race resolves the
38+
# other way.
39+
_KNOWN_BLOCKING: frozenset[str] = frozenset(
40+
{
41+
"tests/test_asyncio.py::test_async_service_registration",
42+
"tests/test_asyncio.py::test_async_service_registration_with_server_missing",
43+
"tests/test_asyncio.py::test_async_service_registration_same_server_different_ports",
44+
"tests/test_asyncio.py::test_async_service_registration_same_server_same_ports",
45+
"tests/test_asyncio.py::test_async_tasks",
46+
"tests/test_core.py::Framework::test_use_asyncio_false_forces_thread_when_loop_running",
47+
"tests/utils/test_asyncio.py::test_run_coro_with_timeout",
48+
}
49+
)
50+
51+
52+
def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item]) -> None:
53+
"""Mark known-blocking tests xfail so blockbuster doesn't fail the suite."""
54+
if blockbuster_ctx is None:
55+
return
56+
marker = pytest.mark.xfail(
57+
reason="blockbuster: blocking call in asyncio path",
58+
strict=False,
59+
)
60+
for item in items:
61+
if item.nodeid in _KNOWN_BLOCKING:
62+
item.add_marker(marker)
63+
64+
65+
@pytest.fixture(autouse=True)
66+
def blockbuster(
67+
request: pytest.FixtureRequest,
68+
) -> Iterator[BlockBuster | None]:
69+
"""Fail any test that performs a blocking call inside the asyncio loop."""
70+
if blockbuster_ctx is None or _BENCHMARKS_DIR in str(request.node.fspath):
71+
yield None
72+
return
73+
with blockbuster_ctx() as bb:
74+
yield bb
75+
1876

1977
@pytest.fixture(autouse=True)
2078
def verify_threads_ended():

0 commit comments

Comments
 (0)