1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241
|
From: James McCoy <jamessan@debian.org>
Date: Mon, 13 Oct 2025 22:05:45 -0400
Subject: Subject: Fix Python test cases with Python 3.14
Python 3.14 changes the way reference counting works, which causes some
tests to fail. Backport upstream revisions r1926575 and r1927715 to fix
the tests.
Forwarded: not-needed
Signed-off-by: James McCoy <jamessan@debian.org>
---
subversion/bindings/swig/python/tests/mergeinfo.py | 52 +++++++++++++++++++++-
.../bindings/swig/python/tests/repository.py | 49 ++++++++++++++------
subversion/bindings/swig/python/tests/utils.py | 10 +++++
3 files changed, 95 insertions(+), 16 deletions(-)
diff --git a/subversion/bindings/swig/python/tests/mergeinfo.py b/subversion/bindings/swig/python/tests/mergeinfo.py
index 873fc52..739c0e5 100644
--- a/subversion/bindings/swig/python/tests/mergeinfo.py
+++ b/subversion/bindings/swig/python/tests/mergeinfo.py
@@ -18,7 +18,7 @@
# under the License.
#
#
-import unittest, os, sys, gc
+import unittest, os, sys, weakref, gc
from svn import core, repos, fs
import utils
@@ -125,6 +125,9 @@ class SubversionMergeinfoTestCase(unittest.TestCase):
}
self.compare_mergeinfo_catalogs(mergeinfo, expected_mergeinfo)
+ @unittest.skipIf(utils.HAS_DEFERRED_REFCOUNT,
+ "Reference counting tests skipped because of deferred "
+ "reference counting")
def test_mergeinfo_leakage__incorrect_range_t_refcounts(self):
"""Ensure that the ref counts on svn_merge_range_t objects returned by
svn_mergeinfo_parse() are correct."""
@@ -138,7 +141,8 @@ class SubversionMergeinfoTestCase(unittest.TestCase):
# ....and now 3 (incref during iteration of each range object)
refcount = sys.getrefcount(r)
- # ....and finally, 4 (getrefcount() also increfs)
+ # ....and finally, 4 (getrefcount() also increfs, unless deferred
+ # reference counting)
expected = 4
# Note: if path and index are not '/trunk' and 0 respectively, then
@@ -150,8 +154,49 @@ class SubversionMergeinfoTestCase(unittest.TestCase):
"cause: incorrect Py_INCREF/Py_DECREF usage in libsvn_swig_py/"
"swigutil_py.c." % (expected, refcount, path, i)))
+ def test_mergeinfo_leakage__incorrect_range_t_weakrefs(self):
+ """Ensure that the ref counts on svn_merge_range_t objects returned by
+ svn_mergeinfo_parse() are correct."""
+ # When reference counting is working properly, each svn_merge_range_t in
+ # the returned mergeinfo will have a ref count of 1...
+ mergeinfo = core.svn_mergeinfo_parse(self.TEXT_MERGEINFO1)
+ merge_range_refdict = weakref.WeakValueDictionary()
+ merge_range_indexes = []
+ n_merge_range = 0
+ for (path, rangelist) in core._as_list(mergeinfo.items()):
+ # ....and now 2 (incref during iteration of rangelist)
+
+ for (i, r) in enumerate(rangelist):
+ # ....and now 3 (incref during iteration of each range object)
+
+ idx = (path, i)
+ merge_range_refdict[idx] = r
+ merge_range_indexes.append(idx)
+ n_merge_range += 1
+
+ # Note: if path and index are not '/trunk' and 0 respectively, then
+ # only some of the range objects are leaking, which is, as far as
+ # leaks go, even more impressive.
+
+ del rangelist, r
+ gc.collect()
+ # Now (strong) reference count of all svn_merge_range_t should be 1
+ # again and those objects should not be removed yet.
+ for idx in merge_range_indexes:
+ self.assertIn(idx, merge_range_refdict, (
+ "Refarence count error on svn_merge_info_t object for "
+ "(path: %s, index: %d). It should still exists because "
+ "mergeinfo holds its reference, but after GC, it already "
+ "removed." % idx))
del mergeinfo
gc.collect()
+ if merge_range_refdict:
+ # certainly memory leak, but we want to listing up leaked objects
+ # before raise an assertion error.
+ self.assertFalse(merge_range_refdict,
+ "Memory leak! All svn_merge_range_t object holded "
+ "by mergeinfo object should be removed, but at least "
+ "one object still alive.")
def test_mergeinfo_leakage__lingering_range_t_objects_after_del(self):
"""Ensure that there are no svn_merge_range_t objects being tracked by
@@ -162,6 +207,9 @@ class SubversionMergeinfoTestCase(unittest.TestCase):
objects will be garbage collected and thus, not appear in the list of
objects returned by gc.get_objects()."""
mergeinfo = core.svn_mergeinfo_parse(self.TEXT_MERGEINFO1)
+ lingering = get_svn_merge_range_t_objects()
+ self.assertNotEqual(lingering, list())
+ del lingering
del mergeinfo
gc.collect()
lingering = get_svn_merge_range_t_objects()
diff --git a/subversion/bindings/swig/python/tests/repository.py b/subversion/bindings/swig/python/tests/repository.py
index e764ec5..d28f1ad 100644
--- a/subversion/bindings/swig/python/tests/repository.py
+++ b/subversion/bindings/swig/python/tests/repository.py
@@ -87,15 +87,32 @@ class DumpStreamParser(repos.ParseFns3):
class BatonCollector(repos.ChangeCollector):
"""A ChangeCollector with collecting batons, too"""
+
def __init__(self, fs_ptr, root, pool=None, notify_cb=None):
+
+ def get_expected_baton_refcount():
+ """determine expected refcount of batons within a batoun_tuple,
+ by using dumy object"""
+ self.open_root(-1, None)
+ for baton_tuple in self.batons:
+ rc = sys.getrefcount(baton_tuple[2])
+ break
+ return rc
+
repos.ChangeCollector.__init__(self, fs_ptr, root, pool, notify_cb)
- self.batons = []
self.close_called = False
self.abort_called = False
+ # temporary values for get_expected_baton_refcount
+ self.batons = []
+ self.expected_baton_refcount = 0
+ # determin expected_baton_refcount
+ self.expected_baton_refcount = get_expected_baton_refcount()
+ # re-initialize the values after calling get_expected_baton_refcount()
+ self.batons = []
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)))
+ self.batons.append((b'dir baton', b'', bt, self.expected_baton_refcount))
return bt
def add_directory(self, path, parent_baton,
@@ -104,14 +121,14 @@ class BatonCollector(repos.ChangeCollector):
copyfrom_path,
copyfrom_revision,
dir_pool)
- self.batons.append((b'dir baton', path, bt, sys.getrefcount(bt)))
+ self.batons.append((b'dir baton', path, bt, self.expected_baton_refcount))
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)))
+ self.batons.append((b'dir baton', path, bt, self.expected_baton_refcount))
return bt
def add_file(self, path, parent_baton,
@@ -119,13 +136,13 @@ class BatonCollector(repos.ChangeCollector):
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)))
+ self.batons.append((b'file baton', path, bt, self.expected_baton_refcount))
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)))
+ self.batons.append((b'file baton', path, bt, self.expected_baton_refcount))
return bt
def close_edit(self, pool=None):
@@ -429,29 +446,33 @@ class SubversionRepositoryTestCase(unittest.TestCase):
root = fs.revision_root(self.fs, self.rev)
editor = BatonCollector(self.fs, root)
e_ptr, e_baton = delta.make_editor(editor)
+ refcount_at_first = sys.getrefcount(e_ptr)
repos.replay(root, e_ptr, e_baton)
- for baton in editor.batons:
- self.assertEqual(sys.getrefcount(baton[2]), 2,
+ for baton_tuple in editor.batons:
+ # baton_tuple: 4-tuple(baton_type: bytes, node: bytes, bt: baton,
+ # expected_refcount_of_bt: int)
+ self.assertEqual(sys.getrefcount(baton_tuple[2]), baton_tuple[3],
"leak on baton %s after replay without errors"
- % repr(baton))
+ % repr(baton_tuple))
del e_baton
- self.assertEqual(sys.getrefcount(e_ptr), 2,
+ self.assertEqual(sys.getrefcount(e_ptr), refcount_at_first,
"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)
+ refcount_at_first = sys.getrefcount(e_ptr)
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,
+ for baton_tuple in batons:
+ self.assertEqual(sys.getrefcount(baton_tuple[2]), baton_tuple[3],
"leak on baton %s after replay with an error"
- % repr(baton))
- self.assertEqual(sys.getrefcount(e_ptr), 2,
+ % repr(baton_tuple))
+ self.assertEqual(sys.getrefcount(e_ptr), refcount_at_first,
"leak on editor baton after replay with an error")
def test_delta_editor_apply_textdelta_handler_refcount(self):
diff --git a/subversion/bindings/swig/python/tests/utils.py b/subversion/bindings/swig/python/tests/utils.py
index 09061a6..3e29992 100644
--- a/subversion/bindings/swig/python/tests/utils.py
+++ b/subversion/bindings/swig/python/tests/utils.py
@@ -95,3 +95,13 @@ def codecs_eq(a, b):
def is_defaultencoding_utf8():
return codecs_eq(sys.getdefaultencoding(), 'utf-8')
+
+def get_holded_refcount_by_getrefcount():
+ "get refcount holded by sys.getrefcount() if its arg is a local variable"
+ a = []
+ rv = sys.getrefcount(a) - 1
+ return rv
+
+HAS_DEFERRED_REFCOUNT = not get_holded_refcount_by_getrefcount()
+
+del get_holded_refcount_by_getrefcount
|