Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-ZODB for openSUSE:Factory 
checked in at 2024-01-07 21:40:28
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-ZODB (Old)
 and      /work/SRC/openSUSE:Factory/.python-ZODB.new.28375 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-ZODB"

Sun Jan  7 21:40:28 2024 rev:13 rq:1137379 version:5.8.1

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-ZODB/python-ZODB.changes  2022-12-02 
13:13:47.598068328 +0100
+++ /work/SRC/openSUSE:Factory/.python-ZODB.new.28375/python-ZODB.changes       
2024-01-07 21:40:43.912956670 +0100
@@ -1,0 +2,8 @@
+Sun Jan  7 16:42:08 UTC 2024 - Dirk Müller <dmuel...@suse.com>
+
+- update to 5.8.1:
+  * Fix racetest problems. For details see #376.
+  * Fix --with-verify argument in script repozo --recover. For
+    details see #381.
+
+-------------------------------------------------------------------

Old:
----
  ZODB-5.8.0.tar.gz

New:
----
  ZODB-5.8.1.tar.gz

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

Other differences:
------------------
++++++ python-ZODB.spec ++++++
--- /var/tmp/diff_new_pack.T1OZaK/_old  2024-01-07 21:40:44.892992320 +0100
+++ /var/tmp/diff_new_pack.T1OZaK/_new  2024-01-07 21:40:44.892992320 +0100
@@ -1,7 +1,7 @@
 #
 # spec file for package python-ZODB
 #
-# Copyright (c) 2022 SUSE LLC
+# Copyright (c) 2024 SUSE LLC
 # Copyright (c) 2013 LISA GmbH, Bingen, Germany.
 #
 # All modifications and additions to the file contributed by third parties
@@ -19,7 +19,7 @@
 
 %{?!python_module:%define python_module() python-%{**} python3-%{**}}
 Name:           python-ZODB
-Version:        5.8.0
+Version:        5.8.1
 Release:        0
 Summary:        Zope Object Database: object database and persistence
 License:        ZPL-2.1

++++++ ZODB-5.8.0.tar.gz -> ZODB-5.8.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ZODB-5.8.0/CHANGES.rst new/ZODB-5.8.1/CHANGES.rst
--- old/ZODB-5.8.0/CHANGES.rst  2022-11-09 11:19:23.000000000 +0100
+++ new/ZODB-5.8.1/CHANGES.rst  2023-07-18 08:59:19.000000000 +0200
@@ -2,6 +2,16 @@
  Change History
 ================
 
+5.8.1 (2023-07-18)
+==================
+
+- Fix ``racetest`` problems.
+  For details see `#376 <https://github.com/zopefoundation/ZODB/pull/376>`_.
+
+- Fix ``--with-verify`` argument in script repozo ``--recover``.
+  For details see `#381 <https://github.com/zopefoundation/ZODB/pull/381>`_.
+
+
 5.8.0 (2022-11-09)
 ==================
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ZODB-5.8.0/PKG-INFO new/ZODB-5.8.1/PKG-INFO
--- old/ZODB-5.8.0/PKG-INFO     2022-11-09 12:37:51.493553900 +0100
+++ new/ZODB-5.8.1/PKG-INFO     2023-07-18 09:29:35.791757800 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: ZODB
-Version: 5.8.0
+Version: 5.8.1
 Summary: ZODB, a Python object-oriented database
 Home-page: http://zodb-docs.readthedocs.io
 Author: Jim Fulton
@@ -48,8 +48,8 @@
    :target: https://pypi.org/project/ZODB/
    :alt: Supported Python versions
 
-.. image:: https://travis-ci.com/zopefoundation/ZODB.svg?branch=master
-   :target: https://travis-ci.com/zopefoundation/ZODB
+.. image:: 
https://github.com/zopefoundation/ZODB/actions/workflows/tests.yml/badge.svg
+   :target: https://github.com/zopefoundation/ZODB/actions/workflows/tests.yml
    :alt: Build status
 
 .. image:: https://coveralls.io/repos/github/zopefoundation/ZODB/badge.svg
@@ -90,6 +90,16 @@
  Change History
 ================
 
+5.8.1 (2023-07-18)
+==================
+
+- Fix ``racetest`` problems.
+  For details see `#376 <https://github.com/zopefoundation/ZODB/pull/376>`_.
+
+- Fix ``--with-verify`` argument in script repozo ``--recover``.
+  For details see `#381 <https://github.com/zopefoundation/ZODB/pull/381>`_.
+
+
 5.8.0 (2022-11-09)
 ==================
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ZODB-5.8.0/README.rst new/ZODB-5.8.1/README.rst
--- old/ZODB-5.8.0/README.rst   2021-11-02 09:52:39.000000000 +0100
+++ new/ZODB-5.8.1/README.rst   2023-04-14 12:27:25.000000000 +0200
@@ -10,8 +10,8 @@
    :target: https://pypi.org/project/ZODB/
    :alt: Supported Python versions
 
