Skip to content
Merged
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
29 changes: 2 additions & 27 deletions cuda_bindings/tests/test_graphics_apis.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import pyglet
import pytest
from cuda_python_test_helpers.graphics import is_gl_context_unavailable
from cuda_python_test_helpers.graphics import is_gl_context_unavailable, open_gl_window

from cuda.bindings import runtime as cudart

Expand All @@ -24,31 +24,6 @@ def _configure_pyglet_headless():
pyglet.options["headless"] = True


def _open_gl_window():
"""Open a hidden window (or configure EGL headless). Returns the window or None.

Closes the window if switch_to() fails so a partially-constructed window does not leak.
"""
if not pyglet.options.get("headless"):
# Hidden window path (WGL on Windows, GLX/WLS on Linux)
from pyglet import gl

config = gl.Config(double_buffer=False)
win = pyglet.window.Window(visible=False, config=config)
try:
win.switch_to()
except Exception:
with contextlib.suppress(Exception):
win.close()
raise
return win
else:
# Headless EGL path; pyglet will arrange a pbuffer-like headless context
from pyglet.gl import headless # noqa: F401

return None


def _allocate_gl_texture(win):
"""Allocate a 2-D RGBA8 texture. Caller must have a current GL context.

Expand Down Expand Up @@ -80,7 +55,7 @@ def _gl_context():
_configure_pyglet_headless()

try:
win = _open_gl_window()
win = open_gl_window()
except Exception as e:
if is_gl_context_unavailable(e):
pytest.skip(f"Could not create GL context: {type(e).__name__}: {e}")
Expand Down
41 changes: 3 additions & 38 deletions cuda_core/tests/test_graphics.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import numpy as np
import pyglet
import pytest
from cuda_python_test_helpers.graphics import is_gl_context_unavailable
from cuda_python_test_helpers.graphics import is_gl_context_unavailable, open_gl_window

from cuda.core import (
Buffer,
Expand Down Expand Up @@ -58,41 +58,6 @@ def _configure_pyglet_headless():
pyglet.options["headless"] = True


def _open_gl_window():
"""Open a hidden window (or configure EGL headless). Returns the window or None.

Cleans up partial windows and restores the previous GL context if
construction fails. Closes the window if switch_to() fails.
"""
if not pyglet.options.get("headless"):
from pyglet import gl

config = gl.Config(double_buffer=False)
previous_context = gl.current_context
previous_windows = set(pyglet.app.windows)
try:
win = pyglet.window.Window(visible=False, config=config)
except Exception:
for window in set(pyglet.app.windows) - previous_windows:
with contextlib.suppress(Exception):
window.close()
if previous_context is not None:
with contextlib.suppress(Exception):
previous_context.set_current()
raise
try:
win.switch_to()
except Exception:
with contextlib.suppress(Exception):
win.close()
raise
return win
else:
from pyglet.gl import headless # noqa: F401

return None


def _allocate_gl_buffer(win, nbytes):
"""Allocate a GL buffer. Caller must have a current GL context.

Expand Down Expand Up @@ -144,7 +109,7 @@ def _gl_context_and_buffer(nbytes=1024):
_configure_pyglet_headless()

try:
win = _open_gl_window()
win = open_gl_window()
except Exception as e:
if is_gl_context_unavailable(e):
pytest.skip(f"Could not create GL context: {type(e).__name__}: {e}")
Expand Down Expand Up @@ -172,7 +137,7 @@ def _gl_context_and_texture(width=16, height=16):
_configure_pyglet_headless()

try:
win = _open_gl_window()
win = open_gl_window()
except Exception as e:
if is_gl_context_unavailable(e):
pytest.skip(f"Could not create GL context: {type(e).__name__}: {e}")
Expand Down
31 changes: 29 additions & 2 deletions cuda_core/tests/test_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
#
# SPDX-License-Identifier: Apache-2.0

import sys
import time
import types
from unittest.mock import Mock

import pytest
from helpers.buffers import PatternGen, compare_equal_buffers, make_scratch_buffer, thread_unsafe_on_windows
Expand Down Expand Up @@ -354,11 +356,36 @@ def test_oom_diagnostics_probe_basics_is_live_and_cheap(init_cuda):


# ---------------------------------------------------------------------------
# GL context availability predicate tests
# GL helper tests
# ---------------------------------------------------------------------------

import pytest
from cuda_python_test_helpers.graphics import is_gl_context_unavailable
from cuda_python_test_helpers.graphics import is_gl_context_unavailable, open_gl_window


@pytest.mark.thread_unsafe(reason="patches the process-wide pyglet module")
@pytest.mark.agent_authored(model="gpt-5.6-sol")
def test_open_gl_window_cleans_up_failed_construction(monkeypatch):
windows = set()
partial_window = Mock()
previous_context = Mock()

def fail_window(**_kwargs):
windows.add(partial_window)
raise RuntimeError

pyglet = types.ModuleType("pyglet")
pyglet.options = {}
pyglet.app = types.SimpleNamespace(windows=windows)
pyglet.gl = types.SimpleNamespace(Config=Mock(), current_context=previous_context)
pyglet.window = types.SimpleNamespace(Window=fail_window)
monkeypatch.setitem(sys.modules, "pyglet", pyglet)

with pytest.raises(RuntimeError):
open_gl_window()

assert partial_window.close.called
assert previous_context.set_current.called


class _PygletError(Exception):
Expand Down
52 changes: 46 additions & 6 deletions cuda_python_test_helpers/cuda_python_test_helpers/graphics.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,60 @@
#
# SPDX-License-Identifier: Apache-2.0

"""GL availability classification for graphics interop tests.
"""Shared GL helpers for graphics interop tests.

Both ``cuda_core`` and ``cuda_bindings`` graphics tests need to skip
when the GL backend cannot be made current, and that decision must not
hide real bugs in the tests' own GL allocation code. This module owns the
shared predicate so the two test suites stay in sync.

The helper intentionally does **not** import ``pyglet``: importing
``pyglet.gl`` / ``pyglet.window`` triggers pyglet's shadow-window
creation, which fails on headless machines before the test has had a
chance to set ``pyglet.options["headless"]``. Classification is by
exception module/name and tightly matched built-in loader errors instead.
This module intentionally does **not** import ``pyglet`` at module load time:
importing ``pyglet.gl`` / ``pyglet.window`` triggers pyglet's shadow-window
creation, which fails on headless machines before the test has had a chance to
set ``pyglet.options["headless"]``.
"""

import contextlib


def open_gl_window():
"""Open a hidden window after the caller has configured pyglet.

In headless mode, initialize pyglet's headless GL backend and return None.
If window construction fails, clean up any partially-created windows and
restore the GL context that was current before the attempt.
"""
import pyglet

if not pyglet.options.get("headless"):
from pyglet import gl

config = gl.Config(double_buffer=False)
previous_context = gl.current_context
previous_windows = set(pyglet.app.windows)
try:
win = pyglet.window.Window(visible=False, config=config)
except Exception:
for window in set(pyglet.app.windows) - previous_windows:
with contextlib.suppress(Exception):
window.close()
if previous_context is not None:
with contextlib.suppress(Exception):
previous_context.set_current()
raise
try:
win.switch_to()
except Exception:
with contextlib.suppress(Exception):
win.close()
raise
return win

from pyglet.gl import headless # noqa: F401

return None


_GL_CONTEXT_UNAVAILABLE_EXC_NAMES = frozenset(
{
"NoSuchDisplayException",
Expand Down
Loading