Skip to content

Commit 8001a09

Browse files
committed
Report Qt view event failures
1 parent 0ee8436 commit 8001a09

3 files changed

Lines changed: 38 additions & 1 deletion

File tree

molsysviewer/standalone_qt/application.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ def _qt_context_menu(_event: Any) -> None:
129129
reset_action.triggered.connect(lambda *_: view.camera.reset())
130130
menu.exec(QCursor.pos())
131131
except Exception:
132+
# The optional native menu must not break interaction dispatch; Q5 tracks diagnostics.
132133
pass
133134

134135
view.on_context(_qt_context_menu)

molsysviewer/standalone_qt/utils.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import json
4+
import logging
45
import os
56
from pathlib import Path
67
import shutil
@@ -13,6 +14,9 @@
1314
from ..demo import demo
1415
from ..standalone import _resolve_view, build_standalone0_html
1516

17+
18+
logger = logging.getLogger(__name__)
19+
1620
QT_IMPORT_ERROR = (
1721
"PySide6_uibcdf with Qt WebEngine is required for the standalone Qt prototype. "
1822
"Install the UIBCDF conda stack from the uibcdf channel:\n"
@@ -103,6 +107,7 @@ def _load_qt_shell_state() -> dict[str, Any]:
103107
try:
104108
data = json.loads(path.read_text(encoding="utf-8"))
105109
except Exception:
110+
# Shell state is optional; malformed or unreadable state starts a clean shell.
106111
return {"recent_sources": [], "last_source": None, "window_size": None}
107112
if not isinstance(data, dict):
108113
return {"recent_sources": [], "last_source": None, "window_size": None}
@@ -140,11 +145,13 @@ def _capture_window_size(window) -> dict[str, int] | None:
140145
try:
141146
width = int(window.width())
142147
except Exception:
148+
# Some Qt/fake windows expose a failing getter; the tuple fallback below remains available.
143149
width = None
144150
if hasattr(window, "height") and callable(window.height):
145151
try:
146152
height = int(window.height())
147153
except Exception:
154+
# Some Qt/fake windows expose a failing getter; the tuple fallback below remains available.
148155
height = None
149156
size = getattr(window, "size", None)
150157
if (width is None or height is None) and isinstance(size, tuple) and len(size) == 2:
@@ -285,6 +292,7 @@ def _persist_shell_state(current_state: dict[str, Any], window=None) -> None:
285292
try:
286293
_get_helper("_save_qt_shell_state")(current_state)
287294
except Exception:
295+
# Persistence is best-effort and must not interrupt closing or loading the viewer.
288296
return
289297

290298

@@ -367,7 +375,7 @@ def _forward_to_view(self, event: dict[str, Any]) -> None:
367375
try:
368376
self.event_sink(event)
369377
except Exception:
370-
pass
378+
logger.exception("Qt view event failed: %r", event)
371379

372380
def _make_entry(self, message: dict[str, Any]) -> dict[str, Any]:
373381
self.next_id += 1
@@ -595,6 +603,7 @@ def _decode_qt_bridge_event(url: str) -> dict[str, Any] | None:
595603
try:
596604
event = json.loads(payload_values[0])
597605
except Exception:
606+
# Invalid custom-scheme input is rejected at this untrusted transport boundary.
598607
return None
599608
if not isinstance(event, dict) or not isinstance(event.get("event"), str):
600609
return None

tests/test_standalone.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
import logging
23
import os
34
import pytest
45
import sys
@@ -999,6 +1000,32 @@ def singleShot(_t, _cb):
9991000
assert "structure_ready" not in names
10001001

10011002

1003+
def test_qt_bridge_reports_view_event_failure_without_raising(caplog):
1004+
class FakeQTimer:
1005+
@staticmethod
1006+
def singleShot(_t, _cb):
1007+
pass
1008+
1009+
def failing_sink(_event):
1010+
raise RuntimeError("boom")
1011+
1012+
bridge = standalone_qt.QtMessageBridge(
1013+
object(), FakeQTimer, event_sink=failing_sink
1014+
)
1015+
event = {"event": "interaction_click", "kind": "structure"}
1016+
1017+
with caplog.at_level(logging.ERROR, logger="molsysviewer.standalone_qt.utils"):
1018+
bridge.handle_frontend_event(event)
1019+
1020+
record = next(
1021+
record for record in caplog.records if "Qt view event failed" in record.message
1022+
)
1023+
assert record.exc_info is not None
1024+
assert record.exc_info[0] is RuntimeError
1025+
assert str(record.exc_info[1]) == "boom"
1026+
assert repr(event) in record.message
1027+
1028+
10021029
# Tier-1 CI smoke: the real Qt JS->Python event transport WITHOUT Mol*/WebGL.
10031030
# A trivial page (no viewer.js, no WebGL context) posts a `ready` event exactly
10041031
# like the frontend does — fetch("molsysviewer://event?...") — and we assert the

0 commit comments

Comments
 (0)