https://github.com/python/cpython/commit/bba171ea02669b6fdd3c58eaa886f414436ee223
commit: bba171ea02669b6fdd3c58eaa886f414436ee223
branch: main
author: Victor Stinner <[email protected]>
committer: vstinner <[email protected]>
date: 2026-09-16T17:15:07Z
summary:

gh-157628: regrtest: Only report a leak if all runs leak one FD (#157629)

In regrtest, only consider that a test leaks if all test runs leak at
least one file descriptor. For example, ignore "[0, 1, -1] file
descriptors, sum=0" deltas, instead of reporting it as a leak.

Add a regression test.

files:
A Misc/NEWS.d/next/Tests/2026-09-16-17-06-54.gh-issue-157628.KAwhlY.rst
M Lib/test/libregrtest/refleak.py
M Lib/test/test_regrtest.py

diff --git a/Lib/test/libregrtest/refleak.py b/Lib/test/libregrtest/refleak.py
index ffb8438d1b0278f..69a9c9d6e5f1d7c 100644
--- a/Lib/test/libregrtest/refleak.py
+++ b/Lib/test/libregrtest/refleak.py
@@ -188,34 +188,31 @@ def runtest_refleak(test_name, test_func,
     if not quiet:
         print(file=sys.stderr)
 
-    # These checkers return False on success, True on failure
-    def check_rc_deltas(deltas):
-        # Checker for reference counters and memory blocks.
+    failed = False
+    for raw_deltas, item_name in [
+        (rc_deltas, 'references'),
+        (alloc_deltas, 'memory blocks'),
+        (fd_deltas, 'file descriptors')
+    ]:
+        # Ignore warmup runs; convert to a list for reporting
+        deltas = list(raw_deltas[warmups:])
+
+        # Only consider that a test leaks if all deltas are greater than or
+        # equal to 1. Otherwise, ignore deltas.
         #
-        # bpo-30776: Try to ignore false positives:
+        # For example, ignore deltas:
         #
-        #   [3, 0, 0]
-        #   [0, 1, 0]
-        #   [8, -8, 1]
+        #   [3, 0, 0] references, sum=3
+        #   [0, 1, 0] references, sum=1
+        #   [8, -8, 1] references, sum=1
+        #   [0, 1, -1] file descriptors, sum=0
         #
-        # Expected leaks:
+        # Examples of deltas treated as leaks:
         #
-        #   [5, 5, 6]
-        #   [10, 1, 1]
-        return all(delta >= 1 for delta in deltas)
+        #   [5, 5, 6] references, sum=16
+        #   [10, 1, 1] references, sum=12
+        failing = all(delta >= 1 for delta in deltas)
 
-    def check_fd_deltas(deltas):
-        return any(deltas)
-
-    failed = False
-    for raw_deltas, item_name, checker in [
-        (rc_deltas, 'references', check_rc_deltas),
-        (alloc_deltas, 'memory blocks', check_rc_deltas),
-        (fd_deltas, 'file descriptors', check_fd_deltas)
-    ]:
-        # ignore warmup runs; convert to a list for reporting
-        deltas = list(raw_deltas[warmups:])
-        failing = checker(deltas)
         suspicious = any(deltas)
         if failing or suspicious:
             msg = '%s leaked %s %s, sum=%s' % (
diff --git a/Lib/test/test_regrtest.py b/Lib/test/test_regrtest.py
index c966f8659e2abb0..71edec40ade8079 100644
--- a/Lib/test/test_regrtest.py
+++ b/Lib/test/test_regrtest.py
@@ -1315,29 +1315,47 @@ def test_run(self):
                                   forever=True)
 
     @support.requires_jit_disabled
-    def check_leak(self, code, what, *, run_workers=False):
-        test = self.create_test('huntrleaks', code=code)
+    def check_leak(self, code, what, *, run_workers=False,
+                   name='huntrleaks', deltas=(1, 1, 1)):
+        test = self.create_test(name, code=code)
 
+        leak = all(delta >= 1 for delta in deltas)
         filename = 'reflog.txt'
         self.addCleanup(os_helper.unlink, filename)
         cmd = ['--huntrleaks', '3:3:']
         if run_workers:
             cmd.append('-j1')
         cmd.append(test)
+        if leak:
+            exitcode = EXITCODE_BAD_TEST
+            kwargs = dict(failed=test)
+        else:
+            exitcode = 0
+            kwargs = {}
+
+        try:
+            os_helper.unlink(filename)
+        except FileNotFoundError:
+            pass
         output = self.run_tests(*cmd,
-                                exitcode=EXITCODE_BAD_TEST,
+                                exitcode=exitcode,
                                 stderr=subprocess.STDOUT)
-        self.check_executed_tests(output, [test], failed=test, stats=1)
+        self.check_executed_tests(output, [test], stats=1, **kwargs)
 
-        line = r'beginning 6 repetitions. .*\n123:456\n[.0-9X]{3} 111\n'
+        digits = ''.join('1' if delta >= 1 else '.' for delta in deltas)
+        line = r'beginning 6 repetitions. .*\n123:456\n[.0-9X]{3} %s\n' % 
digits
         self.check_line(output, line)
 
-        line2 = '%s leaked [1, 1, 1] %s, sum=3\n' % (test, what)
-        self.assertIn(line2, output)
+        if leak:
+            line2 = f'{test} leaked {repr(list(deltas))} {what}, sum=3\n'
+            self.assertIn(line2, output)
 
-        with open(filename) as fp:
-            reflog = fp.read()
-            self.assertIn(line2, reflog)
+        if leak:
+            with open(filename) as fp:
+                reflog = fp.read()
+                self.assertIn(line2, reflog)
+        else:
+            self.assertFalse(os.path.exists(filename))
 
     @unittest.skipUnless(support.Py_DEBUG, 'need a debug build')
     def check_huntrleaks(self, *, run_workers: bool):
@@ -1414,6 +1432,31 @@ def test_leak(self):
         """)
         self.check_leak(code, 'file descriptors')
 
+        # Ignore false positive: deltas [1, -1, 0]
+        code = textwrap.dedent("""
+            import os
+            import unittest
+
+            RUN = 0
+            FD = None
+
+            class FDLeakTest(unittest.TestCase):
+                def test_leak(self):
+                    global RUN, FD
+                    RUN += 1
+                    if RUN == 4:
+                        # Create a fd without closing it: leak! (delta=1)
+                        FD = os.open(__file__, os.O_RDONLY)
+                    elif RUN == 5:
+                        # Close fd created in previous run (delta=-1)
+                        os.close(FD)
+                    else:
+                        # Do nothing at the warmup (steps 1-3) and step 6 
(delta=0)
+                        pass
+        """)
+        self.check_leak(code, 'file descriptors',
+                        name='no_fd_leak', deltas=(1, -1, 0))
+
     def test_list_tests(self):
         # test --list-tests
         tests = [self.create_test() for i in range(5)]
diff --git 
a/Misc/NEWS.d/next/Tests/2026-09-16-17-06-54.gh-issue-157628.KAwhlY.rst 
b/Misc/NEWS.d/next/Tests/2026-09-16-17-06-54.gh-issue-157628.KAwhlY.rst
new file mode 100644
index 000000000000000..c232ebca619a21d
--- /dev/null
+++ b/Misc/NEWS.d/next/Tests/2026-09-16-17-06-54.gh-issue-157628.KAwhlY.rst
@@ -0,0 +1,3 @@
+In regrtest, only consider that a test leaks if all test runs leak at least
+one file descriptor. For example, ignore "[0, 1, -1] file descriptors,
+sum=0" deltas, instead of reporting a leak. Patch by Victor Stinner.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to