Skip to content

Commit eb37f08

Browse files
authored
Move additional tests to test_core (#559)
1 parent 18b9d0a commit eb37f08

3 files changed

Lines changed: 197 additions & 192 deletions

File tree

tests/__init__.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,45 @@
2020
USA
2121
"""
2222

23+
import socket
24+
from functools import lru_cache
25+
26+
27+
import ifaddr
28+
29+
2330
from zeroconf.core import Zeroconf
2431
from zeroconf.dns import DNSIncoming
2532

2633

2734
def _inject_response(zc: Zeroconf, msg: DNSIncoming) -> None:
2835
"""Inject a DNSIncoming response."""
2936
zc.handle_response(msg)
37+
38+
39+
@lru_cache(maxsize=None)
40+
def has_working_ipv6():
41+
"""Return True if if the system can bind an IPv6 address."""
42+
if not socket.has_ipv6:
43+
return False
44+
45+
try:
46+
sock = socket.socket(socket.AF_INET6)
47+
sock.bind(('::1', 0))
48+
except Exception:
49+
return False
50+
finally:
51+
if sock:
52+
sock.close()
53+
54+
for iface in ifaddr.get_adapters():
55+
for addr in iface.ips:
56+
if addr.is_IPv6 and iface.index is not None:
57+
return True
58+
return False
59+
60+
61+
def _clear_cache(zc):
62+
for name in zc.cache.names():
63+
for record in zc.cache.entries_with_name(name):
64+
zc.cache.remove(record)

tests/test_core.py

Lines changed: 160 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,18 @@
66

77
import itertools
88
import logging
9-
import threading
9+
import os
10+
import socket
1011
import time
1112
import unittest
1213
import unittest.mock
14+
from typing import cast
1315

14-
15-
import pytest
1616
import zeroconf as r
1717
from zeroconf import core
1818

19+
from . import has_working_ipv6, _inject_response
20+
1921
log = logging.getLogger('zeroconf')
2022
original_logging_level = logging.NOTSET
2123

@@ -52,3 +54,158 @@ def test_reaper(self):
5254
assert entries_with_cache != original_entries
5355
assert record_with_10s_ttl in entries
5456
assert record_with_1s_ttl not in entries
57+
58+
59+
class Framework(unittest.TestCase):
60+
def test_launch_and_close(self):
61+
rv = r.Zeroconf(interfaces=r.InterfaceChoice.All)
62+
rv.close()
63+
rv = r.Zeroconf(interfaces=r.InterfaceChoice.Default)
64+
rv.close()
65+
66+
def test_launch_and_close_context_manager(self):
67+
with r.Zeroconf(interfaces=r.InterfaceChoice.All) as rv:
68+
assert rv.done is False
69+
assert rv.done is True
70+
71+
with r.Zeroconf(interfaces=r.InterfaceChoice.Default) as rv:
72+
assert rv.done is False
73+
assert rv.done is True
74+
75+
def test_launch_and_close_unicast(self):
76+
rv = r.Zeroconf(interfaces=r.InterfaceChoice.All, unicast=True)
77+
rv.close()
78+
rv = r.Zeroconf(interfaces=r.InterfaceChoice.Default, unicast=True)
79+
rv.close()
80+
81+
def test_close_multiple_times(self):
82+
rv = r.Zeroconf(interfaces=r.InterfaceChoice.Default)
83+
rv.close()
84+
rv.close()
85+
86+
@unittest.skipIf(not has_working_ipv6(), 'Requires IPv6')
87+
@unittest.skipIf(os.environ.get('SKIP_IPV6'), 'IPv6 tests disabled')
88+
def test_launch_and_close_v4_v6(self):
89+
rv = r.Zeroconf(interfaces=r.InterfaceChoice.All, ip_version=r.IPVersion.All)
90+
rv.close()
91+
rv = r.Zeroconf(interfaces=r.InterfaceChoice.Default, ip_version=r.IPVersion.All)
92+
rv.close()
93+
94+
@unittest.skipIf(not has_working_ipv6(), 'Requires IPv6')
95+
@unittest.skipIf(os.environ.get('SKIP_IPV6'), 'IPv6 tests disabled')
96+
def test_launch_and_close_v6_only(self):
97+
rv = r.Zeroconf(interfaces=r.InterfaceChoice.All, ip_version=r.IPVersion.V6Only)
98+
rv.close()
99+
rv = r.Zeroconf(interfaces=r.InterfaceChoice.Default, ip_version=r.IPVersion.V6Only)
100+
rv.close()
101+
102+
def test_handle_response(self):
103+
def mock_incoming_msg(service_state_change: r.ServiceStateChange) -> r.DNSIncoming:
104+
ttl = 120
105+
generated = r.DNSOutgoing(r._FLAGS_QR_RESPONSE)
106+
107+
if service_state_change == r.ServiceStateChange.Updated:
108+
generated.add_answer_at_time(
109+
r.DNSText(service_name, r._TYPE_TXT, r._CLASS_IN | r._CLASS_UNIQUE, ttl, service_text), 0
110+
)
111+
return r.DNSIncoming(generated.packet())
112+
113+
if service_state_change == r.ServiceStateChange.Removed:
114+
ttl = 0
115+
116+
generated.add_answer_at_time(
117+
r.DNSPointer(service_type, r._TYPE_PTR, r._CLASS_IN, ttl, service_name), 0
118+
)
119+
generated.add_answer_at_time(
120+
r.DNSService(
121+
service_name, r._TYPE_SRV, r._CLASS_IN | r._CLASS_UNIQUE, ttl, 0, 0, 80, service_server
122+
),
123+
0,
124+
)
125+
generated.add_answer_at_time(
126+
r.DNSText(service_name, r._TYPE_TXT, r._CLASS_IN | r._CLASS_UNIQUE, ttl, service_text), 0
127+
)
128+
generated.add_answer_at_time(
129+
r.DNSAddress(
130+
service_server,
131+
r._TYPE_A,
132+
r._CLASS_IN | r._CLASS_UNIQUE,
133+
ttl,
134+
socket.inet_aton(service_address),
135+
),
136+
0,
137+
)
138+
139+
return r.DNSIncoming(generated.packet())
140+
141+
def mock_split_incoming_msg(service_state_change: r.ServiceStateChange) -> r.DNSIncoming:
142+
"""Mock an incoming message for the case where the packet is split."""
143+
ttl = 120
144+
generated = r.DNSOutgoing(r._FLAGS_QR_RESPONSE)
145+
generated.add_answer_at_time(
146+
r.DNSAddress(
147+
service_server,
148+
r._TYPE_A,
149+
r._CLASS_IN | r._CLASS_UNIQUE,
150+
ttl,
151+
socket.inet_aton(service_address),
152+
),
153+
0,
154+
)
155+
generated.add_answer_at_time(
156+
r.DNSService(
157+
service_name, r._TYPE_SRV, r._CLASS_IN | r._CLASS_UNIQUE, ttl, 0, 0, 80, service_server
158+
),
159+
0,
160+
)
161+
return r.DNSIncoming(generated.packet())
162+
163+
service_name = 'name._type._tcp.local.'
164+
service_type = '_type._tcp.local.'
165+
service_server = 'ash-2.local.'
166+
service_text = b'path=/~paulsm/'
167+
service_address = '10.0.1.2'
168+
169+
zeroconf = r.Zeroconf(interfaces=['127.0.0.1'])
170+
171+
try:
172+
# service added
173+
_inject_response(zeroconf, mock_incoming_msg(r.ServiceStateChange.Added))
174+
dns_text = zeroconf.cache.get_by_details(service_name, r._TYPE_TXT, r._CLASS_IN)
175+
assert dns_text is not None
176+
assert cast(r.DNSText, dns_text).text == service_text # service_text is b'path=/~paulsm/'
177+
all_dns_text = zeroconf.cache.get_all_by_details(service_name, r._TYPE_TXT, r._CLASS_IN)
178+
assert [dns_text] == all_dns_text
179+
180+
# https://tools.ietf.org/html/rfc6762#section-10.2
181+
# Instead of merging this new record additively into the cache in addition
182+
# to any previous records with the same name, rrtype, and rrclass,
183+
# all old records with that name, rrtype, and rrclass that were received
184+
# more than one second ago are declared invalid,
185+
# and marked to expire from the cache in one second.
186+
time.sleep(1.1)
187+
188+
# service updated. currently only text record can be updated
189+
service_text = b'path=/~humingchun/'
190+
_inject_response(zeroconf, mock_incoming_msg(r.ServiceStateChange.Updated))
191+
dns_text = zeroconf.cache.get_by_details(service_name, r._TYPE_TXT, r._CLASS_IN)
192+
assert dns_text is not None
193+
assert cast(r.DNSText, dns_text).text == service_text # service_text is b'path=/~humingchun/'
194+
195+
time.sleep(1.1)
196+
197+
# The split message only has a SRV and A record.
198+
# This should not evict TXT records from the cache
199+
_inject_response(zeroconf, mock_split_incoming_msg(r.ServiceStateChange.Updated))
200+
time.sleep(1.1)
201+
dns_text = zeroconf.cache.get_by_details(service_name, r._TYPE_TXT, r._CLASS_IN)
202+
assert dns_text is not None
203+
assert cast(r.DNSText, dns_text).text == service_text # service_text is b'path=/~humingchun/'
204+
205+
# service removed
206+
_inject_response(zeroconf, mock_incoming_msg(r.ServiceStateChange.Removed))
207+
dns_text = zeroconf.cache.get_by_details(service_name, r._TYPE_TXT, r._CLASS_IN)
208+
assert dns_text is None
209+
210+
finally:
211+
zeroconf.close()

0 commit comments

Comments
 (0)