zeroconf 0.149.2__tar.gz → 0.149.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {zeroconf-0.149.2 → zeroconf-0.149.4}/CHANGELOG.md +40 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/PKG-INFO +1 -1
- {zeroconf-0.149.2 → zeroconf-0.149.4}/pyproject.toml +1 -1
- {zeroconf-0.149.2 → zeroconf-0.149.4}/setup.py +1 -1
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/__init__.py +1 -1
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_engine.py +29 -4
- {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/__init__.py +17 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/services/test_browser.py +34 -13
- {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/services/test_info.py +35 -49
- {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/services/test_types.py +8 -8
- {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/test_asyncio.py +34 -16
- {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/test_core.py +16 -11
- {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/test_engine.py +51 -3
- {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/test_handlers.py +17 -6
- {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/test_init.py +3 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/test_services.py +5 -3
- {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/test_updates.py +1 -1
- {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/utils/test_asyncio.py +1 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/COPYING +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/README.rst +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/build_ext.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/docs/Makefile +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/docs/_ext/zeroconfautodocfix.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/docs/api.rst +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/docs/conf.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/docs/index.rst +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_cache.pxd +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_cache.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_core.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_dns.pxd +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_dns.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_exceptions.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_handlers/__init__.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_handlers/answers.pxd +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_handlers/answers.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_handlers/multicast_outgoing_queue.pxd +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_handlers/multicast_outgoing_queue.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_handlers/query_handler.pxd +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_handlers/query_handler.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_handlers/record_manager.pxd +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_handlers/record_manager.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_history.pxd +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_history.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_listener.pxd +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_listener.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_logger.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_protocol/__init__.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_protocol/incoming.pxd +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_protocol/incoming.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_protocol/outgoing.pxd +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_protocol/outgoing.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_record_update.pxd +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_record_update.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_services/__init__.pxd +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_services/__init__.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_services/browser.pxd +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_services/browser.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_services/info.pxd +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_services/info.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_services/registry.pxd +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_services/registry.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_services/types.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_transport.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_updates.pxd +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_updates.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_utils/__init__.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_utils/asyncio.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_utils/ipaddress.pxd +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_utils/ipaddress.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_utils/name.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_utils/net.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_utils/time.pxd +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_utils/time.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/asyncio.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/const.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/py.typed +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/benchmarks/__init__.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/benchmarks/helpers.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/benchmarks/test_cache.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/benchmarks/test_incoming.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/benchmarks/test_outgoing.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/benchmarks/test_send.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/benchmarks/test_txt_properties.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/conftest.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/services/__init__.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/services/test_registry.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/test_cache.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/test_circular_imports.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/test_dns.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/test_exceptions.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/test_history.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/test_listener.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/test_logger.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/test_protocol.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/utils/__init__.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/utils/test_ipaddress.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/utils/test_name.py +0 -0
- {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/utils/test_net.py +0 -0
|
@@ -2,6 +2,46 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v0.149.4 (2026-05-17)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
- **core**: Release sockets when close runs before engine setup completes
|
|
10
|
+
([#1706](https://github.com/python-zeroconf/python-zeroconf/pull/1706),
|
|
11
|
+
[`0deb56b`](https://github.com/python-zeroconf/python-zeroconf/commit/0deb56b78fe6cd701a43ce34dccd4b69d6dd6d36))
|
|
12
|
+
|
|
13
|
+
### Testing
|
|
14
|
+
|
|
15
|
+
- Drop pending multicast responses before TOCTOU assertion
|
|
16
|
+
([#1701](https://github.com/python-zeroconf/python-zeroconf/pull/1701),
|
|
17
|
+
[`d2058d9`](https://github.com/python-zeroconf/python-zeroconf/commit/d2058d95882c70fb2eb786d373a630314e656c15))
|
|
18
|
+
|
|
19
|
+
- Speed up slow loopback tests (closes #1697)
|
|
20
|
+
([#1699](https://github.com/python-zeroconf/python-zeroconf/pull/1699),
|
|
21
|
+
[`dd341a3`](https://github.com/python-zeroconf/python-zeroconf/commit/dd341a378e904cbf9b9a09e5c2ac8ff7c9944cd0))
|
|
22
|
+
|
|
23
|
+
- Speed up slow loopback tests (closes #1700)
|
|
24
|
+
([#1703](https://github.com/python-zeroconf/python-zeroconf/pull/1703),
|
|
25
|
+
[`d03ea36`](https://github.com/python-zeroconf/python-zeroconf/commit/d03ea364dea5866e0301ff11f6b78714ecf991e8))
|
|
26
|
+
|
|
27
|
+
- Speed up test_async_wait_unblocks_on_update
|
|
28
|
+
([#1702](https://github.com/python-zeroconf/python-zeroconf/pull/1702),
|
|
29
|
+
[`653c385`](https://github.com/python-zeroconf/python-zeroconf/commit/653c38559c468672cf907d808d432dec0fb06968))
|
|
30
|
+
|
|
31
|
+
- Widen scheduling buffer in flaky get_info suppression test
|
|
32
|
+
([#1698](https://github.com/python-zeroconf/python-zeroconf/pull/1698),
|
|
33
|
+
[`9b4db62`](https://github.com/python-zeroconf/python-zeroconf/commit/9b4db625e121c2d590ed8fe2f4d187d5e3bb9a73))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
## v0.149.3 (2026-05-17)
|
|
37
|
+
|
|
38
|
+
### Bug Fixes
|
|
39
|
+
|
|
40
|
+
- **ci**: Drop x86_64 mac wheels and clean up obsolete CIBW_SKIP entries
|
|
41
|
+
([#1694](https://github.com/python-zeroconf/python-zeroconf/pull/1694),
|
|
42
|
+
[`104c5d6`](https://github.com/python-zeroconf/python-zeroconf/commit/104c5d6674896612aa83a89fd17b90de5f38a508))
|
|
43
|
+
|
|
44
|
+
|
|
5
45
|
## v0.149.2 (2026-05-17)
|
|
6
46
|
|
|
7
47
|
### Bug Fixes
|
|
@@ -19,7 +19,7 @@ install_requires = \
|
|
|
19
19
|
|
|
20
20
|
setup_kwargs = {
|
|
21
21
|
'name': 'zeroconf',
|
|
22
|
-
'version': '0.149.
|
|
22
|
+
'version': '0.149.4',
|
|
23
23
|
'description': 'A pure python implementation of multicast DNS service discovery',
|
|
24
24
|
'long_description': 'python-zeroconf\n===============\n\n.. image:: https://github.com/python-zeroconf/python-zeroconf/workflows/CI/badge.svg\n :target: https://github.com/python-zeroconf/python-zeroconf?query=workflow%3ACI+branch%3Amaster\n\n.. image:: https://img.shields.io/pypi/v/zeroconf.svg\n :target: https://pypi.python.org/pypi/zeroconf\n\n.. image:: https://codecov.io/gh/python-zeroconf/python-zeroconf/branch/master/graph/badge.svg\n :target: https://codecov.io/gh/python-zeroconf/python-zeroconf\n\n.. image:: https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\n :target: https://codspeed.io/python-zeroconf/python-zeroconf\n :alt: Codspeed.io status for python-zeroconf\n\n.. image:: https://readthedocs.org/projects/python-zeroconf/badge/?version=latest\n :target: https://python-zeroconf.readthedocs.io/en/latest/?badge=latest\n :alt: Documentation Status\n\n`Documentation <https://python-zeroconf.readthedocs.io/en/latest/>`_.\n\nThis is fork of pyzeroconf, Multicast DNS Service Discovery for Python,\noriginally by Paul Scott-Murphy (https://github.com/paulsm/pyzeroconf),\nmodified by William McBrine (https://github.com/wmcbrine/pyzeroconf).\n\nThe original William McBrine\'s fork note::\n\n This fork is used in all of my TiVo-related projects: HME for Python\n (and therefore HME/VLC), Network Remote, Remote Proxy, and pyTivo.\n Before this, I was tracking the changes for zeroconf.py in three\n separate repos. I figured I should have an authoritative source.\n\n Although I make changes based on my experience with TiVos, I expect that\n they\'re generally applicable. This version also includes patches found\n on the now-defunct (?) Launchpad repo of pyzeroconf, and elsewhere\n around the net -- not always well-documented, sorry.\n\nCompatible with:\n\n* Bonjour\n* Avahi\n\nCompared to some other Zeroconf/Bonjour/Avahi Python packages, python-zeroconf:\n\n* isn\'t tied to Bonjour or Avahi\n* doesn\'t use D-Bus\n* doesn\'t force you to use particular event loop or Twisted (asyncio is used under the hood but not required)\n* is pip-installable\n* has PyPI distribution\n* has an optional cython extension for performance (pure python is supported as well)\n\nPython compatibility\n--------------------\n\n* CPython 3.10+\n* PyPy 3.10+\n\nVersioning\n----------\n\nThis project uses semantic versioning.\n\nStatus\n------\n\nThis project is actively maintained.\n\nTraffic Reduction\n-----------------\n\nBefore version 0.32, most traffic reduction techniques described in https://datatracker.ietf.org/doc/html/rfc6762#section-7\nwhere not implemented which could lead to excessive network traffic. It is highly recommended that version 0.32 or later\nis used if this is a concern.\n\nIPv6 support\n------------\n\nIPv6 support is relatively new and currently limited, specifically:\n\n* `InterfaceChoice.All` is an alias for `InterfaceChoice.Default` on non-POSIX\n systems.\n* Dual-stack IPv6 sockets are used, which may not be supported everywhere (some\n BSD variants do not have them).\n* Listening on localhost (`::1`) does not work. Help with understanding why is\n appreciated.\n\nHow to get python-zeroconf?\n===========================\n\n* PyPI page https://pypi.org/project/zeroconf/\n* GitHub project https://github.com/python-zeroconf/python-zeroconf\n\nThe easiest way to install python-zeroconf is using pip::\n\n pip install zeroconf\n\n\n\nHow do I use it?\n================\n\nHere\'s an example of browsing for a service:\n\n.. code-block:: python\n\n from zeroconf import ServiceBrowser, ServiceListener, Zeroconf\n\n\n class MyListener(ServiceListener):\n\n def update_service(self, zc: Zeroconf, type_: str, name: str) -> None:\n print(f"Service {name} updated")\n\n def remove_service(self, zc: Zeroconf, type_: str, name: str) -> None:\n print(f"Service {name} removed")\n\n def add_service(self, zc: Zeroconf, type_: str, name: str) -> None:\n info = zc.get_service_info(type_, name)\n print(f"Service {name} added, service info: {info}")\n\n\n zeroconf = Zeroconf()\n listener = MyListener()\n browser = ServiceBrowser(zeroconf, "_http._tcp.local.", listener)\n try:\n input("Press enter to exit...\\n\\n")\n finally:\n zeroconf.close()\n\n.. note::\n\n Discovery and service registration use *all* available network interfaces by default.\n If you want to customize that you need to specify ``interfaces`` argument when\n constructing ``Zeroconf`` object (see the code for details).\n\nIf you don\'t know the name of the service you need to browse for, try:\n\n.. code-block:: python\n\n from zeroconf import ZeroconfServiceTypes\n print(\'\\n\'.join(ZeroconfServiceTypes.find()))\n\nSee examples directory for more.\n\nChangelog\n=========\n\n`Changelog <CHANGELOG.md>`_\n\nLicense\n=======\n\nLGPL, see COPYING file for details.\n',
|
|
25
25
|
'author': 'Paul Scott-Murphy',
|
|
@@ -88,7 +88,7 @@ from ._utils.time import ( # noqa # import needed for backwards compat
|
|
|
88
88
|
|
|
89
89
|
__author__ = "Paul Scott-Murphy, William McBrine"
|
|
90
90
|
__maintainer__ = "Jakub Stasiak <jakub@stasiak.at>"
|
|
91
|
-
__version__ = "0.149.
|
|
91
|
+
__version__ = "0.149.4"
|
|
92
92
|
__license__ = "LGPL"
|
|
93
93
|
|
|
94
94
|
|
|
@@ -114,10 +114,17 @@ class AsyncEngine:
|
|
|
114
114
|
lambda: AsyncListener(self.zc), # type: ignore[arg-type, return-value]
|
|
115
115
|
sock=s,
|
|
116
116
|
)
|
|
117
|
+
# Register the wrapped transport before releasing the engine's
|
|
118
|
+
# handle so a concurrent shutdown always sees ``s`` in exactly
|
|
119
|
+
# one place; do not add an ``await`` between these two steps.
|
|
117
120
|
self.protocols.append(cast(AsyncListener, protocol))
|
|
118
121
|
self.readers.append(make_wrapped_transport(cast(asyncio.DatagramTransport, transport)))
|
|
119
122
|
if s in sender_sockets:
|
|
120
123
|
self.senders.append(make_wrapped_transport(cast(asyncio.DatagramTransport, transport)))
|
|
124
|
+
if s is self._listen_socket:
|
|
125
|
+
self._listen_socket = None
|
|
126
|
+
if s in self._respond_sockets:
|
|
127
|
+
self._respond_sockets.remove(s)
|
|
121
128
|
|
|
122
129
|
def _async_cache_cleanup(self) -> None:
|
|
123
130
|
"""Periodic cache cleanup."""
|
|
@@ -139,19 +146,37 @@ class AsyncEngine:
|
|
|
139
146
|
async def _async_close(self) -> None:
|
|
140
147
|
"""Cancel and wait for the cleanup task to finish."""
|
|
141
148
|
assert self._setup_task is not None
|
|
142
|
-
|
|
149
|
+
# Swallow CancelledError only if the setup task itself was
|
|
150
|
+
# cancelled (close-before-start); outer-task cancellation must
|
|
151
|
+
# propagate.
|
|
152
|
+
try:
|
|
153
|
+
await self._setup_task
|
|
154
|
+
except asyncio.CancelledError:
|
|
155
|
+
if not self._setup_task.cancelled():
|
|
156
|
+
raise
|
|
143
157
|
self._async_shutdown()
|
|
144
158
|
await asyncio.sleep(0) # flush out any call soons
|
|
145
|
-
|
|
146
|
-
|
|
159
|
+
if self._cleanup_timer is not None:
|
|
160
|
+
self._cleanup_timer.cancel()
|
|
147
161
|
|
|
148
162
|
def _async_shutdown(self) -> None:
|
|
149
|
-
"""Shutdown transports and sockets."""
|
|
163
|
+
"""Shutdown transports and sockets; safe to call repeatedly."""
|
|
150
164
|
assert self.running_future is not None
|
|
151
165
|
assert self.loop is not None
|
|
152
166
|
self.running_future = self.loop.create_future()
|
|
167
|
+
# Cancel pending setup so it can't wrap fresh transports after
|
|
168
|
+
# shutdown has started.
|
|
169
|
+
if self._setup_task is not None and not self._setup_task.done():
|
|
170
|
+
self._setup_task.cancel()
|
|
153
171
|
for wrapped_transport in itertools.chain(self.senders, self.readers):
|
|
154
172
|
wrapped_transport.transport.close()
|
|
173
|
+
# Anything still here was never adopted by a transport.
|
|
174
|
+
if self._listen_socket is not None:
|
|
175
|
+
self._listen_socket.close()
|
|
176
|
+
self._listen_socket = None
|
|
177
|
+
for s in self._respond_sockets:
|
|
178
|
+
s.close()
|
|
179
|
+
self._respond_sockets = []
|
|
155
180
|
|
|
156
181
|
def close(self) -> None:
|
|
157
182
|
"""Close from sync context.
|
|
@@ -91,6 +91,23 @@ def _clear_cache(zc: Zeroconf) -> None:
|
|
|
91
91
|
zc.question_history.clear()
|
|
92
92
|
|
|
93
93
|
|
|
94
|
+
def _backdate_cache(zc: Zeroconf, ms: int = 1100) -> None:
|
|
95
|
+
"""Backdate every cached record's `created` time by `ms` milliseconds.
|
|
96
|
+
|
|
97
|
+
rfc6762#section-10.2 keys off "received more than one second ago", so
|
|
98
|
+
backdating is equivalent to sleeping `ms` in real time without the
|
|
99
|
+
wall-clock wait.
|
|
100
|
+
|
|
101
|
+
Iterate `store.values()`, not the dict directly — when a record is
|
|
102
|
+
re-added with an equal hash, the key stays the original object while
|
|
103
|
+
the value is replaced with the latest; mutating the key would update
|
|
104
|
+
stale objects no one reads.
|
|
105
|
+
"""
|
|
106
|
+
for store in zc.cache.cache.values():
|
|
107
|
+
for record in store.values():
|
|
108
|
+
record.created -= ms
|
|
109
|
+
|
|
110
|
+
|
|
94
111
|
def time_changed_millis(millis: float | None = None) -> None:
|
|
95
112
|
"""Call all scheduled events for a time."""
|
|
96
113
|
loop = asyncio.get_running_loop()
|
|
@@ -331,7 +331,9 @@ class TestServiceBrowser(unittest.TestCase):
|
|
|
331
331
|
service_updated_event.clear()
|
|
332
332
|
service_text = b"path=/~matt2/"
|
|
333
333
|
_inject_response(zeroconf, mock_record_update_incoming_msg(r.ServiceStateChange.Updated))
|
|
334
|
-
|
|
334
|
+
# Negative assertion: a duplicate update must NOT fire the listener. The wait
|
|
335
|
+
# always times out, so keep the budget short rather than reusing wait_time.
|
|
336
|
+
service_updated_event.wait(0.3)
|
|
335
337
|
assert service_added_count == 1
|
|
336
338
|
assert service_updated_count == 2
|
|
337
339
|
assert service_removed_count == 0
|
|
@@ -554,7 +556,7 @@ def test_first_query_delay():
|
|
|
554
556
|
|
|
555
557
|
|
|
556
558
|
@pytest.mark.asyncio
|
|
557
|
-
async def test_asking_default_is_asking_qm_questions_after_the_first_qu():
|
|
559
|
+
async def test_asking_default_is_asking_qm_questions_after_the_first_qu(quick_timing: None) -> None:
|
|
558
560
|
"""Verify the service browser's first questions are QU and refresh queries are QM."""
|
|
559
561
|
service_added = asyncio.Event()
|
|
560
562
|
service_removed = asyncio.Event()
|
|
@@ -656,7 +658,7 @@ async def test_asking_default_is_asking_qm_questions_after_the_first_qu():
|
|
|
656
658
|
|
|
657
659
|
|
|
658
660
|
@pytest.mark.asyncio
|
|
659
|
-
async def test_ttl_refresh_cancelled_rescue_query():
|
|
661
|
+
async def test_ttl_refresh_cancelled_rescue_query(quick_timing: None) -> None:
|
|
660
662
|
"""Verify seeing a name again cancels the rescue query."""
|
|
661
663
|
service_added = asyncio.Event()
|
|
662
664
|
service_removed = asyncio.Event()
|
|
@@ -844,7 +846,7 @@ async def test_asking_qu_questions():
|
|
|
844
846
|
await aiozc.async_close()
|
|
845
847
|
|
|
846
848
|
|
|
847
|
-
def test_legacy_record_update_listener():
|
|
849
|
+
def test_legacy_record_update_listener(quick_timing: None) -> None:
|
|
848
850
|
"""Test a RecordUpdateListener that does not implement update_records."""
|
|
849
851
|
|
|
850
852
|
# instantiate a zeroconf instance
|
|
@@ -1031,7 +1033,7 @@ def test_service_browser_listeners_update_service():
|
|
|
1031
1033
|
|
|
1032
1034
|
|
|
1033
1035
|
def test_service_browser_listeners_no_update_service():
|
|
1034
|
-
"""
|
|
1036
|
+
"""A listener that ignores update events records only add/remove callbacks."""
|
|
1035
1037
|
|
|
1036
1038
|
# instantiate a zeroconf instance
|
|
1037
1039
|
zc = Zeroconf(interfaces=["127.0.0.1"])
|
|
@@ -1049,6 +1051,9 @@ def test_service_browser_listeners_no_update_service():
|
|
|
1049
1051
|
if name == registration_name:
|
|
1050
1052
|
callbacks.append(("remove", type_, name))
|
|
1051
1053
|
|
|
1054
|
+
def update_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-def]
|
|
1055
|
+
pass
|
|
1056
|
+
|
|
1052
1057
|
listener = MyServiceListener()
|
|
1053
1058
|
|
|
1054
1059
|
browser = r.ServiceBrowser(zc, type_, None, listener)
|
|
@@ -1497,10 +1502,15 @@ def test_service_browser_expire_callbacks():
|
|
|
1497
1502
|
# Force the ttl to be 1 second
|
|
1498
1503
|
now = current_time_millis()
|
|
1499
1504
|
for cache_record in list(zc.cache.cache.values()):
|
|
1500
|
-
for record in cache_record:
|
|
1505
|
+
for record in cache_record.values():
|
|
1501
1506
|
zc.cache._async_set_created_ttl(record, now, 1)
|
|
1502
1507
|
|
|
1503
|
-
|
|
1508
|
+
# Wait for the add callback to fire from the original inject_response.
|
|
1509
|
+
for _ in range(30):
|
|
1510
|
+
time.sleep(0.01)
|
|
1511
|
+
if len(callbacks) == 1:
|
|
1512
|
+
break
|
|
1513
|
+
|
|
1504
1514
|
info.port = 400
|
|
1505
1515
|
info._dns_service_cache = None # we are mutating the record so clear the cache
|
|
1506
1516
|
|
|
@@ -1509,8 +1519,8 @@ def test_service_browser_expire_callbacks():
|
|
|
1509
1519
|
mock_incoming_msg([info.dns_service()]),
|
|
1510
1520
|
)
|
|
1511
1521
|
|
|
1512
|
-
for _ in range(
|
|
1513
|
-
time.sleep(0.
|
|
1522
|
+
for _ in range(30):
|
|
1523
|
+
time.sleep(0.01)
|
|
1514
1524
|
if len(callbacks) == 2:
|
|
1515
1525
|
break
|
|
1516
1526
|
|
|
@@ -1519,8 +1529,19 @@ def test_service_browser_expire_callbacks():
|
|
|
1519
1529
|
("update", type_, registration_name),
|
|
1520
1530
|
]
|
|
1521
1531
|
|
|
1522
|
-
|
|
1523
|
-
|
|
1532
|
+
# Re-add every cached record with `created` in the past so the
|
|
1533
|
+
# next reaper tick (0.01s) expires them and fires the remove
|
|
1534
|
+
# callback, instead of waiting the full TTL in real time.
|
|
1535
|
+
# Going through `_async_set_created_ttl` updates the expiration
|
|
1536
|
+
# heap; mutating `record.created` directly would leave the heap
|
|
1537
|
+
# entry pointing at the original `when` so the reaper never wakes.
|
|
1538
|
+
past = current_time_millis() - 2000
|
|
1539
|
+
for cache_record in list(zc.cache.cache.values()):
|
|
1540
|
+
for record in list(cache_record.values()):
|
|
1541
|
+
zc.cache._async_set_created_ttl(record, past, 1)
|
|
1542
|
+
|
|
1543
|
+
for _ in range(30):
|
|
1544
|
+
time.sleep(0.01)
|
|
1524
1545
|
if len(callbacks) == 3:
|
|
1525
1546
|
break
|
|
1526
1547
|
|
|
@@ -1565,7 +1586,7 @@ def test_scheduled_ptr_query_dunder_methods():
|
|
|
1565
1586
|
|
|
1566
1587
|
|
|
1567
1588
|
@pytest.mark.asyncio
|
|
1568
|
-
async def test_close_zeroconf_without_browser_before_start_up_queries():
|
|
1589
|
+
async def test_close_zeroconf_without_browser_before_start_up_queries(quick_timing: None) -> None:
|
|
1569
1590
|
"""Test that we stop sending startup queries if zeroconf is closed out from under the browser."""
|
|
1570
1591
|
service_added = asyncio.Event()
|
|
1571
1592
|
type_ = "_http._tcp.local."
|
|
@@ -1632,7 +1653,7 @@ async def test_close_zeroconf_without_browser_before_start_up_queries():
|
|
|
1632
1653
|
|
|
1633
1654
|
|
|
1634
1655
|
@pytest.mark.asyncio
|
|
1635
|
-
async def test_close_zeroconf_without_browser_after_start_up_queries():
|
|
1656
|
+
async def test_close_zeroconf_without_browser_after_start_up_queries(quick_timing: None) -> None:
|
|
1636
1657
|
"""Test that we stop sending rescue queries if zeroconf is closed out from under the browser."""
|
|
1637
1658
|
service_added = asyncio.Event()
|
|
1638
1659
|
|
|
@@ -436,12 +436,29 @@ class TestServiceInfo(unittest.TestCase):
|
|
|
436
436
|
service_info_event.set()
|
|
437
437
|
|
|
438
438
|
try:
|
|
439
|
+
# Seed TXT/A/AAAA with a far-future `than` before the
|
|
440
|
+
# helper thread starts. The first (QU) query bypasses
|
|
441
|
+
# suppression so phase 1 still observes 4 questions; the
|
|
442
|
+
# second (QM) query fires ~220-320ms after the first, too
|
|
443
|
+
# tight a window to seed reliably from the test thread on
|
|
444
|
+
# slow runners. async_expire only removes entries where
|
|
445
|
+
# now - than > _DUPLICATE_QUESTION_INTERVAL, so future-
|
|
446
|
+
# dated entries persist for the duration of the test.
|
|
447
|
+
seed_history_questions = (
|
|
448
|
+
r.DNSQuestion(service_name, const._TYPE_A, const._CLASS_IN),
|
|
449
|
+
r.DNSQuestion(service_name, const._TYPE_AAAA, const._CLASS_IN),
|
|
450
|
+
r.DNSQuestion(service_name, const._TYPE_TXT, const._CLASS_IN),
|
|
451
|
+
)
|
|
452
|
+
far_future = r.current_time_millis() + 60_000
|
|
453
|
+
for question in seed_history_questions:
|
|
454
|
+
zc.question_history.add_question_at_time(question, far_future, set())
|
|
455
|
+
|
|
439
456
|
helper_thread = threading.Thread(
|
|
440
457
|
target=get_service_info_helper,
|
|
441
458
|
args=(zc, service_type, service_name),
|
|
442
459
|
)
|
|
443
460
|
helper_thread.start()
|
|
444
|
-
wait_time = (const._LISTENER_TIME + info._AVOID_SYNC_DELAY_RANDOM_INTERVAL[1] +
|
|
461
|
+
wait_time = (const._LISTENER_TIME + info._AVOID_SYNC_DELAY_RANDOM_INTERVAL[1] + 500) / 1000
|
|
445
462
|
|
|
446
463
|
# Expect query for SRV, TXT, A, AAAA
|
|
447
464
|
send_event.wait(wait_time)
|
|
@@ -457,64 +474,29 @@ class TestServiceInfo(unittest.TestCase):
|
|
|
457
474
|
# by the question history
|
|
458
475
|
last_sent = None
|
|
459
476
|
send_event.clear()
|
|
460
|
-
|
|
461
|
-
send_event.wait(
|
|
462
|
-
wait_time * 0.25
|
|
463
|
-
) # Wait long enough to be inside the question history window
|
|
464
|
-
now = r.current_time_millis()
|
|
465
|
-
zc.question_history.add_question_at_time(
|
|
466
|
-
r.DNSQuestion(service_name, const._TYPE_A, const._CLASS_IN),
|
|
467
|
-
now,
|
|
468
|
-
set(),
|
|
469
|
-
)
|
|
470
|
-
zc.question_history.add_question_at_time(
|
|
471
|
-
r.DNSQuestion(service_name, const._TYPE_AAAA, const._CLASS_IN),
|
|
472
|
-
now,
|
|
473
|
-
set(),
|
|
474
|
-
)
|
|
475
|
-
zc.question_history.add_question_at_time(
|
|
476
|
-
r.DNSQuestion(service_name, const._TYPE_TXT, const._CLASS_IN),
|
|
477
|
-
now,
|
|
478
|
-
set(),
|
|
479
|
-
)
|
|
480
|
-
send_event.wait(wait_time * 0.25)
|
|
477
|
+
send_event.wait(wait_time)
|
|
481
478
|
assert last_sent is not None
|
|
482
479
|
assert len(last_sent.questions) == 1 # type: ignore[unreachable]
|
|
483
480
|
assert r.DNSQuestion(service_name, const._TYPE_SRV, const._CLASS_IN) in last_sent.questions
|
|
484
481
|
assert service_info is None
|
|
485
482
|
|
|
483
|
+
# Future-date SRV too: the SRV entry added by the previous
|
|
484
|
+
# QM query has `than = now`, so it expires after
|
|
485
|
+
# _DUPLICATE_QUESTION_INTERVAL — before the next scheduled
|
|
486
|
+
# QM query (~1s + jitter later).
|
|
487
|
+
zc.question_history.add_question_at_time(
|
|
488
|
+
r.DNSQuestion(service_name, const._TYPE_SRV, const._CLASS_IN),
|
|
489
|
+
r.current_time_millis() + 60_000,
|
|
490
|
+
set(),
|
|
491
|
+
)
|
|
492
|
+
|
|
486
493
|
wait_time = (
|
|
487
|
-
const._DUPLICATE_QUESTION_INTERVAL + info._AVOID_SYNC_DELAY_RANDOM_INTERVAL[1] +
|
|
494
|
+
const._DUPLICATE_QUESTION_INTERVAL + info._AVOID_SYNC_DELAY_RANDOM_INTERVAL[1] + 500
|
|
488
495
|
) / 1000
|
|
489
496
|
# Expect no queries as all are suppressed by the question history
|
|
490
497
|
last_sent = None
|
|
491
498
|
send_event.clear()
|
|
492
|
-
|
|
493
|
-
send_event.wait(
|
|
494
|
-
wait_time * 0.25
|
|
495
|
-
) # Wait long enough to be inside the question history window
|
|
496
|
-
now = r.current_time_millis()
|
|
497
|
-
zc.question_history.add_question_at_time(
|
|
498
|
-
r.DNSQuestion(service_name, const._TYPE_A, const._CLASS_IN),
|
|
499
|
-
now,
|
|
500
|
-
set(),
|
|
501
|
-
)
|
|
502
|
-
zc.question_history.add_question_at_time(
|
|
503
|
-
r.DNSQuestion(service_name, const._TYPE_AAAA, const._CLASS_IN),
|
|
504
|
-
now,
|
|
505
|
-
set(),
|
|
506
|
-
)
|
|
507
|
-
zc.question_history.add_question_at_time(
|
|
508
|
-
r.DNSQuestion(service_name, const._TYPE_TXT, const._CLASS_IN),
|
|
509
|
-
now,
|
|
510
|
-
set(),
|
|
511
|
-
)
|
|
512
|
-
zc.question_history.add_question_at_time(
|
|
513
|
-
r.DNSQuestion(service_name, const._TYPE_SRV, const._CLASS_IN),
|
|
514
|
-
now,
|
|
515
|
-
set(),
|
|
516
|
-
)
|
|
517
|
-
send_event.wait(wait_time * 0.25)
|
|
499
|
+
send_event.wait(wait_time)
|
|
518
500
|
# All questions are suppressed so no query should be sent
|
|
519
501
|
assert last_sent is None
|
|
520
502
|
assert service_info is None
|
|
@@ -1547,6 +1529,7 @@ async def test_bad_ip_addresses_ignored_in_cache():
|
|
|
1547
1529
|
info = ServiceInfo(type_, registration_name)
|
|
1548
1530
|
info.load_from_cache(aiozc.zeroconf)
|
|
1549
1531
|
assert info.addresses_by_version(IPVersion.V4Only) == [b"\x7f\x00\x00\x01"]
|
|
1532
|
+
await aiozc.async_close()
|
|
1550
1533
|
|
|
1551
1534
|
|
|
1552
1535
|
@pytest.mark.asyncio
|
|
@@ -1822,6 +1805,7 @@ async def test_address_resolver():
|
|
|
1822
1805
|
aiozc.zeroconf.async_send(outgoing)
|
|
1823
1806
|
assert await resolve_task
|
|
1824
1807
|
assert resolver.addresses == [b"\x7f\x00\x00\x01"]
|
|
1808
|
+
await aiozc.async_close()
|
|
1825
1809
|
|
|
1826
1810
|
|
|
1827
1811
|
@pytest.mark.asyncio
|
|
@@ -1846,6 +1830,7 @@ async def test_address_resolver_ipv4():
|
|
|
1846
1830
|
aiozc.zeroconf.async_send(outgoing)
|
|
1847
1831
|
assert await resolve_task
|
|
1848
1832
|
assert resolver.addresses == [b"\x7f\x00\x00\x01"]
|
|
1833
|
+
await aiozc.async_close()
|
|
1849
1834
|
|
|
1850
1835
|
|
|
1851
1836
|
@pytest.mark.asyncio
|
|
@@ -1872,6 +1857,7 @@ async def test_address_resolver_ipv6():
|
|
|
1872
1857
|
aiozc.zeroconf.async_send(outgoing)
|
|
1873
1858
|
assert await resolve_task
|
|
1874
1859
|
assert resolver.ip_addresses_by_version(IPVersion.All) == [ip_address("fe80::52e:c2f2:bc5f:e9c6")]
|
|
1860
|
+
await aiozc.async_close()
|
|
1875
1861
|
|
|
1876
1862
|
|
|
1877
1863
|
@pytest.mark.asyncio
|
|
@@ -47,10 +47,10 @@ def test_integration_with_listener(disable_duplicate_packet_suppression):
|
|
|
47
47
|
)
|
|
48
48
|
zeroconf_registrar.registry.async_add(info)
|
|
49
49
|
try:
|
|
50
|
-
service_types = ZeroconfServiceTypes.find(interfaces=["127.0.0.1"], timeout=
|
|
50
|
+
service_types = ZeroconfServiceTypes.find(interfaces=["127.0.0.1"], timeout=0.5)
|
|
51
51
|
assert type_ in service_types
|
|
52
52
|
_clear_cache(zeroconf_registrar)
|
|
53
|
-
service_types = ZeroconfServiceTypes.find(zc=zeroconf_registrar, timeout=
|
|
53
|
+
service_types = ZeroconfServiceTypes.find(zc=zeroconf_registrar, timeout=0.5)
|
|
54
54
|
assert type_ in service_types
|
|
55
55
|
|
|
56
56
|
finally:
|
|
@@ -79,10 +79,10 @@ def test_integration_with_listener_v6_records(disable_duplicate_packet_suppressi
|
|
|
79
79
|
)
|
|
80
80
|
zeroconf_registrar.registry.async_add(info)
|
|
81
81
|
try:
|
|
82
|
-
service_types = ZeroconfServiceTypes.find(interfaces=["127.0.0.1"], timeout=
|
|
82
|
+
service_types = ZeroconfServiceTypes.find(interfaces=["127.0.0.1"], timeout=0.5)
|
|
83
83
|
assert type_ in service_types
|
|
84
84
|
_clear_cache(zeroconf_registrar)
|
|
85
|
-
service_types = ZeroconfServiceTypes.find(zc=zeroconf_registrar, timeout=
|
|
85
|
+
service_types = ZeroconfServiceTypes.find(zc=zeroconf_registrar, timeout=0.5)
|
|
86
86
|
assert type_ in service_types
|
|
87
87
|
|
|
88
88
|
finally:
|
|
@@ -115,10 +115,10 @@ def test_integration_with_listener_ipv6(disable_duplicate_packet_suppression):
|
|
|
115
115
|
)
|
|
116
116
|
zeroconf_registrar.registry.async_add(info)
|
|
117
117
|
try:
|
|
118
|
-
service_types = ZeroconfServiceTypes.find(ip_version=r.IPVersion.V6Only, timeout=
|
|
118
|
+
service_types = ZeroconfServiceTypes.find(ip_version=r.IPVersion.V6Only, timeout=0.5)
|
|
119
119
|
assert type_ in service_types
|
|
120
120
|
_clear_cache(zeroconf_registrar)
|
|
121
|
-
service_types = ZeroconfServiceTypes.find(zc=zeroconf_registrar, timeout=
|
|
121
|
+
service_types = ZeroconfServiceTypes.find(zc=zeroconf_registrar, timeout=0.5)
|
|
122
122
|
assert type_ in service_types
|
|
123
123
|
|
|
124
124
|
finally:
|
|
@@ -147,10 +147,10 @@ def test_integration_with_subtype_and_listener(disable_duplicate_packet_suppress
|
|
|
147
147
|
)
|
|
148
148
|
zeroconf_registrar.registry.async_add(info)
|
|
149
149
|
try:
|
|
150
|
-
service_types = ZeroconfServiceTypes.find(interfaces=["127.0.0.1"], timeout=
|
|
150
|
+
service_types = ZeroconfServiceTypes.find(interfaces=["127.0.0.1"], timeout=0.5)
|
|
151
151
|
assert discovery_type in service_types
|
|
152
152
|
_clear_cache(zeroconf_registrar)
|
|
153
|
-
service_types = ZeroconfServiceTypes.find(zc=zeroconf_registrar, timeout=
|
|
153
|
+
service_types = ZeroconfServiceTypes.find(zc=zeroconf_registrar, timeout=0.5)
|
|
154
154
|
assert discovery_type in service_types
|
|
155
155
|
|
|
156
156
|
finally:
|
|
@@ -126,7 +126,7 @@ async def test_sync_within_event_loop_executor() -> None:
|
|
|
126
126
|
|
|
127
127
|
|
|
128
128
|
@pytest.mark.asyncio
|
|
129
|
-
async def test_async_service_registration() -> None:
|
|
129
|
+
async def test_async_service_registration(quick_timing: None) -> None:
|
|
130
130
|
"""Test registering services broadcasts the registration by default."""
|
|
131
131
|
aiozc = AsyncZeroconf(interfaces=["127.0.0.1"])
|
|
132
132
|
type_ = "_test1-srvc-type._tcp.local."
|
|
@@ -193,7 +193,7 @@ async def test_async_service_registration() -> None:
|
|
|
193
193
|
|
|
194
194
|
|
|
195
195
|
@pytest.mark.asyncio
|
|
196
|
-
async def test_async_service_registration_with_server_missing() -> None:
|
|
196
|
+
async def test_async_service_registration_with_server_missing(quick_timing: None) -> None:
|
|
197
197
|
"""Test registering a service with the server not specified.
|
|
198
198
|
|
|
199
199
|
For backwards compatibility, the server should be set to the
|
|
@@ -394,7 +394,7 @@ async def test_async_service_registration_same_server_same_ports(quick_timing: N
|
|
|
394
394
|
|
|
395
395
|
|
|
396
396
|
@pytest.mark.asyncio
|
|
397
|
-
async def test_async_service_registration_name_conflict() -> None:
|
|
397
|
+
async def test_async_service_registration_name_conflict(quick_timing: None) -> None:
|
|
398
398
|
"""Test registering services throws on name conflict."""
|
|
399
399
|
aiozc = AsyncZeroconf(interfaces=["127.0.0.1"])
|
|
400
400
|
type_ = "_test-srvc2-type._tcp.local."
|
|
@@ -500,10 +500,11 @@ async def test_async_service_registration_name_strict_check(quick_timing: None)
|
|
|
500
500
|
|
|
501
501
|
await aiozc.async_unregister_service(info)
|
|
502
502
|
await aiozc.async_close()
|
|
503
|
+
zc.close()
|
|
503
504
|
|
|
504
505
|
|
|
505
506
|
@pytest.mark.asyncio
|
|
506
|
-
async def test_async_tasks() -> None:
|
|
507
|
+
async def test_async_tasks(quick_timing: None) -> None:
|
|
507
508
|
"""Test awaiting broadcast tasks"""
|
|
508
509
|
|
|
509
510
|
aiozc = AsyncZeroconf(interfaces=["127.0.0.1"])
|
|
@@ -569,7 +570,7 @@ async def test_async_tasks() -> None:
|
|
|
569
570
|
|
|
570
571
|
|
|
571
572
|
@pytest.mark.asyncio
|
|
572
|
-
async def test_async_wait_unblocks_on_update() -> None:
|
|
573
|
+
async def test_async_wait_unblocks_on_update(quick_timing: None) -> None:
|
|
573
574
|
"""Test async_wait will unblock on update."""
|
|
574
575
|
|
|
575
576
|
aiozc = AsyncZeroconf(interfaces=["127.0.0.1"])
|
|
@@ -605,7 +606,7 @@ async def test_async_wait_unblocks_on_update() -> None:
|
|
|
605
606
|
|
|
606
607
|
|
|
607
608
|
@pytest.mark.asyncio
|
|
608
|
-
async def test_service_info_async_request() -> None:
|
|
609
|
+
async def test_service_info_async_request(quick_timing: None) -> None:
|
|
609
610
|
"""Test registering services broadcasts and query with AsyncServceInfo.async_request."""
|
|
610
611
|
if not has_working_ipv6() or os.environ.get("SKIP_IPV6"):
|
|
611
612
|
pytest.skip("Requires IPv6")
|
|
@@ -695,26 +696,41 @@ async def test_service_info_async_request() -> None:
|
|
|
695
696
|
assert aiosinfos[1] is not None
|
|
696
697
|
assert aiosinfos[1].addresses == [socket.inet_aton("10.0.1.5")]
|
|
697
698
|
|
|
699
|
+
# Drop pending multicast responses queued under the original
|
|
700
|
+
# `info` / `info2` registrations. Their snapshots predate the
|
|
701
|
+
# `async_update_service` swap, so if they flushed after
|
|
702
|
+
# `_clear_cache` below they would poison `aiosinfo.server`
|
|
703
|
+
# with `ash-1.local.` and the `_is_complete=False` loop would
|
|
704
|
+
# then keep asking for an A record nobody answers. The pending
|
|
705
|
+
# `loop.call_at` for each queue fires harmlessly on the empty
|
|
706
|
+
# deque.
|
|
707
|
+
aiozc.zeroconf.out_queue.queue.clear()
|
|
708
|
+
aiozc.zeroconf.out_delay_queue.queue.clear()
|
|
698
709
|
aiosinfo = AsyncServiceInfo(type_, registration_name)
|
|
699
710
|
_clear_cache(aiozc.zeroconf)
|
|
700
711
|
# Generating the race condition is almost impossible
|
|
701
|
-
# without patching since its a TOCTOU race
|
|
712
|
+
# without patching since its a TOCTOU race. 1500ms covers
|
|
713
|
+
# the initial _LISTENER_TIME + random delay (200-320ms) and
|
|
714
|
+
# leaves plenty of margin for the loopback response to land
|
|
715
|
+
# before the loop times out.
|
|
702
716
|
with patch("zeroconf.asyncio.AsyncServiceInfo._is_complete", False):
|
|
703
|
-
await aiosinfo.async_request(aiozc.zeroconf,
|
|
717
|
+
await aiosinfo.async_request(aiozc.zeroconf, 1500)
|
|
704
718
|
assert aiosinfo is not None
|
|
705
719
|
assert aiosinfo.addresses == [socket.inet_aton("10.0.1.3")]
|
|
706
720
|
|
|
707
721
|
task = await aiozc.async_unregister_service(new_info)
|
|
708
722
|
await task
|
|
709
723
|
|
|
710
|
-
|
|
724
|
+
# Cap timeout: the service is gone, so this is expected to return None;
|
|
725
|
+
# waiting the default 3000ms is pure overhead.
|
|
726
|
+
aiosinfo = await aiozc.async_get_service_info(type_, registration_name, timeout=200)
|
|
711
727
|
assert aiosinfo is None
|
|
712
728
|
|
|
713
729
|
await aiozc.async_close()
|
|
714
730
|
|
|
715
731
|
|
|
716
732
|
@pytest.mark.asyncio
|
|
717
|
-
async def test_async_service_browser() -> None:
|
|
733
|
+
async def test_async_service_browser(quick_timing: None) -> None:
|
|
718
734
|
"""Test AsyncServiceBrowser."""
|
|
719
735
|
aiozc = AsyncZeroconf(interfaces=["127.0.0.1"])
|
|
720
736
|
type_ = "_test9-srvc-type._tcp.local."
|
|
@@ -774,7 +790,7 @@ async def test_async_service_browser() -> None:
|
|
|
774
790
|
|
|
775
791
|
|
|
776
792
|
@pytest.mark.asyncio
|
|
777
|
-
async def test_async_context_manager() -> None:
|
|
793
|
+
async def test_async_context_manager(quick_timing: None) -> None:
|
|
778
794
|
"""Test using an async context manager."""
|
|
779
795
|
type_ = "_test10-sr-type._tcp.local."
|
|
780
796
|
name = "xxxyyy"
|
|
@@ -906,10 +922,10 @@ async def test_async_zeroconf_service_types(quick_timing: None) -> None:
|
|
|
906
922
|
await asyncio.sleep(0.2)
|
|
907
923
|
_clear_cache(zeroconf_registrar.zeroconf)
|
|
908
924
|
try:
|
|
909
|
-
service_types = await AsyncZeroconfServiceTypes.async_find(interfaces=["127.0.0.1"], timeout=
|
|
925
|
+
service_types = await AsyncZeroconfServiceTypes.async_find(interfaces=["127.0.0.1"], timeout=0.5)
|
|
910
926
|
assert type_ in service_types
|
|
911
927
|
_clear_cache(zeroconf_registrar.zeroconf)
|
|
912
|
-
service_types = await AsyncZeroconfServiceTypes.async_find(aiozc=zeroconf_registrar, timeout=
|
|
928
|
+
service_types = await AsyncZeroconfServiceTypes.async_find(aiozc=zeroconf_registrar, timeout=0.5)
|
|
913
929
|
assert type_ in service_types
|
|
914
930
|
|
|
915
931
|
finally:
|
|
@@ -974,7 +990,7 @@ async def test_service_browser_instantiation_generates_add_events_from_cache():
|
|
|
974
990
|
|
|
975
991
|
|
|
976
992
|
@pytest.mark.asyncio
|
|
977
|
-
async def test_integration():
|
|
993
|
+
async def test_integration(quick_timing: None) -> None:
|
|
978
994
|
service_added = asyncio.Event()
|
|
979
995
|
service_removed = asyncio.Event()
|
|
980
996
|
unexpected_ttl = asyncio.Event()
|
|
@@ -1166,9 +1182,11 @@ async def test_info_asking_default_is_asking_qm_questions_after_the_first_qu():
|
|
|
1166
1182
|
# patch the zeroconf send
|
|
1167
1183
|
with patch.object(zeroconf_info, "async_send", send):
|
|
1168
1184
|
aiosinfo = AsyncServiceInfo(type_, registration_name)
|
|
1169
|
-
# Patch _is_complete so we send multiple times
|
|
1185
|
+
# Patch _is_complete so we send multiple times. 500ms covers
|
|
1186
|
+
# the QU query at 0ms plus the QM query at ~_LISTENER_TIME +
|
|
1187
|
+
# max random delay (~320ms).
|
|
1170
1188
|
with patch("zeroconf.asyncio.AsyncServiceInfo._is_complete", False):
|
|
1171
|
-
await aiosinfo.async_request(aiozc.zeroconf,
|
|
1189
|
+
await aiosinfo.async_request(aiozc.zeroconf, 500)
|
|
1172
1190
|
try:
|
|
1173
1191
|
assert first_outgoing.questions[0].unicast is True # type: ignore[union-attr]
|
|
1174
1192
|
assert second_outgoing.questions[0].unicast is False # type: ignore[attr-defined]
|