diff --git a/devel/py-subversion/Makefile b/devel/py-subversion/Makefile index 3841e4229632..7c7983ec4820 100644 --- a/devel/py-subversion/Makefile +++ b/devel/py-subversion/Makefile @@ -1,34 +1,36 @@ PKGNAMEPREFIX= ${PYTHON_PKGNAMEPREFIX} MAINTAINER= michaelo@apache.org COMMENT= Python bindings for version control system WWW= https://subversion.apache.org/ BUILD_DEPENDS= ${LOCALBASE}/include/py3c.h:devel/py3c USES= python USE_PYTHON= flavors py3kplist # There is bug in python bindings Makefile MAKE_JOBS_UNSAFE=yes -PORTREVISION_LATEST= 0 -PORTREVISION_LTS= 0 +PORTREVISION_LATEST= 1 +PORTREVISION_LTS= 1 SVN_BUILD_BINDINGS= yes .include "${.CURDIR}/../../devel/subversion/Makefile.addons" CATEGORIES+= python +EXTRA_PATCHES+= ${PATCHDIR}/extra-patch-check-swig-py314 CONFIGURE_ARGS+= --with-swig-python=${PYTHON_CMD} \ --without-swig-perl \ --without-swig-ruby \ --without-gnome-keyring \ --without-kwallet \ --with-apxs=no -ALL_TARGET= swig-py check-swig-py +ALL_TARGET= swig-py +TEST_TARGET= check-swig-py INSTALL_TARGET= install-swig-py .include diff --git a/devel/subversion-lts/files/extra-patch-check-swig-py314 b/devel/subversion-lts/files/extra-patch-check-swig-py314 new file mode 100644 index 000000000000..5156311eb6d4 --- /dev/null +++ b/devel/subversion-lts/files/extra-patch-check-swig-py314 @@ -0,0 +1,231 @@ +This patch was extracted by the command +svn diff -c r1929147 --patch-compatible \ + https://svn.apache.org/repos/asf/subversion/branches/1.14.x | tail -n +17 +And it can be applied only for Subversion < 1.15.0 +Index: subversion/bindings/swig/python/tests/mergeinfo.py +=================================================================== +--- subversion/bindings/swig/python/tests/mergeinfo.py (revision 1929146) ++++ subversion/bindings/swig/python/tests/mergeinfo.py (revision 1929147) +@@ -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 @@ + } + 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 @@ + # ....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 @@ + "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 @@ + 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() +Index: subversion/bindings/swig/python/tests/repository.py +=================================================================== +--- subversion/bindings/swig/python/tests/repository.py (revision 1929146) ++++ subversion/bindings/swig/python/tests/repository.py (revision 1929147) +@@ -87,15 +87,32 @@ + + 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,7 +121,7 @@ + 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, +@@ -111,7 +128,7 @@ + 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 @@ + 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,18 +446,22 @@ + 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 +@@ -447,11 +468,11 @@ + # 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): +Index: subversion/bindings/swig/python/tests/utils.py +=================================================================== +--- subversion/bindings/swig/python/tests/utils.py (revision 1929146) ++++ subversion/bindings/swig/python/tests/utils.py (revision 1929147) +@@ -95,3 +95,13 @@ + + 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 diff --git a/devel/subversion/files/extra-patch-check-swig-py314 b/devel/subversion/files/extra-patch-check-swig-py314 new file mode 100644 index 000000000000..5156311eb6d4 --- /dev/null +++ b/devel/subversion/files/extra-patch-check-swig-py314 @@ -0,0 +1,231 @@ +This patch was extracted by the command +svn diff -c r1929147 --patch-compatible \ + https://svn.apache.org/repos/asf/subversion/branches/1.14.x | tail -n +17 +And it can be applied only for Subversion < 1.15.0 +Index: subversion/bindings/swig/python/tests/mergeinfo.py +=================================================================== +--- subversion/bindings/swig/python/tests/mergeinfo.py (revision 1929146) ++++ subversion/bindings/swig/python/tests/mergeinfo.py (revision 1929147) +@@ -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 @@ + } + 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 @@ + # ....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 @@ + "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 @@ + 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() +Index: subversion/bindings/swig/python/tests/repository.py +=================================================================== +--- subversion/bindings/swig/python/tests/repository.py (revision 1929146) ++++ subversion/bindings/swig/python/tests/repository.py (revision 1929147) +@@ -87,15 +87,32 @@ + + 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,7 +121,7 @@ + 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, +@@ -111,7 +128,7 @@ + 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 @@ + 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,18 +446,22 @@ + 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 +@@ -447,11 +468,11 @@ + # 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): +Index: subversion/bindings/swig/python/tests/utils.py +=================================================================== +--- subversion/bindings/swig/python/tests/utils.py (revision 1929146) ++++ subversion/bindings/swig/python/tests/utils.py (revision 1929147) +@@ -95,3 +95,13 @@ + + 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