Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 61 additions & 18 deletions cuda_bindings/tests/test_graphics_apis.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,50 @@

from cuda.bindings import runtime as cudart

# pyglet raises these when GL context/window creation fails. Matched by type
# name (not by class) because importing pyglet.gl / pyglet.window at
# module top triggers pyglet's shadow-window creation, which fails on
# headless machines before _configure_pyglet_headless() has set the
# headless option. A bug in our own setup code (e.g. a TypeError) comes
# from builtins, not pyglet, so it re-raises and fails the test rather than
# being hidden as a skip.
_GL_UNAVAILABLE_EXC_NAMES = frozenset(
{
"NoSuchDisplayException",
"NoSuchConfigException",
"NoSuchScreenModeException",
"WindowException",
"ContextException",
"GLException",
}
)


def _is_gl_unavailable(exc):
if type(exc).__module__.startswith("pyglet") and type(exc).__name__ in _GL_UNAVAILABLE_EXC_NAMES:
return True
# Windows CI runners may lack opengl32.dll; pyglet's WGL backend raises
# FileNotFoundError from ctypes.windll.opengl32. On newer Python
# (3.12+) ctypes.LibraryLoader catches that and re-raises
# AttributeError(dll_name). Match narrowly on the dll name so a
# different FileNotFoundError or AttributeError from our own code
# does not match.
return isinstance(exc, (FileNotFoundError, AttributeError)) and "opengl32" in str(exc)

def _configure_pyglet_headless(pyglet):

def _configure_pyglet_headless():
"""On headless Linux: enable EGL mode or skip if EGL is absent."""
if sys.platform.startswith("linux") and not (os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY")):
if ctypes.util.find_library("EGL") is None:
pytest.skip("No DISPLAY and no EGL runtime available for headless context.")
pyglet.options["headless"] = True


def _setup_gl_texture(pyglet):
"""Open a GL context and allocate a 2-D RGBA8 texture. Returns (win, tex_id, target)."""
def _setup_gl_texture():
"""Open a GL context and allocate a 2-D RGBA8 texture. Returns (win, tex_id, target).

Cleans up the window if texture allocation raises, so partial resources do not leak.
"""
if not pyglet.options.get("headless"):
# Hidden window path (WGL on Windows, GLX/WLS on Linux)
from pyglet import gl
Expand All @@ -36,29 +69,39 @@ def _setup_gl_texture(pyglet):

win = None

# Make a tiny texture so we have a real GL object to register
from pyglet.gl import gl as _gl

tex_id = _gl.GLuint(0)
_gl.glGenTextures(1, ctypes.byref(tex_id))
target = _gl.GL_TEXTURE_2D
_gl.glBindTexture(target, tex_id.value)
_gl.glTexParameteri(target, _gl.GL_TEXTURE_MIN_FILTER, _gl.GL_NEAREST)
_gl.glTexParameteri(target, _gl.GL_TEXTURE_MAG_FILTER, _gl.GL_NEAREST)
width, height = 16, 16
_gl.glTexImage2D(target, 0, _gl.GL_RGBA8, width, height, 0, _gl.GL_RGBA, _gl.GL_UNSIGNED_BYTE, None)
return win, tex_id, target
try:
# Make a tiny texture so we have a real GL object to register
from pyglet.gl import gl as _gl

tex_id = _gl.GLuint(0)
_gl.glGenTextures(1, ctypes.byref(tex_id))
target = _gl.GL_TEXTURE_2D
_gl.glBindTexture(target, tex_id.value)
_gl.glTexParameteri(target, _gl.GL_TEXTURE_MIN_FILTER, _gl.GL_NEAREST)
_gl.glTexParameteri(target, _gl.GL_TEXTURE_MAG_FILTER, _gl.GL_NEAREST)
width, height = 16, 16
_gl.glTexImage2D(target, 0, _gl.GL_RGBA8, width, height, 0, _gl.GL_RGBA, _gl.GL_UNSIGNED_BYTE, None)
return win, tex_id, target
except Exception:
try:
if win is not None:
win.close()
except Exception: # noqa: S110
pass
raise


@contextlib.contextmanager
def _gl_context():
"""Yield ``(tex_id, tex_target)`` with a current GL context, or skip if GL is unavailable."""
_configure_pyglet_headless(pyglet)
_configure_pyglet_headless()

try:
win, tex_id, target = _setup_gl_texture(pyglet)
win, tex_id, target = _setup_gl_texture()
except Exception as e:
pytest.skip(f"Could not create GL context/texture: {type(e).__name__}: {e}")
if _is_gl_unavailable(e):
pytest.skip(f"Could not create GL context/texture: {type(e).__name__}: {e}")
raise

try:
yield int(tex_id.value), int(target)
Expand Down
85 changes: 85 additions & 0 deletions cuda_core/tests/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,88 @@ Follow these rules when adding or moving shared test code:
`conftest.py`.
- In directories without `__init__.py`, keep test-module basenames unique
within this test suite.

## Skip only real setup failures

`pytest.skip(reason)` records the test as SKIPPED with `reason` in the
report. A helper that wraps `yield` in `except Exception: pytest.skip(...)`
therefore records every test-body failure as a skip — a real regression, a
`TypeError`, an `AttributeError` all become "SKIPPED: <reason>" instead of
"FAILED", and the suite goes green regardless of whether the code under
test works.

Catch only the specific exception that legitimately means "not available",
and only around the setup call — never around `yield`:

```python
@contextlib.contextmanager
def _gl_context():
try:
win, tex_id = _setup_gl_texture() # setup only
except (pyglet.NoSuchConfigException, GLContextError) as e:
pytest.skip(f"GL unavailable: {e}")
try:
yield tex_id # body exceptions propagate
finally:
_cleanup(win, tex_id)
```

When a CUDA call's error means "feature refused by this driver" (e.g.
`CUDA_ERROR_OPERATING_SYSTEM` for CUDA-GL interop on WSL), skip at the call
site with a narrow catch on the specific error, not inside the GL helper —
see `_register_gl_buffer` / `_register_gl_image` in `tests/test_graphics.py`.

