]> arthur.barton.de Git - bup.git/commitdiff
Fix all __del__ complaints; fail tests on AssertionErrors
authorRob Browning <rlb@defaultvalue.org>
Sun, 16 Jan 2022 19:53:09 +0000 (13:53 -0600)
committerRob Browning <rlb@defaultvalue.org>
Sun, 16 Jan 2022 19:53:09 +0000 (13:53 -0600)
lib/bup/client.py
lib/bup/cmd/on.py
lib/bup/cmd/server.py
lib/bup/compat.py
lib/bup/git.py
lib/bup/helpers.py
lib/bup/io.py
test/ext/conftest.py

index 7c284c4af4aa613db536cf5db637382988e1db02..8af357f53071ad5562ac778612bbe32ce66316c5 100644 (file)
@@ -72,74 +72,95 @@ class Client:
         self.closed = False
         self._busy = self.conn = None
         self.sock = self.p = self.pout = self.pin = None
-        is_reverse = environ.get(b'BUP_SERVER_REVERSE')
-        if is_reverse:
-            assert(not remote)
-            remote = b'%s:' % is_reverse
-        (self.protocol, self.host, self.port, self.dir) = parse_remote(remote)
-        # The b'None' here matches python2's behavior of b'%s' % None == 'None',
-        # python3 will (as of version 3.7.5) do the same for str ('%s' % None),
-        # but crashes instead when doing b'%s' % None.
-        cachehost = b'None' if self.host is None else self.host
-        cachedir = b'None' if self.dir is None else self.dir
-        self.cachedir = git.repo(b'index-cache/%s'
-                                 % re.sub(br'[^@\w]',
-                                          b'_',
-                                          b'%s:%s' % (cachehost, cachedir)))
-        if is_reverse:
-            self.pout = os.fdopen(3, 'rb')
-            self.pin = os.fdopen(4, 'wb')
-            self.conn = Conn(self.pout, self.pin)
-        else:
-            if self.protocol in (b'ssh', b'file'):
-                try:
-                    # FIXME: ssh and file shouldn't use the same module
-                    self.p = ssh.connect(self.host, self.port, b'server')
-                    self.pout = self.p.stdout
-                    self.pin = self.p.stdin
-                    self.conn = Conn(self.pout, self.pin)
-                except OSError as e:
-                    reraise(ClientError('connect: %s' % e))
-            elif self.protocol == b'bup':
-                self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-                self.sock.connect((self.host,
-                                   1982 if self.port is None else int(self.port)))
-                self.sockw = self.sock.makefile('wb')
-                self.conn = DemuxConn(self.sock.fileno(), self.sockw)
-        self._available_commands = self._get_available_commands()
-        self._require_command(b'init-dir')
-        self._require_command(b'set-dir')
-        if self.dir:
-            self.dir = re.sub(br'[\r\n]', ' ', self.dir)
-            if create:
-                self.conn.write(b'init-dir %s\n' % self.dir)
+        try:
+            is_reverse = environ.get(b'BUP_SERVER_REVERSE')
+            if is_reverse:
+                assert(not remote)
+                remote = b'%s:' % is_reverse
+            (self.protocol, self.host, self.port, self.dir) = parse_remote(remote)
+            # The b'None' here matches python2's behavior of b'%s' % None == 'None',
+            # python3 will (as of version 3.7.5) do the same for str ('%s' % None),
+            # but crashes instead when doing b'%s' % None.
+            cachehost = b'None' if self.host is None else self.host
+            cachedir = b'None' if self.dir is None else self.dir
+            self.cachedir = git.repo(b'index-cache/%s'
+                                     % re.sub(br'[^@\w]',
+                                              b'_',
+                                              b'%s:%s' % (cachehost, cachedir)))
+            if is_reverse:
+                self.pout = os.fdopen(3, 'rb')
+                self.pin = os.fdopen(4, 'wb')
+                self.conn = Conn(self.pout, self.pin)
             else:
