Skip to content

Commit 4bae30a

Browse files
authored
test: speed up service-info request tests with quick_request_timing fixture (#1709)
1 parent ee3c7d7 commit 4bae30a

4 files changed

Lines changed: 50 additions & 12 deletions

File tree

tests/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@
3535

3636
_MONOTONIC_RESOLUTION = time.get_clock_info("monotonic").resolution
3737

38+
# get_service_info / async_request timeout for tests using the
39+
# `quick_request_timing` fixture. The fixture cuts the initial-query
40+
# delay to ~15ms (10ms _LISTENER_TIME + 1-5ms jitter), so 50ms is
41+
# ample headroom for tests that only need to observe the first one
42+
# or two queries.
43+
QUICK_REQUEST_TIMEOUT_MS = 50
44+
3845

3946
class QuestionHistoryWithoutSuppression(QuestionHistory):
4047
def suppresses(self, question: DNSQuestion, now: float, known_answers: set[DNSRecord]) -> bool:

tests/conftest.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from zeroconf import _core, const
1212
from zeroconf._handlers import query_handler
13+
from zeroconf._services import info as service_info
1314

1415

1516
@pytest.fixture(autouse=True)
@@ -59,3 +60,21 @@ def quick_timing() -> Generator[None]:
5960
patch.object(_core, "_UNREGISTER_TIME", 10),
6061
):
6162
yield
63+
64+
65+
@pytest.fixture
66+
def quick_request_timing() -> Generator[None]:
67+
"""Shorten the initial-query delay used by AsyncServiceInfo.async_request.
68+
69+
The 200ms `_LISTENER_TIME` and 20-120ms random jitter (RFC 6762
70+
§5.2) help spread queries from multiple clients on real networks.
71+
On loopback they're pure overhead — get_service_info-style tests
72+
wait ~250ms before the first query even fires. Opt in by adding
73+
`quick_request_timing` to a test's argument list, then drop the
74+
test's own timeouts (which had to accommodate that delay).
75+
"""
76+
with (
77+
patch.object(service_info, "_LISTENER_TIME", 10),
78+
patch.object(service_info, "_AVOID_SYNC_DELAY_RANDOM_INTERVAL", (1, 5)),
79+
):
80+
yield

tests/services/test_info.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from zeroconf._utils.net import IPVersion
2424
from zeroconf.asyncio import AsyncZeroconf
2525

26-
from .. import _inject_response, has_working_ipv6
26+
from .. import QUICK_REQUEST_TIMEOUT_MS, _inject_response, has_working_ipv6
2727

2828
log = logging.getLogger("zeroconf")
2929
original_logging_level = logging.NOTSET
@@ -511,6 +511,7 @@ def get_service_info_helper(zc, type, name, timeout):
511511
zc.remove_all_service_listeners()
512512
zc.close()
513513

514+
@pytest.mark.usefixtures("quick_request_timing")
514515
def test_get_info_single(self):
515516
zc = r.Zeroconf(interfaces=["127.0.0.1"])
516517

@@ -556,6 +557,9 @@ def get_service_info_helper(zc, type, name):
556557
args=(zc, service_type, service_name),
557558
)
558559
helper_thread.start()
560+
# Positive wait — the first query fires within
561+
# `_LISTENER_TIME` + jitter (~15ms under
562+
# `quick_request_timing`, ~320ms without).
559563
wait_time = 1
560564

561565
# Expect query for SRV, TXT, A, AAAA
@@ -568,7 +572,10 @@ def get_service_info_helper(zc, type, name):
568572
assert r.DNSQuestion(service_name, const._TYPE_AAAA, const._CLASS_IN) in last_sent.questions
569573
assert service_info is None
570574

571-
# Expect no further queries
575+
# Expect no further queries — under `quick_request_timing`
576+
# the next query would have fired ~15ms after the previous
577+
# send, so 100ms is plenty of headroom for the negative
578+
# assertion.
572579
last_sent = None
573580
send_event.clear()
574581
_inject_response(
@@ -602,7 +609,7 @@ def get_service_info_helper(zc, type, name):
602609
]
603610
),
604611
)
605-
send_event.wait(wait_time)
612+
send_event.wait(0.1)
606613
assert last_sent is None
607614
assert service_info is not None
608615

