https://github.com/python/cpython/commit/795271306b50f85c6f60fbca1a056c5a0e28757b
commit: 795271306b50f85c6f60fbca1a056c5a0e28757b
branch: main
author: Victor Stinner <[email protected]>
committer: vstinner <[email protected]>
date: 2026-09-26T16:22:14+02:00
summary:

gh-157649: Check for reference leak in test_cext (#158227)

test_cext now discovers and calls all "test" functions. On a debug
build, it also checks for reference leaks.

Fix a reference leak in test_virtual_object(): use a dealloc function
which calls Py_DECREF() on the type, and trigger an explicit garbage
collection.

files:
A Lib/test/test_cext/runtests.py
M Lib/test/test_cext/__init__.py
M Lib/test/test_cext/extension.c

diff --git a/Lib/test/test_cext/__init__.py b/Lib/test/test_cext/__init__.py
index 457925413c94ec..c4fd2a1e044d89 100644
--- a/Lib/test/test_cext/__init__.py
+++ b/Lib/test/test_cext/__init__.py
@@ -26,6 +26,7 @@
     os.path.join(SOURCE_DIR, 'extension.cpp'),
     os.path.join(SOURCE_DIR, 'setup.py'),
 ]
+RUNTESTS_PY = os.path.join(SOURCE_DIR, 'runtests.py')
 MSVC = support.MS_WINDOWS
 
 
@@ -99,21 +100,10 @@ def run_cmd(operation, cmd):
             cmd.append('-v')
         run_cmd('Install', cmd)
 
-        # Do a reference run. Until we test that running python
-        # doesn't leak references (gh-94755), run it so one can manually check
-        # -X showrefcount results against this baseline.
-        cmd = [python_exe,
-               '-X', 'dev',
-               '-X', 'showrefcount',
-               '-c', 'pass']
-        run_cmd('Reference run', cmd)
-
-        # Import the C/C++ extension
-        cmd = [python_exe,
-               '-X', 'dev',
-               '-X', 'showrefcount',
-               '-c', f"import {extension_name}"]
-        run_cmd('Import', cmd)
+        # Import the extension module and run tests.
+        # On a debug build, check also for reference leaks.
+        cmd = [python_exe, '-X', 'dev', RUNTESTS_PY, extension_name]
+        run_cmd('Tests', cmd)
 
 
 class TestPublicC(BaseTests, unittest.TestCase):
diff --git a/Lib/test/test_cext/extension.c b/Lib/test/test_cext/extension.c
index 038f1a2af46c67..b56c4dbe78a3d8 100644
--- a/Lib/test/test_cext/extension.c
+++ b/Lib/test/test_cext/extension.c
@@ -264,8 +264,11 @@ class VirtualPyObject : public PyObject {
 public:
     VirtualPyObject();
     virtual ~VirtualPyObject() {
+        PyTypeObject *type = Py_TYPE(this);
         delete [] internal_data;
         --instance_count;
+        // Do not call type->tp_free(this), C++ manages the memory
+        Py_DECREF(type);
     }
     virtual void set_internal_data() {
         internal_data[0] = 1;
@@ -295,7 +298,7 @@ _Py_COMP_DIAG_PUSH
 #endif
 
 PyType_Slot VirtualPyObject_Slots[] = {
-    {Py_tp_free, (void*)VirtualPyObject::dealloc},
+    {Py_tp_dealloc, (void*)VirtualPyObject::dealloc},
     {0, _Py_NULL},
 };
 
@@ -333,6 +336,10 @@ test_virtual_object(PyObject *Py_UNUSED(module), PyObject 
*Py_UNUSED(args))
             "instance_count should be 0, got %d",
             VirtualPyObject::instance_count);
     }
+
+    // Force a garbage collection to delete the temporary heap type
+    // used by this test
+    PyGC_Collect();
     Py_RETURN_NONE;
 }
 #endif  // __cplusplus && !Py_TARGET_ABI3T
@@ -356,8 +363,6 @@ static PyMethodDef module_methods[] = {
 static int
 module_exec(PyObject *module)
 {
-    PyObject *result;
-
 #ifdef __STDC_VERSION__
     if (PyModule_AddIntMacro(module, __STDC_VERSION__) < 0) {
         return -1;
@@ -368,31 +373,13 @@ module_exec(PyObject *module)
         return -1;
     }
 #endif
-
-    result = PyObject_CallMethod(module, "test_macros", "");
-    if (!result) return -1;
-    Py_DECREF(result);
-
-    result = PyObject_CallMethod(module, "test_datetime", "");
-    if (!result) return -1;
-    Py_DECREF(result);
-
-    result = PyObject_CallMethod(module, "test_unicode", "");
-    if (!result) return -1;
-    Py_DECREF(result);
-
-#ifdef __cplusplus
-    result = PyObject_CallMethod(module, "test_api_casts", "");
-    if (!result) return -1;
-    Py_DECREF(result);
-#endif
-
-#if defined(__cplusplus) && !defined(Py_TARGET_ABI3T)
-    result = PyObject_CallMethod(module, "test_virtual_object", "");
-    if (!result) return -1;
-    Py_DECREF(result);
+#ifdef _MSVC_LANG
+    if (PyModule_AddIntMacro(module, _MSVC_LANG) < 0) {
+        return -1;
+    }
 #endif
-
+    // Ignore "unused argument" warning when none of these macros is defined
+    (void)module;
     return 0;
 }
 
diff --git a/Lib/test/test_cext/runtests.py b/Lib/test/test_cext/runtests.py
new file mode 100644
index 00000000000000..86aab671200493
--- /dev/null
+++ b/Lib/test/test_cext/runtests.py
@@ -0,0 +1,63 @@
+import array
+import gc
+import importlib
+import sys
+
+
+def run_tests(testmod, verbose=True):
+    for name in dir(testmod):
+        if not name.startswith('test'):
+            continue
+        func = getattr(testmod, name)
+        print(f"{name}()")
+        func()
+
+    print("add()")
+    if testmod.add(11, 23) != 34:
+        raise AssertionError("add() failed badly")
+
+    print(flush=True)
+
+
+def main():
+    if len(sys.argv) < 2:
+        print("usage: python runtests.py TEST_MODULE_NAME")
+        sys.exit(1)
+    module_name = sys.argv[1]
+
+    testmod = importlib.import_module(module_name)
+
+    newline = False
+    for name in ('__STDC_VERSION__', '__cplusplus', '_MSVC_LANG'):
+        try:
+            value = getattr(testmod, name)
+        except AttributeError:
+            pass
+        else:
+            print(f'{name}: {value}')
+            newline = True
+    if newline:
+        print()
+
+    if hasattr(sys, 'gettotalrefcount'):
+        # First run to warm up Python. For example, test_datetime() imports
+        # the datetime module.
+        run_tests(testmod, verbose=False)
+
+        refcount = array.array('q', [0, 0])
+        gc.collect()
+
+        # Check for reference leak
+        refcount[0] = sys.gettotalrefcount()
+        run_tests(testmod)
+        refcount[1] = sys.gettotalrefcount()
+
+        diff = refcount[1] - refcount[0]
+        if diff >= 1:
+            raise AssertionError(f'Tests leaked {diff} references')
+    else:
+        run_tests(testmod)
+
+
+if __name__ == "__main__":
+    main()

_______________________________________________
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