Hello community,

here is the log from the commit of package python-zeroconf for openSUSE:Factory 
checked in at 2020-09-16 19:41:40
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-zeroconf (Old)
 and      /work/SRC/openSUSE:Factory/.python-zeroconf.new.4249 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-zeroconf"

Wed Sep 16 19:41:40 2020 rev:15 rq:834888 version:0.28.3

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-zeroconf/python-zeroconf.changes  
2020-07-21 15:54:22.188586543 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-zeroconf.new.4249/python-zeroconf.changes    
    2020-09-16 19:41:59.654978397 +0200
@@ -1,0 +2,9 @@
+Wed Sep 16 11:22:18 UTC 2020 - Dirk Mueller <[email protected]>
+
+- update to 0.28.3:
+  * Reduced a time an internal lock is held which should eliminate deadlocks 
in high-traffic networks.
+  * Stopped asking questions we already have answers for in cache, thanks to 
Paul Daumlechner.
+  * Removed initial delay before querying for service info, thanks to Erik 
Montnemery.
+  * Fixed a resource leak connected to using ServiceBrowser with multiple types
+
+-------------------------------------------------------------------

Old:
----
  python-zeroconf-0.28.0.tar.gz

New:
----
  python-zeroconf-0.28.3.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-zeroconf.spec ++++++
--- /var/tmp/diff_new_pack.rqssYz/_old  2020-09-16 19:42:00.306979130 +0200
+++ /var/tmp/diff_new_pack.rqssYz/_new  2020-09-16 19:42:00.310979135 +0200
@@ -19,7 +19,7 @@
 %{?!python_module:%define python_module() python-%{**} python3-%{**}}
 %define skip_python2 1
 Name:           python-zeroconf
-Version:        0.28.0
+Version:        0.28.3
 Release:        0
 Summary:        Pure Python Multicast DNS Service Discovery Library 
(Bonjour/Avahi compatible)
 License:        LGPL-2.0-only

++++++ python-zeroconf-0.28.0.tar.gz -> python-zeroconf-0.28.3.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-zeroconf-0.28.0/.gitignore 
new/python-zeroconf-0.28.3/.gitignore
--- old/python-zeroconf-0.28.0/.gitignore       2020-07-07 13:22:12.000000000 
+0200
+++ new/python-zeroconf-0.28.3/.gitignore       2020-08-31 12:57:18.000000000 
+0200
@@ -12,3 +12,5 @@
 .mypy_cache/
 docs/_build/
 .vscode
+/dist/
+/zeroconf.egg-info/
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-zeroconf-0.28.0/README.rst 
new/python-zeroconf-0.28.3/README.rst
--- old/python-zeroconf-0.28.0/README.rst       2020-07-07 13:22:12.000000000 
+0200
+++ new/python-zeroconf-0.28.3/README.rst       2020-08-31 12:57:18.000000000 
+0200
@@ -134,6 +134,23 @@
 Changelog
 =========
 
+0.28.3
+======
+
+* Reduced a time an internal lock is held which should eliminate deadlocks in 
high-traffic networks.
+
+0.28.2
+======
+
+* Stopped asking questions we already have answers for in cache, thanks to 
Paul Daumlechner.
+* Removed initial delay before querying for service info, thanks to Erik 
Montnemery.
+
+0.28.1
+======
+
+* Fixed a resource leak connected to using ServiceBrowser with multiple types, 
thanks to
+  J. Nick Koston.
+
 0.28.0
 ======
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-zeroconf-0.28.0/zeroconf/__init__.py 
new/python-zeroconf-0.28.3/zeroconf/__init__.py
--- old/python-zeroconf-0.28.0/zeroconf/__init__.py     2020-07-07 
13:22:12.000000000 +0200
+++ new/python-zeroconf-0.28.3/zeroconf/__init__.py     2020-08-31 
12:57:18.000000000 +0200
@@ -42,7 +42,7 @@
 
 __author__ = 'Paul Scott-Murphy, William McBrine'
 __maintainer__ = 'Jakub Stasiak <[email protected]>'
-__version__ = '0.28.0'
+__version__ = '0.28.3'
 __license__ = 'LGPL'
 
 
