Skip to content

Commit 963d3d7

Browse files
authored
test: eliminate test_get_info_single race by injecting from the send mock (#1716)
1 parent 91aa21d commit 963d3d7

1 file changed

Lines changed: 66 additions & 75 deletions

File tree

tests/services/test_info.py

Lines changed: 66 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -522,97 +522,88 @@ def test_get_info_single(self):
522522
service_address = "10.0.1.2"
523523

524524
service_info = None
525-
send_event = Event()
526525
service_info_event = Event()
527526

528-
last_sent: r.DNSOutgoing | None = None
527+
ttl = 120
528+
response_records = [
529+
r.DNSText(
530+
service_name,
531+
const._TYPE_TXT,
532+
const._CLASS_IN | const._CLASS_UNIQUE,
533+
ttl,
534+
service_text,
535+
),
536+
r.DNSService(
537+
service_name,
538+
const._TYPE_SRV,
539+
const._CLASS_IN | const._CLASS_UNIQUE,
540+
ttl,
541+
0,
542+
0,
543+
80,
544+
service_server,
545+
),
546+
r.DNSAddress(
547+
service_server,
548+
const._TYPE_A,
549+
const._CLASS_IN | const._CLASS_UNIQUE,
550+
ttl,
551+
socket.inet_pton(socket.AF_INET, service_address),
552+
),
553+
]
529554

530-
def send(out, addr=const._MDNS_ADDR, port=const._MDNS_PORT, v6_flow_scope=()):
531-
"""Sends an outgoing packet."""
532-
nonlocal last_sent
555+
def mock_incoming_msg(records: Iterable[r.DNSRecord]) -> r.DNSIncoming:
556+
generated = r.DNSOutgoing(const._FLAGS_QR_RESPONSE)
557+
for record in records:
558+
generated.add_answer_at_time(record, 0)
559+
return r.DNSIncoming(generated.packets()[0])
533560

534-
last_sent = out
535-
send_event.set()
561+
sent_queries: list[r.DNSOutgoing] = []
562+
563+
def send(out, addr=const._MDNS_ADDR, port=const._MDNS_PORT, v6_flow_scope=()):
564+
"""Capture each query and, on the first one, fill the cache
565+
inline so the next iteration of `async_request` finds
566+
`_is_complete=True` and exits without sending another query.
567+
568+
Running the inject from inside `send` keeps it on the event
569+
loop thread and atomic with the first send — eliminating the
570+
test-thread → `run_coroutine_threadsafe` race that flaked
571+
under PyPy + use_cython when `quick_request_timing` shortens
572+
the inter-iteration delay to ~15ms.
573+
"""
574+
sent_queries.append(out)
575+
if len(sent_queries) == 1:
576+
zc.record_manager.async_updates_from_response(mock_incoming_msg(response_records))
577+
578+
def get_service_info_helper(zc, type, name):
579+
nonlocal service_info
580+
service_info = zc.get_service_info(type, name)
581+
service_info_event.set()
536582

537583
# patch the zeroconf send
538584
with patch.object(zc, "async_send", send):
539-
540-
def mock_incoming_msg(records: Iterable[r.DNSRecord]) -> r.DNSIncoming:
541-
generated = r.DNSOutgoing(const._FLAGS_QR_RESPONSE)
542-
543-
for record in records:
544-
generated.add_answer_at_time(record, 0)
545-
546-
return r.DNSIncoming(generated.packets()[0])
547-
548-
def get_service_info_helper(zc, type, name):
549-
nonlocal service_info
550-
service_info = zc.get_service_info(type, name)
551-
service_info_event.set()
552-
553585
try:
554-
ttl = 120
555586
helper_thread = threading.Thread(
556587
target=get_service_info_helper,
557588
args=(zc, service_type, service_name),
558589
)
559590
helper_thread.start()
560-
# Positive wait — the first query fires within
561-
# `_LISTENER_TIME` + jitter (~15ms under
562-
# `quick_request_timing`, ~320ms without).
563-
wait_time = 1
564-
565-
# Expect query for SRV, TXT, A, AAAA
566-
send_event.wait(wait_time)
567-
assert last_sent is not None
568-
assert len(last_sent.questions) == 4
569-
assert r.DNSQuestion(service_name, const._TYPE_SRV, const._CLASS_IN) in last_sent.questions
570-
assert r.DNSQuestion(service_name, const._TYPE_TXT, const._CLASS_IN) in last_sent.questions
571-
assert r.DNSQuestion(service_name, const._TYPE_A, const._CLASS_IN) in last_sent.questions
572-
assert r.DNSQuestion(service_name, const._TYPE_AAAA, const._CLASS_IN) in last_sent.questions
573-
assert service_info is None
574591

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.
579-
last_sent = None
580-
send_event.clear()
581-
_inject_response(
582-
zc,
583-
mock_incoming_msg(
584-
[
585-
r.DNSText(
586-
service_name,
587-
const._TYPE_TXT,
588-
const._CLASS_IN | const._CLASS_UNIQUE,
589-
ttl,
590-
service_text,
591-
),
592-
r.DNSService(
593-
service_name,
594-
const._TYPE_SRV,
595-
const._CLASS_IN | const._CLASS_UNIQUE,
596-
ttl,
597-
0,
598-
0,
599-
80,
600-
service_server,
601-
),
602-
r.DNSAddress(
603-
service_server,
604-
const._TYPE_A,
605-
const._CLASS_IN | const._CLASS_UNIQUE,
606-
ttl,
607-
socket.inet_pton(socket.AF_INET, service_address),
608-
),
609-
]
610-
),
611-
)
612-
send_event.wait(0.1)
613-
assert last_sent is None
592+
# Helper should complete promptly — the inline inject in
593+
# `send` populates the cache before the request loop's
594+
# next iteration.
595+
service_info_event.wait(1)
614596
assert service_info is not None
615597

598+
# First (and only) query: QU for SRV/TXT/A/AAAA.
599+
assert len(sent_queries) == 1
600+
first_sent = sent_queries[0]
601+
assert len(first_sent.questions) == 4
602+
assert r.DNSQuestion(service_name, const._TYPE_SRV, const._CLASS_IN) in first_sent.questions
603+
assert r.DNSQuestion(service_name, const._TYPE_TXT, const._CLASS_IN) in first_sent.questions
604+
assert r.DNSQuestion(service_name, const._TYPE_A, const._CLASS_IN) in first_sent.questions
605+
assert r.DNSQuestion(service_name, const._TYPE_AAAA, const._CLASS_IN) in first_sent.questions
606+
616607
finally:
617608
helper_thread.join()
618609
zc.remove_all_service_listeners()

0 commit comments

Comments
 (0)