summaryrefslogtreecommitdiff
path: root/subversion/bindings/swig/python/tests/repository.py
diff options
context:
space:
mode:
Diffstat (limited to 'subversion/bindings/swig/python/tests/repository.py')
-rw-r--r--subversion/bindings/swig/python/tests/repository.py149
1 files changed, 143 insertions, 6 deletions
diff --git a/subversion/bindings/swig/python/tests/repository.py b/subversion/bindings/swig/python/tests/repository.py
index abf5d72..bcbe483 100644
--- a/subversion/bindings/swig/python/tests/repository.py
+++ b/subversion/bindings/swig/python/tests/repository.py
@@ -18,11 +18,11 @@
# under the License.
#
#
-import unittest, setup_path, os, sys
+import unittest, setup_path, os, sys, weakref
from sys import version_info # For Python version check
from io import BytesIO
from svn import core, repos, fs, delta
-from svn.core import SubversionException
+from svn.core import SubversionException, Pool
import utils
class ChangeReceiver(delta.Editor):
@@ -40,9 +40,20 @@ class ChangeReceiver(delta.Editor):
return textdelta_handler
class DumpStreamParser(repos.ParseFns3):
- def __init__(self):
+ def __init__(self, stream=None, pool=None):
repos.ParseFns3.__init__(self)
+ self.stream = stream
self.ops = []
+ # for leak checking only. If the parse_fns3 object holds some proxy
+ # object allocated from 'pool' or the 'pool' itself, the 'pool' is not
+ # destroyed until the parse_fns3 object is removed.
+ self.pool = pool
+ def _close_dumpstream(self):
+ if self.stream:
+ self.stream.close()
+ self.stream = None
+ if self.pool:
+ self.pool = None
def magic_header_record(self, version, pool=None):
self.ops.append((b"magic-header", version))
def uuid_record(self, uuid, pool=None):
@@ -74,6 +85,76 @@ class DumpStreamParser(repos.ParseFns3):
self.ops.append((b"set-fulltext", node_baton[0], node_baton[1]))
return None
+class BatonCollector(repos.ChangeCollector):
+ """A ChangeCollector with collecting batons, too"""
+ def __init__(self, fs_ptr, root, pool=None, notify_cb=None):
+ repos.ChangeCollector.__init__(self, fs_ptr, root, pool, notify_cb)
+ self.batons = []
+ self.close_called = False
+ self.abort_called = False
+
+ def open_root(self, base_revision, dir_pool=None):
+ bt = repos.ChangeCollector.open_root(self, base_revision, dir_pool)
+ self.batons.append((b'dir baton', b'', bt, sys.getrefcount(bt)))
+ return bt
+
+ def add_directory(self, path, parent_baton,
+ copyfrom_path, copyfrom_revision, dir_pool=None):
+ bt = repos.ChangeCollector.add_directory(self, path, parent_baton,
+ copyfrom_path,
+ copyfrom_revision,
+ dir_pool)
+ self.batons.append((b'dir baton', path, bt, sys.getrefcount(bt)))
+ return bt
+
+ def open_directory(self, path, parent_baton, base_revision,
+ dir_pool=None):
+ bt = repos.ChangeCollector.open_directory(self, path, parent_baton,
+ base_revision, dir_pool)
+ self.batons.append((b'dir baton', path, bt, sys.getrefcount(bt)))
+ return bt
+
+ def add_file(self, path, parent_baton,
+ copyfrom_path, copyfrom_revision, file_pool=None):
+ bt = repos.ChangeCollector.add_file(self, path, parent_baton,
+ copyfrom_path, copyfrom_revision,
+ file_pool)
+ self.batons.append((b'file baton', path, bt, sys.getrefcount(bt)))
+ return bt
+
+ def open_file(self, path, parent_baton, base_revision, file_pool=None):
+ bt = repos.ChangeCollector.open_file(self, path, parent_baton,
+ base_revision, file_pool)
+ self.batons.append((b'file baton', path, bt, sys.getrefcount(bt)))
+ return bt
+
+ def close_edit(self, pool=None):
+ self.close_called = True
+ return
+
+ def abort_edit(self, pool=None):
+ self.abort_called = True
+ return
+
+class BatonCollectorErrorOnClose(BatonCollector):
+ """Same as BatonCollector, but raises an Exception when close the
+ file/dir specfied by error_path"""
+ def __init__(self, fs_ptr, root, pool=None, notify_cb=None, error_path=b''):
+ BatonCollector.__init__(self, fs_ptr, root, pool, notify_cb)
+ self.error_path = error_path
+
+ def close_directory(self, dir_baton):
+ if dir_baton[0] == self.error_path:
+ raise SubversionException('A Dummy Exception!', core.SVN_ERR_BASE)
+ else:
+ BatonCollector.close_directory(self, dir_baton)
+
+ def close_file(self, file_baton, text_checksum):
+ if file_baton[0] == self.error_path:
+ raise SubversionException('A Dummy Exception!', core.SVN_ERR_BASE)
+ else:
+ return BatonCollector.close_file(self, file_baton, text_checksum)
+
def _authz_callback(root, path, pool):
"A dummy authz callback which always returns success."
@@ -175,13 +256,15 @@ class SubversionRepositoryTestCase(unittest.TestCase):
def is_cancelled():
self.cancel_calls += 1
return None
+ pool = Pool()
+ subpool = Pool(pool)
dump_path = os.path.join(os.path.dirname(sys.argv[0]),
"trac/versioncontrol/tests/svnrepos.dump")
stream = open(dump_path, 'rb')
- dsp = DumpStreamParser()
- ptr, baton = repos.make_parse_fns3(dsp)
+ dsp = DumpStreamParser(stream, subpool)
+ dsp_ref = weakref.ref(dsp)
+ ptr, baton = repos.make_parse_fns3(dsp, subpool)
repos.parse_dumpstream3(stream, ptr, baton, False, is_cancelled)
- stream.close()
self.assertEqual(self.cancel_calls, 76)
expected_list = [
(b"magic-header", 2),
@@ -226,6 +309,13 @@ class SubversionRepositoryTestCase(unittest.TestCase):
# the comparison list gets too long.
self.assertEqual(dsp.ops[:len(expected_list)], expected_list)
+ # _close_dumpstream should be invoked after 'baton' is removed.
+ self.assertEqual(False, stream.closed)
+ del ptr, baton, subpool, dsp
+ self.assertEqual(True, stream.closed)
+ # Issue SVN-4918
+ self.assertEqual(None, dsp_ref())
+
def test_parse_fns3_invalid_set_fulltext(self):
class DumpStreamParserSubclass(DumpStreamParser):
def set_fulltext(self, node_baton):
@@ -290,6 +380,53 @@ class SubversionRepositoryTestCase(unittest.TestCase):
repos.dir_delta(prev_root, b'', b'', this_root, b'', e_ptr, e_baton,
_authz_callback, 1, 1, 0, 0)
+ def test_delta_editor_leak_with_change_collector(self):
+ pool = Pool()
+ subpool = Pool(pool)
+ root = fs.revision_root(self.fs, self.rev, subpool)
+ editor = repos.ChangeCollector(self.fs, root, subpool)
+ editor_ref = weakref.ref(editor)
+ e_ptr, e_baton = delta.make_editor(editor, subpool)
+ repos.replay(root, e_ptr, e_baton, subpool)
+
+ fs.close_root(root)
+ del root
+ self.assertNotEqual(None, editor_ref())
+
+ del e_ptr, e_baton, editor
+ del subpool
+ self.assertEqual(None, editor_ref())
+
+ def test_replay_batons_refcounts(self):
+ """Issue SVN-4917: check ref-count of batons created and used in callbacks"""
+ root = fs.revision_root(self.fs, self.rev)
+ editor = BatonCollector(self.fs, root)
+ e_ptr, e_baton = delta.make_editor(editor)
+ repos.replay(root, e_ptr, e_baton)
+ for baton in editor.batons:
+ self.assertEqual(sys.getrefcount(baton[2]), 2,
+ "leak on baton %s after replay without errors"
+ % repr(baton))
+ del e_baton
+ self.assertEqual(sys.getrefcount(e_ptr), 2,
+ "leak on editor baton after replay without errors")
+
+ editor = BatonCollectorErrorOnClose(self.fs, root,
+ error_path=b'branches/v1x')
+ e_ptr, e_baton = delta.make_editor(editor)
+ self.assertRaises(SubversionException, repos.replay, root, e_ptr, e_baton)
+ batons = editor.batons
+ # As svn_repos_replay calls neither close_edit callback nor abort_edit
+ # if an error has occured during processing, references of Python objects
+ # in decendant batons may live until e_baton is deleted.
+ del e_baton
+ for baton in batons:
+ self.assertEqual(sys.getrefcount(baton[2]), 2,
+ "leak on baton %s after replay with an error"
+ % repr(baton))
+ self.assertEqual(sys.getrefcount(e_ptr), 2,
+ "leak on editor baton after replay with an error")
+
def test_retrieve_and_change_rev_prop(self):
"""Test playing with revprops"""
self.assertEqual(repos.fs_revision_prop(self.repos, self.rev, b"svn:log",