@@ -174,6 +174,10 @@
 _HAS_ONLY_A_TO_Z_NUM_HYPHEN_UNDERSCORE = re.compile(r'^[A-Za-z0-9\-\_]+$')
 _HAS_ASCII_CONTROL_CHARS = re.compile(r'[\x00-\x1f\x7f]')
 
+_EXPIRE_FULL_TIME_PERCENT = 100
+_EXPIRE_STALE_TIME_PERCENT = 50
+_EXPIRE_REFRESH_TIME_PERCENT = 75
+
 try:
     _IPPROTO_IPV6 = socket.IPPROTO_IPV6
 except AttributeError:
@@ -459,8 +463,8 @@
         DNSEntry.__init__(self, name, type_, class_)
         self.ttl = ttl
         self.created = current_time_millis()
-        self._expiration_time = self.get_expiration_time(100)
-        self._stale_time = self.get_expiration_time(50)
+        self._expiration_time = 
self.get_expiration_time(_EXPIRE_FULL_TIME_PERCENT)
+        self._stale_time = self.get_expiration_time(_EXPIRE_STALE_TIME_PERCENT)
 
     def __eq__(self, other: Any) -> bool:
         """Abstract method"""
@@ -506,8 +510,8 @@
         another record."""
         self.created = other.created
         self.ttl = other.ttl
-        self._expiration_time = self.get_expiration_time(100)
-        self._stale_time = self.get_expiration_time(50)
+        self._expiration_time = 
self.get_expiration_time(_EXPIRE_FULL_TIME_PERCENT)
+        self._stale_time = self.get_expiration_time(_EXPIRE_STALE_TIME_PERCENT)
 
     def write(self, out: 'DNSOutgoing') -> None:
         """Abstract method"""
@@ -934,7 +938,7 @@
         self.authorities.append(record)
 
     def add_additional_answer(self, record: DNSRecord) -> None:
-        """ Adds an additional answer
+        """Adds an additional answer
 
         From: RFC 6763, DNS-Based Service Discovery, February 2013
 
@@ -1132,7 +1136,7 @@
         or less in length, except for the case of a single answer which
         will be written out to a single oversized packet no more than
         _MAX_MSG_ABSOLUTE in length (and hence will be subject to IP
-        fragmentation potentially).  """
+        fragmentation potentially)."""
 
         if self.state == self.State.finished:
             return self.packets_data
@@ -1609,7 +1613,7 @@
                     enqueue_callback(ServiceStateChange.Removed, record.name, 
record.alias)
                     return
 
-            expires = record.get_expiration_time(75)
+            expires = record.get_expiration_time(_EXPIRE_REFRESH_TIME_PERCENT)
             if expires < self._next_time[record.name]:
                 self._next_time[record.name] = expires
 
@@ -1649,8 +1653,8 @@
         self.join()
 
     def run(self) -> None:
-        for type_ in self.types:
-            self.zc.add_listener(self, DNSQuestion(type_, _TYPE_PTR, 
_CLASS_IN))
+        questions = [DNSQuestion(type_, _TYPE_PTR, _CLASS_IN) for type_ in 
self.types]
+        self.zc.add_listener(self, questions)
 
         while True:
             now = current_time_millis()
@@ -1890,7 +1894,7 @@
         """
         now = current_time_millis()
         delay = _LISTENER_TIME
-        next_ = now + delay
+        next_ = now
         last = now + timeout
 
         record_types_for_check_cache = [(_TYPE_SRV, _CLASS_IN), (_TYPE_TXT, 
_CLASS_IN)]
@@ -1912,19 +1916,24 @@
                     return False
                 if next_ <= now:
                     out = DNSOutgoing(_FLAGS_QR_QUERY)
-                    out.add_question(DNSQuestion(self.name, _TYPE_SRV, 
_CLASS_IN))
-                    out.add_answer_at_time(zc.cache.get_by_details(self.name, 
_TYPE_SRV, _CLASS_IN), now)
-
-                    out.add_question(DNSQuestion(self.name, _TYPE_TXT, 
_CLASS_IN))
-                    out.add_answer_at_time(zc.cache.get_by_details(self.name, 
_TYPE_TXT, _CLASS_IN), now)
+                    cached_entry = zc.cache.get_by_details(self.name, 
_TYPE_SRV, _CLASS_IN)
+                    if not cached_entry:
+                        out.add_question(DNSQuestion(self.name, _TYPE_SRV, 
_CLASS_IN))
+                        out.add_answer_at_time(cached_entry, now)
+                    cached_entry = zc.cache.get_by_details(self.name, 
_TYPE_TXT, _CLASS_IN)
+                    if not cached_entry:
+                        out.add_question(DNSQuestion(self.name, _TYPE_TXT, 
_CLASS_IN))
+                        out.add_answer_at_time(cached_entry, now)
 
                     if self.server is not None:
