"""The studio-side three-dimensional skeleton: importable, inert, Qt-safe. Acceptance for stage 0 (``docs/architecture/studio-3d-stages.md``): the module tree exists, it can be imported without PyOpenGL, and the one piece that must run before ``QApplication`` — the surface format — selects a complete profile. Nothing here renders. These tests are the floor a later stage must keep: the GL binding is imported only once a real context exists, and the stubs say which stage fills them in. """ from __future__ import annotations import sys import pytest @pytest.fixture def restore_surface_format(): """The default surface format is process-wide; put it back afterwards.""" from PyQt5.QtGui import QSurfaceFormat previous = QSurfaceFormat.defaultFormat() try: yield finally: QSurfaceFormat.setDefaultFormat(previous) def test_the_skeleton_does_not_import_a_gl_binding(qt_app): """PyOpenGL must be imported for the platform Qt chose, not at import time. The binding decides GLX versus EGL when it first loads, so importing it while merely constructing the module tree would freeze a guess. The test fails if a future change adds a module-level ``import OpenGL``. """ for name in [name for name in sys.modules if name == "OpenGL" or name.startswith("OpenGL.")]: del sys.modules[name] import orisys_studio.scene3d.camera # noqa: F401 import orisys_studio.scene3d.coordinator # noqa: F401 import orisys_studio.scene3d.gl_backend # noqa: F401 import orisys_studio.scene3d.panel # noqa: F401 import orisys_studio.scene3d.widget # noqa: F401 loaded = [name for name in sys.modules if name == "OpenGL" or name.startswith("OpenGL.")] assert loaded == [], f"importing the skeleton pulled in {loaded}" def test_configure_surface_format_selects_a_complete_profile(qt_app, restore_surface_format): from PyQt5.QtGui import QSurfaceFormat from orisys_studio.scene3d.gl_backend import configure_surface_format configure_surface_format("desktop33") desktop = QSurfaceFormat.defaultFormat() assert desktop.renderableType() == QSurfaceFormat.OpenGL assert (desktop.majorVersion(), desktop.minorVersion()) == (3, 3) assert desktop.profile() == QSurfaceFormat.CoreProfile assert desktop.depthBufferSize() == 24 assert desktop.samples() == 0, "v1 does not pay for multisampling" configure_surface_format("gles30") embedded = QSurfaceFormat.defaultFormat() assert embedded.renderableType() == QSurfaceFormat.OpenGLES assert (embedded.majorVersion(), embedded.minorVersion()) == (3, 0) with pytest.raises(ValueError): configure_surface_format("vulkan") # type: ignore[arg-type] def test_the_widget_holds_cpu_state_and_touches_no_context(qt_app): """Constructing the widget must not need a current context or a GPU.""" from orisys_app_shared.scene3d.contracts import DisplaySettings, FrameIdentity from orisys_studio.scene3d.camera import OrbitCamera from orisys_studio.scene3d.gl_backend import GLRenderer from orisys_studio.scene3d.widget import Sensor3DWidget widget = Sensor3DWidget(GLRenderer(), OrbitCamera()) try: assert isinstance(widget, Sensor3DWidget) widget.set_settings(DisplaySettings(display_gain=3.0)) assert widget._settings.display_gain == 3.0 # Nothing has been presented yet, and no context was needed to get here. assert widget._frame is None finally: widget.deleteLater() def test_the_stubs_name_the_stage_that_fills_them_in(qt_app): """What is implemented is exercised elsewhere; what is not says so. Stage 1 landed the camera, the dispatcher and the renderer (see ``test_scene3d_stage1.py`` and ``test_scene3d_gl_smoke.py``). The remaining placeholders must still fail loudly with the stage that owns them rather than quietly doing nothing. """ from orisys_app_shared.scene3d.contracts import ( CameraPreset, DisplaySettings, ExpectedGeometry, FrameIdentity, ) from orisys_studio.scene3d.adapters import BoxFrameAdapter, FingerFrameAdapter from orisys_studio.scene3d.camera import OrbitCamera from orisys_studio.scene3d.coordinator import ThreeDCoordinator from orisys_studio.scene3d.gl_backend import GLRenderer from orisys_studio.scene3d.panel import ThreeDPanel from orisys_studio.scene3d.widget import Sensor3DWidget camera = OrbitCamera() renderer = GLRenderer() widget = Sensor3DWidget(renderer, camera) coordinator = ThreeDCoordinator(widget, composer=object(), executor=None, max_fps=30) # type: ignore[arg-type] panel = ThreeDPanel(coordinator, widget) try: assert coordinator.state == "disabled" assert coordinator.generation == 0 assert coordinator.superseded_frames == 0 # the mailbox is real, not a stub # Stage 1 is real: the camera works with no context at all. camera.set_preset( CameraPreset("default", (0.0, 0.0, 1.0), (0.0, 0.0, 0.0), (0.0, 1.0, 0.0), 0.7) ) assert camera.matrices((100, 100)).view.shape == (4, 4) for call, stage in ( (lambda: coordinator.activate(None, "live-A"), "stage 3"), (lambda: coordinator.set_settings(DisplaySettings()), "stage 3"), (lambda: coordinator.set_visible(True), "stage 3"), (lambda: coordinator.close(), "stage 3"), (lambda: panel.set_state("ready"), "stage 4"), (lambda: BoxFrameAdapter().snapshot(None, FrameIdentity("a", 1, 0, 0.0, 1), None), "stage 3"), ( lambda: FingerFrameAdapter().snapshot( None, None, FrameIdentity("a", 1, 0, 0.0, 1) ), "stage 3", ), ): with pytest.raises(NotImplementedError) as excinfo: call() assert stage in str(excinfo.value), (stage, str(excinfo.value)) del ExpectedGeometry finally: widget.deleteLater()