zeroconf 0.149.4__tar.gz → 0.149.6__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.
Potentially problematic release.
This version of zeroconf might be problematic. Click here for more details.
- {zeroconf-0.149.4 → zeroconf-0.149.6}/CHANGELOG.md +40 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/PKG-INFO +1 -1
- {zeroconf-0.149.4 → zeroconf-0.149.6}/pyproject.toml +1 -1
- {zeroconf-0.149.4 → zeroconf-0.149.6}/setup.py +1 -1
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/__init__.py +1 -1
- zeroconf-0.149.6/src/zeroconf/_logger.py +111 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_protocol/incoming.pxd +1 -1
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_protocol/incoming.py +13 -13
- {zeroconf-0.149.4 → zeroconf-0.149.6}/tests/__init__.py +14 -0
- zeroconf-0.149.6/tests/benchmarks/test_mark_seen.py +39 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/tests/conftest.py +19 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/tests/services/test_info.py +84 -77
- {zeroconf-0.149.4 → zeroconf-0.149.6}/tests/services/test_types.py +11 -9
- {zeroconf-0.149.4 → zeroconf-0.149.6}/tests/test_asyncio.py +17 -9
- {zeroconf-0.149.4 → zeroconf-0.149.6}/tests/test_handlers.py +5 -1
- {zeroconf-0.149.4 → zeroconf-0.149.6}/tests/test_logger.py +82 -5
- {zeroconf-0.149.4 → zeroconf-0.149.6}/tests/test_protocol.py +56 -0
- zeroconf-0.149.4/src/zeroconf/_logger.py +0 -88
- {zeroconf-0.149.4 → zeroconf-0.149.6}/COPYING +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/README.rst +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/build_ext.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/docs/Makefile +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/docs/_ext/zeroconfautodocfix.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/docs/api.rst +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/docs/conf.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/docs/index.rst +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_cache.pxd +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_cache.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_core.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_dns.pxd +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_dns.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_engine.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_exceptions.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_handlers/__init__.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_handlers/answers.pxd +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_handlers/answers.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_handlers/multicast_outgoing_queue.pxd +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_handlers/multicast_outgoing_queue.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_handlers/query_handler.pxd +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_handlers/query_handler.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_handlers/record_manager.pxd +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_handlers/record_manager.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_history.pxd +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_history.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_listener.pxd +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_listener.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_protocol/__init__.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_protocol/outgoing.pxd +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_protocol/outgoing.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_record_update.pxd +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_record_update.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_services/__init__.pxd +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_services/__init__.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_services/browser.pxd +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_services/browser.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_services/info.pxd +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_services/info.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_services/registry.pxd +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_services/registry.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_services/types.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_transport.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_updates.pxd +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_updates.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_utils/__init__.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_utils/asyncio.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_utils/ipaddress.pxd +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_utils/ipaddress.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_utils/name.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_utils/net.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_utils/time.pxd +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/_utils/time.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/asyncio.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/const.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/src/zeroconf/py.typed +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/tests/benchmarks/__init__.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/tests/benchmarks/helpers.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/tests/benchmarks/test_cache.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/tests/benchmarks/test_incoming.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/tests/benchmarks/test_outgoing.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/tests/benchmarks/test_send.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/tests/benchmarks/test_txt_properties.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/tests/services/__init__.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/tests/services/test_browser.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/tests/services/test_registry.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/tests/test_cache.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/tests/test_circular_imports.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/tests/test_core.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/tests/test_dns.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/tests/test_engine.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/tests/test_exceptions.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/tests/test_history.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/tests/test_init.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/tests/test_listener.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/tests/test_services.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/tests/test_updates.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/tests/utils/__init__.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/tests/utils/test_asyncio.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/tests/utils/test_ipaddress.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/tests/utils/test_name.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.6}/tests/utils/test_net.py +0 -0
|
@@ -2,6 +2,46 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v0.149.6 (2026-05-18)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
- Bound _seen_logs and stop retaining exc_info
|
|
10
|
+
([#1717](https://github.com/python-zeroconf/python-zeroconf/pull/1717),
|
|
11
|
+
[`95561e2`](https://github.com/python-zeroconf/python-zeroconf/commit/95561e28b24922358f1991e38e3a86d70d72dcec))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## v0.149.5 (2026-05-18)
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
- Bound DNS compression-pointer chain depth in DNSIncoming
|
|
19
|
+
([#1719](https://github.com/python-zeroconf/python-zeroconf/pull/1719),
|
|
20
|
+
[`f9e2359`](https://github.com/python-zeroconf/python-zeroconf/commit/f9e23592137f30fdf7ef710dba065da31c79b1cf))
|
|
21
|
+
|
|
22
|
+
### Testing
|
|
23
|
+
|
|
24
|
+
- Cap helper get_service_info timeout in suppression test (~3.0s → ~1.85s)
|
|
25
|
+
([#1708](https://github.com/python-zeroconf/python-zeroconf/pull/1708),
|
|
26
|
+
[`ee3c7d7`](https://github.com/python-zeroconf/python-zeroconf/commit/ee3c7d74ff45327a3a6d520b86a691e21e2bc219))
|
|
27
|
+
|
|
28
|
+
- Drop ZeroconfServiceTypes.find() timeouts from 500ms to 200ms on loopback
|
|
29
|
+
([#1710](https://github.com/python-zeroconf/python-zeroconf/pull/1710),
|
|
30
|
+
[`64d143d`](https://github.com/python-zeroconf/python-zeroconf/commit/64d143d2ee7874ee1d9cef0dd2799c008b4aa791))
|
|
31
|
+
|
|
32
|
+
- Eliminate test_get_info_single race by injecting from the send mock
|
|
33
|
+
([#1716](https://github.com/python-zeroconf/python-zeroconf/pull/1716),
|
|
34
|
+
[`963d3d7`](https://github.com/python-zeroconf/python-zeroconf/commit/963d3d70e1cde056967eba0d8747ddcd247ae707))
|
|
35
|
+
|
|
36
|
+
- Fix race in test_register_and_lookup_type_by_uppercase_name
|
|
37
|
+
([#1712](https://github.com/python-zeroconf/python-zeroconf/pull/1712),
|
|
38
|
+
[`91aa21d`](https://github.com/python-zeroconf/python-zeroconf/commit/91aa21d52a0873f5fc12d43675b1b521dfe20519))
|
|
39
|
+
|
|
40
|
+
- Speed up service-info request tests with quick_request_timing fixture
|
|
41
|
+
([#1709](https://github.com/python-zeroconf/python-zeroconf/pull/1709),
|
|
42
|
+
[`4bae30a`](https://github.com/python-zeroconf/python-zeroconf/commit/4bae30a2ed0910ee7c4f1d0f92f2c400a7b10f31))
|
|
43
|
+
|
|
44
|
+
|
|
5
45
|
## v0.149.4 (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.6',
|
|
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.6"
|
|
92
92
|
__license__ = "LGPL"
|
|
93
93
|
|
|
94
94
|
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Multicast DNS Service Discovery for Python, v0.14-wmcbrine
|
|
2
|
+
)
|
|
3
|
+
Copyright 2003 Paul Scott-Murphy, 2014 William McBrine
|
|
4
|
+
|
|
5
|
+
This module provides a framework for the use of DNS Service Discovery
|
|
6
|
+
using IP multicast.
|
|
7
|
+
|
|
8
|
+
This library is free software; you can redistribute it and/or
|
|
9
|
+
modify it under the terms of the GNU Lesser General Public
|
|
10
|
+
License as published by the Free Software Foundation; either
|
|
11
|
+
version 2.1 of the License, or (at your option) any later version.
|
|
12
|
+
|
|
13
|
+
This library is distributed in the hope that it will be useful,
|
|
14
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
15
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
16
|
+
Lesser General Public License for more details.
|
|
17
|
+
|
|
18
|
+
You should have received a copy of the GNU Lesser General Public
|
|
19
|
+
License along with this library; if not, write to the Free Software
|
|
20
|
+
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
|
|
21
|
+
USA
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import logging
|
|
27
|
+
import sys
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
log = logging.getLogger(__name__.split(".", maxsplit=1)[0])
|
|
31
|
+
log.addHandler(logging.NullHandler())
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def set_logger_level_if_unset() -> None:
|
|
35
|
+
if log.level == logging.NOTSET:
|
|
36
|
+
log.setLevel(logging.WARN)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
set_logger_level_if_unset()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
_MAX_SEEN_LOGS = 512
|
|
43
|
+
_seen_logs: dict[str, None] = {}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _evict_oldest(seen: dict[str, None]) -> bool:
|
|
47
|
+
"""Pop the oldest entry from ``seen``; return False if it raced.
|
|
48
|
+
|
|
49
|
+
Individual dict ops (``pop`` with a default, ``next``) are atomic
|
|
50
|
+
on the free-threaded build, but the compound ``iter`` → ``next``
|
|
51
|
+
used to pick the FIFO victim can raise ``RuntimeError`` if
|
|
52
|
+
another thread mutates the dict between the two ops. The caller
|
|
53
|
+
breaks its drain loop on False so concurrent mutation can't make
|
|
54
|
+
it spin.
|
|
55
|
+
"""
|
|
56
|
+
try:
|
|
57
|
+
seen.pop(next(iter(seen)), None)
|
|
58
|
+
except (RuntimeError, StopIteration):
|
|
59
|
+
return False
|
|
60
|
+
return True
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _mark_seen(seen: dict[str, None], key: str) -> bool:
|
|
64
|
+
"""Record ``key`` in ``seen`` and return True if it was newly added.
|
|
65
|
+
|
|
66
|
+
Bounds the dict so callers passing attacker-influenced keys (peer
|
|
67
|
+
addresses, packet offsets) cannot grow it without bound. Evicts
|
|
68
|
+
the oldest entries on overflow (dict preserves insertion order on
|
|
69
|
+
Python 3.7+), so ``_MAX_SEEN_LOGS`` is a recency window.
|
|
70
|
+
|
|
71
|
+
The dict is shared across all ``Zeroconf`` instances in the
|
|
72
|
+
process; on the free-threaded build (3.14t) and under multi-
|
|
73
|
+
instance sync use, callers can race the ``len < cap`` check and
|
|
74
|
+
both insert, leaving the dict transiently above the cap. The
|
|
75
|
+
drain loop runs on every call (steady-state-at-cap hits are a
|
|
76
|
+
single ``len`` + compare past the membership check because the
|
|
77
|
+
helper short-circuits) so a contention burst is corrected by the
|
|
78
|
+
next caller regardless of whether it's a hit or a miss.
|
|
79
|
+
"""
|
|
80
|
+
inserting = key not in seen
|
|
81
|
+
# Hit (``inserting`` is False): drain only if drifted above cap.
|
|
82
|
+
# Miss (``inserting`` is True): drain to ``cap - 1`` to make room
|
|
83
|
+
# for the new key. Bool subtracts as 0/1 to pick the right limit.
|
|
84
|
+
while len(seen) > _MAX_SEEN_LOGS - inserting and _evict_oldest(seen):
|
|
85
|
+
pass
|
|
86
|
+
if inserting:
|
|
87
|
+
seen[key] = None
|
|
88
|
+
return inserting
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class QuietLogger:
|
|
92
|
+
@classmethod
|
|
93
|
+
def log_exception_warning(cls, *logger_data: Any) -> None:
|
|
94
|
+
first_time = _mark_seen(_seen_logs, str(sys.exc_info()[1]))
|
|
95
|
+
logger = log.warning if first_time else log.debug
|
|
96
|
+
logger(*(logger_data or ["Exception occurred"]), exc_info=True)
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def log_exception_debug(cls, *logger_data: Any) -> None:
|
|
100
|
+
first_time = _mark_seen(_seen_logs, str(sys.exc_info()[1]))
|
|
101
|
+
log.debug(*(logger_data or ["Exception occurred"]), exc_info=first_time)
|
|
102
|
+
|
|
103
|
+
@classmethod
|
|
104
|
+
def log_warning_once(cls, *args: Any) -> None:
|
|
105
|
+
logger = log.warning if _mark_seen(_seen_logs, args[0]) else log.debug
|
|
106
|
+
logger(*args)
|
|
107
|
+
|
|
108
|
+
@classmethod
|
|
109
|
+
def log_exception_once(cls, exc: Exception, *args: Any) -> None:
|
|
110
|
+
logger = log.warning if _mark_seen(_seen_logs, args[0]) else log.debug
|
|
111
|
+
logger(*args, exc_info=exc)
|
|
@@ -83,7 +83,7 @@ cdef class DNSIncoming:
|
|
|
83
83
|
link_py_int=object,
|
|
84
84
|
linked_labels=cython.list
|
|
85
85
|
)
|
|
86
|
-
cdef unsigned int _decode_labels_at_offset(self, unsigned int off, cython.list labels, cython.set seen_pointers)
|
|
86
|
+
cdef unsigned int _decode_labels_at_offset(self, unsigned int off, cython.list labels, cython.set seen_pointers, unsigned int depth)
|
|
87
87
|
|
|
88
88
|
@cython.locals(offset="unsigned int")
|
|
89
89
|
cdef void _read_header(self)
|
|
@@ -37,7 +37,7 @@ from .._dns import (
|
|
|
37
37
|
DNSText,
|
|
38
38
|
)
|
|
39
39
|
from .._exceptions import IncomingDecodeError
|
|
40
|
-
from .._logger import log
|
|
40
|
+
from .._logger import _mark_seen, log
|
|
41
41
|
from .._utils.time import current_time_millis
|
|
42
42
|
from ..const import (
|
|
43
43
|
_FLAGS_QR_MASK,
|
|
@@ -60,10 +60,10 @@ DNS_COMPRESSION_POINTER_LEN = 2
|
|
|
60
60
|
MAX_DNS_LABELS = 128
|
|
61
61
|
MAX_NAME_LENGTH = 253
|
|
62
62
|
|
|
63
|
-
DECODE_EXCEPTIONS = (IndexError, struct.error, IncomingDecodeError)
|
|
63
|
+
DECODE_EXCEPTIONS = (IndexError, struct.error, IncomingDecodeError, RecursionError)
|
|
64
64
|
|
|
65
65
|
|
|
66
|
-
_seen_logs: dict[str,
|
|
66
|
+
_seen_logs: dict[str, None] = {}
|
|
67
67
|
_str = str
|
|
68
68
|
_int = int
|
|
69
69
|
|
|
@@ -182,13 +182,7 @@ class DNSIncoming:
|
|
|
182
182
|
|
|
183
183
|
@classmethod
|
|
184
184
|
def _log_exception_debug(cls, *logger_data: Any) -> None:
|
|
185
|
-
log_exc_info =
|
|
186
|
-
exc_info = sys.exc_info()
|
|
187
|
-
exc_str = str(exc_info[1])
|
|
188
|
-
if exc_str not in _seen_logs:
|
|
189
|
-
# log the trace only on the first time
|
|
190
|
-
_seen_logs[exc_str] = exc_info
|
|
191
|
-
log_exc_info = True
|
|
185
|
+
log_exc_info = _mark_seen(_seen_logs, str(sys.exc_info()[1]))
|
|
192
186
|
log.debug(*(logger_data or ["Exception occurred"]), exc_info=log_exc_info)
|
|
193
187
|
|
|
194
188
|
def answers(self) -> list[DNSRecord]:
|
|
@@ -409,7 +403,7 @@ class DNSIncoming:
|
|
|
409
403
|
labels: list[str] = []
|
|
410
404
|
seen_pointers: set[int] = set()
|
|
411
405
|
original_offset = self.offset
|
|
412
|
-
self.offset = self._decode_labels_at_offset(original_offset, labels, seen_pointers)
|
|
406
|
+
self.offset = self._decode_labels_at_offset(original_offset, labels, seen_pointers, 0)
|
|
413
407
|
self._name_cache[original_offset] = labels
|
|
414
408
|
name = ".".join(labels) + "."
|
|
415
409
|
if len(name) > MAX_NAME_LENGTH:
|
|
@@ -418,8 +412,14 @@ class DNSIncoming:
|
|
|
418
412
|
)
|
|
419
413
|
return name
|
|
420
414
|
|
|
421
|
-
def _decode_labels_at_offset(
|
|
415
|
+
def _decode_labels_at_offset(
|
|
416
|
+
self, off: _int, labels: list[str], seen_pointers: set[int], depth: _int
|
|
417
|
+
) -> int:
|
|
422
418
|
# This is a tight loop that is called frequently, small optimizations can make a difference.
|
|
419
|
+
if depth > MAX_DNS_LABELS:
|
|
420
|
+
raise IncomingDecodeError(
|
|
421
|
+
f"DNS compression pointer chain exceeds {MAX_DNS_LABELS} at {off} from {self.source}"
|
|
422
|
+
)
|
|
423
423
|
view = self.view
|
|
424
424
|
while off < self._data_len:
|
|
425
425
|
length = view[off]
|
|
@@ -457,7 +457,7 @@ class DNSIncoming:
|
|
|
457
457
|
if not linked_labels:
|
|
458
458
|
linked_labels = []
|
|
459
459
|
seen_pointers.add(link_py_int)
|
|
460
|
-
self._decode_labels_at_offset(link, linked_labels, seen_pointers)
|
|
460
|
+
self._decode_labels_at_offset(link, linked_labels, seen_pointers, depth + 1)
|
|
461
461
|
self._name_cache[link_py_int] = linked_labels
|
|
462
462
|
labels.extend(linked_labels)
|
|
463
463
|
if len(labels) > MAX_DNS_LABELS:
|
|
@@ -35,6 +35,20 @@ from zeroconf._history import QuestionHistory
|
|
|
35
35
|
|
|
36
36
|
_MONOTONIC_RESOLUTION = time.get_clock_info("monotonic").resolution
|
|
37
37
|
|
|
38
|
+
# get_service_info / async_request timeout for tests using the
|
|
39
|
+
# `quick_request_timing` fixture. The fixture cuts the initial-query
|
|
40
|
+
# delay to ~15ms (10ms _LISTENER_TIME + 1-5ms jitter), so 50ms is
|
|
41
|
+
# ample headroom for tests that only need to observe the first one
|
|
42
|
+
# or two queries.
|
|
43
|
+
QUICK_REQUEST_TIMEOUT_MS = 50
|
|
44
|
+
|
|
45
|
+
# Timeout for ZeroconfServiceTypes.find() / AsyncZeroconfServiceTypes.async_find()
|
|
46
|
+
# in loopback integration tests. `find()` is just `time.sleep(timeout)` —
|
|
47
|
+
# it doesn't short-circuit on the first matching response — so the
|
|
48
|
+
# timeout becomes a lower bound on the test runtime. On loopback the
|
|
49
|
+
# registrar's response lands within a few ms; 200ms is ~50x headroom.
|
|
50
|
+
LOOPBACK_FIND_TIMEOUT = 0.2
|
|
51
|
+
|
|
38
52
|
|
|
39
53
|
class QuestionHistoryWithoutSuppression(QuestionHistory):
|
|
40
54
|
def suppresses(self, question: DNSQuestion, now: float, known_answers: set[DNSRecord]) -> bool:
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Benchmark for _logger._mark_seen."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pytest_codspeed import BenchmarkFixture
|
|
6
|
+
|
|
7
|
+
from zeroconf._logger import _MAX_SEEN_LOGS, _mark_seen
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_mark_seen_hit(benchmark: BenchmarkFixture) -> None:
|
|
11
|
+
"""Benchmark the cache-hit path (same key repeated)."""
|
|
12
|
+
seen: dict[str, None] = {"warm": None}
|
|
13
|
+
|
|
14
|
+
@benchmark
|
|
15
|
+
def _hit() -> None:
|
|
16
|
+
for _ in range(1000):
|
|
17
|
+
_mark_seen(seen, "warm")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_mark_seen_fill(benchmark: BenchmarkFixture) -> None:
|
|
21
|
+
"""Benchmark filling from empty up to the cap (no evictions)."""
|
|
22
|
+
keys = [f"key-{i}" for i in range(_MAX_SEEN_LOGS)]
|
|
23
|
+
|
|
24
|
+
@benchmark
|
|
25
|
+
def _fill() -> None:
|
|
26
|
+
seen: dict[str, None] = {}
|
|
27
|
+
for k in keys:
|
|
28
|
+
_mark_seen(seen, k)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_mark_seen_churn(benchmark: BenchmarkFixture) -> None:
|
|
32
|
+
"""Benchmark sustained eviction (every call past the cap drops oldest)."""
|
|
33
|
+
keys = [f"churn-{i}" for i in range(_MAX_SEEN_LOGS * 4)]
|
|
34
|
+
|
|
35
|
+
@benchmark
|
|
36
|
+
def _churn() -> None:
|
|
37
|
+
seen: dict[str, None] = {}
|
|
38
|
+
for k in keys:
|
|
39
|
+
_mark_seen(seen, k)
|
|
@@ -10,6 +10,7 @@ import pytest
|
|
|
10
10
|
|
|
11
11
|
from zeroconf import _core, const
|
|
12
12
|
from zeroconf._handlers import query_handler
|
|
13
|
+
from zeroconf._services import info as service_info
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
@pytest.fixture(autouse=True)
|
|
@@ -59,3 +60,21 @@ def quick_timing() -> Generator[None]:
|
|
|
59
60
|
patch.object(_core, "_UNREGISTER_TIME", 10),
|
|
60
61
|
):
|
|
61
62
|
yield
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@pytest.fixture
|
|
66
|
+
def quick_request_timing() -> Generator[None]:
|
|
67
|
+
"""Shorten the initial-query delay used by AsyncServiceInfo.async_request.
|
|
68
|
+
|
|
69
|
+
The 200ms `_LISTENER_TIME` and 20-120ms random jitter (RFC 6762
|
|
70
|
+
§5.2) help spread queries from multiple clients on real networks.
|
|
71
|
+
On loopback they're pure overhead — get_service_info-style tests
|
|
72
|
+
wait ~250ms before the first query even fires. Opt in by adding
|
|
73
|
+
`quick_request_timing` to a test's argument list, then drop the
|
|
74
|
+
test's own timeouts (which had to accommodate that delay).
|
|
75
|
+
"""
|
|
76
|
+
with (
|
|
77
|
+
patch.object(service_info, "_LISTENER_TIME", 10),
|
|
78
|
+
patch.object(service_info, "_AVOID_SYNC_DELAY_RANDOM_INTERVAL", (1, 5)),
|
|
79
|
+
):
|
|
80
|
+
yield
|
|
@@ -23,7 +23,7 @@ from zeroconf._services.info import ServiceInfo
|
|
|
23
23
|
from zeroconf._utils.net import IPVersion
|
|
24
24
|
from zeroconf.asyncio import AsyncZeroconf
|
|
25
25
|
|
|
26
|
-
from .. import _inject_response, has_working_ipv6
|
|
26
|
+
from .. import QUICK_REQUEST_TIMEOUT_MS, _inject_response, has_working_ipv6
|
|
27
27
|
|
|
28
28
|
log = logging.getLogger("zeroconf")
|
|
29
29
|
original_logging_level = logging.NOTSET
|
|
@@ -430,9 +430,9 @@ class TestServiceInfo(unittest.TestCase):
|
|
|
430
430
|
|
|
431
431
|
return r.DNSIncoming(generated.packets()[0])
|
|
432
432
|
|
|
433
|
-
def get_service_info_helper(zc, type, name):
|
|
433
|
+
def get_service_info_helper(zc, type, name, timeout):
|
|
434
434
|
nonlocal service_info
|
|
435
|
-
service_info = zc.get_service_info(type, name)
|
|
435
|
+
service_info = zc.get_service_info(type, name, timeout)
|
|
436
436
|
service_info_event.set()
|
|
437
437
|
|
|
438
438
|
try:
|
|
@@ -453,9 +453,14 @@ class TestServiceInfo(unittest.TestCase):
|
|
|
453
453
|
for question in seed_history_questions:
|
|
454
454
|
zc.question_history.add_question_at_time(question, far_future, set())
|
|
455
455
|
|
|
456
|
+
# No answers ever come back (all queries are suppressed),
|
|
457
|
+
# so cap the helper at the worst-case sum of the three
|
|
458
|
+
# phase waits below plus margin instead of the 3000ms
|
|
459
|
+
# default. Phase 3 waits ~1.6s (the 999ms QM gap plus
|
|
460
|
+
# jitter and 500ms buffer); 1500ms covers it.
|
|
456
461
|
helper_thread = threading.Thread(
|
|
457
462
|
target=get_service_info_helper,
|
|
458
|
-
args=(zc, service_type, service_name),
|
|
463
|
+
args=(zc, service_type, service_name, 1500),
|
|
459
464
|
)
|
|
460
465
|
helper_thread.start()
|
|
461
466
|
wait_time = (const._LISTENER_TIME + info._AVOID_SYNC_DELAY_RANDOM_INTERVAL[1] + 500) / 1000
|
|
@@ -506,6 +511,7 @@ class TestServiceInfo(unittest.TestCase):
|
|
|
506
511
|
zc.remove_all_service_listeners()
|
|
507
512
|
zc.close()
|
|
508
513
|
|
|
514
|
+
@pytest.mark.usefixtures("quick_request_timing")
|
|
509
515
|
def test_get_info_single(self):
|
|
510
516
|
zc = r.Zeroconf(interfaces=["127.0.0.1"])
|
|
511
517
|
|
|
@@ -516,91 +522,88 @@ class TestServiceInfo(unittest.TestCase):
|
|
|
516
522
|
service_address = "10.0.1.2"
|
|
517
523
|
|
|
518
524
|
service_info = None
|
|
519
|
-
send_event = Event()
|
|
520
525
|
service_info_event = Event()
|
|
521
526
|
|
|
522
|
-
|
|
527
|
+
ttl = 120
|
|
528
|
+
response_records = [
|
|
529
|
+
r.DNSText(
|
|
530
|
+
service_name,
|
|
531
|
+
const._TYPE_TXT,
|
|
532
|
+
const._CLASS_IN | const._CLASS_UNIQUE,
|
|
533
|
+
ttl,
|
|
534
|
+
service_text,
|
|
535
|
+
),
|
|
536
|
+
r.DNSService(
|
|
537
|
+
service_name,
|
|
538
|
+
const._TYPE_SRV,
|
|
539
|
+
const._CLASS_IN | const._CLASS_UNIQUE,
|
|
540
|
+
ttl,
|
|
541
|
+
0,
|
|
542
|
+
0,
|
|
543
|
+
80,
|
|
544
|
+
service_server,
|
|
545
|
+
),
|
|
546
|
+
r.DNSAddress(
|
|
547
|
+
service_server,
|
|
548
|
+
const._TYPE_A,
|
|
549
|
+
const._CLASS_IN | const._CLASS_UNIQUE,
|
|
550
|
+
ttl,
|
|
551
|
+
socket.inet_pton(socket.AF_INET, service_address),
|
|
552
|
+
),
|
|
553
|
+
]
|
|
523
554
|
|
|
524
|
-
def
|
|
525
|
-
|
|
526
|
-
|
|
555
|
+
def mock_incoming_msg(records: Iterable[r.DNSRecord]) -> r.DNSIncoming:
|
|
556
|
+
generated = r.DNSOutgoing(const._FLAGS_QR_RESPONSE)
|
|
557
|
+
for record in records:
|
|
558
|
+
generated.add_answer_at_time(record, 0)
|
|
559
|
+
return r.DNSIncoming(generated.packets()[0])
|
|
527
560
|
|
|
528
|
-
|
|
529
|
-
|
|
561
|
+
sent_queries: list[r.DNSOutgoing] = []
|
|
562
|
+
|
|
563
|
+
def send(out, addr=const._MDNS_ADDR, port=const._MDNS_PORT, v6_flow_scope=()):
|
|
564
|
+
"""Capture each query and, on the first one, fill the cache
|
|
565
|
+
inline so the next iteration of `async_request` finds
|
|
566
|
+
`_is_complete=True` and exits without sending another query.
|
|
567
|
+
|
|
568
|
+
Running the inject from inside `send` keeps it on the event
|
|
569
|
+
loop thread and atomic with the first send — eliminating the
|
|
570
|
+
test-thread → `run_coroutine_threadsafe` race that flaked
|
|
571
|
+
under PyPy + use_cython when `quick_request_timing` shortens
|
|
572
|
+
the inter-iteration delay to ~15ms.
|
|
573
|
+
"""
|
|
574
|
+
sent_queries.append(out)
|
|
575
|
+
if len(sent_queries) == 1:
|
|
576
|
+
zc.record_manager.async_updates_from_response(mock_incoming_msg(response_records))
|
|
577
|
+
|
|
578
|
+
def get_service_info_helper(zc, type, name):
|
|
579
|
+
nonlocal service_info
|
|
580
|
+
service_info = zc.get_service_info(type, name)
|
|
581
|
+
service_info_event.set()
|
|
530
582
|
|
|
531
583
|
# patch the zeroconf send
|
|
532
584
|
with patch.object(zc, "async_send", send):
|
|
533
|
-
|
|
534
|
-
def mock_incoming_msg(records: Iterable[r.DNSRecord]) -> r.DNSIncoming:
|
|
535
|
-
generated = r.DNSOutgoing(const._FLAGS_QR_RESPONSE)
|
|
536
|
-
|
|
537
|
-
for record in records:
|
|
538
|
-
generated.add_answer_at_time(record, 0)
|
|
539
|
-
|
|
540
|
-
return r.DNSIncoming(generated.packets()[0])
|
|
541
|
-
|
|
542
|
-
def get_service_info_helper(zc, type, name):
|
|
543
|
-
nonlocal service_info
|
|
544
|
-
service_info = zc.get_service_info(type, name)
|
|
545
|
-
service_info_event.set()
|
|
546
|
-
|
|
547
585
|
try:
|
|
548
|
-
ttl = 120
|
|
549
586
|
helper_thread = threading.Thread(
|
|
550
587
|
target=get_service_info_helper,
|
|
551
588
|
args=(zc, service_type, service_name),
|
|
552
589
|
)
|
|
553
590
|
helper_thread.start()
|
|
554
|
-
wait_time = 1
|
|
555
591
|
|
|
556
|
-
#
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
assert r.DNSQuestion(service_name, const._TYPE_SRV, const._CLASS_IN) in last_sent.questions
|
|
561
|
-
assert r.DNSQuestion(service_name, const._TYPE_TXT, const._CLASS_IN) in last_sent.questions
|
|
562
|
-
assert r.DNSQuestion(service_name, const._TYPE_A, const._CLASS_IN) in last_sent.questions
|
|
563
|
-
assert r.DNSQuestion(service_name, const._TYPE_AAAA, const._CLASS_IN) in last_sent.questions
|
|
564
|
-
assert service_info is None
|
|
565
|
-
|
|
566
|
-
# Expect no further queries
|
|
567
|
-
last_sent = None
|
|
568
|
-
send_event.clear()
|
|
569
|
-
_inject_response(
|
|
570
|
-
zc,
|
|
571
|
-
mock_incoming_msg(
|
|
572
|
-
[
|
|
573
|
-
r.DNSText(
|
|
574
|
-
service_name,
|
|
575
|
-
const._TYPE_TXT,
|
|
576
|
-
const._CLASS_IN | const._CLASS_UNIQUE,
|
|
577
|
-
ttl,
|
|
578
|
-
service_text,
|
|
579
|
-
),
|
|
580
|
-
r.DNSService(
|
|
581
|
-
service_name,
|
|
582
|
-
const._TYPE_SRV,
|
|
583
|
-
const._CLASS_IN | const._CLASS_UNIQUE,
|
|
584
|
-
ttl,
|
|
585
|
-
0,
|
|
586
|
-
0,
|
|
587
|
-
80,
|
|
588
|
-
service_server,
|
|
589
|
-
),
|
|
590
|
-
r.DNSAddress(
|
|
591
|
-
service_server,
|
|
592
|
-
const._TYPE_A,
|
|
593
|
-
const._CLASS_IN | const._CLASS_UNIQUE,
|
|
594
|
-
ttl,
|
|
595
|
-
socket.inet_pton(socket.AF_INET, service_address),
|
|
596
|
-
),
|
|
597
|
-
]
|
|
598
|
-
),
|
|
599
|
-
)
|
|
600
|
-
send_event.wait(wait_time)
|
|
601
|
-
assert last_sent is None
|
|
592
|
+
# Helper should complete promptly — the inline inject in
|
|
593
|
+
# `send` populates the cache before the request loop's
|
|
594
|
+
# next iteration.
|
|
595
|
+
service_info_event.wait(1)
|
|
602
596
|
assert service_info is not None
|
|
603
597
|
|
|
598
|
+
# First (and only) query: QU for SRV/TXT/A/AAAA.
|
|
599
|
+
assert len(sent_queries) == 1
|
|
600
|
+
first_sent = sent_queries[0]
|
|
601
|
+
assert len(first_sent.questions) == 4
|
|
602
|
+
assert r.DNSQuestion(service_name, const._TYPE_SRV, const._CLASS_IN) in first_sent.questions
|
|
603
|
+
assert r.DNSQuestion(service_name, const._TYPE_TXT, const._CLASS_IN) in first_sent.questions
|
|
604
|
+
assert r.DNSQuestion(service_name, const._TYPE_A, const._CLASS_IN) in first_sent.questions
|
|
605
|
+
assert r.DNSQuestion(service_name, const._TYPE_AAAA, const._CLASS_IN) in first_sent.questions
|
|
606
|
+
|
|
604
607
|
finally:
|
|
605
608
|
helper_thread.join()
|
|
606
609
|
zc.remove_all_service_listeners()
|
|
@@ -980,7 +983,7 @@ def test_serviceinfo_accepts_bytes_or_string_dict():
|
|
|
980
983
|
assert info_service.dns_text().text == b"\x0epath=/~paulsm/"
|
|
981
984
|
|
|
982
985
|
|
|
983
|
-
def test_asking_qu_questions():
|
|
986
|
+
def test_asking_qu_questions(quick_request_timing):
|
|
984
987
|
"""Verify explicitly asking QU questions."""
|
|
985
988
|
type_ = "_quservice._tcp.local."
|
|
986
989
|
zeroconf = r.Zeroconf(interfaces=["127.0.0.1"])
|
|
@@ -999,12 +1002,14 @@ def test_asking_qu_questions():
|
|
|
999
1002
|
|
|
1000
1003
|
# patch the zeroconf send
|
|
1001
1004
|
with patch.object(zeroconf, "async_send", send):
|
|
1002
|
-
zeroconf.get_service_info(
|
|
1005
|
+
zeroconf.get_service_info(
|
|
1006
|
+
f"name.{type_}", type_, QUICK_REQUEST_TIMEOUT_MS, question_type=r.DNSQuestionType.QU
|
|
1007
|
+
)
|
|
1003
1008
|
assert first_outgoing.questions[0].unicast is True # type: ignore[union-attr]
|
|
1004
1009
|
zeroconf.close()
|
|
1005
1010
|
|
|
1006
1011
|
|
|
1007
|
-
def test_asking_qm_questions():
|
|
1012
|
+
def test_asking_qm_questions(quick_request_timing):
|
|
1008
1013
|
"""Verify explicitly asking QM questions."""
|
|
1009
1014
|
type_ = "_quservice._tcp.local."
|
|
1010
1015
|
zeroconf = r.Zeroconf(interfaces=["127.0.0.1"])
|
|
@@ -1023,7 +1028,9 @@ def test_asking_qm_questions():
|
|
|
1023
1028
|
|
|
1024
1029
|
# patch the zeroconf send
|
|
1025
1030
|
with patch.object(zeroconf, "async_send", send):
|
|
1026
|
-
zeroconf.get_service_info(
|
|
1031
|
+
zeroconf.get_service_info(
|
|
1032
|
+
f"name.{type_}", type_, QUICK_REQUEST_TIMEOUT_MS, question_type=r.DNSQuestionType.QM
|
|
1033
|
+
)
|
|
1027
1034
|
assert first_outgoing.questions[0].unicast is False # type: ignore[union-attr]
|
|
1028
1035
|
zeroconf.close()
|
|
1029
1036
|
|