|
19 | 19 | import pytest |
20 | 20 |
|
21 | 21 | import zeroconf as r |
22 | | -from zeroconf import NotRunningException, Zeroconf, const, current_time_millis |
| 22 | +from zeroconf import NotRunningException, Zeroconf, _listener, const, current_time_millis |
23 | 23 | from zeroconf._listener import _TC_DELAY_RANDOM_INTERVAL, AsyncListener, _WrappedTransport |
24 | 24 | from zeroconf._protocol.incoming import DNSIncoming |
25 | 25 | from zeroconf.asyncio import AsyncZeroconf |
@@ -794,6 +794,101 @@ def test_tc_bit_defer_window_is_bounded(): |
794 | 794 | zc.close() |
795 | 795 |
|
796 | 796 |
|
| 797 | +def _make_distinct_tc_packets(count: int, name_prefix: str = "q") -> list[bytes]: |
| 798 | + """Generate ``count`` byte-distinct TC-flagged query packets for flood inputs.""" |
| 799 | + packets = [] |
| 800 | + for i in range(count): |
| 801 | + out = r.DNSOutgoing(const._FLAGS_QR_QUERY | const._FLAGS_TC) |
| 802 | + out.add_question(r.DNSQuestion(f"{name_prefix}{i}._tcp.local.", const._TYPE_PTR, const._CLASS_IN)) |
| 803 | + packets.append(out.packets()[0]) |
| 804 | + return packets |
| 805 | + |
| 806 | + |
| 807 | +def _synthetic_source_ip(i: int) -> str: |
| 808 | + """Distinct synthetic source IPs from the documentation ranges.""" |
| 809 | + if i < 256: |
| 810 | + return f"203.0.113.{i}" |
| 811 | + if i < 512: |
| 812 | + return f"198.51.100.{i - 256}" |
| 813 | + return f"192.0.2.{i - 512}" |
| 814 | + |
| 815 | + |
| 816 | +def test_tc_bit_per_addr_queue_is_bounded(quick_timing: None) -> None: |
| 817 | + """Per-addr deferred queue must not grow past ``_MAX_DEFERRED_PER_ADDR``.""" |
| 818 | + zc = Zeroconf(interfaces=["127.0.0.1"]) |
| 819 | + _wait_for_start(zc) |
| 820 | + protocol = zc.engine.protocols[0] |
| 821 | + source_ip = "203.0.113.21" |
| 822 | + |
| 823 | + extra = 4 |
| 824 | + packets = _make_distinct_tc_packets(const._MAX_DEFERRED_PER_ADDR + extra) |
| 825 | + |
| 826 | + # Push the reassembly timer well past any possible test runtime |
| 827 | + # so the bound under test is the only thing that can drop entries. |
| 828 | + with patch.object(_listener, "_TC_DELAY_RANDOM_INTERVAL", (60_000, 60_001)): |
| 829 | + for raw in packets: |
| 830 | + threadsafe_query(zc, protocol, r.DNSIncoming(raw), source_ip, const._MDNS_PORT, Mock(), ()) |
| 831 | + |
| 832 | + assert len(protocol._deferred[source_ip]) == const._MAX_DEFERRED_PER_ADDR |
| 833 | + # Last ``extra`` packets must have been dropped, not displaced; the |
| 834 | + # earlier ``_MAX_DEFERRED_PER_ADDR`` entries are the ones retained. |
| 835 | + retained = [incoming.data for incoming in protocol._deferred[source_ip]] |
| 836 | + assert retained == packets[: const._MAX_DEFERRED_PER_ADDR] |
| 837 | + |
| 838 | + zc.close() |
| 839 | + |
| 840 | + |
| 841 | +def test_tc_bit_total_addrs_is_bounded(quick_timing: None) -> None: |
| 842 | + """Distinct addrs with deferred state must not exceed ``_MAX_DEFERRED_ADDRS``.""" |
| 843 | + zc = Zeroconf(interfaces=["127.0.0.1"]) |
| 844 | + _wait_for_start(zc) |
| 845 | + protocol = zc.engine.protocols[0] |
| 846 | + |
| 847 | + raw = _make_distinct_tc_packets(1)[0] |
| 848 | + extra = 4 |
| 849 | + addrs = [_synthetic_source_ip(i) for i in range(const._MAX_DEFERRED_ADDRS + extra)] |
| 850 | + |
| 851 | + # Push the reassembly timer well past any possible test runtime |
| 852 | + # so the bound under test is the only thing that can drop entries; |
| 853 | + # without this, PyPy / slow runners can fire timers between the |
| 854 | + # last enqueue and the assertion. |
| 855 | + with patch.object(_listener, "_TC_DELAY_RANDOM_INTERVAL", (60_000, 60_001)): |
| 856 | + for source_ip in addrs: |
| 857 | + threadsafe_query(zc, protocol, r.DNSIncoming(raw), source_ip, const._MDNS_PORT, Mock(), ()) |
| 858 | + |
| 859 | + assert len(protocol._deferred) == const._MAX_DEFERRED_ADDRS |
| 860 | + assert len(protocol._timers) == const._MAX_DEFERRED_ADDRS |
| 861 | + |
| 862 | + zc.close() |
| 863 | + |
| 864 | + |
| 865 | +def test_tc_bit_eviction_drops_oldest_addr(quick_timing: None) -> None: |
| 866 | + """Adding a new addr at capacity drops the oldest insertion (FIFO).""" |
| 867 | + zc = Zeroconf(interfaces=["127.0.0.1"]) |
| 868 | + _wait_for_start(zc) |
| 869 | + protocol = zc.engine.protocols[0] |
| 870 | + |
| 871 | + raw = _make_distinct_tc_packets(1)[0] |
| 872 | + fillers = [_synthetic_source_ip(i) for i in range(const._MAX_DEFERRED_ADDRS)] |
| 873 | + new_addr = _synthetic_source_ip(const._MAX_DEFERRED_ADDRS) |
| 874 | + oldest = fillers[0] |
| 875 | + |
| 876 | + with patch.object(_listener, "_TC_DELAY_RANDOM_INTERVAL", (60_000, 60_001)): |
| 877 | + for source_ip in fillers: |
| 878 | + threadsafe_query(zc, protocol, r.DNSIncoming(raw), source_ip, const._MDNS_PORT, Mock(), ()) |
| 879 | + assert len(protocol._deferred) == const._MAX_DEFERRED_ADDRS |
| 880 | + assert oldest in protocol._deferred |
| 881 | + |
| 882 | + # One more distinct addr must evict the oldest insertion-order entry. |
| 883 | + threadsafe_query(zc, protocol, r.DNSIncoming(raw), new_addr, const._MDNS_PORT, Mock(), ()) |
| 884 | + assert oldest not in protocol._deferred |
| 885 | + assert oldest not in protocol._timers |
| 886 | + assert new_addr in protocol._deferred |
| 887 | + assert len(protocol._deferred) == const._MAX_DEFERRED_ADDRS |
| 888 | + |
| 889 | + zc.close() |
| 890 | + |
| 891 | + |
797 | 892 | @pytest.mark.asyncio |
798 | 893 | async def test_open_close_twice_from_async() -> None: |
799 | 894 | """Test we can close twice from a coroutine when using Zeroconf. |
|
0 commit comments