https://github.com/python/cpython/commit/dd78c513d289ec48f10e3f4a96e48e40bff49b66
commit: dd78c513d289ec48f10e3f4a96e48e40bff49b66
branch: 3.14
author: Victor Stinner <[email protected]>
committer: vstinner <[email protected]>
date: 2026-10-13T13:07:02Z
summary:

[3.14] gh-157649: Check for reference leak in test_cext (#159296) (#159314)

* [3.15] gh-157649: Check for reference leak in test_cext (#159296)

* 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.

(cherry picked from commit 795271306b50f85c6f60fbca1a056c5a0e28757b)

* gh-149044: Build test_cext with -Wall -Wextra (#158867)

(cherry picked from commit b22a8c9fd44855d7cc013eb49f689dfd396639a7)
(cherry picked from commit 8514386c4d40777509032c36f16f7e1529126c28)

* Fix refleak in test_datetime()

Only call PyDateTime_IMPORT once to initialize PyDateTimeAPI.

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

diff --git a/Lib/test/test_cext/__init__.py b/Lib/test/test_cext/__init__.py
index 74e11a0e857b2b..b4b33502472255 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
 
 
@@ -97,21 +98,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 cd729e7747635f..5dcac39f8750db 100644
--- a/Lib/test/test_cext/extension.c
+++ b/Lib/test/test_cext/extension.c
@@ -130,9 +130,11 @@ test_datetime(PyObject *Py_UNUSED(module), PyObject 
*Py_UNUSED(args))
 {
     // datetime.h is excluded from the limited C API
 #ifndef Py_LIMITED_API
-    PyDateTime_IMPORT;
-    if (PyErr_Occurred()) {
-        return NULL;
+    if (PyDateTimeAPI == NULL) {
+        PyDateTime_IMPORT;
+        if (PyErr_Occurred()) {
+            return NULL;
+        }
     }
 #endif
 
@@ -365,8 +367,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;
@@ -387,31 +387,8 @@ 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
-
+    // 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..e8288ee5e11e44
--- /dev/null
+++ b/Lib/test/test_cext/runtests.py
@@ -0,0 +1,68 @@
+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__',
+        '__STRICT_ANSI__',
+        '__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()
diff --git a/Lib/test/test_cext/setup.py b/Lib/test/test_cext/setup.py
index 1a5e59d50777d6..60337064a72a5c 100644
--- a/Lib/test/test_cext/setup.py
+++ b/Lib/test/test_cext/setup.py
@@ -21,6 +21,8 @@
 if not MSVC:
     # C compiler flags for GCC and clang
     BASE_CFLAGS = [
+        '-Wall',
+        '-Wextra',
         # The purpose of test_cext extension is to check that building a C
         # extension using the Python C API does not emit C compiler warnings.
         '-Werror',
@@ -42,7 +44,11 @@
             # with the -Werror=declaration-after-statement compiler flag.
             '-Werror=declaration-after-statement',
         )
-    INTERNAL_CFLAGS = [*BASE_CFLAGS]
+    INTERNAL_CFLAGS = [
+        *BASE_CFLAGS,
+        # Do not complain about unused parameteres in the internal C API
+        '-Wno-unused-parameter',
+    ]
 else:
     # MSVC compiler flags
     BASE_CFLAGS = [
@@ -65,14 +71,16 @@
 
 if not MSVC:
     # C++ compiler flags for GCC and clang
-    CPPFLAGS = [
+    CXXFLAGS = [
+        '-Wall',
+        '-Wextra',
         # gh-91321: The purpose of _testcppext extension is to check that 
building
         # a C++ extension using the Python C API does not emit C++ compiler
         # warnings
         '-Werror',
     ]
 
-    CPPFLAGS_PEDANTIC = [
+    PEDANTIC_CXXFLAGS = [
         # Ask for strict(er) compliance with the standard.
         # We cannot do this for c++03 unlimited API, since several headers in
         # Include/cpython/ use commas at end of `enum` declarations, a C++11
@@ -82,15 +90,20 @@
         # We also use `long long`, a C++11 feature we can enable individually.
         '-Wno-long-long',
     ]
+    INTERNAL_CXXFLAGS = [
+        # Do not complain about unused parameteres in the internal C API
+        '-Wno-unused-parameter',
+    ]
 else:
     # MSVC compiler flags
-    CPPFLAGS = [
+    CXXFLAGS = [
         # Display warnings level 1 to 4
         '/W4',
         # Treat all compiler warnings as compiler errors
         '/WX',
     ]
-    CPPFLAGS_PEDANTIC = []
+    PEDANTIC_CXXFLAGS = []
+    INTERNAL_CXXFLAGS = []
 
 
 def main():
@@ -103,10 +116,13 @@ def main():
     libdirs = os.environ.get("CPYTHON_TEST_EXTRA_LIBDIRS", "")
     extra_cflags = os.environ.get("CPYTHON_TEST_EXTRA_CFLAGS", "")
 
+    if language not in ('C', 'C++'):
+        raise ValueError(f"invalid language: {language}")
+
     source = SOURCE[language]
 
     if language == 'C++':
-        flags = list(CPPFLAGS)
+        flags = list(CXXFLAGS)
     else:
         if not internal:
             flags = list(PUBLIC_CFLAGS)
@@ -121,9 +137,13 @@ def main():
         else:
             flags.append(f'-std={std}')
 
-    if language == 'C++' and (limited or (std != 'c++03') and not internal):
-        # See CPPFLAGS_PEDANTIC docstring
-        flags.extend(CPPFLAGS_PEDANTIC)
+    if language == 'C++':
+        if limited or (std != 'c++03') and not internal:
+            # See PEDANTIC_CXXFLAGS comment
+            flags.extend(PEDANTIC_CXXFLAGS)
+        elif internal:
+            flags.extend(INTERNAL_CXXFLAGS)
+
 
     # gh-105776: When "gcc -std=11" is used as the C++ compiler, -std=c11
     # option emits a C++ compiler warning. Remove "-std11" option from the

_______________________________________________
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