Files
eco/tests/test_console_window_qt.py
gac berninaandClaude Sonnet 5 166a5ef44b Fix desktop console/launcher lag, add startup-script export, default to dark theme
Real bug found via manual testing ("even not closing, desktop is super
unresponsive and laggy... the qt console, when entering something takes
ages to update, 30s ish"): eco.utilities.config.Proxy (the lazy device
proxy) only special-cases __class__ to avoid resolving while unresolved --
__dir__ isn't one of lazy_object_proxy's protected operations, so it
forwards straight to __wrapped__, fully constructing the device (real
EPICS calls) the moment anything calls dir() on it. Confirmed for real:
dir() on a still-lazy complex device took 62.7s. Since eco desktop's
console now has ~180 bare device names in its namespace (many still lazy),
and any completion mechanism scanning that namespace calls dir() on
candidates, typing in the console could trigger this on nearly every
keystroke. Added Proxy.__dir__, mirroring __class__'s existing "stay shy
to introspection" pattern exactly. Also measured Jedi-based completion as
independently slower (5.8s vs 0.001s) and less correct (0 matches vs 63)
than the classic completer for this dynamic a namespace -- now disabled
for every eco console (build_console_widget, so it applies uniformly to
both in-process and subprocess kernels).

Second, separate perf issue in the Namespace launcher panel: its
live-refresh timer (every 2s) fully rebuilds the table, and the Required
column (added last session) used a real QCheckBox+QWidget+QHBoxLayout per
row -- constructing/destroying ~120 native Qt widgets twice a second at
bernina's scale is real, visible GUI-thread cost. Switched to checkable
QTableWidgetItems (blockSignals()'d during the rebuild so programmatic
setCheckState() calls don't themselves trigger spurious required_names()
writes) -- same functionality, far cheaper to rebuild.

New: "Save Startup Script..." in the desktop window's Workspace menu --
writes a standalone, executable .sh (paired with a .json workspace file,
same format as Save Workspace) that relaunches `eco desktop` with just
this session's open widgets reopened. Lazy loading means nothing else in
the namespace gets touched, so this is a fast, minimal per-task dashboard
instead of the full namespace. New --workspace PATH flag on both eco_cli's
desktop subcommand and eco.widgets.desktop_app's own CLI loads a workspace
on startup; desktop_app._main() now builds the window and loads it before
entering run()'s blocking loop, instead of auto-starting straight into
that block.

Also: the Namespace launcher's lazy (not actively loading) entries no
longer show a spinner-adjacent hourglass -- gray text alone marks
"not built yet"; the animated spinner is now reserved for "actually
initializing right now". And the desktop window's Material dark theme
(qt-material's dark_teal.xml) is now the default -- it already existed as
an opt-in --theme dark, but wasn's what a first `eco desktop` showed; added
'none' as an explicit choice for anyone who wants native OS style back,
and qt-material to eco[gui]'s extras so pip installs get the real theme
too, not just the built-in approximation.

All confirmed via scripted offscreen reproductions of each bug before
writing its fix (not just after), plus the full test suite (336/338 --
the 2 failures are pre-existing in test_config_lazy_init.py, confirmed
via git stash, unrelated to any of this).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-21 22:17:40 +02:00

91 lines
3.1 KiB
Python

import pytest
pytest.importorskip("qtpy")
from qtpy import QtWidgets
from eco.widgets.console_window_qt import ConsoleWindowQt
class _FakeSession:
def log_event(self, *a, **kw):
pass
class _FakeConsoleWidget(QtWidgets.QWidget):
"""See tests/test_desktop_app.py's _FakeConsoleWidget for why a real
console_kernel.LoggingJupyterWidget isn't constructed in these tests --
a plain QWidget (unlike RichJupyterWidget) carries none of that risk,
and ConsoleWindowQt._build_window() needs a real QWidget to hand to
setCentralWidget()."""
def __init__(self, session=None):
super().__init__()
self.session = session
self.kernel_manager = None
self.kernel_client = None
self.banner = ""
self.executed = []
def execute(self, code, hidden=False):
self.executed.append(code)
@pytest.fixture(autouse=True)
def _patch_kernel_build(monkeypatch):
QtWidgets.QApplication.instance() or QtWidgets.QApplication([])
calls = {}
def fake_build_subprocess(kind, label=None):
calls["kind"] = kind
calls["label"] = label
return "manager", "client", _FakeSession()
monkeypatch.setattr("eco.widgets.console_kernel.build_subprocess_kernel", fake_build_subprocess)
monkeypatch.setattr("eco.widgets.console_kernel.LoggingJupyterWidget", _FakeConsoleWidget)
return calls
def test_console_window_qt_builds_a_subprocess_kernel_with_scope_startup_code(_patch_kernel_build):
win = ConsoleWindowQt(scope="bernina", lazy=True, auto_start=False)
win._build_window()
try:
assert win._kernel_manager == "manager"
assert _patch_kernel_build["kind"] == "console"
assert _patch_kernel_build["label"] == "bernina"
# startup code is run through the finished console widget's own
# .execute(), not passed to build_subprocess_kernel. index 0 is
# every console's hidden jedi-disable call (see
# console_kernel.build_console_widget's docstring); index 1 is
# this console's own scope-loading startup code.
assert len(win._console.executed) == 2
assert "build_namespace(scope='bernina', lazy=True)" in win._console.executed[1]
assert win.window is not None
assert "eco console: bernina" in win.window.windowTitle()
finally:
win.stop()
def test_console_window_qt_uses_custom_label(_patch_kernel_build):
win = ConsoleWindowQt(scope="bernina", label="alignment", auto_start=False)
win._build_window()
try:
assert _patch_kernel_build["label"] == "alignment"
assert "eco console: alignment" in win.window.windowTitle()
finally:
win.stop()
def test_console_window_qt_stop_clears_window_and_kernel_refs(_patch_kernel_build):
win = ConsoleWindowQt(scope="bernina", auto_start=False)
win._build_window()
win.stop()
assert win.window is None
assert win._kernel_manager is None
assert win._kernel_client is None
assert win._kernel_session is None
def test_console_window_qt_stop_before_start_is_a_no_op():
win = ConsoleWindowQt(scope="bernina", auto_start=False)
win.stop() # must not raise