@@ -985,7 +992,7 @@ def test_serviceinfo_accepts_bytes_or_string_dict():
985992
assert info_service.dns_text().text == b"\x0epath=/~paulsm/"
986993

987994

988-
def test_asking_qu_questions():
995+
def test_asking_qu_questions(quick_request_timing):
989996
"""Verify explicitly asking QU questions."""
990997
type_ = "_quservice._tcp.local."
991998
zeroconf = r.Zeroconf(interfaces=["127.0.0.1"])
@@ -1004,12 +1011,14 @@ def send(out, addr=const._MDNS_ADDR, port=const._MDNS_PORT):
10041011

10051012
# patch the zeroconf send
10061013
with patch.object(zeroconf, "async_send", send):
1007-
zeroconf.get_service_info(f"name.{type_}", type_, 500, question_type=r.DNSQuestionType.QU)
1014+
zeroconf.get_service_info(
1015+
f"name.{type_}", type_, QUICK_REQUEST_TIMEOUT_MS, question_type=r.DNSQuestionType.QU
1016+
)
10081017
assert first_outgoing.questions[0].unicast is True # type: ignore[union-attr]
10091018
zeroconf.close()
10101019

10111020

1012-
def test_asking_qm_questions():
1021+
def test_asking_qm_questions(quick_request_timing):
10131022
"""Verify explicitly asking QM questions."""
10141023
type_ = "_quservice._tcp.local."
10151024
zeroconf = r.Zeroconf(interfaces=["127.0.0.1"])
@@ -1028,7 +1037,9 @@ def send(out, addr=const._MDNS_ADDR, port=const._MDNS_PORT):
10281037

10291038
# patch the zeroconf send
10301039
with patch.object(zeroconf, "async_send", send):
1031-
zeroconf.get_service_info(f"name.{type_}", type_, 500, question_type=r.DNSQuestionType.QM)
1040+
zeroconf.get_service_info(
1041+
f"name.{type_}", type_, QUICK_REQUEST_TIMEOUT_MS, question_type=r.DNSQuestionType.QM
1042+
)
10321043
assert first_outgoing.questions[0].unicast is False # type: ignore[union-attr]
10331044
zeroconf.close()
10341045

tests/test_asyncio.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
from zeroconf.const import _LISTENER_TIME
4444

4545
from . import (
46+
QUICK_REQUEST_TIMEOUT_MS,
4647
QuestionHistoryWithoutSuppression,
4748
_clear_cache,
4849
has_working_ipv6,
@@ -1139,7 +1140,7 @@ def send(out, addr=const._MDNS_ADDR, port=const._MDNS_PORT, v6_flow_scope=()):
11391140

11401141

11411142
@pytest.mark.asyncio
1142-
async def test_info_asking_default_is_asking_qm_questions_after_the_first_qu():
1143+
async def test_info_asking_default_is_asking_qm_questions_after_the_first_qu(quick_request_timing):
11431144
"""Verify the service info first question is QU and subsequent ones are QM questions."""
11441145
type_ = "_quservice._tcp.local."
11451146
aiozc = AsyncZeroconf(interfaces=["127.0.0.1"])
@@ -1182,11 +1183,11 @@ def send(out, addr=const._MDNS_ADDR, port=const._MDNS_PORT):
11821183
# patch the zeroconf send
11831184
with patch.object(zeroconf_info, "async_send", send):
11841185
aiosinfo = AsyncServiceInfo(type_, registration_name)
1185-
# Patch _is_complete so we send multiple times. 500ms covers
1186-
# the QU query at 0ms plus the QM query at ~_LISTENER_TIME +
1187-
# max random delay (~320ms).
1186+
# Patch _is_complete so we send multiple times. Under
1187+
# `quick_request_timing` both the QU query at 0ms and the QM
1188+
# query at ~15ms land well inside QUICK_REQUEST_TIMEOUT_MS.
11881189
with patch("zeroconf.asyncio.AsyncServiceInfo._is_complete", False):
1189-
await aiosinfo.async_request(aiozc.zeroconf, 500)
1190+
await aiosinfo.async_request(aiozc.zeroconf, QUICK_REQUEST_TIMEOUT_MS)
11901191
try:
11911192
assert first_outgoing.questions[0].unicast is True # type: ignore[union-attr]
11921193
assert second_outgoing.questions[0].unicast is False # type: ignore[attr-defined]

0 commit comments

Comments
 (0)