@@ -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