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]