-
Notifications
You must be signed in to change notification settings - Fork 227
Expand file tree
/
Copy pathtest_engine.py
More file actions
122 lines (105 loc) · 5.14 KB
/
test_engine.py
File metadata and controls
122 lines (105 loc) · 5.14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
"""Unit tests for zeroconf._engine"""
from __future__ import annotations
import asyncio
import itertools
import logging
from unittest.mock import patch
import pytest
import zeroconf as r
from zeroconf import _engine, const
from zeroconf.asyncio import AsyncZeroconf
log = logging.getLogger("zeroconf")
original_logging_level = logging.NOTSET
def setup_module():
global original_logging_level
original_logging_level = log.level
log.setLevel(logging.DEBUG)
def teardown_module():
if original_logging_level != logging.NOTSET:
log.setLevel(original_logging_level)
# This test uses asyncio because it needs to access the cache directly
# which is not threadsafe
@pytest.mark.asyncio
async def test_reaper():
with patch.object(_engine, "_CACHE_CLEANUP_INTERVAL", 0.01):
aiozc = AsyncZeroconf(interfaces=["127.0.0.1"])
zeroconf = aiozc.zeroconf
cache = zeroconf.cache
original_entries = list(itertools.chain(*(cache.entries_with_name(name) for name in cache.names())))
record_with_10s_ttl = r.DNSAddress("a", const._TYPE_SOA, const._CLASS_IN, 10, b"a")
record_with_1s_ttl = r.DNSAddress("a", const._TYPE_SOA, const._CLASS_IN, 1, b"b")
# Backdate the short-lived record so it expires at the next
# reaper tick instead of waiting the full TTL in real time.
record_with_1s_ttl.created -= 2000
zeroconf.cache.async_add_records([record_with_10s_ttl, record_with_1s_ttl])
question = r.DNSQuestion("_hap._tcp._local.", const._TYPE_PTR, const._CLASS_IN)
now = r.current_time_millis()
# Add the question at `past` so the reaper's next tick will see
# `current_time - past > _DUPLICATE_QUESTION_INTERVAL` and prune it,
# while the initial `suppresses(now, ...)` check still sees the
# question as recent (since `now - past == 999`, not strictly `> 999`).
past = now - 999
other_known_answers: set[r.DNSRecord] = {
r.DNSPointer(
"_hap._tcp.local.",
const._TYPE_PTR,
const._CLASS_IN,
10000,
"known-to-other._hap._tcp.local.",
)
}
zeroconf.question_history.add_question_at_time(question, past, other_known_answers)
assert zeroconf.question_history.suppresses(question, now, other_known_answers)
entries_with_cache = list(itertools.chain(*(cache.entries_with_name(name) for name in cache.names())))
await asyncio.sleep(0.1)
entries = list(itertools.chain(*(cache.entries_with_name(name) for name in cache.names())))
assert zeroconf.cache.get(record_with_1s_ttl) is None
await aiozc.async_close()
assert not zeroconf.question_history.suppresses(question, now, other_known_answers)
assert entries != original_entries
assert entries_with_cache != original_entries
assert record_with_10s_ttl in entries
assert record_with_1s_ttl not in entries
@pytest.mark.asyncio
async def test_setup_releases_socket_ownership(aiozc_loopback: AsyncZeroconf) -> None:
"""Engine releases its pending-socket refs once each socket has a transport."""
await aiozc_loopback.zeroconf.async_wait_for_start()
engine = aiozc_loopback.zeroconf.engine
assert engine._listen_socket is None
assert engine._respond_sockets == []
assert engine.readers
assert engine.senders
@pytest.mark.asyncio
async def test_async_close_propagates_outer_cancellation(aiozc_loopback: AsyncZeroconf) -> None:
"""Outer-task cancellation while awaiting setup propagates to the caller."""
await aiozc_loopback.zeroconf.async_wait_for_start()
engine = aiozc_loopback.zeroconf.engine
loop = asyncio.get_running_loop()
original_task = engine._setup_task
fake_task = loop.create_future()
fake_task.set_exception(asyncio.CancelledError())
engine._setup_task = fake_task # type: ignore[assignment]
try:
with pytest.raises(asyncio.CancelledError):
await engine._async_close()
finally:
engine._setup_task = original_task
@pytest.mark.asyncio
async def test_reaper_aborts_when_done():
"""Ensure cache cleanup stops when zeroconf is done."""
with patch.object(_engine, "_CACHE_CLEANUP_INTERVAL", 0.01):
aiozc = AsyncZeroconf(interfaces=["127.0.0.1"])
zeroconf = aiozc.zeroconf
record_with_10s_ttl = r.DNSAddress("a", const._TYPE_SOA, const._CLASS_IN, 10, b"a")
record_with_1s_ttl = r.DNSAddress("a", const._TYPE_SOA, const._CLASS_IN, 1, b"b")
zeroconf.cache.async_add_records([record_with_10s_ttl, record_with_1s_ttl])
assert zeroconf.cache.get(record_with_10s_ttl) is not None
assert zeroconf.cache.get(record_with_1s_ttl) is not None
await aiozc.async_close()
# Backdate to immediate expiry so we don't have to wait the full
# TTL; the assertion is that the reaper has stopped, so a
# short sleep is enough to give it a chance to (incorrectly) run.
record_with_1s_ttl.created -= 2000
await asyncio.sleep(0.1)
assert zeroconf.cache.get(record_with_10s_ttl) is not None
assert zeroconf.cache.get(record_with_1s_ttl) is not None