-.. image:: https://travis-ci.com/zopefoundation/ZODB.svg?branch=master
-   :target: https://travis-ci.com/zopefoundation/ZODB
+.. image:: 
https://github.com/zopefoundation/ZODB/actions/workflows/tests.yml/badge.svg
+   :target: https://github.com/zopefoundation/ZODB/actions/workflows/tests.yml
    :alt: Build status
 
 .. image:: https://coveralls.io/repos/github/zopefoundation/ZODB/badge.svg
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ZODB-5.8.0/docs/tutorial.rst 
new/ZODB-5.8.1/docs/tutorial.rst
--- old/ZODB-5.8.0/docs/tutorial.rst    2022-03-16 14:46:04.000000000 +0100
+++ new/ZODB-5.8.1/docs/tutorial.rst    2023-07-18 08:56:15.000000000 +0200
@@ -166,10 +166,7 @@
 You can use BTrees to build indexes for efficient search, when
 necessary.  If your application is search centric, or if you prefer to
 approach data access that way, then ZODB might not be the best
-technology for you. Before you turn your back on the ZODB, it
-may be worth checking out the up-and-coming Newt DB [#newtdb]_ project, 
-which combines the ZODB with Postgresql for indexing, search and access 
-from non-Python applications.
+technology for you.
 
 Transactions
 ============
@@ -248,6 +245,3 @@
    Objects aren't actually evicted, but their state is released, so
    they take up much less memory and any objects they referenced can
    be removed from memory.
-
-.. [#newtdb]
-   Here is an overview of the Newt DB architecture: 
http://www.newtdb.org/en/latest/how-it-works.html
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ZODB-5.8.0/setup.py new/ZODB-5.8.1/setup.py
--- old/ZODB-5.8.0/setup.py     2022-11-09 11:19:42.000000000 +0100
+++ new/ZODB-5.8.1/setup.py     2023-07-18 08:56:46.000000000 +0200
@@ -15,7 +15,7 @@
 from setuptools import setup
 
 
-version = '5.8.0'
+version = '5.8.1'
 
 classifiers = """\
 Intended Audience :: Developers
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ZODB-5.8.0/src/ZODB/Connection.py 
new/ZODB-5.8.1/src/ZODB/Connection.py
--- old/ZODB-5.8.0/src/ZODB/Connection.py       2022-11-09 09:17:08.000000000 
+0100
+++ new/ZODB-5.8.1/src/ZODB/Connection.py       2023-07-18 08:56:15.000000000 
+0200
@@ -53,7 +53,6 @@
 from ZODB.serialize import ObjectWriter
 from ZODB.utils import oid_repr
 from ZODB.utils import p64
-from ZODB.utils import positive_id
 from ZODB.utils import u64
 from ZODB.utils import z64
 
@@ -948,15 +947,6 @@
             c.close(False)
 
     ##########################################################################
-    # Python protocol
-
-    def __repr__(self):
-        return '<Connection at %08x>' % (positive_id(self),)
-
-    # Python protocol
-    ##########################################################################
-
-    ##########################################################################
     # DEPRECATION candidates
 
     __getitem__ = get
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ZODB-5.8.0/src/ZODB/cross-database-references.rst 
new/ZODB-5.8.1/src/ZODB/cross-database-references.rst
--- old/ZODB-5.8.0/src/ZODB/cross-database-references.rst       2022-11-09 
09:17:08.000000000 +0100
+++ new/ZODB-5.8.1/src/ZODB/cross-database-references.rst       2023-07-18 
08:56:15.000000000 +0200
@@ -63,7 +63,7 @@
     ...
     InvalidObjectReference:
       ('Attempt to store an object from a foreign database connection',
-       <Connection at ...>,
+       <ZODB.Connection.Connection object at ...>,
        <ZODB.tests.testcrossdatabasereferences.MyClass...>)
 
     >>> tm.abort()
@@ -92,7 +92,7 @@
     InvalidObjectReference:
     ("A new object is reachable from multiple databases. Won't try to
     guess which one was correct!",
-    <Connection at ...>,
+    <ZODB.Connection.Connection object at ...>,
     <ZODB.tests.testcrossdatabasereferences.MyClass...>)
 
     >>> tm.abort()
@@ -120,7 +120,7 @@
     InvalidObjectReference:
     ("A new object is reachable from multiple databases. Won't try to guess
     which one was correct!",
-    <Connection at ...>,
+    <ZODB.Connection.Connection object at ...>,
     <ZODB.tests.testcrossdatabasereferences.MyClass...>)
 
     >>> tm.abort()
@@ -167,7 +167,7 @@
     ...
     InvalidObjectReference:
     ("Database '2' doesn't allow implicit cross-database references",
-    <Connection at ...>,
+    <ZODB.Connection.Connection object at ...>,
     {'x': {}})
 
     >>> transaction.abort()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ZODB-5.8.0/src/ZODB/scripts/repozo.py 
new/ZODB-5.8.1/src/ZODB/scripts/repozo.py
--- old/ZODB-5.8.0/src/ZODB/scripts/repozo.py   2022-11-09 09:17:08.000000000 
+0100
+++ new/ZODB-5.8.1/src/ZODB/scripts/repozo.py   2023-07-18 08:56:15.000000000 
+0200
@@ -74,7 +74,7 @@
         automatically.
 
     -w
-    --with-verification
+    --with-verify
         Verify on the fly the backup files on recovering. This option runs
         the same checks as when repozo is run in -V/--verify mode, and
         allows to verify and recover a backup in one single step. If a sanity
@@ -179,7 +179,7 @@
                                     'kill-old-on-full',
                                     'date=',
                                     'output=',
-                                    'with-verification',
+                                    'with-verify',
                                     ])
     except getopt.error as msg:
         usage(1, msg)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ZODB-5.8.0/src/ZODB/scripts/zodbload.py 
new/ZODB-5.8.1/src/ZODB/scripts/zodbload.py
--- old/ZODB-5.8.0/src/ZODB/scripts/zodbload.py 2022-11-09 09:17:08.000000000 
+0100
+++ new/ZODB-5.8.1/src/ZODB/scripts/zodbload.py 2023-04-14 12:27:25.000000000 
+0200
@@ -227,7 +227,7 @@
     except:  # noqa: E722 do not use bare 'except'
         return 0
     else:
-        l_ = list(filter(lambda l: l[:7] == 'VmSize:', lines))
+        l_ = list(filter(lambda l: l[:7] == 'VmSize:', lines))  # noqa: E741
         if l_:
             l_ = l_[0][7:].strip().split()[0]
             return int(l_)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ZODB-5.8.0/src/ZODB/tests/multidb.txt 
new/ZODB-5.8.1/src/ZODB/tests/multidb.txt
--- old/ZODB-5.8.0/src/ZODB/tests/multidb.txt   2021-11-02 09:52:39.000000000 
+0100
+++ new/ZODB-5.8.1/src/ZODB/tests/multidb.txt   2023-07-18 08:56:15.000000000 
+0200
@@ -86,12 +86,12 @@
     >>> tm = transaction.TransactionManager()
     >>> cn = db.open(transaction_manager=tm)
     >>> cn                  # doctest: +ELLIPSIS
-    <Connection at ...>
+    <ZODB.Connection.Connection object at ...>
 
 This is the only connection in this collection right now:
 
     >>> cn.connections      # doctest: +ELLIPSIS
-    {'root': <Connection at ...>}
+    {'root': <ZODB.Connection.Connection object at ...>}
 
 Getting a connection to a different database from an existing connection in the
 same database collection (this enables 'connection binding' within a given
@@ -99,7 +99,7 @@
 
     >>> cn2 = cn.get_connection('notroot')
     >>> cn2                  # doctest: +ELLIPSIS
-    <Connection at ...>
+    <ZODB.Connection.Connection object at ...>
 
 The second connection gets the same transaction manager as the first:
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ZODB-5.8.0/src/ZODB/tests/racetest.py 
new/ZODB-5.8.1/src/ZODB/tests/racetest.py
--- old/ZODB-5.8.0/src/ZODB/tests/racetest.py   2022-11-09 09:17:08.000000000 
+0100
+++ new/ZODB-5.8.1/src/ZODB/tests/racetest.py   2023-04-20 10:25:48.000000000 
+0200
@@ -40,6 +40,7 @@
 https://github.com/zopefoundation/ZODB/issues/290 and
 https://github.com/zopefoundation/ZEO/issues/166.
 """
+from __future__ import print_function
 
 import threading
 from random import randint
@@ -80,7 +81,7 @@
     """T2ObjectsInc is specification with behaviour where two objects obj1
     and obj2 are incremented synchronously.
 
-    It is used in tests where bugs can be immedeately observed after the race.
+    It is used in tests where bugs can be immediately observed after the race.
 
     invariant:  obj1 == obj2
     """
@@ -159,10 +160,7 @@
         # Access to half of the objects is organized to always trigger loading
         # from zstor. Access to the other half goes through zconn cache and so
         # verifies whether the cache is not stale.
-        failed = threading.Event()
-        failure = [None]
-
-        def verify():
+        def verify(tg):
             transaction.begin()
             zconn = db.open()
             root = zconn.root()
@@ -176,8 +174,7 @@
             except AssertionError as e:
                 msg = "verify: %s\n" % e
                 msg += _state_details(root)
-                failure[0] = msg
-                failed.set()
+                tg.fail(msg)
 
             # we did not changed anything; also fails with commit:
             transaction.abort()
@@ -186,7 +183,7 @@
         # `modify` changes objects in the database by executing "next" step.
         #
         # Spec invariant should be preserved.
-        def modify():
+        def modify(tg):
             transaction.begin()
             zconn = db.open()
 
@@ -199,32 +196,21 @@
 
         # `xrun` runs f in a loop until either N iterations, or until failed is
         # set.
-        def xrun(f, N):
-            try:
-                for i in range(N):
-                    # print('%s.%d' % (f.__name__, i))
-                    f()
-                    if failed.is_set():
-                        break
-            except:  # noqa: E722 do not use bare 'except'
-                failed.set()
-                raise
+        def xrun(tg, tx, f, N):
+            for i in range(N):
+                # print('%s.%d' % (f.__name__, i))
+                f(tg)
+                if tg.failed():
+                    break
 
         # loop verify and modify concurrently.
         init()
 
         N = 500
-        tverify = threading.Thread(
-            name='Tverify', target=xrun, args=(verify, N))
-        tmodify = threading.Thread(
-            name='Tmodify', target=xrun, args=(modify, N))
-        tverify.start()
-        tmodify.start()
-        tverify.join(60)
-        tmodify.join(60)
-
-        if failed.is_set():
-            self.fail(failure[0])
+        tg = TestWorkGroup(self)
+        tg.go(xrun, verify, N, name='Tverify')
+        tg.go(xrun, modify, N, name='Tmodify')
+        tg.wait(120)
 
     # client-server storages like ZEO, NEO and RelStorage allow several storage
     # clients to be connected to single storage server.
@@ -285,10 +271,7 @@
         #
         # Once in a while T tries to modify the database executing spec "next"
         # as test source of changes for other workers.
-        failed = threading.Event()
-        failure = [None] * nwork  # [tx] is failure from T(tx)
-
-        def T(tx, N):
+        def T(tg, tx, N):
             db = self.dbopen()
 
             def t_():
@@ -305,8 +288,7 @@
                 except AssertionError as e:
                     msg = "T%s: %s\n" % (tx, e)
                     msg += _state_details(root)
-                    failure[tx] = msg
-                    failed.set()
+                    tg.fail(msg)
 
                 # change objects once in a while
                 if randint(0, 4) == 0:
@@ -326,11 +308,8 @@
                 for i in range(N):
                     # print('T%s.%d' % (tx, i))
                     t_()
-                    if failed.is_set():
+                    if tg.failed():
                         break
-            except:  # noqa: E722 do not use bare 'except'
-                failed.set()
-                raise
             finally:
                 db.close()
 
@@ -338,24 +317,17 @@
         init()
 
         N = 100
-        tg = []
-        for x in range(nwork):
-            t = threading.Thread(name='T%d' % x, target=T, args=(x, N))
-            t.start()
-            tg.append(t)
-
-        for t in tg:
-            t.join(60)
-
-        if failed.is_set():
-            self.fail('\n\n'.join([_ for _ in failure if _]))
+        tg = TestWorkGroup(self)
+        for _ in range(nwork):
+            tg.go(T, N)
+        tg.wait(120)
 
     # verify storage for race in between client disconnect and external
     # invalidations. https://github.com/zopefoundation/ZEO/issues/209
     #
-    # This test is simlar to check_race_load_vs_external_invalidate, but
+    # This test is similar to check_race_load_vs_external_invalidate, but
     # increases the number of workers and also makes every worker to repeatedly
-    # reconnect to the storage, so that the probability of disconection is
+    # reconnect to the storage, so that the probability of disconnection is
     # high. It also uses T2ObjectsInc2Phase instead of T2ObjectsInc because if
     # an invalidation is skipped due to the disconnect/invalidation race,
     # T2ObjectsInc won't catch the bug as both objects will be either in old
@@ -381,10 +353,7 @@
 
         # `T` is similar to the T from _check_race_load_vs_external_invalidate
         # but reconnects to the database often.
-        failed = threading.Event()
-        failure = [None] * nwork  # [tx] is failure from T(tx)
-
-        def T(tx, N):
+        def T(tg, tx, N):
             def t_():
                 def work1(db):
                     transaction.begin()
@@ -400,8 +369,7 @@
                     except AssertionError as e:
                         msg = "T%s: %s\n" % (tx, e)
                         msg += _state_details(root)
-                        failure[tx] = msg
-                        failed.set()
+                        tg.fail(msg)
 
                         zconn.close()
                         transaction.abort()
@@ -424,37 +392,26 @@
                 db = self.dbopen()
                 try:
                     for i in range(4):
-                        if failed.is_set():
+                        if tg.failed():
                             break
                         work1(db)
                 finally:
                     db.close()
 
-            try:
-                for i in range(N):
-                    # print('T%s.%d' % (tx, i))
-                    if failed.is_set():
-                        break
-                    t_()
-            except:  # noqa: E722 do not use bare 'except'
-                failed.set()
-                raise
+            for i in range(N):
+                # print('T%s.%d' % (tx, i))
+                if tg.failed():
+                    break
+                t_()
 
         # run the workers concurrently.
         init()
 
         N = 100 // (2*4)  # N reduced to save time
-        tg = []
-        for x in range(nwork):
-            t = threading.Thread(name='T%d' % x, target=T, args=(x, N))
-            t.start()
-            tg.append(t)
-
-        for t in tg:
-            t.join(60)
-
-        if failed.is_set():
-            self.fail('\n\n'.join([_ for _ in failure if _]))
+        tg = TestWorkGroup(self)
+        for _ in range(nwork):
+            tg.go(T, N)
+        tg.wait(120)
 
 
 # `_state_init` initializes the database according to the spec.
@@ -468,7 +425,7 @@
     zconn.close()
 
 
-# `_state_invalidate_half1` invalidatates first 50% of database objects, so
+# `_state_invalidate_half1` invalidates first 50% of database objects, so
 # that the next time they are accessed, they are reloaded from the storage.
 def _state_invalidate_half1(root):
     keys = list(sorted(root.keys()))
@@ -524,3 +481,158 @@
             txt += load(k)
 
     return txt
+
+
+class TestWorkGroup(object):
+    """TestWorkGroup represents group of threads that run together to verify
+       something.
+
+       - .go() adds test thread to the group.
+       - .wait() waits for all spawned threads to finish and reports all
+         collected failures to containing testcase.
+       - a test should indicate failure by call to .fail(), it
+         can check for a failure with .failed()
+    """
+
+    def __init__(self, testcase):
+        self.testcase = testcase
+        self.failed_event = threading.Event()
+        self.fail_mu = threading.Lock()
+        self.failv = []           # failures registered by .fail
+        self.threadv = []         # spawned threads
+        self.waitg = WaitGroup()  # to wait for spawned threads
+
+    def fail(self, msg):
+        """fail adds failure to test result."""
+        with self.fail_mu:
+            self.failv.append(msg)
+        self.failed_event.set()
+
+    def failed(self):
+        """did the test already fail."""
+        return self.failed_event.is_set()
+
+    def go(self, f, *argv, **kw):
+        """go spawns f(self, #thread, *argv, **kw) in new test thread."""
+        self.waitg.add(1)
+        tx = len(self.threadv)
+        tname = kw.pop('name', 'T%d' % tx)
+        t = Daemon(name=tname, target=self._run, args=(f, tx, argv, kw))
+        self.threadv.append(t)
+        t.start()
+
+    def _run(self, f, tx, argv, kw):
+        tname = self.threadv[tx].name
+        try:
+            f(self, tx, *argv, **kw)
+        except Exception as e:
+            self.fail("Unhandled exception %r in thread %s"
+                      % (e, tname))
+            raise
+        finally:
+            self.waitg.done()
+
+    def wait(self, timeout):
+        """wait waits for all test threads to complete and reports all
+           collected failures to containing testcase."""
+        if not self.waitg.wait(timeout):
+            self.fail("test did not finish within %s seconds" % timeout)
+
+        failed_to_finish = []
+        for t in self.threadv:
+            try:
+                t.join(1)
+            except AssertionError:
+                self.failed_event.set()
+                failed_to_finish.append(t.name)
+        if failed_to_finish:
+            self.fail("threads did not finish: %s" % failed_to_finish)
+        del self.threadv  # avoid cyclic garbage
+
+        if self.failed():
+            self.testcase.fail('\n\n'.join(self.failv))
+
+
+class Daemon(threading.Thread):
+    """auxiliary class to create daemon threads and fail if not stopped.
+
+    In addition, the class ensures that reports for uncaught exceptions
+    are output holding a lock. This prevents that concurrent reports
+    get intermixed and facilitates the exception analysis.
+    """
+    def __init__(self, **kw):
+        super(Daemon, self).__init__(**kw)
+        self.daemon = True
+        if hasattr(self, "_invoke_excepthook"):
+            # Python 3.8+
+            ori_invoke_excepthook = self._invoke_excepthook
+
+            def invoke_excepthook(*args, **kw):
+                with exc_lock:
+                    return ori_invoke_excepthook(*args, **kw)
+
+            self._invoke_excepthook = invoke_excepthook
+        else:
+            # old Python
+            ori_run = self.run
+
+            def run():
+                from threading import _format_exc
+                from threading import _sys
+                try:
+                    ori_run()
+                except SystemExit:
+                    pass
+                except BaseException:
+                    if _sys and _sys.stderr is not None:
+                        with exc_lock:
+                            print("Exception in thread %s:\n%s" %
+                                  (self.name, _format_exc()),
+                                  file=_sys.stderr)
+                    else:
+                        raise
+                finally:
+                    del self.run
+
+            self.run = run
+
+    def join(self, *args, **kw):
+        super(Daemon, self).join(*args, **kw)
+        if self.is_alive():
+            raise AssertionError("Thread %s did not stop" % self.name)
+
+
+# lock to ensure that Daemon exception reports are output atomically
+exc_lock = threading.Lock()
+
+
+class WaitGroup(object):
+    """WaitGroup provides service to wait for spawned workers to be done.
+
+       - .add() adds workers
+       - .done() indicates that one worker is done
+       - .wait() waits until all workers are done
+    """
+    def __init__(self):
+        self.n = 0
+        self.condition = threading.Condition()
+
+    def add(self, delta):
+        with self.condition:
+            self.n += delta
+            if self.n < 0:
+                raise AssertionError("#workers is negative")
+            if self.n == 0:
+                self.condition.notify_all()
+
+    def done(self):
+        self.add(-1)
+
+    def wait(self, timeout):  # -> ok
+        with self.condition:
+            if self.n == 0:
+                return True
+            ok = self.condition.wait(timeout)
+            if ok is None:  # py2
+                ok = (self.n == 0)
+            return ok
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ZODB-5.8.0/src/ZODB/tests/test_racetest.py 
new/ZODB-5.8.1/src/ZODB/tests/test_racetest.py
--- old/ZODB-5.8.0/src/ZODB/tests/test_racetest.py      1970-01-01 
01:00:00.000000000 +0100
+++ new/ZODB-5.8.1/src/ZODB/tests/test_racetest.py      2023-04-20 
10:25:48.000000000 +0200
@@ -0,0 +1,118 @@
+##############################################################################
+#
+# Copyright (c) 2019 - 2023 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+from time import sleep
+from unittest import TestCase
+
+from .racetest import TestWorkGroup
+
+
+class TestWorkGroupTests(TestCase):
+    def setUp(self):
+        self._failed = failed = []
+        case_mockup = SimpleNamespace(fail=failed.append)
+        self.tg = TestWorkGroup(case_mockup)
+
+    @property
+    def failed(self):
+        return "\n\n".join(self._failed)
+
+    def test_success(self):
+        tg = self.tg
+        tg.go(tg_test_function)
+        tg.wait(10)
+        self.assertEqual(self.failed, "")
+
+    def test_failure1(self):
+        tg = self.tg
+        tg.go(tg_test_function, T_FAIL)
+        tg.wait(10)
+        self.assertEqual(self.failed, "T0 failed")
+
+    def test_failure1_okmany(self):
+        tg = self.tg
+        tg.go(tg_test_function, T_SUCCESS)
+        tg.go(tg_test_function, T_SUCCESS)
+        tg.go(tg_test_function, T_SUCCESS)
+        tg.go(tg_test_function, T_FAIL)
+        tg.wait(10)
+        self.assertEqual(self.failed, "T3 failed")
+
+    def test_failure_many(self):
+        tg = self.tg
+        tg.go(tg_test_function, T_FAIL)
+        tg.go(tg_test_function, T_SUCCESS)
+        tg.go(tg_test_function, T_FAIL)
+        tg.go(tg_test_function, T_SUCCESS)
+        tg.go(tg_test_function, T_FAIL)
+        tg.wait(10)
+        self.assertIn("T0 failed", self.failed)
+        self.assertIn("T2 failed", self.failed)
+        self.assertIn("T4 failed", self.failed)
+        self.assertNotIn("T1 failed", self.failed)
+        self.assertNotIn("T3 failed", self.failed)
+
+    def test_exception(self):
+        tg = self.tg
+        tg.go(tg_test_function, T_EXC)
+        tg.wait(10)
+        self.assertIn("Unhandled exception", self.failed)
+        self.assertIn("in thread T0", self.failed)
+
+    def test_timeout(self):
+        tg = self.tg
+        tg.go(tg_test_function, T_SLOW)
+        tg.wait(0.1)
+        self.assertEqual(self.failed,
+                         "test did not finish within 0.1 seconds")
+
+    def test_thread_unfinished(self):
+        tg = self.tg
+        tg.go(tg_test_function, T_SLOW)
+        tg.go(tg_test_function, T_SLOW, 2)
+        tg.go(tg_test_function, T_SLOW, wait_time=2)
+        tg.wait(0.1)
+        self.assertEqual(self.failed,
+                         "test did not finish within 0.1 seconds\n\n"
+                         "threads did not finish: ['T2']")
+
+
+T_SUCCESS = 0
+T_SLOW = 1
+T_EXC = 3
+T_FAIL = 4
+
+
+def tg_test_function(tg, tx, mode=T_SUCCESS, waits=1, wait_time=0.2):
+    if mode == T_SUCCESS:
+        return
+    if mode == T_FAIL:
+        tg.fail("T%d failed" % tx)
+        return
+    if mode == T_EXC:
+        raise ValueError(str(tx))
+    assert mode == T_SLOW
+    while waits:
+        waits -= 1
+        if tg.failed():
+            return
+        sleep(wait_time)
+
+
+try:
+    from types import SimpleNamespace
+except ImportError:
+    # PY2
+    class SimpleNamespace(object):
+        def __init__(self, **kw):
+            self.__dict__.update(kw)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/ZODB-5.8.0/src/ZODB/tests/testcrossdatabasereferences.py 
new/ZODB-5.8.1/src/ZODB/tests/testcrossdatabasereferences.py
--- old/ZODB-5.8.0/src/ZODB/tests/testcrossdatabasereferences.py        
2022-11-09 09:17:08.000000000 +0100
+++ new/ZODB-5.8.1/src/ZODB/tests/testcrossdatabasereferences.py        
2023-07-18 08:56:15.000000000 +0200
@@ -61,7 +61,7 @@
     InvalidObjectReference:
     ('Attempt to store a reference to an object from a separate connection to
     the same database or multidatabase',
-    <Connection at ...>,
+    <ZODB.Connection.Connection object at ...>,
     <ZODB.tests.testcrossdatabasereferences.MyClass object at ...>)
 
     >>> tm.abort()
@@ -80,7 +80,7 @@
     InvalidObjectReference:
     ('Attempt to store a reference to an object from a separate connection
     to the same database or multidatabase',
-    <Connection at ...>,
+    <ZODB.Connection.Connection object at ...>,
     <ZODB.tests.testcrossdatabasereferences.MyClass object at ...>)
 
     >>> tm.abort()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ZODB-5.8.0/src/ZODB/tests/warnhook.py 
new/ZODB-5.8.1/src/ZODB/tests/warnhook.py
--- old/ZODB-5.8.0/src/ZODB/tests/warnhook.py   2022-03-16 14:46:04.000000000 
+0100
+++ new/ZODB-5.8.1/src/ZODB/tests/warnhook.py   1970-01-01 01:00:00.000000000 
+0100
@@ -1,58 +0,0 @@
-##############################################################################
-#
-# Copyright (c) 2004 Zope Foundation and Contributors.
-# All Rights Reserved.
-#
-# This software is subject to the provisions of the Zope Public License,
-# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
-# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
-# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
-# FOR A PARTICULAR PURPOSE.
-#
-##############################################################################
-import warnings
-
-
-class WarningsHook(object):
-    """Hook to capture warnings generated by Python.
-
-    The function warnings.showwarning() is designed to be hooked by
-    application code, allowing the application to customize the way it
-    handles warnings.
-
-    This hook captures the unformatted warning information and stores
-    it in a list.  A test can inspect this list after the test is over.
-
-    Issues:
-
-    The warnings module has lots of delicate internal state.  If
-    a warning has been reported once, it won't be reported again.  It
-    may be necessary to extend this class with a mechanism for
-    modifying the internal state so that we can be guaranteed a
-    warning will be reported.
-
-    If Python is run with a warnings filter, e.g. python -Werror,
-    then a test that is trying to inspect a particular warning will
-    fail.  Perhaps this class can be extended to install more-specific
-    filters the test to work anyway.
-    """
-
-    def __init__(self):
-        self.original = None
-        self.warnings = []
-
-    def install(self):
-        self.original = warnings.showwarning
-        warnings.showwarning = self.showwarning
-
-    def uninstall(self):
-        assert self.original is not None
-        warnings.showwarning = self.original
-        self.original = None
-
-    def showwarning(self, message, category, filename, lineno):
-        self.warnings.append((str(message), category, filename, lineno))
-
-    def clear(self):
-        self.warnings = []
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ZODB-5.8.0/src/ZODB/utils.py 
new/ZODB-5.8.1/src/ZODB/utils.py
--- old/ZODB-5.8.0/src/ZODB/utils.py    2022-11-09 09:17:08.000000000 +0100
+++ new/ZODB-5.8.1/src/ZODB/utils.py    2023-07-18 08:56:15.000000000 +0200
@@ -41,7 +41,6 @@
            'oid_repr',
            'serial_repr',
            'tid_repr',
-           'positive_id',
            'readable_tid_repr',
            'get_pickle_metadata',
            'locked',
@@ -184,28 +183,6 @@
         result = "%s %s" % (result, TimeStamp(tid))
     return result
 
-# Addresses can "look negative" on some boxes, some of the time.  If you
-# feed a "negative address" to an %x format, Python 2.3 displays it as
-# unsigned, but produces a FutureWarning, because Python 2.4 will display
-# it as signed.  So when you want to prodce an address, use positive_id() to
-# obtain it.
-# _ADDRESS_MASK is 2**(number_of_bits_in_a_native_pointer).  Adding this to
-# a negative address gives a positive int with the same hex representation as
-# the significant bits in the original.
-
-
-_ADDRESS_MASK = 256 ** struct.calcsize('P')
-
-
-def positive_id(obj):
-    """Return id(obj) as a non-negative integer."""
-
-    result = id(obj)
-    if result < 0:
-        result += _ADDRESS_MASK
-        assert result > 0
-    return result
-
 # Given a ZODB pickle, return pair of strings (module_name, class_name).
 # Do this without importing the module or class object.
 # See ZODB/serialize.py's module docstring for the only docs that exist about
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ZODB-5.8.0/src/ZODB/utils.rst 
new/ZODB-5.8.1/src/ZODB/utils.rst
--- old/ZODB-5.8.0/src/ZODB/utils.rst   2021-11-02 09:52:39.000000000 +0100
+++ new/ZODB-5.8.1/src/ZODB/utils.rst   2023-04-14 12:27:25.000000000 +0200
@@ -15,7 +15,7 @@
 
 ZODB uses 64-bit transaction ids that are typically represented as
 strings, but are sometimes manipulated as integers.  Object ids are
-strings too and it is common to ise 64-bit strings that are just
+strings too and it is common to use 64-bit strings that are just
 packed integers.
 
 Functions p64 and u64 pack and unpack integers as strings:
@@ -26,7 +26,7 @@
     >>> print(ZODB.utils.u64(b'\x03yi\xf7"\xa8\xfb '))
     250347764455111456
 
-The contant z64 has zero packed as a 64-bit string:
+The constant z64 has zero packed as a 64-bit string:
 
     >>> ZODB.utils.z64
     '\x00\x00\x00\x00\x00\x00\x00\x00'
@@ -36,7 +36,7 @@
 
 Storages assign transaction ids as transactions are committed.  These
 are based on UTC time, but must be strictly increasing.  The
-newTid function akes this pretty easy.
+newTid function makes this pretty easy.
 
 To see this work (in a predictable way), we'll first hack time.time:
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ZODB-5.8.0/src/ZODB.egg-info/PKG-INFO 
new/ZODB-5.8.1/src/ZODB.egg-info/PKG-INFO
--- old/ZODB-5.8.0/src/ZODB.egg-info/PKG-INFO   2022-11-09 12:37:51.000000000 
+0100
+++ new/ZODB-5.8.1/src/ZODB.egg-info/PKG-INFO   2023-07-18 09:29:35.000000000 
+0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: ZODB
-Version: 5.8.0
+Version: 5.8.1
 Summary: ZODB, a Python object-oriented database
 Home-page: http://zodb-docs.readthedocs.io
 Author: Jim Fulton
@@ -48,8 +48,8 @@
    :target: https://pypi.org/project/ZODB/
    :alt: Supported Python versions
 
-.. image:: https://travis-ci.com/zopefoundation/ZODB.svg?branch=master
-   :target: https://travis-ci.com/zopefoundation/ZODB
+.. image:: 
https://github.com/zopefoundation/ZODB/actions/workflows/tests.yml/badge.svg
+   :target: https://github.com/zopefoundation/ZODB/actions/workflows/tests.yml
    :alt: Build status
 
 .. image:: https://coveralls.io/repos/github/zopefoundation/ZODB/badge.svg
@@ -90,6 +90,16 @@
  Change History
 ================
 
+5.8.1 (2023-07-18)
+==================
+
+- Fix ``racetest`` problems.
+  For details see `#376 <https://github.com/zopefoundation/ZODB/pull/376>`_.
+
+- Fix ``--with-verify`` argument in script repozo ``--recover``.
+  For details see `#381 <https://github.com/zopefoundation/ZODB/pull/381>`_.
+
+
 5.8.0 (2022-11-09)
 ==================
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ZODB-5.8.0/src/ZODB.egg-info/SOURCES.txt 
new/ZODB-5.8.1/src/ZODB.egg-info/SOURCES.txt
--- old/ZODB-5.8.0/src/ZODB.egg-info/SOURCES.txt        2022-11-09 
12:37:51.000000000 +0100
+++ new/ZODB-5.8.1/src/ZODB.egg-info/SOURCES.txt        2023-07-18 
09:29:35.000000000 +0200
@@ -241,6 +241,7 @@
 src/ZODB/tests/test_fsdump.py
 src/ZODB/tests/test_mvccadapter.py
 src/ZODB/tests/test_prefetch.py
+src/ZODB/tests/test_racetest.py
 src/ZODB/tests/test_storage.py
 src/ZODB/tests/testblob.py
 src/ZODB/tests/testconflictresolution.py
@@ -251,5 +252,4 @@
 src/ZODB/tests/testhistoricalconnections.py
 src/ZODB/tests/testmvcc.py
 src/ZODB/tests/testpersistentclass.py
-src/ZODB/tests/util.py
-src/ZODB/tests/warnhook.py
\ No newline at end of file
+src/ZODB/tests/util.py
\ No newline at end of file

Reply via email to