-
Notifications
You must be signed in to change notification settings - Fork 227
Expand file tree
/
Copy pathconftest.py
More file actions
208 lines (174 loc) · 7.77 KB
/
conftest.py
File metadata and controls
208 lines (174 loc) · 7.77 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
"""conftest for zeroconf tests."""
from __future__ import annotations
import threading
from collections.abc import AsyncGenerator, Generator, Iterator
from unittest.mock import patch
import pytest
import pytest_asyncio
from zeroconf import Zeroconf, _core, const
from zeroconf._handlers import query_handler
from zeroconf._services import browser as service_browser
from zeroconf._services import info as service_info
from zeroconf.asyncio import AsyncZeroconf
try:
from blockbuster import BlockBuster, blockbuster_ctx
except ImportError: # platforms without blockbuster (e.g. PyPy under QEMU)
BlockBuster = None # type: ignore[assignment,misc]
blockbuster_ctx = None # type: ignore[assignment]
_BENCHMARKS_DIR = "tests/benchmarks"
# Tests that perform sync IO inside the asyncio event loop and trip
# blockbuster. Marked xfail (strict=False) so CI stays green; pop
# entries as the underlying blocking calls get fixed. Most of the
# `test_async_service_registration*` and `test_async_tasks` entries
# share a single root cause: `Zeroconf.async_close()` -> ... ->
# `ServiceBrowser.cancel()` calls `Thread.join()` to drain the
# dedicated browser thread, and on Python 3.10-3.12 the thread is
# still alive when the join happens. `test_use_asyncio_false_*` is
# by design (sync bootstrap when `use_asyncio=False` is requested from
# inside a running loop); `test_run_coro_with_timeout` exercises the
# sync-from-thread bridge intentionally. The strict=False marker keeps
# the suite green on the Python versions where the race resolves the
# other way.
_KNOWN_BLOCKING: frozenset[str] = frozenset(
{
"tests/test_asyncio.py::test_async_service_registration",
"tests/test_asyncio.py::test_async_service_registration_with_server_missing",
"tests/test_asyncio.py::test_async_service_registration_same_server_different_ports",
"tests/test_asyncio.py::test_async_service_registration_same_server_same_ports",
"tests/test_asyncio.py::test_async_tasks",
"tests/test_core.py::Framework::test_use_asyncio_false_forces_thread_when_loop_running",
"tests/utils/test_asyncio.py::test_run_coro_with_timeout",
}
)
def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item]) -> None:
"""Mark known-blocking tests xfail so blockbuster doesn't fail the suite."""
if blockbuster_ctx is None:
return
marker = pytest.mark.xfail(
reason="blockbuster: blocking call in asyncio path",
strict=False,
)
for item in items:
if item.nodeid in _KNOWN_BLOCKING:
item.add_marker(marker)
@pytest.fixture(autouse=True)
def blockbuster(
request: pytest.FixtureRequest,
) -> Iterator[BlockBuster | None]:
"""Fail any test that performs a blocking call inside the asyncio loop."""
if blockbuster_ctx is None or _BENCHMARKS_DIR in str(request.node.fspath):
yield None
return
with blockbuster_ctx() as bb:
yield bb
@pytest.fixture(autouse=True)
def verify_threads_ended():
"""Verify that the threads are not running after the test."""
threads_before = frozenset(threading.enumerate())
yield
threads = frozenset(threading.enumerate()) - threads_before
assert not threads
@pytest.fixture
def zc_loopback() -> Generator[Zeroconf]:
"""Yield a loopback `Zeroconf` and close it on teardown.
Replaces the inline `zc = Zeroconf(interfaces=["127.0.0.1"])` +
explicit `zc.close()` pattern duplicated across the suite. Calling
`zc.close()` inside a test is still safe — `close()` is idempotent.
"""
zc = Zeroconf(interfaces=["127.0.0.1"])
try:
yield zc
finally:
zc.close()
@pytest_asyncio.fixture
async def aiozc_loopback() -> AsyncGenerator[AsyncZeroconf]:
"""Yield a loopback `AsyncZeroconf` and close it on teardown.
Replaces the inline `aiozc = AsyncZeroconf(interfaces=["127.0.0.1"])`
+ explicit `await aiozc.async_close()` pattern duplicated across the
suite. Calling `async_close()` inside a test is still safe.
"""
aiozc = AsyncZeroconf(interfaces=["127.0.0.1"])
try:
yield aiozc
finally:
await aiozc.async_close()
@pytest.fixture
def run_isolated():
"""Change the mDNS port to run the test in isolation."""
with (
patch.object(query_handler, "_MDNS_PORT", 5454),
patch.object(_core, "_MDNS_PORT", 5454),
patch.object(const, "_MDNS_PORT", 5454),
):
yield
@pytest.fixture
def disable_duplicate_packet_suppression():
"""Disable duplicate packet suppress.
Some tests run too slowly because of the duplicate
packet suppression.
"""
with patch.object(const, "_DUPLICATE_PACKET_SUPPRESSION_INTERVAL", 0):
yield
@pytest.fixture
def quick_timing() -> Generator[None]:
"""Shorten the probe/announce/goodbye/first-query intervals for tests on loopback.
The production values (_CHECK_TIME=500ms, _REGISTER_TIME=225ms,
_UNREGISTER_TIME=125ms, _PROBE_RANDOM_DELAY_INTERVAL=150-250ms,
_FIRST_QUERY_DELAY_RANDOM_INTERVAL=20-120ms) exist for RFC 6762
interop on real networks (§8.1 thundering-herd avoidance for
probing, §5.2 for the initial-query delay). Tests on 127.0.0.1
do not need them and pay 1-2s per register/unregister cycle,
150-250ms per probe, and 20-120ms per ServiceBrowser startup
without this fixture. Opt in either by adding `quick_timing`
to a test's argument list or via
`@pytest.mark.usefixtures("quick_timing")` on the test or
its class.
"""
with (
patch.object(_core, "_CHECK_TIME", 10),
patch.object(_core, "_REGISTER_TIME", 10),
patch.object(_core, "_UNREGISTER_TIME", 10),
patch.object(_core, "_PROBE_RANDOM_DELAY_INTERVAL", (1, 5)),
patch.object(service_browser, "_FIRST_QUERY_DELAY_RANDOM_INTERVAL", (1, 5)),
):
yield
@pytest.fixture
def quick_aggregation_timing() -> Generator[None]:
"""Scale multicast aggregation / network-protection delays 10x for tests.
The aggregation tests in `tests/test_handlers.py` verify timing-
dependent behaviour of `MulticastOutgoingQueue`: aggregation window,
network protection (~1s), and protected aggregation. The behaviour
under test is a ratio of these constants — the exact wall-clock
values are not the contract — so scaling them down and the test
sleeps in lock-step preserves what is tested while dropping each
test from ~3s to ~0.3s.
The patches must be in place before `AsyncZeroconf(...)` is
constructed because `MulticastOutgoingQueue` reads the constants at
init time and stashes them on the instance. The per-queue
`_multicast_delay_random_min` / `_max` jitter (1-5ms here) can
still be set on the queue instance after construction by the test
itself — those slots are `cdef public` in the .pxd.
"""
with (
patch.object(_core, "_AGGREGATION_DELAY", 50),
patch.object(_core, "_PROTECTED_AGGREGATION_DELAY", 20),
patch.object(_core, "_ONE_SECOND", 100),
):
yield
@pytest.fixture
def quick_request_timing() -> Generator[None]:
"""Shorten the initial-query delay used by AsyncServiceInfo.async_request.
The 200ms `_LISTENER_TIME` and 20-120ms random jitter (RFC 6762
§5.2) help spread queries from multiple clients on real networks.
On loopback they're pure overhead — get_service_info-style tests
wait ~250ms before the first query even fires. Opt in either by
adding `quick_request_timing` to a test's argument list or via
`@pytest.mark.usefixtures("quick_request_timing")` on the test
or its class, then drop the test's own timeouts (which had to
accommodate that delay).
"""
with (
patch.object(service_info, "_LISTENER_TIME", 10),
patch.object(service_info, "_AVOID_SYNC_DELAY_RANDOM_INTERVAL", (1, 5)),
):
yield