Skip to content

Commit 71d352d

Browse files
committed
Convert test_integration to asyncio to avoid testing threading races
Fixes #768
1 parent 82f80c3 commit 71d352d

2 files changed

Lines changed: 96 additions & 93 deletions

File tree

tests/services/test_browser.py

Lines changed: 0 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -647,98 +647,6 @@ def on_service_state_change(zeroconf, service_type, state_change, name):
647647
zeroconf_browser.close()
648648

649649

650-
def test_integration():
651-
service_added = Event()
652-
service_removed = Event()
653-
unexpected_ttl = Event()
654-
got_query = Event()
655-
656-
type_ = "_http._tcp.local."
657-
registration_name = "xxxyyy.%s" % type_
658-
659-
def on_service_state_change(zeroconf, service_type, state_change, name):
660-
if name == registration_name:
661-
if state_change is ServiceStateChange.Added:
662-
service_added.set()
663-
elif state_change is ServiceStateChange.Removed:
664-
service_removed.set()
665-
666-
zeroconf_browser = Zeroconf(interfaces=['127.0.0.1'])
667-
_wait_for_start(zeroconf_browser)
668-
669-
# we are going to patch the zeroconf send to check packet sizes
670-
old_send = zeroconf_browser.async_send
671-
672-
time_offset = 0.0
673-
674-
def current_time_millis():
675-
"""Current system time in milliseconds"""
676-
return time.time() * 1000 + time_offset * 1000
677-
678-
expected_ttl = const._DNS_HOST_TTL
679-
nbr_answers = 0
680-
681-
def send(out, addr=const._MDNS_ADDR, port=const._MDNS_PORT, v6_flow_scope=()):
682-
"""Sends an outgoing packet."""
683-
pout = r.DNSIncoming(out.packets()[0])
684-
nonlocal nbr_answers
685-
for answer in pout.answers:
686-
nbr_answers += 1
687-
if not answer.ttl > expected_ttl / 2:
688-
unexpected_ttl.set()
689-
690-
got_query.set()
691-
got_query.clear()
692-
693-
old_send(out, addr=addr, port=port, v6_flow_scope=v6_flow_scope)
694-
695-
# patch the zeroconf send
696-
# patch the zeroconf current_time_millis
697-
# patch the backoff limit to ensure we always get one query every 1/4 of the DNS TTL
698-
with unittest.mock.patch.object(zeroconf_browser, "async_send", send), unittest.mock.patch.object(
699-
_services_browser, "current_time_millis", current_time_millis
700-
), unittest.mock.patch.object(_services_browser, "_BROWSER_BACKOFF_LIMIT", int(expected_ttl / 4)):
701-
service_added = Event()
702-
service_removed = Event()
703-
704-
browser = ServiceBrowser(zeroconf_browser, type_, [on_service_state_change])
705-
706-
zeroconf_registrar = Zeroconf(interfaces=['127.0.0.1'])
707-
desc = {'path': '/~paulsm/'}
708-
info = ServiceInfo(
709-
type_, registration_name, 80, 0, 0, desc, "ash-2.local.", addresses=[socket.inet_aton("10.0.1.2")]
710-
)
711-
zeroconf_registrar.register_service(info)
712-
713-
try:
714-
service_added.wait(1)
715-
assert service_added.is_set()
716-
717-
# Test that we receive queries containing answers only if the remaining TTL
718-
# is greater than half the original TTL
719-
sleep_count = 0
720-
test_iterations = 50
721-
722-
while nbr_answers < test_iterations:
723-
# Increase simulated time shift by 1/4 of the TTL in seconds
724-
time_offset += expected_ttl / 4
725-
zeroconf_browser.loop.call_soon_threadsafe(browser.query_scheduler.set_schedule_changed)
726-
sleep_count += 1
727-
got_query.wait(0.5)
728-
# Prevent the test running indefinitely in an error condition
729-
assert sleep_count < test_iterations * 4
730-
assert not unexpected_ttl.is_set()
731-
732-
# Don't remove service, allow close() to cleanup
733-
734-
finally:
735-
zeroconf_registrar.close()
736-
service_removed.wait(1)
737-
assert service_removed.is_set()
738-
browser.cancel()
739-
zeroconf_browser.close()
740-
741-
742650
def test_legacy_record_update_listener():
743651
"""Test a RecordUpdateListener that does not implement update_records."""
744652

tests/test_aio.py

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,18 @@
77
import asyncio
88
import logging
99
import socket
10+
import time
1011
import threading
1112
import unittest.mock
1213

1314
import pytest
1415

