https://github.com/python/cpython/commit/86c17926b7c97024f162cde6a6e8f72424b3dd8a
commit: 86c17926b7c97024f162cde6a6e8f72424b3dd8a
branch: main
author: Victor Stinner <[email protected]>
committer: vstinner <[email protected]>
date: 2026-09-17T16:32:27+02:00
summary:

gh-154137, regrtest: Check for Windows handle leaks (#154140)

files:
A Misc/NEWS.d/next/Tests/2026-07-19-15-59-41.gh-issue-154137.N689bE.rst
M Lib/test/libregrtest/refleak.py
M Lib/test/test_regrtest.py

diff --git a/Lib/test/libregrtest/refleak.py b/Lib/test/libregrtest/refleak.py
index 45064115463c2fe..26b52e02aa237b3 100644
--- a/Lib/test/libregrtest/refleak.py
+++ b/Lib/test/libregrtest/refleak.py
@@ -114,12 +114,15 @@ def runtest_refleak(test_name, test_func,
     rc_deltas = array('q', [0]) * repcount
     alloc_deltas = array('q', [0]) * repcount
     fd_deltas = array('q', [0]) * repcount
+    handle_deltas = array('q', [0]) * repcount
     getallocatedblocks = sys.getallocatedblocks
     gettotalrefcount = sys.gettotalrefcount
     getunicodeinternedsize = sys.getunicodeinternedsize
     fd_count = os_helper.fd_count
+    handle_count = os_helper.handle_count
     # initialize variables to make pyflakes quiet
     rc_before = alloc_before = fd_before = interned_immortal_before = 0
+    handle_before = 0
 
     if not quiet:
         print("beginning", repcount, "repetitions. Showing number of leaks "
@@ -154,13 +157,17 @@ def runtest_refleak(test_name, test_func,
         alloc_after = getallocatedblocks() - interned_immortal_after
         rc_after = gettotalrefcount()
         fd_after = fd_count()
+        handle_after = handle_count()
 
         rc_deltas[i] = rc_after - rc_before
         alloc_deltas[i] = alloc_after - alloc_before
         fd_deltas[i] = fd_after - fd_before
+        handle_deltas[i] = handle_after - handle_before
 
         if not quiet:
-            total_leaks = max(rc_deltas[i], alloc_deltas[i], fd_deltas[i])
+            # use max, not sum, so total_leaks is one of the pooled ints
+            total_leaks = max(rc_deltas[i], alloc_deltas[i],
+                              fd_deltas[i], handle_deltas[i])
             if total_leaks <= 0:
                 symbol = '.'
             elif total_leaks < 10:
@@ -178,6 +185,7 @@ def runtest_refleak(test_name, test_func,
         alloc_before = alloc_after
         rc_before = rc_after
         fd_before = fd_after
+        handle_before = handle_after
         interned_immortal_before = interned_immortal_after
 
         restore_support_xml(xml_filename)
@@ -185,11 +193,21 @@ def runtest_refleak(test_name, test_func,
     if not quiet:
         print(file=sys.stderr)
 
+    if ('multiprocessing' in test_name
+        or 'concurrent_futures' in test_name):
+        # gh-154208: Disable check for Windows handle leaks when
+        # multiprocessing is used. There is a known race condition in
+        # multiprocessing causing handle leak. Disable the multiprocessing
+        # tests to be able to check for leaks for all other tests.
+        for i in range(len(handle_deltas)):
+            handle_deltas[i] = 0
+
     failed = False
     for raw_deltas, item_name in [
         (rc_deltas, 'references'),
         (alloc_deltas, 'memory blocks'),
-        (fd_deltas, 'file descriptors')
+        (fd_deltas, 'file descriptors'),
+        (handle_deltas, 'handles'),
     ]:
         # Ignore warmup runs; convert to a list for reporting
         deltas = list(raw_deltas[warmups:])
diff --git a/Lib/test/test_regrtest.py b/Lib/test/test_regrtest.py
index 71edec40ade8079..53f0a2863dc4e45 100644
--- a/Lib/test/test_regrtest.py
+++ b/Lib/test/test_regrtest.py
@@ -1457,6 +1457,35 @@ def test_leak(self):
         self.check_leak(code, 'file descriptors',
                         name='no_fd_leak', deltas=(1, -1, 0))
 
+    @unittest.skipUnless(support.Py_DEBUG, 'need a debug build')
+    @unittest.skipUnless(support.MS_WINDOWS, 'test specific to Windows')
+    def test_huntrleaks_handle_leak(self):
+        # test --huntrleaks for Windows handle leak
+        code = textwrap.dedent("""
+            import unittest
+            import _winapi
+
+            handle = None
+
+            class HandleLeakTest(unittest.TestCase):
+                def test_leak(self):
+                    global handle
+                    if handle is None:
+                        handle = _winapi.CreateFile(
+                                        __file__, _winapi.GENERIC_READ,
+                                        0, _winapi.NULL,
+                                        _winapi.OPEN_EXISTING,
+                                        0, _winapi.NULL)
+                    else:
+                        hproc = _winapi.GetCurrentProcess()
+                        copy = _winapi.DuplicateHandle(
+                                    hproc, handle,
+                                    hproc, 0, False,
+                                    _winapi.DUPLICATE_SAME_ACCESS)
+                    # bug! the new handle is never closed
+        """)
+        self.check_leak(code, 'handles')
+
     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-07-19-15-59-41.gh-issue-154137.N689bE.rst 
b/Misc/NEWS.d/next/Tests/2026-07-19-15-59-41.gh-issue-154137.N689bE.rst
new file mode 100644
index 000000000000000..39809e44f721856
--- /dev/null
+++ b/Misc/NEWS.d/next/Tests/2026-07-19-15-59-41.gh-issue-154137.N689bE.rst
@@ -0,0 +1 @@
+Check for Windows handle leaks in regrtest. Patch by Victor Stinner.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to