]> granicus.if.org Git - python/commitdiff
Issue #18174: "python -m test --huntrleaks ..." now also checks for leak of
authorVictor Stinner <victor.stinner@gmail.com>
Fri, 2 Oct 2015 22:20:56 +0000 (00:20 +0200)
committerVictor Stinner <victor.stinner@gmail.com>
Fri, 2 Oct 2015 22:20:56 +0000 (00:20 +0200)
file descriptors. Patch written by Richard Oudkerk.

Lib/test/libregrtest/refleak.py
Lib/test/test_regrtest.py
Misc/NEWS

index 9be0dec4d117d3058662413131c26c4e050c7ee3..59dc49fe77cc75ae353b59fb630eb9937d89e59e 100644 (file)
@@ -1,3 +1,4 @@
+import errno
 import os
 import re
 import sys
@@ -6,6 +7,36 @@ from inspect import isabstract
 from test import support
 
 
+try:
+    MAXFD = os.sysconf("SC_OPEN_MAX")
+except Exception:
+    MAXFD = 256
+
+
+def fd_count():
+    """Count the number of open file descriptors"""
+    if sys.platform.startswith(('linux', 'freebsd')):
+        try:
+            names = os.listdir("/proc/self/fd")
+            return len(names)
+        except FileNotFoundError:
+            pass
+
+    count = 0
+    for fd in range(MAXFD):
+        try:
+            # Prefer dup() over fstat(). fstat() can require input/output
+            # whereas dup() doesn't.
+            fd2 = os.dup(fd)
+        except OSError as e:
+            if e.errno != errno.EBADF:
+                raise
+        else:
+            os.close(fd2)
+            count += 1
+    return count
+
+
 def dash_R(the_module, test, indirect_test, huntrleaks):
     """Run a test multiple times, looking for reference leaks.
 
@@ -42,20 +73,25 @@ def dash_R(the_module, test, indirect_test, huntrleaks):
     repcount = nwarmup + ntracked
     rc_deltas = [0] * repcount
     alloc_deltas = [0] * repcount
+    fd_deltas = [0] * repcount
 
     print("beginning", repcount, "repetitions", file=sys.stderr)
     print(("1234567890"*(repcount//10 + 1))[:repcount], file=sys.stderr,
           flush=True)
     # initialize variables to make pyflakes quiet
-    rc_before = alloc_before = 0
+    rc_before = alloc_before = fd_before = 0
     for i in range(repcount):
         indirect_test()
-        alloc_after, rc_after = dash_R_cleanup(fs, ps, pic, zdc, abcs)
+        alloc_after, rc_after, fd_after = dash_R_cleanup(fs, ps, pic, zdc,
+                                                         abcs)
         print('.', end='', flush=True)
         if i >= nwarmup:
             rc_deltas[i] = rc_after - rc_before
             alloc_deltas[i] = alloc_after - alloc_before
-        alloc_before, rc_before = alloc_after, rc_after
+            fd_deltas[i] = fd_after - fd_before
+        alloc_before = alloc_after
+        rc_before = rc_after
+        fd_before = fd_after
     print(file=sys.stderr)
     # These checkers return False on success, True on failure
     def check_rc_deltas(deltas):
@@ -71,7 +107,8 @@ def dash_R(the_module, test, indirect_test, huntrleaks):
     failed = False
     for deltas, item_name, checker in [
         (rc_deltas, 'references', check_rc_deltas),
-        (alloc_deltas, 'memory blocks', check_alloc_deltas)]:
+        (alloc_deltas, 'memory blocks', check_alloc_deltas),
+        (fd_deltas, 'file descriptors', check_rc_deltas)]:
         if checker(deltas):
             msg = '%s leaked %s %s, sum=%s' % (
                 test, deltas[nwarmup:], item_name, sum(deltas))
@@ -151,7 +188,7 @@ def dash_R_cleanup(fs, ps, pic, zdc, abcs):
     func1 = sys.getallocatedblocks
     func2 = sys.gettotalrefcount
     gc.collect()
-    return func1(), func2()
+    return func1(), func2(), fd_count()
 
 
 def warm_caches():
index 0f5f22ca4e8a5707731e29af9e2c8f9255fcfb67..8b2449a0f5cf52e24dacdff18db87a42f1667ef6 100644 (file)
@@ -383,27 +383,32 @@ class BaseTestCase(unittest.TestCase):
         self.assertTrue(0 <= randseed <= 10000000, randseed)
         return randseed
 
-    def run_command(self, args, input=None, exitcode=0):
+    def run_command(self, args, input=None, exitcode=0, **kw):
         if not input:
             input = ''
+        if 'stderr' not in kw:
+            kw['stderr'] = subprocess.PIPE
         proc = subprocess.run(args,
                               universal_newlines=True,
                               input=input,
                               stdout=subprocess.PIPE,
-                              stderr=subprocess.PIPE)
+                              **kw)
         if proc.returncode != exitcode:
-            self.fail("Command %s failed with exit code %s\n"
-                      "\n"
-                      "stdout:\n"
-                      "---\n"
-                      "%s\n"
-                      "---\n"
-                      "\n"
-                      "stderr:\n"
-                      "---\n"
-                      "%s"
-                      "---\n"
-                      % (str(args), proc.returncode, proc.stdout, proc.stderr))
+            msg = ("Command %s failed with exit code %s\n"
+                   "\n"
+                   "stdout:\n"
+                   "---\n"
+                   "%s\n"
+                   "---\n"
+                   % (str(args), proc.returncode, proc.stdout))
+            if proc.stderr:
+                msg += ("\n"
+                        "stderr:\n"
+                        "---\n"
+                        "%s"
+                        "---\n"
+                        % proc.stderr)
+            self.fail(msg)
         return proc
 
 
@@ -637,6 +642,36 @@ class ArgsTestCase(BaseTestCase):
         output = self.run_tests('--forever', test, exitcode=1)
         self.check_executed_tests(output, [test]*3, failed=test)
 
+    def test_huntrleaks_fd_leak(self):
+        # test --huntrleaks for file descriptor leak
+        code = textwrap.dedent("""
+            import os
+            import unittest
+
+            class FDLeakTest(unittest.TestCase):
+                def test_leak(self):
+                    fd = os.open(__file__, os.O_RDONLY)
+                    # bug: never cloes the file descriptor
+        """)
+        test = self.create_test(code=code)
+
+        filename = 'reflog.txt'
+        self.addCleanup(support.unlink, filename)
+        output = self.run_tests('--huntrleaks', '3:3:', test,
+                                exitcode=1,
+                                stderr=subprocess.STDOUT)
+        self.check_executed_tests(output, [test], failed=test)
+
+        line = 'beginning 6 repetitions\n123456\n......\n'
+        self.check_line(output, re.escape(line))
+
+        line2 = '%s leaked [1, 1, 1] file descriptors, sum=3\n' % test
+        self.check_line(output, re.escape(line2))
+
+        with open(filename) as fp:
+            reflog = fp.read()
+            self.assertEqual(reflog, line2)
+
 
 if __name__ == '__main__':
     unittest.main()
index 8aad689cfdaefc4a931d8687126d5e274787daf5..5b708ea32e124f1666e97d24b2c25cdaf44a6865 100644 (file)
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -170,6 +170,9 @@ Documentation
 Tests
 -----
 
+- Issue #18174: ``python -m test --huntrleaks ...`` now also checks for leak of
+  file descriptors. Patch written by Richard Oudkerk.
+
 - Issue #25260: Fix ``python -m test --coverage`` on Windows. Remove the
   list of ignored directories.