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
37 changes: 37 additions & 0 deletions cuda_core/tests/test_graphics.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
import ctypes.util
import gc
import os
import pathlib
import subprocess
import sys
import textwrap

import numpy as np
import pyglet
Expand All @@ -18,6 +21,7 @@
select_headless_egl_device_for_cuda,
)

import cuda_python_test_helpers
from cuda.core import (
Buffer,
Device,
Expand Down Expand Up @@ -71,6 +75,39 @@ def _configure_pyglet_headless():
pyglet.options["headless_device"] = egl_device


@pytest.mark.agent_authored(model="claude-fable-5-1")
def test_egl_device_probe_does_not_import_pyglet_gl():
"""The probe runs before ``headless_device`` is set.

Importing ``pyglet.gl`` there creates pyglet's shadow window, which in
headless mode opens EGL device 0 and keeps it for the whole session: the
failure that #2865 fixed. A subprocess gives a clean ``sys.modules``.
"""
if not sys.platform.startswith("linux") or ctypes.util.find_library("EGL") is None:
pytest.skip("needs a Linux EGL runtime")
code = textwrap.dedent(
"""
import sys

import pyglet
from cuda_python_test_helpers.graphics import select_headless_egl_device_for_cuda

pyglet.options["headless"] = True
select_headless_egl_device_for_cuda(0)
assert "pyglet.gl" not in sys.modules, "the EGL device probe imported pyglet.gl"
"""
)
# The child inherits neither this process's sys.path nor conftest's
# source-checkout fallback, so point it at the package the parent imported.
helpers_root = pathlib.Path(cuda_python_test_helpers.__file__).parents[1]
env = dict(os.environ)
env["PYTHONPATH"] = os.pathsep.join(p for p in (str(helpers_root), env.get("PYTHONPATH")) if p)
proc = subprocess.run( # noqa: S603 - trusted argv: this interpreter
[sys.executable, "-c", code], capture_output=True, text=True, env=env
)
Comment on lines +105 to +107

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

suggestion: Bound the EGL probe subprocess. If EGL initialization or the probe blocks, subprocess.run waits indefinitely and stalls the test run. Set a timeout and report subprocess.TimeoutExpired as a test failure. Based on learnings: flag process-spawning calls without a timeout because a hung child can block indefinitely.

🧰 Tools
🪛 ast-grep (0.45.3)

[error] 104-106: Command coming from incoming request
Context: subprocess.run( # noqa: S603 - trusted argv: this interpreter
[sys.executable, "-c", code], capture_output=True, text=True, env=env
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(subprocess-from-request)

Source: Learnings

assert proc.returncode == 0, proc.stderr


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

Expand Down
30 changes: 24 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 @@ -12,13 +12,27 @@
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"]``.
set ``pyglet.options["headless"]``. For the same reason,
``select_headless_egl_device_for_cuda`` must not import ``pyglet.gl``: that
import creates the shadow window, and in headless mode the shadow window opens
the EGL display on ``pyglet.options["headless_device"]`` before the caller has
had a chance to set it.
"""

import contextlib
import ctypes


def _is_pyglet_missing_function_exception(exc: BaseException) -> bool:
"""True for ``pyglet.gl.lib.MissingFunctionException``.

Matched by name, like ``is_gl_context_unavailable``, so that the check does
not import ``pyglet.gl``.
"""
exc_type = type(exc)
return exc_type.__module__.startswith("pyglet") and exc_type.__name__ == "MissingFunctionException"


def select_headless_egl_device_for_cuda(cuda_device_ordinal: int) -> int | None:
"""Return the pyglet ``headless_device`` index matching *cuda_device_ordinal*.

Expand All @@ -39,7 +53,6 @@ def select_headless_egl_device_for_cuda(cuda_device_ordinal: int) -> int | None:
"""
egl_cuda_device_nv = 0x323A

from pyglet.gl.lib import MissingFunctionException
from pyglet.libs.egl import egl, eglext
from pyglet.libs.egl.lib import link_EGL

Expand All @@ -50,8 +63,11 @@ def select_headless_egl_device_for_cuda(cuda_device_ordinal: int) -> int | None:
)

# link_EGL returns a stub for an unresolvable entry point that raises
# MissingFunctionException when called, so that is the only "extension not
# available" signal. Anything else is a real bug and must propagate.
# pyglet.gl.lib.MissingFunctionException when called, so that is the only
# "extension not available" signal. Anything else is a real bug and must
# propagate. The exception is matched by name: importing pyglet.gl here
# would create the shadow window on EGL device 0 before the caller has set
# pyglet.options["headless_device"] (see the module docstring).
try:
num_devices = egl.EGLint()
if not eglext.eglQueryDevicesEXT(0, None, ctypes.byref(num_devices)) or num_devices.value <= 0:
Expand All @@ -66,8 +82,10 @@ def select_headless_egl_device_for_cuda(cuda_device_ordinal: int) -> int | None:
found = egl_query_device_attrib_ext(devices[index], egl_cuda_device_nv, ctypes.byref(queried_ordinal))
if found and queried_ordinal.value == cuda_device_ordinal:
return index
except MissingFunctionException:
return None
except Exception as exc:
if _is_pyglet_missing_function_exception(exc):
return None
raise

return None

Expand Down
Loading