|
7 | 7 | import asyncio |
8 | 8 | import logging |
9 | 9 | import socket |
| 10 | +import time |
10 | 11 | import threading |
11 | 12 | import unittest.mock |
12 | 13 |
|
13 | 14 | import pytest |
14 | 15 |
|
15 | 16 | from zeroconf.aio import AsyncServiceBrowser, AsyncServiceInfo, AsyncZeroconf, AsyncZeroconfServiceTypes |
16 | | -from zeroconf import Zeroconf |
| 17 | +from zeroconf import DNSIncoming, ServiceStateChange, Zeroconf, const |
17 | 18 | from zeroconf.const import _LISTENER_TIME |
18 | 19 | from zeroconf._exceptions import BadTypeInNameException, NonUniqueNameException, ServiceNameAlreadyRegistered |
19 | 20 | from zeroconf._services import ServiceListener |
| 21 | +import zeroconf._services.browser as _services_browser |
20 | 22 | from zeroconf._services.info import ServiceInfo |
21 | 23 | from zeroconf._utils.time import current_time_millis |
22 | 24 |
|
@@ -657,3 +659,96 @@ def update_service(self, zc, type_, name) -> None: |
657 | 659 | await browser.async_cancel() |
658 | 660 |
|
659 | 661 | 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