-                self.conn.write(b'set-dir %s\n' % self.dir)
-            self.check_ok()
-        self.sync_indexes()
+                if self.protocol in (b'ssh', b'file'):
+                    try:
+                        # FIXME: ssh and file shouldn't use the same module
+                        self.p = ssh.connect(self.host, self.port, b'server')
+                        self.pout = self.p.stdout
+                        self.pin = self.p.stdin
+                        self.conn = Conn(self.pout, self.pin)
+                    except OSError as e:
+                        reraise(ClientError('connect: %s' % e))
+                elif self.protocol == b'bup':
+                    self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+                    self.sock.connect((self.host,
+                                       1982 if self.port is None else int(self.port)))
+                    self.sockw = self.sock.makefile('wb')
+                    self.conn = DemuxConn(self.sock.fileno(), self.sockw)
+            self._available_commands = self._get_available_commands()
+            self._require_command(b'init-dir')
+            self._require_command(b'set-dir')
+            if self.dir:
+                self.dir = re.sub(br'[\r\n]', ' ', self.dir)
+                if create:
+                    self.conn.write(b'init-dir %s\n' % self.dir)
+                else:
+                    self.conn.write(b'set-dir %s\n' % self.dir)
+                self.check_ok()
+            self.sync_indexes()
+        except BaseException as ex:
+            with pending_raise(ex):
+                self.close()
 
     def close(self):
+        if self.closed:
+            return
         self.closed = True
-        if self.conn and not self._busy:
-            self.conn.write(b'quit\n')
-        if self.pin:
-            self.pin.close()
-        if self.sock and self.sockw:
-            self.sockw.close()
-            self.sock.shutdown(socket.SHUT_WR)
-        if self.conn:
-            self.conn.close()
-        if self.pout:
-            self.pout.close()
-        if self.sock:
-            self.sock.close()
-        if self.p:
-            self.p.wait()
-            rv = self.p.wait()
-            if rv:
-                raise ClientError('server tunnel returned exit code %d' % rv)
-        self.conn = None
-        self.sock = self.p = self.pin = self.pout = None
+        try:
+            if self.conn and not self._busy:
+                self.conn.write(b'quit\n')
+        finally:
+            try:
+                if self.pin:
+                    self.pin.close()
+            finally:
+                try:
+                    self.pin = None
+                    if self.sock and self.sockw:
+                        self.sockw.close()
+                        self.sock.shutdown(socket.SHUT_WR)
+                finally:
+                    try:
+                        if self.conn:
+                            self.conn.close()
+                    finally:
+                        try:
+                            self.conn = None
+                            if self.pout:
+                                self.pout.close()
+                        finally:
+                            try:
+                                self.pout = None
+                                if self.sock:
+                                    self.sock.close()
+                            finally:
+                                self.sock = None
+                                if self.p:
+                                    self.p.wait()
+                                    rv = self.p.wait()
+                                    if rv:
+                                        raise ClientError('server tunnel returned exit code %d' % rv)
+                                self.p = None
 
     def __del__(self):
         assert self.closed
@@ -537,6 +558,7 @@ class PackWriter_Remote(git.PackWriter):
         self.remote_closed = True
         id = self._end()
         self.file = None
+        super(PackWriter_Remote, self).close()
         return id
 
     def __del__(self):
index bb5e3f3117a41eded776545426b66672bc63dd42..983283cebf45730406cb23034c0cb05a660bea26 100755 (executable)
@@ -56,9 +56,9 @@ def main(argv):
             p.stdin.close()
             p.stdout.close()
             # Demultiplex remote client's stderr (back to stdout/stderr).
-            dmc = DemuxConn(p.stderr.fileno(), open(os.devnull, "wb"))
-            for line in iter(dmc.readline, b''):
-                out.write(line)
+            with DemuxConn(p.stderr.fileno(), open(os.devnull, "wb")) as dmc:
+                for line in iter(dmc.readline, b''):
+                    out.write(line)
         finally:
             while 1:
                 # if we get a signal while waiting, we have to keep waiting, just
index 3d077dd65d90a7e0253f1c10fa2352883a4a670c..b4f047271858b054d936e2174b00446dec7fe3e5 100755 (executable)
@@ -301,10 +301,10 @@ def main(argv):
     # FIXME: this protocol is totally lame and not at all future-proof.
     # (Especially since we abort completely as soon as *anything* bad happens)
     sys.stdout.flush()
