2020
2121import zeroconf as r
2222from zeroconf import NotRunningException , Zeroconf , const , current_time_millis
23- from zeroconf ._listener import AsyncListener , _WrappedTransport
23+ from zeroconf ._listener import _TC_DELAY_RANDOM_INTERVAL , AsyncListener , _WrappedTransport
2424from zeroconf ._protocol .incoming import DNSIncoming
2525from zeroconf .asyncio import AsyncZeroconf
2626
@@ -699,36 +699,41 @@ def test_tc_bit_defers_last_response_missing():
699699 assert len (packets ) == 4
700700 expected_deferred = []
701701
702- next_packet = r .DNSIncoming (packets .pop (0 ))
703- expected_deferred .append (next_packet )
704- threadsafe_query (zc , protocol , next_packet , source_ip , const ._MDNS_PORT , Mock (), ())
705- assert protocol ._deferred [source_ip ] == expected_deferred
706- timer1 = protocol ._timers [source_ip ]
707-
708- next_packet = r .DNSIncoming (packets .pop (0 ))
709- expected_deferred .append (next_packet )
710- threadsafe_query (zc , protocol , next_packet , source_ip , const ._MDNS_PORT , Mock (), ())
711- assert protocol ._deferred [source_ip ] == expected_deferred
712- timer2 = protocol ._timers [source_ip ]
713- assert timer1 .cancelled ()
714- assert timer2 != timer1
715-
716- # Send the same packet again to similar multi interfaces
717- threadsafe_query (zc , protocol , next_packet , source_ip , const ._MDNS_PORT , Mock (), ())
718- assert protocol ._deferred [source_ip ] == expected_deferred
719- assert source_ip in protocol ._timers
720- timer3 = protocol ._timers [source_ip ]
721- assert not timer3 .cancelled ()
722- assert timer3 == timer2
723-
724- next_packet = r .DNSIncoming (packets .pop (0 ))
725- expected_deferred .append (next_packet )
726- threadsafe_query (zc , protocol , next_packet , source_ip , const ._MDNS_PORT , Mock (), ())
727- assert protocol ._deferred [source_ip ] == expected_deferred
728- assert source_ip in protocol ._timers
729- timer4 = protocol ._timers [source_ip ]
730- assert timer3 .cancelled ()
731- assert timer4 != timer3
702+ # Pin per-packet delay to the minimum so each successive fire_at lands
703+ # before the deadline established by the first packet — keeps the
704+ # timer-replacement assertions deterministic under bounded TC-deferral.
705+ min_delay_ms = _TC_DELAY_RANDOM_INTERVAL [0 ]
706+ with patch ("zeroconf._listener.random.randint" , return_value = min_delay_ms ):
707+ next_packet = r .DNSIncoming (packets .pop (0 ))
708+ expected_deferred .append (next_packet )
709+ threadsafe_query (zc , protocol , next_packet , source_ip , const ._MDNS_PORT , Mock (), ())
710+ assert protocol ._deferred [source_ip ] == expected_deferred
711+ timer1 = protocol ._timers [source_ip ]
712+
713+ next_packet = r .DNSIncoming (packets .pop (0 ))
714+ expected_deferred .append (next_packet )
715+ threadsafe_query (zc , protocol , next_packet , source_ip , const ._MDNS_PORT , Mock (), ())
716+ assert protocol ._deferred [source_ip ] == expected_deferred
717+ timer2 = protocol ._timers [source_ip ]
718+ assert timer1 .cancelled ()
719+ assert timer2 != timer1
720+
721+ # Send the same packet again to similar multi interfaces
722+ threadsafe_query (zc , protocol , next_packet , source_ip , const ._MDNS_PORT , Mock (), ())
723+ assert protocol ._deferred [source_ip ] == expected_deferred
724+ assert source_ip in protocol ._timers
725+ timer3 = protocol ._timers [source_ip ]
726+ assert not timer3 .cancelled ()
727+ assert timer3 == timer2
728+
729+ next_packet = r .DNSIncoming (packets .pop (0 ))
730+ expected_deferred .append (next_packet )
731+ threadsafe_query (zc , protocol , next_packet , source_ip , const ._MDNS_PORT , Mock (), ())
732+ assert protocol ._deferred [source_ip ] == expected_deferred
733+ assert source_ip in protocol ._timers
734+ timer4 = protocol ._timers [source_ip ]
735+ assert timer3 .cancelled ()
736+ assert timer4 != timer3
732737
733738 for _ in range (8 ):
734739 time .sleep (0.1 )
@@ -743,6 +748,52 @@ def test_tc_bit_defers_last_response_missing():
743748 zc .close ()
744749
745750
751+ def test_tc_bit_defer_window_is_bounded ():
752+ """TC-deferral assembly window must not slide past first_arrival + max delay."""
753+ zc = Zeroconf (interfaces = ["127.0.0.1" ])
754+ _wait_for_start (zc )
755+ type_ = "_boundeddefer._tcp.local."
756+ registration_name = f"knownname.{ type_ } "
757+
758+ info = r .ServiceInfo (
759+ type_ ,
760+ registration_name ,
761+ 80 ,
762+ 0 ,
763+ 0 ,
764+ {"path" : "/~paulsm/" },
765+ "ash-2.local." ,
766+ addresses = [socket .inet_aton ("10.0.1.2" )],
767+ )
768+ zc .registry .async_add (info )
769+
770+ protocol = zc .engine .protocols [0 ]
771+ now_ms = r .current_time_millis ()
772+ _clear_cache (zc )
773+ source_ip = "203.0.113.99"
774+
775+ generated = r .DNSOutgoing (const ._FLAGS_QR_QUERY )
776+ generated .add_question (r .DNSQuestion (type_ , const ._TYPE_PTR , const ._CLASS_IN ))
777+ for _ in range (300 ):
778+ generated .add_answer_at_time (info .dns_pointer (), now_ms )
779+ packets = generated .packets ()
780+ assert len (packets ) >= 3
781+
782+ # Pin the per-packet delay at its maximum so any subsequent reset would
783+ # land past the deadline established by the first packet.
784+ max_delay_ms = _TC_DELAY_RANDOM_INTERVAL [1 ]
785+ with patch ("zeroconf._listener.random.randint" , return_value = max_delay_ms ):
786+ threadsafe_query (zc , protocol , r .DNSIncoming (packets [0 ]), source_ip , const ._MDNS_PORT , Mock (), ())
787+ first_when = protocol ._timers [source_ip ].when ()
788+
789+ for raw in packets [1 :- 1 ]:
790+ threadsafe_query (zc , protocol , r .DNSIncoming (raw ), source_ip , const ._MDNS_PORT , Mock (), ())
791+ assert protocol ._timers [source_ip ].when () <= first_when
792+
793+ zc .registry .async_remove (info )
794+ zc .close ()
795+
796+
746797@pytest .mark .asyncio
747798async def test_open_close_twice_from_async () -> None :
748799 """Test we can close twice from a coroutine when using Zeroconf.
0 commit comments