-                        out.add_question(DNSQuestion(self.server, _TYPE_A, 
_CLASS_IN))
-                        
out.add_answer_at_time(zc.cache.get_by_details(self.server, _TYPE_A, 
_CLASS_IN), now)
-                        out.add_question(DNSQuestion(self.server, _TYPE_AAAA, 
_CLASS_IN))
-                        out.add_answer_at_time(
-                            zc.cache.get_by_details(self.server, _TYPE_AAAA, 
_CLASS_IN), now
-                        )
+                        cached_entry = zc.cache.get_by_details(self.server, 
_TYPE_A, _CLASS_IN)
+                        if not cached_entry:
+                            out.add_question(DNSQuestion(self.server, _TYPE_A, 
_CLASS_IN))
+                            out.add_answer_at_time(cached_entry, now)
+                        cached_entry = zc.cache.get_by_details(self.name, 
_TYPE_AAAA, _CLASS_IN)
+                        if not cached_entry:
+                            out.add_question(DNSQuestion(self.server, 
_TYPE_AAAA, _CLASS_IN))
+                            out.add_answer_at_time(cached_entry, now)
                     zc.send(out)
                     next_ = now + delay
                     delay *= 2
@@ -2595,16 +2604,20 @@
             i += 1
             next_time += _CHECK_TIME
 
-    def add_listener(self, listener: RecordUpdateListener, question: 
Optional[DNSQuestion]) -> None:
+    def add_listener(
+        self, listener: RecordUpdateListener, question: 
Optional[Union[DNSQuestion, List[DNSQuestion]]]
+    ) -> None:
         """Adds a listener for a given question.  The listener will have
         its update_record method called when information is available to
-        answer the question."""
+        answer the question(s)."""
         now = current_time_millis()
         self.listeners.append(listener)
         if question is not None:
-            for record in self.cache.entries_with_name(question.name):
-                if question.answered_by(record) and not record.is_expired(now):
-                    listener.update_record(self, now, record)
+            questions = [question] if isinstance(question, DNSQuestion) else 
question
+            for single_question in questions:
+                for record in 
self.cache.entries_with_name(single_question.name):
+                    if single_question.answered_by(record) and not 
record.is_expired(now):
+                        listener.update_record(self, now, record)
         self.notify_all()
 
     def remove_listener(self, listener: RecordUpdateListener) -> None:
@@ -2625,45 +2638,52 @@
     def handle_response(self, msg: DNSIncoming) -> None:
         """Deal with incoming response packets.  All answers
         are held in the cache, and listeners are notified."""
+        updates = []  # type: List[Tuple[float, DNSRecord, 
Optional[DNSRecord]]]
+        now = current_time_millis()
+        for record in msg.answers:
 
-        with self._handlers_lock:
+            updated = True
 
-            now = current_time_millis()
-            for record in msg.answers:
+            if record.unique:  # 
https://tools.ietf.org/html/rfc6762#section-10.2
+                # Since the cache format is keyed on the lower case record name
+                # we can avoid iterating everything in the cache and
+                # only look though entries for the specific name.
+                # entries_with_name will take care of converting to lowercase
+                #
+                # We make a copy of the list that entries_with_name returns
+                # since we cannot iterate over something we might remove
+                for entry in self.cache.entries_with_name(record.name).copy():
+
+                    if entry == record:
+                        updated = False
+
+                    # Check the time first because it is far cheaper
+                    # than the __eq__
+                    if (record.created - entry.created > 1000) and 
DNSEntry.__eq__(entry, record):
+                        self.cache.remove(entry)
 
-                updated = True
+            expired = record.is_expired(now)
+            maybe_entry = self.cache.get(record)
+            if not expired:
+                if maybe_entry is not None:
+                    maybe_entry.reset_ttl(record)
+                else:
+                    self.cache.add(record)
+                if updated:
+                    updates.append((now, record, None))
+            elif maybe_entry is not None:
+                updates.append((now, record, maybe_entry))
 
-                if record.unique:  # 
https://tools.ietf.org/html/rfc6762#section-10.2
-                    # Since the cache format is keyed on the lower case record 
name
-                    # we can avoid iterating everything in the cache and
-                    # only look though entries for the specific name.
-                    # entries_with_name will take care of converting to 
lowercase
-                    #
-                    # We make a copy of the list that entries_with_name returns
-                    # since we cannot iterate over something we might remove
-                    for entry in 
self.cache.entries_with_name(record.name).copy():
-
-                        if entry == record:
-                            updated = False
-
-                        # Check the time first because it is far cheaper
-                        # than the __eq__
-                        if (record.created - entry.created > 1000) and 
DNSEntry.__eq__(entry, record):
-                            self.cache.remove(entry)
+        if not updates:
+            return
 
-                expired = record.is_expired(now)
-                maybe_entry = self.cache.get(record)
-                if not expired:
-                    if maybe_entry is not None:
-                        maybe_entry.reset_ttl(record)
-                    else:
-                        self.cache.add(record)
-                    if updated:
-                        self.update_record(now, record)
-                else:
-                    if maybe_entry is not None:
-                        self.update_record(now, record)
-                        self.cache.remove(maybe_entry)
+        # Only hold the lock if we have updates
+        with self._handlers_lock:
+            for update in updates:
+                now, record, entry_to_remove = update
+                self.update_record(update[0], update[1])
+                if entry_to_remove:
+                    self.cache.remove(entry_to_remove)
 
     def handle_query(self, msg: DNSIncoming, addr: Optional[str], port: int) 
-> None:
         """Deal with incoming query packets.  Provides a response if
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-zeroconf-0.28.0/zeroconf/test.py 
new/python-zeroconf-0.28.3/zeroconf/test.py
--- old/python-zeroconf-0.28.0/zeroconf/test.py 2020-07-07 13:22:12.000000000 
+0200
+++ new/python-zeroconf-0.28.3/zeroconf/test.py 2020-08-31 12:57:18.000000000 
+0200
@@ -9,6 +9,7 @@
 import os
 import socket
 import struct
