Skip to content

Commit 75232cc

Browse files
committed
Improve test coverage
Add more needed shutdown cleanup found via additional test coverage. Force timeout calculation from milli to seconds to use floating point.
1 parent ad3c248 commit 75232cc

4 files changed

Lines changed: 111 additions & 24 deletions

File tree

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,12 @@
11
build/
2+
*.pyc
3+
*.pyo
4+
Thumbs.db
5+
.DS_Store
6+
.project
7+
.pydevproject
8+
.settings
9+
.idea
10+
.vslick
11+
.cache
212

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ universal = 1
55
show-source = 1
66
import-order-style=google
77
application-import-names=zeroconf
8+
max-line-length=110

test_zeroconf.py

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import logging
88
import socket
99
import struct
10+
import time
1011
import unittest
1112
from threading import Event
1213

@@ -149,6 +150,84 @@ def test_launch_and_close(self):
149150
rv.close()
150151

151152

153+
class Exceptions(unittest.TestCase):
154+
155+
def test_bad_service_info_name(self):
156+
browser = Zeroconf()
157+
self.assertRaises(r.BadTypeInNameException,
158+
browser.get_service_info, "type", "type_not")
159+
browser.close()
160+
161+
162+
class Listener(unittest.TestCase):
163+
164+
def test_integration_with_listener_class(self):
165+
166+
service_added = Event()
167+
service_removed = Event()
168+
169+
type_ = "_http._tcp.local."
170+
name = "xxxyyy"
171+
registration_name = "%s.%s" % (name, type_)
172+
173+
class MyListener(object):
174+
def add_service(self, zeroconf, type, name):
175+
zeroconf.get_service_info(type, name)
176+
service_added.set()
177+
178+
def remove_service(self, zeroconf, type, name):
179+
service_removed.set()
180+
181+
zeroconf_browser = Zeroconf()
182+
zeroconf_browser.add_service_listener(type_, MyListener())
183+
184+
properties = dict(
185+
prop_none=None,
186+
prop_string=b'a_prop',
187+
prop_float=1.0,
188+
prop_blank=b'a blanked string',
189+
prop_true=1,
190+
prop_false=0,
191+
)
192+
193+
zeroconf_registrar = Zeroconf()
194+
desc = {'path': '/~paulsm/'}
195+
desc.update(properties)
196+
info = ServiceInfo(
197+
type_, registration_name,
198+
socket.inet_aton("10.0.1.2"), 80, 0, 0,
199+
desc, "ash-2.local.")
200+
zeroconf_registrar.register_service(info)
201+
202+
try:
203+
service_added.wait(1)
204+
assert service_added.is_set()
205+
206+
# short pause to allow multicast timers to expire
207+
time.sleep(2)
208+
209+
# clear the answer cache to force query
210+
for record in zeroconf_browser.cache.entries():
211+
zeroconf_browser.cache.remove(record)
212+
213+
# get service info without answer cache
214+
info = zeroconf_browser.get_service_info(type_, registration_name)
215+
216+
assert info.properties[b'prop_none'] is False
217+
assert info.properties[b'prop_string'] == properties['prop_string']
218+
assert info.properties[b'prop_float'] is False
219+
assert info.properties[b'prop_blank'] == properties['prop_blank']
220+
assert info.properties[b'prop_true'] is True
221+
assert info.properties[b'prop_false'] is False
222+
223+
zeroconf_registrar.unregister_service(info)
224+
service_removed.wait(1)
225+
assert service_removed.is_set()
226+
finally:
227+
zeroconf_registrar.close()
228+
zeroconf_browser.close()
229+
230+
152231
def test_integration():
153232
service_added = Event()
154233
service_removed = Event()
@@ -177,9 +256,8 @@ def on_service_state_change(zeroconf, service_type, state_change, name):
177256
try:
178257
service_added.wait(1)
179258
assert service_added.is_set()
180-
zeroconf_registrar.unregister_service(info)
181-
service_removed.wait(1)
182-
assert service_removed.is_set()
259+
# Don't remove service, allow close() to cleanup
260+
183261
finally:
184262
zeroconf_registrar.close()
185263
browser.cancel()

zeroconf.py

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,6 @@ def emit(self, record):
6464
if log.level == logging.NOTSET:
6565
log.setLevel(logging.WARN)
6666

67-
# hook for threads
68-
69-
_GLOBAL_DONE = False
70-
7167
# Some timing constants
7268

7369
_UNREGISTER_TIME = 125
@@ -292,7 +288,7 @@ def get_expiration_time(self, percent):
292288

293289
def get_remaining_ttl(self, now):
294290
"""Returns the remaining TTL in seconds."""
295-
return max(0, (self.get_expiration_time(100) - now) / 1000)
291+
return max(0, (self.get_expiration_time(100) - now) / 1000.0)
296292

297293
def is_expired(self, now):
298294
"""Returns true if this record has expired."""
@@ -820,7 +816,7 @@ def __init__(self, zc):
820816
self.start()
821817

