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]