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.