Skip to content

Commit 5110c9f

Browse files
[3.14] gh-157628: regrtest: Only report a leak if all runs leak one FD (GH-157629) (GH-157753) (#159088)
[3.15] gh-157628: regrtest: Only report a leak if all runs leak one FD (GH-157629) (GH-157753) gh-157628: regrtest: Only report a leak if all runs leak one FD (GH-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. (cherry picked from commit 0a4f255) (cherry picked from commit bba171e) Co-authored-by: Victor Stinner <vstinner@python.org>
1 parent 2a76adb commit 5110c9f

3 files changed

Lines changed: 76 additions & 33 deletions

File tree

‎Lib/test/libregrtest/refleak.py‎

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -185,34 +185,31 @@ def get_pooled_int(value):
185185
if not quiet:
186186
print(file=sys.stderr)
187187

188-
# These checkers return False on success, True on failure
189-
def check_rc_deltas(deltas):
190-
# Checker for reference counters and memory blocks.
188+
failed = False
189+
for raw_deltas, item_name in [
190+
(rc_deltas, 'references'),
191+
(alloc_deltas, 'memory blocks'),
192+
(fd_deltas, 'file descriptors')
193+
]:
194+
# Ignore warmup runs; convert to a list for reporting
195+
deltas = list(raw_deltas[warmups:])
196+
197+
# Only consider that a test leaks if all deltas are greater than or
198+
# equal to 1. Otherwise, ignore deltas.
191199
#
192-
# bpo-30776: Try to ignore false positives:
200+
# For example, ignore deltas:
193201
#
194-
# [3, 0, 0]
195-
# [0, 1, 0]
196-
# [8, -8, 1]
202+
# [3, 0, 0] references, sum=3
203+
# [0, 1, 0] references, sum=1
204+
# [8, -8, 1] references, sum=1
205+
# [0, 1, -1] file descriptors, sum=0
197206
#
198-
# Expected leaks:
207+
# Examples of deltas treated as leaks:
199208
#
200-
# [5, 5, 6]
201-
# [10, 1, 1]
202-
return all(delta >= 1 for delta in deltas)
209+
# [5, 5, 6] references, sum=16
210+
# [10, 1, 1] references, sum=12
211+
failing = all(delta >= 1 for delta in deltas)
203212

204-
def check_fd_deltas(deltas):
205-
return any(deltas)
206-
207-
failed = False
208-
for deltas, item_name, checker in [
209-
(rc_deltas, 'references', check_rc_deltas),
210-
(alloc_deltas, 'memory blocks', check_rc_deltas),
211-
(fd_deltas, 'file descriptors', check_fd_deltas)
212-
]:
213-
# ignore warmup runs
214-
deltas = deltas[warmups:]
215-
failing = checker(deltas)
216213
suspicious = any(deltas)
217214
if failing or suspicious:
218215
msg = '%s leaked %s %s, sum=%s' % (

‎Lib/test/test_regrtest.py‎

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1284,29 +1284,47 @@ def test_run(self):
12841284
forever=True)
12851285

12861286
@support.requires_jit_disabled
1287-
def check_leak(self, code, what, *, run_workers=False):
1288-
test = self.create_test('huntrleaks', code=code)
1287+
def check_leak(self, code, what, *, run_workers=False,
1288+
name='huntrleaks', deltas=(1, 1, 1)):
1289+
test = self.create_test(name, code=code)
12891290

1291+
leak = all(delta >= 1 for delta in deltas)
12901292
filename = 'reflog.txt'
12911293
self.addCleanup(os_helper.unlink, filename)
12921294
cmd = ['--huntrleaks', '3:3:']
12931295
if run_workers:
12941296
cmd.append('-j1')
12951297
cmd.append(test)
1298+
if leak:
1299+
exitcode = EXITCODE_BAD_TEST
1300+
kwargs = dict(failed=test)
1301+
else:
1302+
exitcode = 0
1303+
kwargs = {}
1304+
1305+
try:
1306+
os_helper.unlink(filename)
1307+
except FileNotFoundError:
1308+
pass
12961309
output = self.run_tests(*cmd,
1297-
exitcode=EXITCODE_BAD_TEST,
1310+
exitcode=exitcode,
12981311
stderr=subprocess.STDOUT)
1299-
self.check_executed_tests(output, [test], failed=test, stats=1)
1312+
self.check_executed_tests(output, [test], stats=1, **kwargs)
13001313

1301-
line = r'beginning 6 repetitions. .*\n123:456\n[.0-9X]{3} 111\n'
1314+
digits = ''.join('1' if delta >= 1 else '.' for delta in deltas)
1315+
line = r'beginning 6 repetitions. .*\n123:456\n[.0-9X]{3} %s\n' % digits
13021316
self.check_line(output, line)
13031317

1304-
line2 = '%s leaked [1, 1, 1] %s, sum=3\n' % (test, what)
1305-
self.assertIn(line2, output)
1318+
if leak:
1319+
line2 = f'{test} leaked {repr(list(deltas))} {what}, sum=3\n'
1320+
self.assertIn(line2, output)
13061321

1307-
with open(filename) as fp:
1308-
reflog = fp.read()
1309-
self.assertIn(line2, reflog)
1322+
if leak:
1323+
with open(filename) as fp:
1324+
reflog = fp.read()
1325+
self.assertIn(line2, reflog)
1326+
else:
1327+
self.assertFalse(os.path.exists(filename))
13101328

13111329
@unittest.skipUnless(support.Py_DEBUG, 'need a debug build')
13121330
def check_huntrleaks(self, *, run_workers: bool):
@@ -1383,6 +1401,31 @@ def test_leak(self):
13831401
""")
13841402
self.check_leak(code, 'file descriptors')
13851403

1404+
# Ignore false positive: deltas [1, -1, 0]
1405+
code = textwrap.dedent("""
1406+
import os
1407+
import unittest
1408+
1409+
RUN = 0
1410+
FD = None
1411+
1412+
class FDLeakTest(unittest.TestCase):
1413+
def test_leak(self):
1414+
global RUN, FD
1415+
RUN += 1
1416+
if RUN == 4:
1417+
# Create a fd without closing it: leak! (delta=1)
1418+
FD = os.open(__file__, os.O_RDONLY)
1419+
elif RUN == 5:
1420+
# Close fd created in previous run (delta=-1)
1421+
os.close(FD)
1422+
else:
1423+
# Do nothing at the warmup (steps 1-3) and step 6 (delta=0)
1424+
pass
1425+
""")
1426+
self.check_leak(code, 'file descriptors',
1427+
name='no_fd_leak', deltas=(1, -1, 0))
1428+
13861429
def test_list_tests(self):
13871430
# test --list-tests
13881431
tests = [self.create_test() for i in range(5)]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
In regrtest, only consider that a test leaks if all test runs leak at least
2+
one file descriptor. For example, ignore "[0, 1, -1] file descriptors,
3+
sum=0" deltas, instead of reporting a leak. Patch by Victor Stinner.

0 commit comments

Comments
 (0)