-    conn = Conn(byte_stream(sys.stdin), byte_stream(sys.stdout))
-    lr = linereader(conn)
-    with finalized(None, lambda _: repo and repo.close()), \
+    with Conn(byte_stream(sys.stdin), byte_stream(sys.stdout)) as conn, \
+         finalized(None, lambda _: repo and repo.close()), \
          finalized(None, lambda _: suspended_w and suspended_w.close()):
+        lr = linereader(conn)
         for _line in lr:
             line = _line.strip()
             if not line:
index dfeb51aeacc50b62c495b3c3327dd82681e4f6d2..00575036e27cc46f7fb6b3408e273831526b9daa 100644 (file)
@@ -14,7 +14,6 @@ if py3:
 
     # pylint: disable=unused-import
     from contextlib import ExitStack, nullcontext
-    from mmap import mmap
     from os import environb as environ
     from os import fsdecode, fsencode
     from shlex import quote
@@ -89,7 +88,6 @@ if py3:
 else:  # Python 2
 
     from contextlib import contextmanager
-    import mmap as py_mmap
 
     ModuleNotFoundError = ImportError
 
@@ -225,27 +223,6 @@ else:  # Python 2
 
     buffer = buffer
 
-    assert not hasattr(py_mmap.mmap, '__del__')
-    assert not hasattr(py_mmap.mmap, '__enter__')
-    assert not hasattr(py_mmap.mmap, '__exit__')
-
-    class mmap(py_mmap.mmap):
-        def __init__(self, *args, **kwargs):
-            self._bup_closed = False
-            # Silence deprecation warnings.  mmap's current parent is
-            # object, which accepts no params and as of at least 2.7
-            # warns about them.
-            if py_mmap.mmap.__init__ is not object.__init__:
-                super(mmap, self).__init__(self, *args, **kwargs)
-        def __enter__(self):
-            return self
-        def __exit__(self, type, value, traceback):
-            self._bup_closed = True
-            with pending_raise(value, rethrow=False):
-                self.close()
-        def __del__(self):
-            assert self._bup_closed
-
 try:
     import bup_main
 except ModuleNotFoundError:
index 73a50b88773f6585c01e264775cfe9a91c0bd120..270f1eb41e5fbf01e4efdc0d7d47f296f420f4be 100644 (file)
@@ -772,7 +772,7 @@ def _make_objcache():
 # bup-gc assumes that it can disable all PackWriter activities
 # (bloom/midx/cache) via the constructor and close() arguments.
 
-class PackWriter:
+class PackWriter(object):
     """Writes Git objects inside a pack file."""
     def __init__(self, objcache_maker=_make_objcache, compression_level=1,
                  run_midx=True, on_pack_finish=None,
@@ -918,16 +918,15 @@ class PackWriter:
 
     def _end(self, run_midx=True, abort=False):
         # Ignores run_midx during abort
-        if not self.file:
-            return None
+        self.parentfd, pfd, = None, self.parentfd
         self.file, f = None, self.file
         self.idx, idx = None, self.idx
-        self.parentfd, pfd, = None, self.parentfd
-
         try:
             with nullcontext_if_not(self.objcache), \
                  finalized(pfd, lambda x: x is not None and os.close(x)), \
-                 f:
+                 nullcontext_if_not(f):
+                if not f:
+                    return None
 
                 if abort:
                     os.unlink(self.filename + b'.pack')
index fdc683bd7c2ca739699e760a0b1eda5c3a8f156d..965b6d072228ff9c6fdaa752cd280c4bf881ec2b 100644 (file)
@@ -12,6 +12,7 @@ import hashlib, heapq, math, operator, time, tempfile
 
 from bup import _helpers
 from bup import compat
+from bup import io
 from bup.compat import argv_bytes, byte_int, nullcontext, pending_raise
 from bup.io import byte_stream, path_msg
 # This function should really be in helpers, not in bup.options.  But we
@@ -459,7 +460,13 @@ class BaseConn:
 
     def close(self):
         self._base_closed = True
-        while self._read(65536): pass
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, exc_type, exc_value, tb):
+        with pending_raise(exc_value, rethrow=False):
+            self.close()
 
     def __del__(self):
         assert self._base_closed
