Skip to content

Commit 64856ad

Browse files
vstinnershibturn
andauthored
bpo-18174: regrtest -R 3:3 now also detects FD leak (#7409)
"python -m test --huntrleaks ..." now also checks for leak of file descriptors. Co-Authored-By: Richard Oudkerk <[email protected]>
1 parent 2705819 commit 64856ad

File tree

3 files changed

+117
-20
lines changed

3 files changed

+117
-20
lines changed

‎Lib/test/regrtest.py‎

Lines changed: 42 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1416,37 +1416,59 @@ def run_the_test():
14161416
nwarmup, ntracked, fname = huntrleaks
14171417
fname = os.path.join(support.SAVEDCWD, fname)
14181418
repcount = nwarmup + ntracked
1419+
rc_deltas = [0] * ntracked
1420+
fd_deltas = [0] * ntracked
1421+
14191422
print >> sys.stderr, "beginning", repcount, "repetitions"
14201423
print >> sys.stderr, ("1234567890"*(repcount//10 + 1))[:repcount]
14211424
dash_R_cleanup(fs, ps, pic, zdc, abcs)
1425+
# initialize variables to make pyflakes quiet
1426+
rc_before = fd_before = 0
14221427
for i in range(repcount):
1423-
rc_before = sys.gettotalrefcount()
14241428
run_the_test()
14251429
sys.stderr.write('.')
14261430
dash_R_cleanup(fs, ps, pic, zdc, abcs)
14271431
rc_after = sys.gettotalrefcount()
1432+
fd_after = support.fd_count()
14281433
if i >= nwarmup:
1429-
deltas.append(rc_after - rc_before)
1434+
rc_deltas[i - nwarmup] = rc_after - rc_before
1435+
fd_deltas[i - nwarmup] = fd_after - fd_before
1436+
rc_before = rc_after
1437+
fd_before = fd_after
14301438
print >> sys.stderr
14311439

1432-
# bpo-30776: Try to ignore false positives:
1433-
#
1434-
# [3, 0, 0]
1435-
# [0, 1, 0]
1436-
# [8, -8, 1]
1437-
#
1438-
# Expected leaks:
1439-
#
1440-
# [5, 5, 6]
1441-
# [10, 1, 1]
1442-
if all(delta >= 1 for delta in deltas):
1443-
msg = '%s leaked %s references, sum=%s' % (test, deltas, sum(deltas))
1444-
print >> sys.stderr, msg
1445-
with open(fname, "a") as refrep:
1446-
print >> refrep, msg
1447-
refrep.flush()
1448-
return True
1449-
return False
1440+
# These checkers return False on success, True on failure
1441+
def check_rc_deltas(deltas):
1442+
# Checker for reference counters and memomry blocks.
1443+
#
1444+
# bpo-30776: Try to ignore false positives:
1445+
#
1446+
# [3, 0, 0]
1447+
# [0, 1, 0]
1448+
# [8, -8, 1]
1449+
#
1450+
# Expected leaks:
1451+
#
1452+
# [5, 5, 6]
1453+
# [10, 1, 1]
1454+
return all(delta >= 1 for delta in deltas)
1455+
1456+
def check_fd_deltas(deltas):
1457+
return any(deltas)
1458+
1459+
failed = False
1460+
for deltas, item_name, checker in [
1461+
(rc_deltas, 'references', check_rc_deltas),
1462+
(fd_deltas, 'file descriptors', check_fd_deltas)
1463+
]:
1464+
if checker(deltas):
1465+
msg = '%s leaked %s %s, sum=%s' % (test, deltas, item_name, sum(deltas))
1466+
print >> sys.stderr, msg
1467+
with open(fname, "a") as refrep:
1468+
print >> refrep, msg
1469+
refrep.flush()
1470+
failed = True
1471+
return failed
14501472

14511473
def dash_R_cleanup(fs, ps, pic, zdc, abcs):
14521474
import gc, copy_reg

‎Lib/test/support/__init__.py‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2062,6 +2062,63 @@ def _crash_python():
20622062
_testcapi._read_null()
20632063

20642064

2065+
def fd_count():
2066+
"""Count the number of open file descriptors.
2067+
"""
2068+
if sys.platform.startswith(('linux', 'freebsd')):
2069+
try:
2070+
names = os.listdir("/proc/self/fd")
2071+
return len(names)
2072+
except FileNotFoundError:
2073+
pass
2074+
2075+
old_modes = None
2076+
if sys.platform == 'win32':
2077+
# bpo-25306, bpo-31009: Call CrtSetReportMode() to not kill the process
2078+
# on invalid file descriptor if Python is compiled in debug mode
2079+
try:
2080+
import msvcrt
2081+
msvcrt.CrtSetReportMode
2082+
except (AttributeError, ImportError):
2083+
# no msvcrt or a release build
2084+
pass
2085+
else:
2086+
old_modes = {}
2087+
for report_type in (msvcrt.CRT_WARN,
2088+
msvcrt.CRT_ERROR,
2089+
msvcrt.CRT_ASSERT):
2090+
old_modes[report_type] = msvcrt.CrtSetReportMode(report_type, 0)
2091+
2092+
MAXFD = 256
2093+
if hasattr(os, 'sysconf'):
2094+
try:
2095+
MAXFD = os.sysconf("SC_OPEN_MAX")
2096+
except OSError:
2097+
pass
2098+
2099+
try:
2100+
count = 0
2101+
for fd in range(MAXFD):
2102+
try:
2103+
# Prefer dup() over fstat(). fstat() can require input/output
2104+
# whereas dup() doesn't.
2105+
fd2 = os.dup(fd)
2106+
except OSError as e:
2107+
if e.errno != errno.EBADF:
2108+
raise
2109+
else:
2110+
os.close(fd2)
2111+
count += 1
2112+
finally:
2113+
if old_modes is not None:
2114+
for report_type in (msvcrt.CRT_WARN,
2115+
msvcrt.CRT_ERROR,
2116+
msvcrt.CRT_ASSERT):
2117+
msvcrt.CrtSetReportMode(report_type, old_modes[report_type])
2118+
2119+
return count
2120+
2121+
20652122
class SaveSignals:
20662123
"""
20672124
Save an restore signal handlers.

‎Lib/test/test_regrtest.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,24 @@ def test_main():
511511
""")
512512
self.check_leak(code, 'references')
513513

514+
@unittest.skipUnless(Py_DEBUG, 'need a debug build')
515+
def test_huntrleaks_fd_leak(self):
516+
# test --huntrleaks for file descriptor leak
517+
code = textwrap.dedent("""
518+
import os
519+
import unittest
520+
from test import support
521+
522+
class FDLeakTest(unittest.TestCase):
523+
def test_leak(self):
524+
fd = os.open(__file__, os.O_RDONLY)
525+
# bug: never close the file descriptor
526+
527+
def test_main():
528+
support.run_unittest(FDLeakTest)
529+
""")
530+
self.check_leak(code, 'file descriptors')
531+
514532
def test_list_tests(self):
515533
# test --list-tests
516534
tests = [self.create_test() for i in range(5)]

0 commit comments

Comments
 (0)