From 590a26010d5d7f27890f89820645580bb8f28547 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 18 Mar 2024 20:15:20 +0100 Subject: gh-116869: Add test_cext test: build a C extension (#116954) --- Lib/test/test_cext/__init__.py | 92 ++++++++++++++++++++++ Lib/test/test_cext/extension.c | 80 +++++++++++++++++++ Lib/test/test_cext/setup.py | 47 +++++++++++ Makefile.pre.in | 1 + .../2024-03-18-10-58-47.gh-issue-116869.lN0GBl.rst | 2 + 5 files changed, 222 insertions(+) create mode 100644 Lib/test/test_cext/__init__.py create mode 100644 Lib/test/test_cext/extension.c create mode 100644 Lib/test/test_cext/setup.py create mode 100644 Misc/NEWS.d/next/C API/2024-03-18-10-58-47.gh-issue-116869.lN0GBl.rst diff --git a/Lib/test/test_cext/__init__.py b/Lib/test/test_cext/__init__.py new file mode 100644 index 0000000..302ea3d --- /dev/null +++ b/Lib/test/test_cext/__init__.py @@ -0,0 +1,92 @@ +# gh-116869: Build a basic C test extension to check that the Python C API +# does not emit C compiler warnings. + +import os.path +import shutil +import subprocess +import sysconfig +import unittest +from test import support + + +SOURCE = os.path.join(os.path.dirname(__file__), 'extension.c') +SETUP = os.path.join(os.path.dirname(__file__), 'setup.py') + + +# gh-110119: pip does not currently support 't' in the ABI flag use by +# --disable-gil builds. Once it does, we can remove this skip. +@unittest.skipIf(support.Py_GIL_DISABLED, + 'test does not work with --disable-gil') +@support.requires_subprocess() +@support.requires_resource('cpu') +class TestExt(unittest.TestCase): + def test_build_c99(self): + self.check_build('c99', '_test_c99_ext') + + def test_build_c11(self): + self.check_build('c11', '_test_c11_ext') + + # With MSVC, the linker fails with: cannot open file 'python311.lib' + # https://github.com/python/cpython/pull/32175#issuecomment-1111175897 + @unittest.skipIf(support.MS_WINDOWS, 'test fails on Windows') + # Building and running an extension in clang sanitizing mode is not + # straightforward + @unittest.skipIf( + '-fsanitize' in (sysconfig.get_config_var('PY_CFLAGS') or ''), + 'test does not work with analyzing builds') + # the test uses venv+pip: skip if it's not available + @support.requires_venv_with_pip() + def check_build(self, clang_std, extension_name): + venv_dir = 'env' + with support.setup_venv_with_pip_setuptools_wheel(venv_dir) as python_exe: + self._check_build(clang_std, extension_name, python_exe) + + def _check_build(self, clang_std, extension_name, python_exe): + pkg_dir = 'pkg' + os.mkdir(pkg_dir) + shutil.copy(SETUP, os.path.join(pkg_dir, os.path.basename(SETUP))) + shutil.copy(SOURCE, os.path.join(pkg_dir, os.path.basename(SOURCE))) + + def run_cmd(operation, cmd): + env = os.environ.copy() + env['CPYTHON_TEST_STD'] = clang_std + env['CPYTHON_TEST_EXT_NAME'] = extension_name + if support.verbose: + print('Run:', ' '.join(cmd)) + subprocess.run(cmd, check=True, env=env) + else: + proc = subprocess.run(cmd, + env=env, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True) + if proc.returncode: + print(proc.stdout, end='') + self.fail( + f"{operation} failed with exit code {proc.returncode}") + + # Build and install the C extension + cmd = [python_exe, '-X', 'dev', + '-m', 'pip', 'install', '--no-build-isolation', + os.path.abspath(pkg_dir)] + 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 extension + cmd = [python_exe, + '-X', 'dev', + '-X', 'showrefcount', + '-c', f"import {extension_name}"] + run_cmd('Import', cmd) + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_cext/extension.c b/Lib/test/test_cext/extension.c new file mode 100644 index 0000000..cfecad3 --- /dev/null +++ b/Lib/test/test_cext/extension.c @@ -0,0 +1,80 @@ +// gh-116869: Basic C test extension to check that the Python C API +// does not emit C compiler warnings. + +// Always enable assertions +#undef NDEBUG + +#include "Python.h" + +#if defined (__STDC_VERSION__) && __STDC_VERSION__ > 201710L +# define NAME _test_c2x_ext +#elif defined (__STDC_VERSION__) && __STDC_VERSION__ >= 201112L +# define NAME _test_c11_ext +#else +# define NAME _test_c99_ext +#endif + +#define _STR(NAME) #NAME +#define STR(NAME) _STR(NAME) + +PyDoc_STRVAR(_testcext_add_doc, +"add(x, y)\n" +"\n" +"Return the sum of two integers: x + y."); + +static PyObject * +_testcext_add(PyObject *Py_UNUSED(module), PyObject *args) +{ + long i, j; + if (!PyArg_ParseTuple(args, "ll:foo", &i, &j)) { + return NULL; + } + long res = i + j; + return PyLong_FromLong(res); +} + + +static PyMethodDef _testcext_methods[] = { + {"add", _testcext_add, METH_VARARGS, _testcext_add_doc}, + {NULL, NULL, 0, NULL} // sentinel +}; + + +static int +_testcext_exec(PyObject *module) +{ + if (PyModule_AddIntMacro(module, __STDC_VERSION__) < 0) { + return -1; + } + return 0; +} + +static PyModuleDef_Slot _testcext_slots[] = { + {Py_mod_exec, _testcext_exec}, + {0, NULL} +}; + + +PyDoc_STRVAR(_testcext_doc, "C test extension."); + +static struct PyModuleDef _testcext_module = { + PyModuleDef_HEAD_INIT, // m_base + STR(NAME), // m_name + _testcext_doc, // m_doc + 0, // m_size + _testcext_methods, // m_methods + _testcext_slots, // m_slots + NULL, // m_traverse + NULL, // m_clear + NULL, // m_free +}; + + +#define _FUNC_NAME(NAME) PyInit_ ## NAME +#define FUNC_NAME(NAME) _FUNC_NAME(NAME) + +PyMODINIT_FUNC +FUNC_NAME(NAME)(void) +{ + return PyModuleDef_Init(&_testcext_module); +} diff --git a/Lib/test/test_cext/setup.py b/Lib/test/test_cext/setup.py new file mode 100644 index 0000000..dd57a5f --- /dev/null +++ b/Lib/test/test_cext/setup.py @@ -0,0 +1,47 @@ +# gh-91321: Build a basic C test extension to check that the Python C API is +# compatible with C and does not emit C compiler warnings. +import os +import shlex +import sys +import sysconfig +from test import support + +from setuptools import setup, Extension + + +SOURCE = 'extension.c' +if not support.MS_WINDOWS: + # C compiler flags for GCC and clang + CFLAGS = [ + # 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', + ] +else: + # Don't pass any compiler flag to MSVC + CFLAGS = [] + + +def main(): + std = os.environ["CPYTHON_TEST_STD"] + name = os.environ["CPYTHON_TEST_EXT_NAME"] + cflags = [*CFLAGS, f'-std={std}'] + + # Remove existing -std options to only test ours + cmd = (sysconfig.get_config_var('CC') or '') + if cmd is not None: + cmd = shlex.split(cmd) + cmd = [arg for arg in cmd if not arg.startswith('-std=')] + cmd = shlex.join(cmd) + # CC env var overrides sysconfig CC variable in setuptools + os.environ['CC'] = cmd + + ext = Extension( + name, + sources=[SOURCE], + extra_compile_args=cflags) + setup(name='internal' + name, version='0.0', ext_modules=[ext]) + + +if __name__ == "__main__": + main() diff --git a/Makefile.pre.in b/Makefile.pre.in index 5958b6b..404e7ee 100644 --- a/Makefile.pre.in +++ b/Makefile.pre.in @@ -2330,6 +2330,7 @@ TESTSUBDIRS= idlelib/idle_test \ test/support/interpreters \ test/test_asyncio \ test/test_capi \ + test/test_cext \ test/test_concurrent_futures \ test/test_cppext \ test/test_ctypes \ diff --git a/Misc/NEWS.d/next/C API/2024-03-18-10-58-47.gh-issue-116869.lN0GBl.rst b/Misc/NEWS.d/next/C API/2024-03-18-10-58-47.gh-issue-116869.lN0GBl.rst new file mode 100644 index 0000000..71044b4 --- /dev/null +++ b/Misc/NEWS.d/next/C API/2024-03-18-10-58-47.gh-issue-116869.lN0GBl.rst @@ -0,0 +1,2 @@ +Add ``test_cext`` test: build a C extension to check if the Python C API +emits C compiler warnings. Patch by Victor Stinner. -- cgit v0.12