822818
def run(self):
823-
while not _GLOBAL_DONE:
819+
while not self.zc._GLOBAL_DONE:
824820
with self.condition:
825821
rs = self.readers.keys()
826822
if len(rs) == 0:
@@ -831,7 +827,7 @@ def run(self):
831827
if len(rs) != 0:
832828
try:
833829
rr, wr, er = select.select(rs, [], [], self.timeout)
834-
if not _GLOBAL_DONE:
830+
if not self.zc._GLOBAL_DONE:
835831
for socket_ in rr:
836832
reader = self.readers.get(socket_)
837833
if reader:
@@ -840,7 +836,7 @@ def run(self):
840836
except socket.error as e:
841837
# If the socket was closed by another thread, during
842838
# shutdown, ignore it and exit
843-
if e.errno != socket.EBADF or not _GLOBAL_DONE:
839+
if e.errno != socket.EBADF or not self.zc._GLOBAL_DONE:
844840
raise
845841

846842
def add_reader(self, reader, socket_):
@@ -901,7 +897,7 @@ def __init__(self, zc):
901897
def run(self):
902898
while True:
903899
self.zc.wait(10 * 1000)
904-
if _GLOBAL_DONE:
900+
if self.zc._GLOBAL_DONE:
905901
return
906902
now = current_time_millis()
907903
for record in self.zc.cache.entries():
@@ -1035,7 +1031,7 @@ def run(self):
10351031
now = current_time_millis()
10361032
if len(self._handlers_to_call) == 0 and self.next_time > now:
10371033
self.zc.wait(self.next_time - now)
1038-
if _GLOBAL_DONE or self.done:
1034+
if self.zc._GLOBAL_DONE or self.done:
10391035
return
10401036
now = current_time_millis()
10411037

@@ -1049,7 +1045,7 @@ def run(self):
10491045
self.next_time = now + self.delay
10501046
self.delay = min(20 * 1000, self.delay * 2)
10511047

1052-
if len(self._handlers_to_call) > 0 and not _GLOBAL_DONE:
1048+
if len(self._handlers_to_call) > 0 and not self.zc._GLOBAL_DONE:
10531049
handler = self._handlers_to_call.pop(0)
10541050
handler(self.zc)
10551051

@@ -1345,8 +1341,8 @@ def __init__(
13451341
13461342
:type interfaces: :class:`InterfaceChoice` or sequence of ip addresses
13471343
"""
1348-
global _GLOBAL_DONE
1349-
_GLOBAL_DONE = False
1344+
# hook for threads
1345+
self._GLOBAL_DONE = False
13501346

13511347
self._listen_socket = new_socket()
13521348
interfaces = normalize_interface_choice(interfaces, socket.AF_INET)
@@ -1398,7 +1394,7 @@ def wait(self, timeout):
13981394
"""Calling thread waits for a given number of milliseconds or
13991395
until notified."""
14001396
with self.condition:
1401-
self.condition.wait(timeout / 1000)
1397+
self.condition.wait(timeout / 1000.0)
14021398

14031399
def notify_all(self):
14041400
"""Notifies all waiting threads"""
@@ -1429,7 +1425,7 @@ def remove_service_listener(self, listener):
14291425

14301426
def remove_all_service_listeners(self):
14311427
"""Removes a listener from the set that is currently listening."""
1432-
for listener in self.browsers.keys():
1428+
for listener in [k for k in self.browsers]:
14331429
self.remove_service_listener(listener)
14341430

14351431
def register_service(self, info, ttl=_DNS_TTL):
@@ -1674,7 +1670,7 @@ def send(self, out, addr=_MDNS_ADDR, port=_MDNS_PORT):
16741670
packet = out.packet()
16751671
log.debug('Sending %r as %r...', out, packet)
16761672
for s in self._respond_sockets:
1677-
if _GLOBAL_DONE:
1673+
if self._GLOBAL_DONE:
16781674
return
16791675
bytes_sent = s.sendto(packet, 0, (addr, port))
16801676
if bytes_sent != len(packet):
@@ -1685,17 +1681,19 @@ def send(self, out, addr=_MDNS_ADDR, port=_MDNS_PORT):
16851681
def close(self):
16861682
"""Ends the background threads, and prevent this instance from
16871683
servicing further queries."""
1688-
global _GLOBAL_DONE
1689-
if not _GLOBAL_DONE:
1690-
_GLOBAL_DONE = True
1684+
if not self._GLOBAL_DONE:
1685+
self._GLOBAL_DONE = True
1686+
# remove service listeners
1687+
self.remove_all_service_listeners()
1688+
self.unregister_all_services()
1689+
16911690
# shutdown recv socket and thread
16921691
self.engine.del_reader(self._listen_socket)
16931692
self._listen_socket.close()
16941693
self.engine.join()
16951694

16961695
# shutdown the rest
16971696
self.notify_all()
1698-
self.unregister_all_services()
1699-
self.remove_all_service_listeners()
1697+
self.reaper.join()
17001698
for s in self._respond_sockets:
17011699
s.close()

0 commit comments

Comments
 (0)