Skip to content

Commit dbb33b6

Browse files
Andy-Jostclaude
andauthored
test(helpers): keep the EGL device probe from importing pyglet.gl (#3027)
* test(helpers): keep the EGL device probe from importing pyglet.gl select_headless_egl_device_for_cuda runs before the graphics tests set pyglet.options["headless_device"]. Importing pyglet.gl there creates pyglet's shadow window, which in headless mode opens the EGL display on device 0 and caches it, so the later headless_device assignment has no effect and the GL context lands on EGL device 0 again (#2864). Match MissingFunctionException by module and name instead, as is_gl_context_unavailable already does, and add a subprocess test that asserts the probe leaves pyglet.gl unimported. Follow-up to #3020. Co-Authored-By: Claude Fable 5.1 <[email protected]> * test(graphics): hand the probe child the test-helpers package path CI does not install cuda_python_test_helpers; the parent imports it only through the conftest sys.path fallback, which the -c child does not inherit, so test_egl_device_probe_does_not_import_pyglet_gl failed in every Linux row with ModuleNotFoundError. The child now gets a PYTHONPATH that names the parent directory of the package the parent imported, and a failure reports the child's stderr. Co-Authored-By: Claude Fable 5.1 <[email protected]> --------- Co-authored-by: Claude Fable 5.1 <[email protected]>
1 parent 8c66b43 commit dbb33b6

2 files changed

Lines changed: 61 additions & 6 deletions

File tree

‎cuda_core/tests/test_graphics.py‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
import ctypes.util
88
import gc
99
import os
10+
import pathlib
11+
import subprocess
1012
import sys
13+
import textwrap
1114

1215
import numpy as np
1316
import pyglet
@@ -18,6 +21,7 @@
1821
select_headless_egl_device_for_cuda,
1922
)
2023

24+
import cuda_python_test_helpers
2125
from cuda.core import (
2226
Buffer,
2327
Device,
@@ -71,6 +75,39 @@ def _configure_pyglet_headless():
7175
pyglet.options["headless_device"] = egl_device
7276

7377

78+
@pytest.mark.agent_authored(model="claude-fable-5-1")
79+
def test_egl_device_probe_does_not_import_pyglet_gl():
80+
"""The probe runs before ``headless_device`` is set.
81+
82+
Importing ``pyglet.gl`` there creates pyglet's shadow window, which in
83+
headless mode opens EGL device 0 and keeps it for the whole session: the
84+
failure that #2865 fixed. A subprocess gives a clean ``sys.modules``.
85+
"""
86+
if not sys.platform.startswith("linux") or ctypes.util.find_library("EGL") is None:
87+
pytest.skip("needs a Linux EGL runtime")
88+
code = textwrap.dedent(
89+
"""
90+
import sys
91+
92+
import pyglet
93+
from cuda_python_test_helpers.graphics import select_headless_egl_device_for_cuda
94+
95+
pyglet.options["headless"] = True
96+
select_headless_egl_device_for_cuda(0)
97+
assert "pyglet.gl" not in sys.modules, "the EGL device probe imported pyglet.gl"
98+
"""
99+
)
100+
# The child inherits neither this process's sys.path nor conftest's
101+
# source-checkout fallback, so point it at the package the parent imported.
102+
helpers_root = pathlib.Path(cuda_python_test_helpers.__file__).parents[1]
103+
env = dict(os.environ)
104+
env["PYTHONPATH"] = os.pathsep.join(p for p in (str(helpers_root), env.get("PYTHONPATH")) if p)
105+
proc = subprocess.run( # noqa: S603 - trusted argv: this interpreter
106+
[sys.executable, "-c", code], capture_output=True, text=True, env=env
107+
)
108+
assert proc.returncode == 0, proc.stderr
109+
110+
74111
def _allocate_gl_buffer(win, nbytes):
75112
"""Allocate a GL buffer. Caller must have a current GL context.
76113

‎cuda_python_test_helpers/cuda_python_test_helpers/graphics.py‎

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,27 @@
1212
This module intentionally does **not** import ``pyglet`` at module load time:
1313
importing ``pyglet.gl`` / ``pyglet.window`` triggers pyglet's shadow-window
1414
creation, which fails on headless machines before the test has had a chance to
15-
set ``pyglet.options["headless"]``.
15+
set ``pyglet.options["headless"]``. For the same reason,
16+
``select_headless_egl_device_for_cuda`` must not import ``pyglet.gl``: that
17+
import creates the shadow window, and in headless mode the shadow window opens
18+
the EGL display on ``pyglet.options["headless_device"]`` before the caller has
19+
had a chance to set it.
1620
"""
1721

1822
import contextlib
1923
import ctypes
2024

2125

26+
def _is_pyglet_missing_function_exception(exc: BaseException) -> bool:
27+
"""True for ``pyglet.gl.lib.MissingFunctionException``.
28+
29+
Matched by name, like ``is_gl_context_unavailable``, so that the check does
30+
not import ``pyglet.gl``.
31+
"""
32+
exc_type = type(exc)
33+
return exc_type.__module__.startswith("pyglet") and exc_type.__name__ == "MissingFunctionException"
34+
35+
2236
def select_headless_egl_device_for_cuda(cuda_device_ordinal: int) -> int | None:
2337
"""Return the pyglet ``headless_device`` index matching *cuda_device_ordinal*.
2438
@@ -39,7 +53,6 @@ def select_headless_egl_device_for_cuda(cuda_device_ordinal: int) -> int | None:
3953
"""
4054
egl_cuda_device_nv = 0x323A
4155

42-
from pyglet.gl.lib import MissingFunctionException
4356
from pyglet.libs.egl import egl, eglext
4457
from pyglet.libs.egl.lib import link_EGL
4558

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

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

7290
return None
7391

0 commit comments

Comments
 (0)