+import threading
 import time
 import unittest
 from threading import Event
@@ -24,6 +25,7 @@
     ServiceStateChange,
     Zeroconf,
     ZeroconfServiceTypes,
+    _EXPIRE_REFRESH_TIME_PERCENT,
 )
 
 log = logging.getLogger('zeroconf')
@@ -108,7 +110,14 @@
         name = "xxxyyy"
         registration_name = "%s.%s" % (name, type_)
         info = ServiceInfo(
-            type_, registration_name, 80, 0, 0, b'', "ash-2.local.", 
addresses=[socket.inet_aton("10.0.1.2")],
+            type_,
+            registration_name,
+            80,
+            0,
+            0,
+            b'',
+            "ash-2.local.",
+            addresses=[socket.inet_aton("10.0.1.2")],
         )
 
         assert not info != info
@@ -860,6 +869,20 @@
         cache.remove(record2)
         assert 'a' not in cache.cache
 
+    def test_cache_empty_multiple_calls_does_not_throw(self):
+        record1 = r.DNSAddress('a', r._TYPE_SOA, r._CLASS_IN, 1, b'a')
+        record2 = r.DNSAddress('a', r._TYPE_SOA, r._CLASS_IN, 1, b'b')
+        cache = r.DNSCache()
+        cache.add(record1)
+        cache.add(record2)
+        assert 'a' in cache.cache
+        cache.remove(record1)
+        cache.remove(record2)
+        # Ensure multiple removes does not throw
+        cache.remove(record1)
+        cache.remove(record2)
+        assert 'a' not in cache.cache
+
 
 class ServiceTypesQuery(unittest.TestCase):
     def test_integration_with_listener(self):
@@ -1237,16 +1260,242 @@
             assert service_removed_count == 1
 
         finally:
+            assert len(zeroconf.listeners) == 1
             service_browser.cancel()
+            assert len(zeroconf.listeners) == 0
             zeroconf.remove_all_service_listeners()
             zeroconf.close()
 
 
