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]