From 71bcc9d0b8a886a1ed22c52aa8debd4bb926f151 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 25 Sep 2026 18:13:09 +0200 Subject: [PATCH 1/2] gh-157649: Check for reference leak in test_cext 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. --- Lib/test/test_cext/__init__.py | 20 ++++--------- Lib/test/test_cext/extension.c | 36 +++++------------------ Lib/test/test_cext/runtests.py | 53 ++++++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 43 deletions(-) create mode 100644 Lib/test/test_cext/runtests.py diff --git a/Lib/test/test_cext/__init__.py b/Lib/test/test_cext/__init__.py index 457925413c94ec8..c4fd2a1e044d892 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 038f1a2af46c671..59a92991db2f929 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,6 @@ 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); -#endif - return 0; } diff --git a/Lib/test/test_cext/runtests.py b/Lib/test/test_cext/runtests.py new file mode 100644 index 000000000000000..b8711bab7fcdde1 --- /dev/null +++ b/Lib/test/test_cext/runtests.py @@ -0,0 +1,53 @@ +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) + if verbose: + print(f"{name}()") + func() + + print("add()") + if testmod.add(11, 23) != 34: + raise AssertionError("add() failed badly") + + if verbose: + 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) + + 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() From 6a110c0b3bbd2f56028ad5224c5c546856a0b6e7 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sat, 26 Sep 2026 14:26:26 +0200 Subject: [PATCH 2/2] Log _MSVC_LANG on MSVC --- Lib/test/test_cext/extension.c | 7 +++++++ Lib/test/test_cext/runtests.py | 18 ++++++++++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_cext/extension.c b/Lib/test/test_cext/extension.c index 59a92991db2f929..b56c4dbe78a3d85 100644 --- a/Lib/test/test_cext/extension.c +++ b/Lib/test/test_cext/extension.c @@ -373,6 +373,13 @@ module_exec(PyObject *module) return -1; } #endif +#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 index b8711bab7fcdde1..86aab6712004933 100644 --- a/Lib/test/test_cext/runtests.py +++ b/Lib/test/test_cext/runtests.py @@ -9,16 +9,14 @@ def run_tests(testmod, verbose=True): if not name.startswith('test'): continue func = getattr(testmod, name) - if verbose: - print(f"{name}()") + print(f"{name}()") func() print("add()") if testmod.add(11, 23) != 34: raise AssertionError("add() failed badly") - if verbose: - print(flush=True) + print(flush=True) def main(): @@ -29,6 +27,18 @@ def main(): 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.