+class TestServiceInfo(unittest.TestCase):
+    def test_get_info_partial(self):
+
+        zc = r.Zeroconf(interfaces=['127.0.0.1'])
+
+        service_name = 'name._type._tcp.local.'
+        service_type = '_type._tcp.local.'
+        service_server = 'ash-1.local.'
+        service_text = b'path=/~matt1/'
+        service_address = '10.0.1.2'
+
+        service_info = None
+        send_event = Event()
+        service_info_event = Event()
+
+        last_sent = None  # type: Optional[r.DNSOutgoing]
+
+        def send(out, addr=r._MDNS_ADDR, port=r._MDNS_PORT):
+            """Sends an outgoing packet."""
+            nonlocal last_sent
+
+            last_sent = out
+            send_event.set()
+
+        # monkey patch the zeroconf send
+        setattr(zc, "send", send)
+
+        def mock_incoming_msg(records) -> r.DNSIncoming:
+
+            generated = r.DNSOutgoing(r._FLAGS_QR_RESPONSE)
+
+            for record in records:
+                generated.add_answer_at_time(record, 0)
+
+            return r.DNSIncoming(generated.packet())
+
+        def get_service_info_helper(zc, type, name):
+            nonlocal service_info
+            service_info = zc.get_service_info(type, name)
+            service_info_event.set()
+
+        try:
+            ttl = 120
+            helper_thread = threading.Thread(
+                target=get_service_info_helper, args=(zc, service_type, 
service_name)
+            )
+            helper_thread.start()
+            wait_time = 1
+
+            # Expext query for SRV, TXT, A, AAAA
+            send_event.wait(wait_time)
+            assert last_sent is not None
+            assert len(last_sent.questions) == 4
+            assert r.DNSQuestion(service_name, r._TYPE_SRV, r._CLASS_IN) in 
last_sent.questions
+            assert r.DNSQuestion(service_name, r._TYPE_TXT, r._CLASS_IN) in 
last_sent.questions
+            assert r.DNSQuestion(service_name, r._TYPE_A, r._CLASS_IN) in 
last_sent.questions
+            assert r.DNSQuestion(service_name, r._TYPE_AAAA, r._CLASS_IN) in 
last_sent.questions
+            assert service_info is None
+
+            # Expext query for SRV, A, AAAA
+            last_sent = None
+            send_event.clear()
+            zc.handle_response(
+                mock_incoming_msg(
+                    [r.DNSText(service_name, r._TYPE_TXT, r._CLASS_IN | 
r._CLASS_UNIQUE, ttl, service_text)]
+                )
+            )
+            send_event.wait(wait_time)
+            assert last_sent is not None
+            assert len(last_sent.questions) == 3
+            assert r.DNSQuestion(service_name, r._TYPE_SRV, r._CLASS_IN) in 
last_sent.questions
+            assert r.DNSQuestion(service_name, r._TYPE_A, r._CLASS_IN) in 
last_sent.questions
+            assert r.DNSQuestion(service_name, r._TYPE_AAAA, r._CLASS_IN) in 
last_sent.questions
+            assert service_info is None
+
+            # Expext query for A, AAAA
+            last_sent = None
+            send_event.clear()
+            zc.handle_response(
+                mock_incoming_msg(
+                    [
+                        r.DNSService(
+                            service_name,
+                            r._TYPE_SRV,
+                            r._CLASS_IN | r._CLASS_UNIQUE,
+                            ttl,
+                            0,
+                            0,
+                            80,
+                            service_server,
+                        )
+                    ]
+                )
+            )
+            send_event.wait(wait_time)
+            assert last_sent is not None
+            assert len(last_sent.questions) == 2
+            assert r.DNSQuestion(service_server, r._TYPE_A, r._CLASS_IN) in 
last_sent.questions
+            assert r.DNSQuestion(service_server, r._TYPE_AAAA, r._CLASS_IN) in 
last_sent.questions
+            last_sent = None
+            assert service_info is None
+
+            # Expext no further queries
+            last_sent = None
+            send_event.clear()
+            zc.handle_response(
+                mock_incoming_msg(
+                    [
+                        r.DNSAddress(
+                            service_server,
+                            r._TYPE_A,
+                            r._CLASS_IN | r._CLASS_UNIQUE,
+                            ttl,
+                            socket.inet_pton(socket.AF_INET, service_address),
+                        )
+                    ]
+                )
+            )
+            send_event.wait(wait_time)
+            assert last_sent is None
+            assert service_info is not None
+
+        finally:
+            helper_thread.join()
+            zc.remove_all_service_listeners()
+            zc.close()
+
+    def test_get_info_single(self):
+
+        zc = r.Zeroconf(interfaces=['127.0.0.1'])
+
+        service_name = 'name._type._tcp.local.'
+        service_type = '_type._tcp.local.'
+        service_server = 'ash-1.local.'
+        service_text = b'path=/~matt1/'
+        service_address = '10.0.1.2'
+
+        service_info = None
+        send_event = Event()
+        service_info_event = Event()
+
+        last_sent = None  # type: Optional[r.DNSOutgoing]
+
+        def send(out, addr=r._MDNS_ADDR, port=r._MDNS_PORT):
+            """Sends an outgoing packet."""
+            nonlocal last_sent
+
+            last_sent = out
+            send_event.set()
+
+        # monkey patch the zeroconf send
+        setattr(zc, "send", send)
+
+        def mock_incoming_msg(records) -> r.DNSIncoming:
+
+            generated = r.DNSOutgoing(r._FLAGS_QR_RESPONSE)
+
+            for record in records:
+                generated.add_answer_at_time(record, 0)
+
+            return r.DNSIncoming(generated.packet())
+
+        def get_service_info_helper(zc, type, name):
+            nonlocal service_info
+            service_info = zc.get_service_info(type, name)
+            service_info_event.set()
+
+        try:
+            ttl = 120
+            helper_thread = threading.Thread(
+                target=get_service_info_helper, args=(zc, service_type, 
service_name)
+            )
+            helper_thread.start()
+            wait_time = 1
+
+            # Expext query for SRV, TXT, A, AAAA
+            send_event.wait(wait_time)
+            assert last_sent is not None
+            assert len(last_sent.questions) == 4
+            assert r.DNSQuestion(service_name, r._TYPE_SRV, r._CLASS_IN) in 
last_sent.questions
+            assert r.DNSQuestion(service_name, r._TYPE_TXT, r._CLASS_IN) in 
last_sent.questions
+            assert r.DNSQuestion(service_name, r._TYPE_A, r._CLASS_IN) in 
last_sent.questions
+            assert r.DNSQuestion(service_name, r._TYPE_AAAA, r._CLASS_IN) in 
last_sent.questions
+            assert service_info is None
+
+            # Expext no further queries
+            last_sent = None
+            send_event.clear()
+            zc.handle_response(
+                mock_incoming_msg(
+                    [
+                        r.DNSText(
+                            service_name, r._TYPE_TXT, r._CLASS_IN | 
r._CLASS_UNIQUE, ttl, service_text
+                        ),
+                        r.DNSService(
+                            service_name,
+                            r._TYPE_SRV,
+                            r._CLASS_IN | r._CLASS_UNIQUE,
+                            ttl,
+                            0,
+                            0,
+                            80,
+                            service_server,
+                        ),
+                        r.DNSAddress(
+                            service_server,
+                            r._TYPE_A,
+                            r._CLASS_IN | r._CLASS_UNIQUE,
+                            ttl,
+                            socket.inet_pton(socket.AF_INET, service_address),
+                        ),
+                    ]
+                )
+            )
+            send_event.wait(wait_time)
+            assert last_sent is None
+            assert service_info is not None
+
+        finally:
+            helper_thread.join()
+            zc.remove_all_service_listeners()
+            zc.close()
+
+
 class TestServiceBrowserMultipleTypes(unittest.TestCase):
     def test_update_record(self):
 