@@ -764,7 +771,7 @@ def _mmap_do(f, sz, flags, prot, close):
         # string has all the same behaviour of a zero-length map, ie. it has
         # no elements :)
         return ''
-    map = compat.mmap(f.fileno(), sz, flags, prot)
+    map = io.mmap(f.fileno(), sz, flags, prot)
     if close:
         f.close()  # map will persist beyond file close
     return map
@@ -826,18 +833,19 @@ if _mincore:
             pos = _fmincore_chunk_size * ci;
             msize = min(_fmincore_chunk_size, st.st_size - pos)
             try:
-                m = compat.mmap(fd, msize, mmap.MAP_PRIVATE, 0, 0, pos)
+                m = io.mmap(fd, msize, mmap.MAP_PRIVATE, 0, 0, pos)
             except mmap.error as ex:
                 if ex.errno == errno.EINVAL or ex.errno == errno.ENODEV:
                     # Perhaps the file was a pipe, i.e. "... | bup split ..."
                     return None
                 raise ex
-            try:
-                _mincore(m, msize, 0, result, ci * pages_per_chunk)
-            except OSError as ex:
-                if ex.errno == errno.ENOSYS:
-                    return None
-                raise
+            with m:
+                try:
+                    _mincore(m, msize, 0, result, ci * pages_per_chunk)
+                except OSError as ex:
+                    if ex.errno == errno.ENOSYS:
+                        return None
+                    raise
         return result
 
 
index 512d36434ed57e800f3b1f647a8982a909ec6ac2..a384f1007e5d5f0452d646633a658b01b1dad9f0 100644 (file)
@@ -1,7 +1,9 @@
 
 from __future__ import absolute_import, print_function
+import mmap as py_mmap
 
 from bup import compat
+from bup.compat import pending_raise
 
 
 if compat.py_maj > 2:
@@ -20,3 +22,47 @@ else:
         """Return a string representation of a path."""
         # FIXME: configurability (might git-config quotePath be involved?)
         return x
+
+
+assert not hasattr(py_mmap.mmap, '__del__')
+if hasattr(py_mmap.mmap, '__enter__'):
+    assert hasattr(py_mmap.mmap, '__exit__')
+
+class mmap(py_mmap.mmap):
+    '''mmap.mmap wrapper that detects and complains about any instances
+    that aren't explicitly closed.
+
+    '''
+
+    def __init__(self, *args, **kwargs):
+        self._bup_closed = True
+        # Silence deprecation warnings.  mmap's current parent is
+        # object, which accepts no params and as of at least 2.7
+        # warns about them.
+        if py_mmap.mmap.__init__ is not object.__init__:
+            super(mmap, self).__init__(self, *args, **kwargs)
+        self._bup_closed = False
+
+    def close(self):
+        self._bup_closed = True
+        super(mmap, self).close()
+
+    if hasattr(py_mmap.mmap, '__enter__'):
+        def __enter__(self):
+            super(mmap, self).__enter__()
+            return self
+        def __exit__(self, type, value, traceback):
+            # Don't call self.close() when the parent has its own __exit__;
+            # defer to it.
+            self._bup_closed = True
+            result = super(mmap, self).__exit__(type, value, traceback)
+            return result
+    else:
+        def __enter__(self):
+            return self
+        def __exit__(self, type, value, traceback):
+            with pending_raise(value, rethrow=False):
+                self.close()
+
+    def __del__(self):
+        assert self._bup_closed
index 2c8f03e8e9d45dd00cea7ebd26bd6ea2c320b625..ed30973e39f4d8c2e49e5958ca4aa4323bc60fda 100644 (file)
@@ -36,6 +36,8 @@ class BupSubprocTestRunner(pytest.Item):
         failures = [line for line in lines
                     if (line.startswith(b'!')
                         and line.lower().endswith(b' failed'))]
+        if b'AssertionError' in out:
+            raise BupSubprocFailure('AssertionError detected')
         if failures or p.returncode != 0:
             raise BupSubprocFailure('%s failed (exit %d, %d failures)'
                                     % (cmd, p.returncode, len(failures)),