git: b8e6b2a99628 - main - devel/py-subversion: split test target, with fix tests in python >= 3.14

From: Michael Osipov <michaelo_at_FreeBSD.org>
Date: Sun, 06 Sep 2026 14:17:49 UTC
The branch main has been updated by michaelo:

URL: https://cgit.FreeBSD.org/ports/commit/?id=b8e6b2a99628582839f56de7301796fba0aa0326

commit b8e6b2a99628582839f56de7301796fba0aa0326
Author:     FUTATSUKI Yasuhito <futatuki@yf.bsdclub.org>
AuthorDate: 2026-07-04 17:26:23 +0000
Commit:     Michael Osipov <michaelo@FreeBSD.org>
CommitDate: 2026-09-06 14:07:05 +0000

    devel/py-subversion: split test target, with fix tests in python >= 3.14
    
    To fix both of latest and LTS, add a patch to fix the tests
    both in devel/subversion and devel/subversion-lts.
    
    PR:             294183
    MFH:            2026Q3
---
 devel/py-subversion/Makefile                       |   8 +-
 .../files/extra-patch-check-swig-py314             | 231 +++++++++++++++++++++
 .../subversion/files/extra-patch-check-swig-py314  | 231 +++++++++++++++++++++
 3 files changed, 467 insertions(+), 3 deletions(-)

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
@@ -12,14 +12,15 @@ 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 \
@@ -28,7 +29,8 @@ CONFIGURE_ARGS+=	--with-swig-python=${PYTHON_CMD} \
 			--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 <bsd.port.post.mk>
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