-        service_names = ['name._type._tcp.local.', 'name._type._udp.local']
-        service_types = ['_type._tcp.local.', '_type._udp.local.']
+        service_names = ['name2._type2._tcp.local.', 'name._type._tcp.local.', 
'name._type._udp.local']
+        service_types = ['_type2._tcp.local.', '_type._tcp.local.', 
'_type._udp.local.']
 
         service_added_count = 0
         service_removed_count = 0
@@ -1257,25 +1506,19 @@
             def add_service(self, zc, type_, name) -> None:
                 nonlocal service_added_count
                 service_added_count += 1
-                if service_added_count == 2:
+                if service_added_count == 3:
                     service_add_event.set()
 
             def remove_service(self, zc, type_, name) -> None:
                 nonlocal service_removed_count
                 service_removed_count += 1
-                if service_removed_count == 2:
+                if service_removed_count == 3:
                     service_removed_event.set()
 
         def mock_incoming_msg(
-            service_state_change: r.ServiceStateChange, service_type: str, 
service_name: str
+            service_state_change: r.ServiceStateChange, service_type: str, 
service_name: str, ttl: int
         ) -> r.DNSIncoming:
             generated = r.DNSOutgoing(r._FLAGS_QR_RESPONSE)
-
-            if service_state_change == r.ServiceStateChange.Removed:
-                ttl = 0
-            else:
-                ttl = 120
-
             generated.add_answer_at_time(
                 r.DNSPointer(service_type, r._TYPE_PTR, r._CLASS_IN, ttl, 
service_name), 0
             )
