From 5e75eebed87dfd741a9bb50741e65faf571d52d1 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 15 Sep 2026 23:18:12 +0200 Subject: [PATCH 1/3] gh-157628: regrtest: Only report a leak if all runs leak one FD 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. --- Lib/test/libregrtest/refleak.py | 33 +++++----- Lib/test/test_regrtest.py | 63 ++++++++++++++++--- ...-09-16-17-06-54.gh-issue-157628.KAwhlY.rst | 3 + 3 files changed, 71 insertions(+), 28 deletions(-) create mode 100644 Misc/NEWS.d/next/Tests/2026-09-16-17-06-54.gh-issue-157628.KAwhlY.rst diff --git a/Lib/test/libregrtest/refleak.py b/Lib/test/libregrtest/refleak.py index ffb8438d1b0278f..fa6a532e19d8d7f 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 there is a leak if all deltas are greater than or + # equal to 1. Treat other non-zeros are false positive. # - # bpo-30776: Try to ignore false positives: + # For example, ignore deltas: # # [3, 0, 0] # [0, 1, 0] # [8, -8, 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) + 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..b1cdbda9e45c843 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: + # 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 in 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..4575e3829d8dbb9 --- /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 it as a leak. Patch by Victor Stinner. From 975b3b8a908fee177738fdd6589039de7e719af2 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 16 Sep 2026 18:39:15 +0200 Subject: [PATCH 2/3] Rephrase comment --- Lib/test/libregrtest/refleak.py | 14 +++++++------- Lib/test/test_regrtest.py | 4 ++-- .../2026-09-16-17-06-54.gh-issue-157628.KAwhlY.rst | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Lib/test/libregrtest/refleak.py b/Lib/test/libregrtest/refleak.py index fa6a532e19d8d7f..58f4b7c2a294ffc 100644 --- a/Lib/test/libregrtest/refleak.py +++ b/Lib/test/libregrtest/refleak.py @@ -197,20 +197,20 @@ def runtest_refleak(test_name, test_func, # ignore warmup runs; convert to a list for reporting deltas = list(raw_deltas[warmups:]) - # Only consider that there is a leak if all deltas are greater than or - # equal to 1. Treat other non-zeros are false positive. + # Only consider that a test leaks if all deltas are greater than or + # equal to 1. Otherwise, ignore deltas. # # 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 # # Examples of deltas treated as leaks: # - # [5, 5, 6] - # [10, 1, 1] + # [5, 5, 6] references, sum=16 + # [10, 1, 1] references, sum=12 failing = all(delta >= 1 for delta in deltas) suspicious = any(deltas) diff --git a/Lib/test/test_regrtest.py b/Lib/test/test_regrtest.py index b1cdbda9e45c843..71edec40ade8079 100644 --- a/Lib/test/test_regrtest.py +++ b/Lib/test/test_regrtest.py @@ -1445,13 +1445,13 @@ def test_leak(self): global RUN, FD RUN += 1 if RUN == 4: - # Leak (delta=1) + # 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 in the warmup (steps 1-3) and step 6 (delta=0) + # Do nothing at the warmup (steps 1-3) and step 6 (delta=0) pass """) self.check_leak(code, 'file descriptors', 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 index 4575e3829d8dbb9..c232ebca619a21d 100644 --- 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 @@ -1,3 +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 it as a leak. Patch by Victor Stinner. +sum=0" deltas, instead of reporting a leak. Patch by Victor Stinner. From 0c7ff7345c732d73ea0683eebabc4573be39c868 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 16 Sep 2026 18:42:37 +0200 Subject: [PATCH 3/3] comment consistency: title case --- Lib/test/libregrtest/refleak.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/libregrtest/refleak.py b/Lib/test/libregrtest/refleak.py index 58f4b7c2a294ffc..69a9c9d6e5f1d7c 100644 --- a/Lib/test/libregrtest/refleak.py +++ b/Lib/test/libregrtest/refleak.py @@ -194,7 +194,7 @@ def runtest_refleak(test_name, test_func, (alloc_deltas, 'memory blocks'), (fd_deltas, 'file descriptors') ]: - # ignore warmup runs; convert to a list for reporting + # 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