## `importorskip` is for optional dependencies only

`pytest.importorskip("X")` is correct when `X` is genuinely optional
(platform-gated binding, parametrized "test each available module"). It is
dead code when `X` is a declared test or runtime dependency: the skip then
fires only when the environment is broken, which is the case you want to fail
loudly, not hide. Use a bare top-level `import` for declared deps.

Before adding `importorskip`, check `cuda_core/pyproject.toml`'s `test`
and `test-cu*` groups and `cuda_core`'s `dependencies`. If the target is
listed, import it directly.

## Capability probes must not swallow real bugs

A probe function that answers "is feature X available?" by catching
`Exception` and returning `False` will report "not available" even when the
probed API failed for a real, unexpected reason — silently enabling a skip
that hides the bug. Catch only the exception that genuinely means "not
available", and split the checks so each catch is narrow:

```python
def _is_nvfatbin_available():
try:
from cuda.bindings import nvfatbin
except ImportError:
return False
try:
nvfatbin.version()
except nvfatbin.nvFatbinError:
return False
return True
```

Do not catch `ImportError` for a hard runtime dependency (e.g.
`cuda.bindings` for `cuda.core`) — that is a broken environment and should
surface at collection time.

## Tests that touch CUDA must establish their own context

The `init_cuda` fixture pops the CUDA context on teardown, so a test
that calls a CUDA API without `init_cuda` (or an explicit
`Device.set_current()`) inherits whatever context the previous test happened
to leave current on the thread — possibly none. With `pytest-randomly` that
makes the pass/fail outcome depend on test order, so it moves seed to seed and
looks like flakiness. Request `init_cuda` for any test that calls into the
driver, or set up and tear down a context yourself.

## Assert on behavior, not implementation

Pin on observable behavior the contract guarantees — return values, raised
exception types, public state transitions. Avoid asserting on internal
call counts, private helper invocation order, or error message substrings
that are not part of the contract. A refactor that preserves behavior but
changes internals should not break the test.
2 changes: 1 addition & 1 deletion cuda_core/tests/graph/test_device_launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import pytest
from cuda_python_test_helpers.marks import requires_module