1516
from zeroconf.aio import AsyncServiceBrowser, AsyncServiceInfo, AsyncZeroconf, AsyncZeroconfServiceTypes
16-
from zeroconf import Zeroconf
17+
from zeroconf import DNSIncoming, ServiceStateChange, Zeroconf, const
1718
from zeroconf.const import _LISTENER_TIME
1819
from zeroconf._exceptions import BadTypeInNameException, NonUniqueNameException, ServiceNameAlreadyRegistered
1920
from zeroconf._services import ServiceListener
21+
import zeroconf._services.browser as _services_browser
2022
from zeroconf._services.info import ServiceInfo
2123
from zeroconf._utils.time import current_time_millis
2224

@@ -657,3 +659,96 @@ def update_service(self, zc, type_, name) -> None:
657659
await browser.async_cancel()
658660

659661
await aiozc.async_close()
662+
663+
664+
@pytest.mark.asyncio
665+
async def test_integration():
666+
service_added = asyncio.Event()
667+
service_removed = asyncio.Event()
668+
unexpected_ttl = asyncio.Event()
669+
got_query = asyncio.Event()
670+
671+
type_ = "_http._tcp.local."
672+
registration_name = "xxxyyy.%s" % type_
673+
674+
def on_service_state_change(zeroconf, service_type, state_change, name):
675+
if name == registration_name:
676+
if state_change is ServiceStateChange.Added:
677+
service_added.set()
678+
elif state_change is ServiceStateChange.Removed:
679+
service_removed.set()
680+
681+
aiozc = AsyncZeroconf(interfaces=['127.0.0.1'])
682+
zeroconf_browser = aiozc.zeroconf
683+
await zeroconf_browser.async_wait_for_start()
684+
685+
# we are going to patch the zeroconf send to check packet sizes
686+
old_send = zeroconf_browser.async_send
687+
688+
time_offset = 0.0
689+
690+
def current_time_millis():
691+
"""Current system time in milliseconds"""
692+
return (time.time() * 1000) + (time_offset * 1000)
693+
694+
expected_ttl = const._DNS_HOST_TTL
695+
nbr_answers = 0
696+
697+
def send(out, addr=const._MDNS_ADDR, port=const._MDNS_PORT, v6_flow_scope=()):
698+
"""Sends an outgoing packet."""
699+
pout = DNSIncoming(out.packets()[0])
700+
nonlocal nbr_answers
701+
for answer in pout.answers:
702+
nbr_answers += 1
703+
if not answer.ttl > expected_ttl / 2:
704+
unexpected_ttl.set()
705+
706+
got_query.set()
707+
got_query.clear()
708+
709+
old_send(out, addr=addr, port=port, v6_flow_scope=v6_flow_scope)
710+
711+
# patch the zeroconf send
712+
# patch the zeroconf current_time_millis
713+
# patch the backoff limit to ensure we always get one query every 1/4 of the DNS TTL
714+
with unittest.mock.patch.object(zeroconf_browser, "async_send", send), unittest.mock.patch(
715+
"zeroconf._services.browser.current_time_millis", current_time_millis
716+
), unittest.mock.patch.object(_services_browser, "_BROWSER_BACKOFF_LIMIT", int(expected_ttl / 4)):
717+
service_added = asyncio.Event()
718+
service_removed = asyncio.Event()
719+
720+
browser = AsyncServiceBrowser(zeroconf_browser, type_, [on_service_state_change])
721+
722+
aio_zeroconf_registrar = AsyncZeroconf(interfaces=['127.0.0.1'])
723+
desc = {'path': '/~paulsm/'}
724+
info = ServiceInfo(
725+
type_, registration_name, 80, 0, 0, desc, "ash-2.local.", addresses=[socket.inet_aton("10.0.1.2")]
726+
)
727+
task = await aio_zeroconf_registrar.async_register_service(info)
728+
await task
729+
730+
try:
731+
await asyncio.wait_for(service_added.wait(), 1)
732+
assert service_added.is_set()
733+
734+
# Test that we receive queries containing answers only if the remaining TTL
735+
# is greater than half the original TTL
736+
sleep_count = 0
737+
test_iterations = 50
738+
739+
while nbr_answers < test_iterations:
740+
# Increase simulated time shift by 1/4 of the TTL in seconds
741+
time_offset += expected_ttl / 4
742+
browser.query_scheduler.set_schedule_changed()
743+
sleep_count += 1
744+
await asyncio.wait_for(got_query.wait(), 0.5)
745+
# Prevent the test running indefinitely in an error condition
746+
assert sleep_count < test_iterations * 4
747+
assert not unexpected_ttl.is_set()
748+
# Don't remove service, allow close() to cleanup
749+
finally:
750+
await aio_zeroconf_registrar.async_close()
751+
await asyncio.wait_for(service_removed.wait(), 1)
752+
assert service_removed.is_set()
753+
await browser.async_cancel()
754+
await aiozc.async_close()

0 commit comments

Comments
 (0)