Files
Renderive/render_3D/datoviz/testing/test_python_run_helper.py
T
2026-08-14 01:54:38 +08:00

222 lines
6.7 KiB
Python

#!/usr/bin/env python3
"""Tests for the top-level Python run helper."""
from __future__ import annotations
import builtins
import ctypes
import importlib
import sys
import types
class _Resources(ctypes.Structure):
_fields_ = [('window_host', ctypes.py_object)]
class _Raw(types.ModuleType):
def __init__(self):
super().__init__('datoviz.raw')
self.calls = []
self.app = object()
self.view = object()
self.window_host = object()
self.should_close = False
def dvz_app(self, scene):
self.calls.append(('app', scene))
return self.app
def dvz_app_with_resources(self, scene, config, resources):
self.calls.append(('app_with_resources', scene, config, resources._obj.window_host))
return self.app
def dvz_app_resources(self):
self.calls.append(('app_resources',))
return _Resources()
def dvz_view_window(self, app, figure, width, height, title):
self.calls.append(('view_window', app, figure, width, height, title))
return self.view
def dvz_app_run(self, app, frame_count):
self.calls.append(('app_run', app, frame_count))
def dvz_app_render_once(self, app):
self.calls.append(('app_render_once', app))
return 0
def dvz_app_reap_closed_views(self, app):
self.calls.append(('app_reap_closed_views', app))
return True
def dvz_view_request_frame(self, view):
self.calls.append(('view_request_frame', view))
return 0
def dvz_app_destroy(self, app):
self.calls.append(('app_destroy', app))
def dvz_scene_destroy(self, scene):
self.calls.append(('scene_destroy', scene))
def dvz_window_host(self):
self.calls.append(('window_host',))
return self.window_host
def dvz_window_host_poll(self, window_host):
self.calls.append(('window_host_poll', window_host))
def dvz_window_host_destroy(self, window_host):
self.calls.append(('window_host_destroy', window_host))
def dvz_window_should_close(self, window):
self.calls.append(('window_should_close', window))
return self.should_close
def dvz_app_should_exit(self, app):
self.calls.append(('app_should_exit', app))
return self.should_close
class TerminalInteractiveShell:
def __init__(self):
self.enabled = []
def enable_gui(self, name):
self.enabled.append(name)
class _NeverReadyContext:
def input_is_ready(self):
return False
def _install_raw(monkeypatch, datoviz):
raw = _Raw()
monkeypatch.setitem(sys.modules, 'datoviz.raw', raw)
monkeypatch.setattr(datoviz, 'raw', raw, raising=False)
return raw
def test_run_blocks_and_cleans_up_outside_ipython(monkeypatch):
import datoviz
raw = _install_raw(monkeypatch, datoviz)
monkeypatch.delattr(builtins, 'get_ipython', raising=False)
result = datoviz.run('scene', 'figure', 320, 240, 'Title')
assert result is None
assert ('app_run', raw.app, 0) in raw.calls
assert raw.calls[-1] == ('app_destroy', raw.app)
assert ('scene_destroy', 'scene') not in raw.calls
def test_run_returns_live_session_in_terminal_ipython(monkeypatch):
import datoviz
dvz_run = importlib.import_module('datoviz.run')
raw = _install_raw(monkeypatch, datoviz)
shell = TerminalInteractiveShell()
monkeypatch.setattr(builtins, 'get_ipython', lambda: shell, raising=False)
monkeypatch.setattr(dvz_run, '_enable_ipython_inputhook', lambda sh: sh is shell)
session = datoviz.run('scene', 'figure', 320, 240, 'Title')
assert session.running
assert ('app_run', raw.app, 0) not in raw.calls
assert ('app_with_resources', 'scene', None, raw.window_host) in raw.calls
assert ('view_request_frame', raw.view) in raw.calls
session.render_once()
assert raw.calls[-3:] == [
('window_host_poll', raw.window_host),
('app_should_exit', raw.app),
('app_render_once', raw.app),
]
session.close()
assert raw.calls[-2:] == [
('app_destroy', raw.app),
('window_host_destroy', raw.window_host),
]
assert ('scene_destroy', 'scene') not in raw.calls
def test_run_can_be_forced_blocking_in_terminal_ipython(monkeypatch):
import datoviz
raw = _install_raw(monkeypatch, datoviz)
monkeypatch.setattr(builtins, 'get_ipython', lambda: TerminalInteractiveShell(), raising=False)
result = datoviz.run('scene', 'figure', 320, 240, 'Title', blocking=True)
assert result is None
assert ('app_run', raw.app, 0) in raw.calls
assert ('scene_destroy', 'scene') not in raw.calls
def test_run_can_reopen_borrowed_scene_after_blocking_close(monkeypatch):
import datoviz
raw = _install_raw(monkeypatch, datoviz)
monkeypatch.delattr(builtins, 'get_ipython', raising=False)
datoviz.run('scene', 'figure', 320, 240, 'Title')
datoviz.run('scene', 'figure', 320, 240, 'Title')
assert [call[0] for call in raw.calls].count('app') == 2
assert [call[0] for call in raw.calls].count('app_run') == 2
assert [call[0] for call in raw.calls].count('app_destroy') == 2
assert ('scene_destroy', 'scene') not in raw.calls
def test_live_session_closes_when_native_window_requests_close(monkeypatch):
dvz_run = importlib.import_module('datoviz.run')
raw = _Raw()
session = dvz_run.RunSession(raw, 'scene', raw.app, raw.view, window_host=raw.window_host)
raw.should_close = True
assert session.render_once() is None
assert not session.running
assert raw.calls[-5:] == [
('window_host_poll', raw.window_host),
('app_should_exit', raw.app),
('app_reap_closed_views', raw.app),
('app_destroy', raw.app),
('window_host_destroy', raw.window_host),
]
assert ('scene_destroy', 'scene') not in raw.calls
def test_inputhook_reaps_and_returns_once_session_closes(monkeypatch):
dvz_run = importlib.import_module('datoviz.run')
raw = _Raw()
dvz_run._close_live_runs()
session = dvz_run.RunSession(
raw, 'scene', raw.app, raw.view, window_host=raw.window_host, fps=0
)
raw.should_close = True
monkeypatch.setattr(dvz_run.time, 'sleep', lambda seconds: None)
dvz_run._datoviz_inputhook(_NeverReadyContext())
assert session._closed
assert not session.running
assert raw.calls[-5:] == [
('window_host_poll', raw.window_host),
('app_should_exit', raw.app),
('app_reap_closed_views', raw.app),
('app_destroy', raw.app),
('window_host_destroy', raw.window_host),
]
def test_inputhook_returns_when_no_live_sessions():
dvz_run = importlib.import_module('datoviz.run')
dvz_run._close_live_runs()
dvz_run._datoviz_inputhook(_NeverReadyContext())