import cuda.pathfinder as pathfinder
from cuda.core import (
Device,
LaunchConfig,
Expand Down Expand Up @@ -48,7 +49,6 @@ def _compile_device_launcher_kernel():

Raises pytest.skip if libcudadevrt.a cannot be found.
"""
pathfinder = pytest.importorskip("cuda.pathfinder")
try:
cudadevrt_path = pathfinder.find_static_lib("cudadevrt")
except pathfinder.StaticLibNotFoundError as e:
Expand Down
31 changes: 26 additions & 5 deletions cuda_core/tests/graph/test_graph_definition_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from helpers.memory import xfail_on_graph_mempool_oom

from cuda.core import Device, EventOptions, LaunchConfig, Program, ProgramOptions
from cuda.core._utils.cuda_utils import driver, handle_return
from cuda.core._utils.cuda_utils import CUDAError, driver, handle_return
from cuda.core.graph import GraphDefinition

SIZEOF_FLOAT = 4
Expand Down Expand Up @@ -121,15 +121,35 @@ def _nvrtc_opts():
return ProgramOptions(std="c++17", arch=f"sm_{arch}")


# NVRTC diagnostic phrases that indicate cudaGraphConditionalHandle itself is unknown
# to the compiler (older NVRTC builds predate the type). Matched narrowly so a
# genuine compile error (syntax error, etc.) is not hidden as a skip.
# Phrase #1 is the exact diagnostic observed on this machine's NVRTC; phrase #2
# is a common clang/NVRTC wording for an unknown type, not verified against the
# cudaGraphConditionalHandle case on an old NVRTC build.
_COND_HANDLE_UNKNOWN = (
'identifier "cudaGraphConditionalHandle" is undefined',
'unknown type name "cudaGraphConditionalHandle"',
)


def _skip_if_nvrtc_lacks_conditional_handle(exc):
msg = str(exc)
if any(phrase in msg for phrase in _COND_HANDLE_UNKNOWN):
pytest.skip("NVRTC does not support cudaGraphConditionalHandle")
raise


def _compile_heat_kernels():
prog = Program(_HEAT_KERNEL_SOURCE, code_type="c++", options=_nvrtc_opts())
try:
mod = prog.compile(
"cubin",
name_expressions=("heat_step", "countdown"),
)
except Exception:
pytest.skip("NVRTC does not support cudaGraphConditionalHandle")
except CUDAError as exc:
_skip_if_nvrtc_lacks_conditional_handle(exc)
raise
return mod.get_kernel("heat_step"), mod.get_kernel("countdown")


Expand All @@ -145,8 +165,9 @@ def _compile_bisect_kernels():
prog = Program(_BISECT_KERNEL_SOURCE, code_type="c++", options=_nvrtc_opts())
try:
mod = prog.compile("cubin", name_expressions=names)
except Exception:
pytest.skip("NVRTC does not support cudaGraphConditionalHandle")
except CUDAError as exc:
_skip_if_nvrtc_lacks_conditional_handle(exc)
raise
return tuple(mod.get_kernel(n) for n in names)


Expand Down
5 changes: 3 additions & 2 deletions cuda_core/tests/helpers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import functools
import os

from cuda.core._utils.cuda_utils import handle_return
from cuda.core._utils.cuda_utils import CUDAError, handle_return
from cuda.pathfinder import get_cuda_path_or_home
from cuda_python_test_helpers import *

Expand Down Expand Up @@ -49,5 +49,6 @@ def supports_ipc_mempool(device_id: int | object) -> bool:
# Check POSIX FD handle type support via bitmask
posix_fd = driver.CUmemAllocationHandleType.CU_MEM_HANDLE_TYPE_POSIX_FILE_DESCRIPTOR
return (int(mask) & int(posix_fd)) != 0
except Exception:
except CUDAError:
# cuInit or cuDeviceGetAttribute failed: IPC mempool not usable here.
return False
9 changes: 5 additions & 4 deletions cuda_core/tests/test_build_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,15 @@
from pathlib import Path
from unittest import mock

# build_hooks.py imports Cython and setuptools at the top level; both are
# declared test dependencies, so a missing install must surface as an
# ImportError at collection time rather than being hidden by importorskip.
import Cython # noqa: F401
import pytest
import setuptools # noqa: F401

from cuda.pathfinder import get_cuda_path_or_home

# build_hooks.py imports Cython and setuptools at the top level, so skip if not available
pytest.importorskip("Cython")
pytest.importorskip("setuptools")


def _load_build_hooks():
"""Load build_hooks module from source without permanently modifying sys.path.
Expand Down
Loading
Loading