@@ -1287,30 +1530,54 @@
         try:
             wait_time = 3
 
-            # both services added
+            # all three services added
             zeroconf.handle_response(
-                mock_incoming_msg(r.ServiceStateChange.Added, 
service_types[0], service_names[0])
+                mock_incoming_msg(r.ServiceStateChange.Added, 
service_types[0], service_names[0], 120)
             )
             zeroconf.handle_response(
-                mock_incoming_msg(r.ServiceStateChange.Added, 
service_types[1], service_names[1])
+                mock_incoming_msg(r.ServiceStateChange.Added, 
service_types[1], service_names[1], 120)
             )
+            zeroconf.handle_response(
+                mock_incoming_msg(r.ServiceStateChange.Added, 
service_types[2], service_names[2], 120)
+            )
+
+            called_with_refresh_time_check = False
+
+            def _mock_get_expiration_time(self, percent):
+                nonlocal called_with_refresh_time_check
+                if percent == _EXPIRE_REFRESH_TIME_PERCENT:
+                    called_with_refresh_time_check = True
+                    return 0
+                return self.created + (percent * self.ttl * 10)
+
+            # Set an expire time that will force a refresh
+            with unittest.mock.patch("zeroconf.DNSRecord.get_expiration_time", 
new=_mock_get_expiration_time):
+                zeroconf.handle_response(
+                    mock_incoming_msg(r.ServiceStateChange.Added, 
service_types[2], service_names[2], 120)
+                )
             service_add_event.wait(wait_time)
-            assert service_added_count == 2
+            assert called_with_refresh_time_check is True
+            assert service_added_count == 3
             assert service_removed_count == 0
 
-            # both services removed
+            # all three services removed
             zeroconf.handle_response(
-                mock_incoming_msg(r.ServiceStateChange.Removed, 
service_types[0], service_names[0])
+                mock_incoming_msg(r.ServiceStateChange.Removed, 
service_types[0], service_names[0], 0)
             )
             zeroconf.handle_response(
-                mock_incoming_msg(r.ServiceStateChange.Removed, 
service_types[1], service_names[1])
+                mock_incoming_msg(r.ServiceStateChange.Removed, 
service_types[1], service_names[1], 0)
+            )
+            zeroconf.handle_response(
+                mock_incoming_msg(r.ServiceStateChange.Removed, 
service_types[2], service_names[2], 0)
             )
             service_removed_event.wait(wait_time)
-            assert service_added_count == 2
-            assert service_removed_count == 2
+            assert service_added_count == 3
+            assert service_removed_count == 3
 
         finally:
+            assert len(zeroconf.listeners) == 1
             service_browser.cancel()
+            assert len(zeroconf.listeners) == 0
             zeroconf.remove_all_service_listeners()
             zeroconf.close()
 
@@ -1505,7 +1772,14 @@
         address_v6 = socket.inet_pton(socket.AF_INET6, address_v6_parsed)
         infos = [
             ServiceInfo(
-                type_, registration_name, 80, 0, 0, desc, "ash-2.local.", 
addresses=[address, address_v6],
+                type_,
+                registration_name,
+                80,
+                0,
+                0,
+                desc,
+                "ash-2.local.",
+                addresses=[address, address_v6],
             ),
             ServiceInfo(
                 type_,


Reply via email to