From 41a20a91d07483d130e60ef18a7dd8fcda84afd1 Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Fri, 24 Apr 2026 15:27:13 -0400 Subject: [PATCH 1/4] Build cython tests as part of the main test suite --- .github/workflows/build-wheel.yml | 4 +- cuda_bindings/AGENTS.md | 3 - cuda_bindings/README.md | 11 -- cuda_bindings/pixi.toml | 7 -- cuda_bindings/tests/cython/build_tests.bat | 10 -- cuda_bindings/tests/cython/build_tests.sh | 18 --- cuda_bindings/tests/cython/test_cython.py | 134 +++++++++++++++++---- cuda_core/AGENTS.md | 3 - cuda_core/README.md | 11 -- cuda_core/pixi.toml | 4 +- cuda_core/tests/cython/build_tests.sh | 18 --- cuda_core/tests/cython/test_cython.py | 134 +++++++++++++++++---- scripts/run_tests.sh | 10 +- 13 files changed, 227 insertions(+), 140 deletions(-) delete mode 100644 cuda_bindings/tests/cython/build_tests.bat delete mode 100755 cuda_bindings/tests/cython/build_tests.sh delete mode 100755 cuda_core/tests/cython/build_tests.sh diff --git a/.github/workflows/build-wheel.yml b/.github/workflows/build-wheel.yml index fbb8d092b06..4d7d11ece4d 100644 --- a/.github/workflows/build-wheel.yml +++ b/.github/workflows/build-wheel.yml @@ -350,7 +350,7 @@ jobs: run: | pip install ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }}/*.whl --group ./cuda_bindings/pyproject.toml:test pushd ${{ env.CUDA_BINDINGS_CYTHON_TESTS_DIR }} - bash build_tests.sh + python test_cython.py popd - name: Upload cuda.bindings Cython tests @@ -364,7 +364,7 @@ jobs: run: | pip install ${{ env.CUDA_CORE_ARTIFACTS_DIR }}/"cu${BUILD_CUDA_MAJOR}"/*.whl --group ./cuda_core/pyproject.toml:test pushd ${{ env.CUDA_CORE_CYTHON_TESTS_DIR }} - bash build_tests.sh + python test_cython.py popd - name: Upload cuda.core Cython tests diff --git a/cuda_bindings/AGENTS.md b/cuda_bindings/AGENTS.md index 9688c9f94ca..330cd7b776b 100644 --- a/cuda_bindings/AGENTS.md +++ b/cuda_bindings/AGENTS.md @@ -39,9 +39,6 @@ subpackage in the `cuda-python` monorepo. ## Testing expectations - **Primary tests**: `pytest tests/` -- **Cython tests**: - - build: `tests/cython/build_tests.sh` (or platform equivalent) - - run: `pytest tests/cython/` - **Examples**: example coverage is pytest-based under `examples/`. - **Benchmarks**: run with `pytest --benchmark-only benchmarks/` when needed. - **Orchestrated run**: from repo root, `scripts/run_tests.sh bindings`. diff --git a/cuda_bindings/README.md b/cuda_bindings/README.md index 2a18f5a2df2..c7abb471737 100644 --- a/cuda_bindings/README.md +++ b/cuda_bindings/README.md @@ -37,17 +37,6 @@ To run these tests: * `python -m pytest tests/` against editable installations * `pytest tests/` against installed packages -### Cython Unit Tests - -Cython tests are located in `tests/cython` and need to be built. These builds have the same CUDA Toolkit header requirements as [Installing from Source](https://nvidia.github.io/cuda-python/cuda-bindings/latest/install.html#requirements) where the major.minor version must match `cuda.bindings`. To build them: - -1. Setup environment variable `CUDA_PATH` (or `CUDA_HOME`) with the path to the CUDA Toolkit installation. Note: If both are set, `CUDA_PATH` takes precedence. -2. Run `build_tests` script located in `test/cython` appropriate to your platform. This will both cythonize the tests and build them. - -To run these tests: -* `python -m pytest tests/cython/` against editable installations -* `pytest tests/cython/` against installed packages - ### Samples Various [CUDA Samples](https://github.com/NVIDIA/cuda-samples/tree/master) that were rewritten using CUDA Python are located in `examples`. diff --git a/cuda_bindings/pixi.toml b/cuda_bindings/pixi.toml index 0d46eee8fcb..25022b67699 100644 --- a/cuda_bindings/pixi.toml +++ b/cuda_bindings/pixi.toml @@ -151,12 +151,6 @@ libnvfatbin = "*" [package.target.linux.run-dependencies] libcufile = "*" -[target.linux.tasks.build-cython-tests] -cmd = ["$PIXI_PROJECT_ROOT/tests/cython/build_tests.sh"] - -[target.win-64.tasks.build-cython-tests] -cmd = ["$PIXI_PROJECT_ROOT/tests/cython/build_tests.bat"] - [target.linux.tasks.test] cmd = [ "pytest", @@ -164,7 +158,6 @@ cmd = [ "--override-ini", "norecursedirs=examples", # include cython tests (ignore by default config) ] -depends-on = [{ task = "build-cython-tests" }] [target.linux.tasks.build-docs] cmd = [ diff --git a/cuda_bindings/tests/cython/build_tests.bat b/cuda_bindings/tests/cython/build_tests.bat deleted file mode 100644 index e1bf73af170..00000000000 --- a/cuda_bindings/tests/cython/build_tests.bat +++ /dev/null @@ -1,10 +0,0 @@ -@echo off - -REM SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -REM SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE - -setlocal - set CL=%CL% /I"%CUDA_HOME%\include" - REM Use -j 1 to side-step any process-pool issues and ensure deterministic single-threaded builds - cythonize -3 -j 1 -i -Xfreethreading_compatible=True %~dp0test_*.pyx -endlocal diff --git a/cuda_bindings/tests/cython/build_tests.sh b/cuda_bindings/tests/cython/build_tests.sh deleted file mode 100755 index c2ddc9ea79d..00000000000 --- a/cuda_bindings/tests/cython/build_tests.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash - -# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE - -UNAME=$(uname) -if [ "$UNAME" == "Linux" ] ; then - SCRIPTPATH=$(dirname $(realpath "$0")) - export CPLUS_INCLUDE_PATH=$CUDA_HOME/include:$CPLUS_INCLUDE_PATH -elif [[ "$UNAME" == CYGWIN* || "$UNAME" == MINGW* || "$UNAME" == MSYS* ]] ; then - SCRIPTPATH="$(dirname $(cygpath -w $(realpath "$0")))" - export CL="/I\"${CUDA_HOME}\\include\" ${CL}" -else - exit 1 -fi - -# Use -j 1 to side-step any process-pool issues and ensure deterministic single-threaded builds -cythonize -3 -j 1 -i -Xfreethreading_compatible=True ${SCRIPTPATH}/test_*.pyx diff --git a/cuda_bindings/tests/cython/test_cython.py b/cuda_bindings/tests/cython/test_cython.py index 3e14b48e0ff..4f498bc9ea3 100644 --- a/cuda_bindings/tests/cython/test_cython.py +++ b/cuda_bindings/tests/cython/test_cython.py @@ -1,36 +1,122 @@ -# SPDX-FileCopyrightText: Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE -import functools import importlib -import sys +import os +import re +from pathlib import Path +import pytest +from Cython.Build import cythonize +from setuptools import Extension +from setuptools.dist import Distribution -def py_func(func): - """ - Wraps func in a plain Python function. - """ +TESTS_DIR = Path(__file__).resolve().parent +CYTHON_TEST_MODULES = ["test_ccuda", "test_ccudart", "test_interoperability_cython"] +TEST_NAME_RE = re.compile(r"^\s*def\s+(test_[A-Za-z0-9_]+)\s*\(") - @functools.wraps(func) - def wrapped(*args, **kwargs): - return func(*args, **kwargs) +def _get_cuda_include_dir(): + cuda_home = os.environ.get("CUDA_PATH") or os.environ.get("CUDA_HOME") + if cuda_home: + return Path(cuda_home) / "include" + return None + + +def build_cython_test_modules(): + include_dirs = [] + cuda_include_dir = _get_cuda_include_dir() + if cuda_include_dir: + include_dirs.append(str(cuda_include_dir)) + + extensions = [ + Extension( + name=module_name, + sources=[str(TESTS_DIR / f"{module_name}.pyx")], + include_dirs=include_dirs, + ) + for module_name in CYTHON_TEST_MODULES + ] + + ext_modules = cythonize( + extensions, + compiler_directives={"language_level": "3", "freethreading_compatible": True}, + nthreads=1, + ) + + distribution = Distribution( + { + "name": "cuda-bindings-cython-tests", + "ext_modules": ext_modules, + } + ) + build_ext = distribution.get_command_obj("build_ext") + build_ext.inplace = True + build_ext.build_temp = str(TESTS_DIR / "build" / "temp") + distribution.run_command("build_ext") + + +def _import_cython_test_modules(rebuild_if_needed): + imported_modules = {} + build_attempted = False + + for module_name in CYTHON_TEST_MODULES: + try: + imported_modules[module_name] = importlib.import_module(module_name) + except ImportError: + if not rebuild_if_needed: + raise + + if not build_attempted: + build_cython_test_modules() + importlib.invalidate_caches() + build_attempted = True + + imported_modules[module_name] = importlib.import_module(module_name) + + return imported_modules + + +def _discover_test_function_names(module_name): + module_source = TESTS_DIR / f"{module_name}.pyx" + test_names = [] + + with module_source.open(encoding="utf-8") as f: + for line in f: + match = TEST_NAME_RE.match(line) + if match: + test_names.append(match.group(1)) + + return test_names + + +@pytest.fixture(scope="session") +def cython_test_modules(): + return _import_cython_test_modules(rebuild_if_needed=True) + + +def _make_wrapped_test(module_name, test_name): + def wrapped(cython_test_modules): + test_func = getattr(cython_test_modules[module_name], test_name) + return test_func() + + wrapped.__name__ = test_name + wrapped.__module__ = __name__ return wrapped -cython_test_modules = ["test_ccuda", "test_ccudart", "test_interoperability_cython"] +registered_tests = set() +for module_name in CYTHON_TEST_MODULES: + for test_name in _discover_test_function_names(module_name): + if test_name in registered_tests: + raise RuntimeError(f"duplicate cython test name discovered: {test_name}") + registered_tests.add(test_name) + globals()[test_name] = _make_wrapped_test(module_name, test_name) + + +def main(): + build_cython_test_modules() -for mod in cython_test_modules: - try: - # For each callable in `mod` with name `test_*`, - # wrap the callable in a plain Python function - # and set the result as an attribute of this module. - mod = importlib.import_module(mod) - for name in dir(mod): - item = getattr(mod, name) - if callable(item) and name.startswith("test_"): - item = py_func(item) - setattr(sys.modules[__name__], name, item) - except ImportError: - raise +if __name__ == "__main__": + main() diff --git a/cuda_core/AGENTS.md b/cuda_core/AGENTS.md index 357e228360d..15f6fa04ae2 100644 --- a/cuda_core/AGENTS.md +++ b/cuda_core/AGENTS.md @@ -35,9 +35,6 @@ This file describes `cuda_core`, the high-level Pythonic CUDA subpackage in the ## Testing expectations - **Primary tests**: `pytest tests/` -- **Cython tests**: - - build: `tests/cython/build_tests.sh` (or platform equivalent) - - run: `pytest tests/cython/` - **Examples**: validate affected examples in `examples/` when changing user workflows or public APIs. - **Orchestrated run**: from repo root, `scripts/run_tests.sh core`. diff --git a/cuda_core/README.md b/cuda_core/README.md index 7959dfb00bb..8749eeb39aa 100644 --- a/cuda_core/README.md +++ b/cuda_core/README.md @@ -29,14 +29,3 @@ Alternatively, from the repository root you can use a simple script: * `./scripts/run_tests.sh core` to run only `cuda_core` tests * `./scripts/run_tests.sh` to run all package tests (pathfinder → bindings → core) * `./scripts/run_tests.sh smoke` to run meta-level smoke tests under `tests/integration` - -### Cython Unit Tests - -Cython tests are located in `tests/cython` and need to be built. These builds have the same CUDA Toolkit header requirements as [those of cuda.bindings](https://nvidia.github.io/cuda-python/cuda-bindings/latest/install.html#requirements) where the major.minor version must match `cuda.bindings`. To build them: - -1. Set up environment variable `CUDA_PATH` (or `CUDA_HOME`) with the path to the CUDA Toolkit installation. Note: If both are set, `CUDA_PATH` takes precedence. -2. Run `build_tests` script located in `tests/cython` appropriate to your platform. This will both cythonize the tests and build them. - -To run these tests: -* `python -m pytest tests/cython/` with editable installations -* `pytest tests/cython/` with installed packages diff --git a/cuda_core/pixi.toml b/cuda_core/pixi.toml index 1008fe9711f..9377c81e9f1 100644 --- a/cuda_core/pixi.toml +++ b/cuda_core/pixi.toml @@ -187,7 +187,7 @@ cuda-bindings = "*" cuda-pathfinder = "*" [target.linux.tasks.build-cython-tests] -cmd = ["$PIXI_PROJECT_ROOT/tests/cython/build_tests.sh"] +cmd = ["python", "$PIXI_PROJECT_ROOT/tests/cython/test_cython.py"] [target.linux.tasks.docs-build] cmd = ["$PIXI_PROJECT_ROOT/docs/build_docs.sh"] @@ -203,7 +203,7 @@ env = { SPHINXOPTS = "-v -j 1 -d build/.doctrees" } default-environment = "docs" [target.win-64.tasks.build-cython-tests] -cmd = ["$PIXI_PROJECT_ROOT/tests/cython/build_tests.bat"] +cmd = ["python", "$PIXI_PROJECT_ROOT/tests/cython/test_cython.py"] [target.linux.tasks.test] cmd = [ diff --git a/cuda_core/tests/cython/build_tests.sh b/cuda_core/tests/cython/build_tests.sh deleted file mode 100755 index 3e20136133a..00000000000 --- a/cuda_core/tests/cython/build_tests.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash - -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -UNAME=$(uname) -if [ "$UNAME" == "Linux" ] ; then - SCRIPTPATH=$(dirname $(realpath "$0")) - export CPLUS_INCLUDE_PATH=${SCRIPTPATH}/../../cuda/core/_include:$CUDA_HOME/include:$CPLUS_INCLUDE_PATH -elif [[ "$UNAME" == CYGWIN* || "$UNAME" == MINGW* || "$UNAME" == MSYS* ]] ; then - SCRIPTPATH="$(dirname $(cygpath -w $(realpath "$0")))" - CUDA_CORE_INCLUDE_PATH=$(echo "${SCRIPTPATH}\..\..\cuda\core\_include" | sed 's/\\/\\\\/g') - export CL="/I\"${CUDA_CORE_INCLUDE_PATH}\" /I\"${CUDA_HOME}\\include\" ${CL}" -else - exit 1 -fi - -cythonize -3 -i -Xfreethreading_compatible=True ${SCRIPTPATH}/test_*.pyx diff --git a/cuda_core/tests/cython/test_cython.py b/cuda_core/tests/cython/test_cython.py index 87b97e8a2a4..69b46d315a8 100644 --- a/cuda_core/tests/cython/test_cython.py +++ b/cuda_core/tests/cython/test_cython.py @@ -1,39 +1,121 @@ -# SPDX-FileCopyrightText: Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # SPDX-License-Identifier: Apache-2.0 -import functools import importlib -import sys +import os +import re +from pathlib import Path +import pytest +from Cython.Build import cythonize +from setuptools import Extension +from setuptools.dist import Distribution -def py_func(func): - """ - Wraps func in a plain Python function. - """ +TESTS_DIR = Path(__file__).resolve().parent +CYTHON_TEST_MODULES = [ + "test_get_cuda_native_handle", +] +TEST_NAME_RE = re.compile(r"^\s*def\s+(test_[A-Za-z0-9_]+)\s*\(") + + +def _get_include_dirs(): + include_dirs = [str((TESTS_DIR / "../../cuda/core/_include").resolve())] + cuda_home = os.environ.get("CUDA_PATH") or os.environ.get("CUDA_HOME") + if cuda_home: + include_dirs.append(str((Path(cuda_home) / "include").resolve())) + return include_dirs + + +def build_cython_test_modules(): + extensions = [ + Extension( + name=module_name, + sources=[str(TESTS_DIR / f"{module_name}.pyx")], + include_dirs=_get_include_dirs(), + ) + for module_name in CYTHON_TEST_MODULES + ] + + ext_modules = cythonize( + extensions, + compiler_directives={"language_level": "3", "freethreading_compatible": True}, + nthreads=1, + ) + + distribution = Distribution( + { + "name": "cuda-core-cython-tests", + "ext_modules": ext_modules, + } + ) + build_ext = distribution.get_command_obj("build_ext") + build_ext.inplace = True + build_ext.build_temp = str(TESTS_DIR / "build" / "temp") + distribution.run_command("build_ext") + + +def _import_cython_test_modules(rebuild_if_needed): + imported_modules = {} + build_attempted = False + + for module_name in CYTHON_TEST_MODULES: + try: + imported_modules[module_name] = importlib.import_module(module_name) + except ImportError: + if not rebuild_if_needed: + raise + + if not build_attempted: + build_cython_test_modules() + importlib.invalidate_caches() + build_attempted = True + + imported_modules[module_name] = importlib.import_module(module_name) - @functools.wraps(func) - def wrapped(*args, **kwargs): - return func(*args, **kwargs) + return imported_modules + +def _discover_test_function_names(module_name): + module_source = TESTS_DIR / f"{module_name}.pyx" + test_names = [] + + with module_source.open(encoding="utf-8") as f: + for line in f: + match = TEST_NAME_RE.match(line) + if match: + test_names.append(match.group(1)) + + return test_names + + +@pytest.fixture(scope="session") +def cython_test_modules(): + return _import_cython_test_modules(rebuild_if_needed=True) + + +def _make_wrapped_test(module_name, test_name): + def wrapped(cython_test_modules): + test_func = getattr(cython_test_modules[module_name], test_name) + return test_func() + + wrapped.__name__ = test_name + wrapped.__module__ = __name__ return wrapped -cython_test_modules = [ - "test_get_cuda_native_handle", -] +registered_tests = set() +for module_name in CYTHON_TEST_MODULES: + for test_name in _discover_test_function_names(module_name): + if test_name in registered_tests: + raise RuntimeError(f"duplicate cython test name discovered: {test_name}") + registered_tests.add(test_name) + globals()[test_name] = _make_wrapped_test(module_name, test_name) + + +def main(): + build_cython_test_modules() -for mod in cython_test_modules: - try: - # For each callable in `mod` with name `test_*`, - # wrap the callable in a plain Python function - # and set the result as an attribute of this module. - mod = importlib.import_module(mod) - for name in dir(mod): - item = getattr(mod, name) - if callable(item) and name.startswith("test_"): - item = py_func(item) - setattr(sys.modules[__name__], name, item) - except ImportError: - raise +if __name__ == "__main__": + main() diff --git a/scripts/run_tests.sh b/scripts/run_tests.sh index e53cc7a3903..ec552c0ae37 100755 --- a/scripts/run_tests.sh +++ b/scripts/run_tests.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 set -euo pipefail @@ -211,9 +211,9 @@ run_bindings() { add_result "bindings-examples" "${rc_ex}" fi if [ ${RUN_CYTHON} -eq 1 ] && [ -d tests/cython ]; then - if [ -x tests/cython/build_tests.sh ]; then + if [ -f tests/cython/test_cython.py ]; then echo "[build] cuda_bindings cython tests" - ( cd tests/cython && ./build_tests.sh ) || true + ( cd tests/cython && python test_cython.py ) || true fi run_pytest tests/cython local rc_cy=$? @@ -236,12 +236,12 @@ run_core() { add_result "core-examples" "${rc_ex}" fi if [ ${RUN_CYTHON} -eq 1 ] && [ -d tests/cython ]; then - if [ -x tests/cython/build_tests.sh ]; then + if [ -f tests/cython/test_cython.py ]; then echo "[build] cuda_core cython tests" if [ -z "${CUDA_HOME-}" ]; then echo "[skip] CUDA_HOME not set; skipping cython tests" else - ( cd tests/cython && ./build_tests.sh ) || true + ( cd tests/cython && python test_cython.py ) || true fi fi run_pytest tests/cython From 421b31f0cf4b2386fdcaf4f07c6f52648ce191b4 Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Fri, 24 Apr 2026 15:45:33 -0400 Subject: [PATCH 2/4] Improve --- cuda_bindings/tests/cython/test_cython.py | 36 ++++++++--------------- cuda_core/tests/cython/test_cython.py | 36 ++++++++--------------- scripts/run_tests.sh | 4 +-- 3 files changed, 28 insertions(+), 48 deletions(-) diff --git a/cuda_bindings/tests/cython/test_cython.py b/cuda_bindings/tests/cython/test_cython.py index 4f498bc9ea3..a3977d24cc3 100644 --- a/cuda_bindings/tests/cython/test_cython.py +++ b/cuda_bindings/tests/cython/test_cython.py @@ -1,9 +1,9 @@ # SPDX-FileCopyrightText: Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE +import contextlib import importlib import os -import re from pathlib import Path import pytest @@ -13,7 +13,6 @@ TESTS_DIR = Path(__file__).resolve().parent CYTHON_TEST_MODULES = ["test_ccuda", "test_ccudart", "test_interoperability_cython"] -TEST_NAME_RE = re.compile(r"^\s*def\s+(test_[A-Za-z0-9_]+)\s*\(") def _get_cuda_include_dir(): @@ -53,10 +52,13 @@ def build_cython_test_modules(): build_ext = distribution.get_command_obj("build_ext") build_ext.inplace = True build_ext.build_temp = str(TESTS_DIR / "build" / "temp") - distribution.run_command("build_ext") + # Ensure in-place extension outputs are written into tests/cython. + with contextlib.chdir(TESTS_DIR): + distribution.run_command("build_ext") -def _import_cython_test_modules(rebuild_if_needed): + +def _import_cython_test_modules(): imported_modules = {} build_attempted = False @@ -64,9 +66,6 @@ def _import_cython_test_modules(rebuild_if_needed): try: imported_modules[module_name] = importlib.import_module(module_name) except ImportError: - if not rebuild_if_needed: - raise - if not build_attempted: build_cython_test_modules() importlib.invalidate_caches() @@ -77,22 +76,9 @@ def _import_cython_test_modules(rebuild_if_needed): return imported_modules -def _discover_test_function_names(module_name): - module_source = TESTS_DIR / f"{module_name}.pyx" - test_names = [] - - with module_source.open(encoding="utf-8") as f: - for line in f: - match = TEST_NAME_RE.match(line) - if match: - test_names.append(match.group(1)) - - return test_names - - @pytest.fixture(scope="session") def cython_test_modules(): - return _import_cython_test_modules(rebuild_if_needed=True) + return _import_cython_test_modules() def _make_wrapped_test(module_name, test_name): @@ -106,8 +92,12 @@ def wrapped(cython_test_modules): registered_tests = set() -for module_name in CYTHON_TEST_MODULES: - for test_name in _discover_test_function_names(module_name): +for module_name, module in _import_cython_test_modules().items(): + for test_name in dir(module): + item = getattr(module, test_name) + if not callable(item) or not test_name.startswith("test_"): + continue + if test_name in registered_tests: raise RuntimeError(f"duplicate cython test name discovered: {test_name}") registered_tests.add(test_name) diff --git a/cuda_core/tests/cython/test_cython.py b/cuda_core/tests/cython/test_cython.py index 69b46d315a8..48cb3193837 100644 --- a/cuda_core/tests/cython/test_cython.py +++ b/cuda_core/tests/cython/test_cython.py @@ -2,9 +2,9 @@ # # SPDX-License-Identifier: Apache-2.0 +import contextlib import importlib import os -import re from pathlib import Path import pytest @@ -16,7 +16,6 @@ CYTHON_TEST_MODULES = [ "test_get_cuda_native_handle", ] -TEST_NAME_RE = re.compile(r"^\s*def\s+(test_[A-Za-z0-9_]+)\s*\(") def _get_include_dirs(): @@ -52,10 +51,13 @@ def build_cython_test_modules(): build_ext = distribution.get_command_obj("build_ext") build_ext.inplace = True build_ext.build_temp = str(TESTS_DIR / "build" / "temp") - distribution.run_command("build_ext") + # Ensure in-place extension outputs are written into tests/cython. + with contextlib.chdir(TESTS_DIR): + distribution.run_command("build_ext") -def _import_cython_test_modules(rebuild_if_needed): + +def _import_cython_test_modules(): imported_modules = {} build_attempted = False @@ -63,9 +65,6 @@ def _import_cython_test_modules(rebuild_if_needed): try: imported_modules[module_name] = importlib.import_module(module_name) except ImportError: - if not rebuild_if_needed: - raise - if not build_attempted: build_cython_test_modules() importlib.invalidate_caches() @@ -76,22 +75,9 @@ def _import_cython_test_modules(rebuild_if_needed): return imported_modules -def _discover_test_function_names(module_name): - module_source = TESTS_DIR / f"{module_name}.pyx" - test_names = [] - - with module_source.open(encoding="utf-8") as f: - for line in f: - match = TEST_NAME_RE.match(line) - if match: - test_names.append(match.group(1)) - - return test_names - - @pytest.fixture(scope="session") def cython_test_modules(): - return _import_cython_test_modules(rebuild_if_needed=True) + return _import_cython_test_modules() def _make_wrapped_test(module_name, test_name): @@ -105,8 +91,12 @@ def wrapped(cython_test_modules): registered_tests = set() -for module_name in CYTHON_TEST_MODULES: - for test_name in _discover_test_function_names(module_name): +for module_name, module in _import_cython_test_modules().items(): + for test_name in dir(module): + item = getattr(module, test_name) + if not callable(item) or not test_name.startswith("test_"): + continue + if test_name in registered_tests: raise RuntimeError(f"duplicate cython test name discovered: {test_name}") registered_tests.add(test_name) diff --git a/scripts/run_tests.sh b/scripts/run_tests.sh index ec552c0ae37..7170349cd05 100755 --- a/scripts/run_tests.sh +++ b/scripts/run_tests.sh @@ -238,8 +238,8 @@ run_core() { if [ ${RUN_CYTHON} -eq 1 ] && [ -d tests/cython ]; then if [ -f tests/cython/test_cython.py ]; then echo "[build] cuda_core cython tests" - if [ -z "${CUDA_HOME-}" ]; then - echo "[skip] CUDA_HOME not set; skipping cython tests" + if [ -z "${CUDA_HOME-}" ] && [ -z "${CUDA_PATH-}" ]; then + echo "[skip] CUDA_HOME/CUDA_PATH not set; skipping cython tests" else ( cd tests/cython && python test_cython.py ) || true fi From 5a51c67222b77db113746a7ed69abe173161bbdf Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Fri, 24 Apr 2026 16:14:47 -0400 Subject: [PATCH 3/4] Fix for ancient Python --- cuda_bindings/tests/cython/test_cython.py | 7 +++++-- cuda_core/tests/cython/test_cython.py | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/cuda_bindings/tests/cython/test_cython.py b/cuda_bindings/tests/cython/test_cython.py index a3977d24cc3..7d0348fc632 100644 --- a/cuda_bindings/tests/cython/test_cython.py +++ b/cuda_bindings/tests/cython/test_cython.py @@ -1,7 +1,6 @@ # SPDX-FileCopyrightText: Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE -import contextlib import importlib import os from pathlib import Path @@ -54,8 +53,12 @@ def build_cython_test_modules(): build_ext.build_temp = str(TESTS_DIR / "build" / "temp") # Ensure in-place extension outputs are written into tests/cython. - with contextlib.chdir(TESTS_DIR): + cwd = os.getcwd() + os.chdir(TESTS_DIR) + try: distribution.run_command("build_ext") + finally: + os.chdir(cwd) def _import_cython_test_modules(): diff --git a/cuda_core/tests/cython/test_cython.py b/cuda_core/tests/cython/test_cython.py index 48cb3193837..b65f378554d 100644 --- a/cuda_core/tests/cython/test_cython.py +++ b/cuda_core/tests/cython/test_cython.py @@ -2,7 +2,6 @@ # # SPDX-License-Identifier: Apache-2.0 -import contextlib import importlib import os from pathlib import Path @@ -53,8 +52,12 @@ def build_cython_test_modules(): build_ext.build_temp = str(TESTS_DIR / "build" / "temp") # Ensure in-place extension outputs are written into tests/cython. - with contextlib.chdir(TESTS_DIR): + cwd = os.getcwd() + os.chdir(TESTS_DIR) + try: distribution.run_command("build_ext") + finally: + os.chdir(cwd) def _import_cython_test_modules(): From c298610d186a681a8e64cc7d14150a13f7ee97da Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Mon, 27 Apr 2026 09:37:06 -0400 Subject: [PATCH 4/4] Also allow for ModuleNotFoundError --- cuda_core/tests/cython/test_cython.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cuda_core/tests/cython/test_cython.py b/cuda_core/tests/cython/test_cython.py index b65f378554d..cbe14a15e77 100644 --- a/cuda_core/tests/cython/test_cython.py +++ b/cuda_core/tests/cython/test_cython.py @@ -67,7 +67,7 @@ def _import_cython_test_modules(): for module_name in CYTHON_TEST_MODULES: try: imported_modules[module_name] = importlib.import_module(module_name) - except ImportError: + except ImportError, ModuleNotFoundError: if not build_attempted: build_cython_test_modules() importlib.invalidate_caches()