Previously only eco desktop's console had an activity log (via eco.widgets.console_kernel's ZMQ-message interception, which only works for a kernel with a real iopub channel a Qt widget attaches to) -- a plain terminal `eco console` session and JupyterLab-native/Voila kernels had none. eco.widgets.kernel_registry.install_shell_logger() closes that gap using IPython's pre_run_cell/post_run_cell events instead, which work identically for any real InteractiveShell. Wired into startup_inline.py (terminal), eco_cli.py's generated kernel profile (JupyterLab-native), and voila_app.ipynb/jupyterlab_app.ipynb (Voila / bare `eco jupyterlab`). Narrower than console_kernel's approach by design: only input plus the resulting value/error are captured, not raw stdout/stderr streaming. Also proactively set use_jedi=False for the terminal's `eco console` startup script, matching the reasoning already applied to desktop's console (see console_kernel.build_console_widget's docstring) -- Jedi's introspection was measured slower and less correct against eco's dynamic, lazily-resolving namespace. Every kernel log's entries are now tagged with which stream they came from (TimelineEntry.session, a "kind:label · HHMMSS" label derived from the log's own session_start record) so eco.logs's timeline keeps merging every session by default but LogTimelineQt gained a second, AND-combined "Stream:" filter row alongside the existing "Show:" kind row to separate them back out on demand. Fixed a same-second label collision the first version of the label had (two sessions of the same kind+label started within the same second, e.g. under test, produced identical labels) by falling back to a trailing id fragment from the log filename. Cross-user session browsing (the log_dir parameter already supports it) is deliberately deferred, not built this pass.
205 lines
8.1 KiB
Python
205 lines
8.1 KiB
Python
"""Coverage for LogTimelineQt's kind-filter checkboxes, multi/range
|
|
selection, and "copy selected as script" -- constructing a real
|
|
LogTimelineQt has been confirmed safe under pytest+offscreen (unlike some
|
|
of eco's other real-Qt-widget constructions -- see
|
|
tests/test_indicator_widgets.py's docstring for that fragility), verified
|
|
manually before writing this file.
|
|
"""
|
|
import pytest
|
|
|
|
pytest.importorskip("qtpy")
|
|
from qtpy import QtCore, QtWidgets
|
|
|
|
from eco.widgets.log_timeline_common import TimelineEntry
|
|
from eco.widgets.log_timeline_qt import LogTimelineQt
|
|
|
|
|
|
def _entries():
|
|
return [
|
|
TimelineEntry(t=1000.0, kind="input", text="mono.get_current_value()"),
|
|
TimelineEntry(t=1000.2, kind="result", text="12000.0"),
|
|
TimelineEntry(t=1005.0, kind="widget_control", text="cam_west.widget()"),
|
|
TimelineEntry(t=1010.0, kind="input", text="att.set_target_value(0.5)"),
|
|
TimelineEntry(t=1010.1, kind="stream", text="[stdout] moving..."),
|
|
TimelineEntry(t=1015.0, kind="error", text="RuntimeError: timeout"),
|
|
]
|
|
|
|
|
|
def _select_all_visible(viewer):
|
|
viewer.list.clearSelection()
|
|
for i in range(viewer.list.count()):
|
|
item = viewer.list.item(i)
|
|
if item.data(QtCore.Qt.UserRole) is not None and not item.isHidden():
|
|
item.setSelected(True)
|
|
|
|
|
|
def _visible_kinds(viewer):
|
|
kinds = set()
|
|
for i in range(viewer.list.count()):
|
|
item = viewer.list.item(i)
|
|
e = item.data(QtCore.Qt.UserRole)
|
|
if e is not None and not item.isHidden():
|
|
kinds.add(e.kind)
|
|
return kinds
|
|
|
|
|
|
def test_filter_checkboxes_cover_every_distinct_kind():
|
|
app = QtWidgets.QApplication.instance() or QtWidgets.QApplication([])
|
|
viewer = LogTimelineQt(_entries(), title="test")
|
|
assert set(viewer._kind_checkboxes.keys()) == {
|
|
"input", "result", "widget_control", "stream", "error",
|
|
}
|
|
assert all(cb.isChecked() for cb in viewer._kind_checkboxes.values())
|
|
|
|
|
|
def test_unchecking_a_kind_hides_only_its_rows():
|
|
app = QtWidgets.QApplication.instance() or QtWidgets.QApplication([])
|
|
viewer = LogTimelineQt(_entries(), title="test")
|
|
|
|
viewer._kind_checkboxes["stream"].setChecked(False)
|
|
viewer._kind_checkboxes["result"].setChecked(False)
|
|
|
|
assert _visible_kinds(viewer) == {"input", "widget_control", "error"}
|
|
|
|
|
|
def test_session_row_absent_without_any_per_entry_session():
|
|
"""A pure scilog timeline has no entry.session at all -- nothing to
|
|
filter by, so the Stream row shouldn't be built."""
|
|
app = QtWidgets.QApplication.instance() or QtWidgets.QApplication([])
|
|
viewer = LogTimelineQt(_entries(), title="test")
|
|
assert viewer._session_checkboxes == {}
|
|
|
|
|
|
def _multi_stream_entries():
|
|
return [
|
|
TimelineEntry(t=1000.0, kind="input", text="mono.get_current_value()", session="desktop:bernina"),
|
|
TimelineEntry(t=1000.2, kind="result", text="12000.0", session="desktop:bernina"),
|
|
TimelineEntry(t=1010.0, kind="input", text="att.set_target_value(0.5)", session="console:bernina"),
|
|
TimelineEntry(t=1010.1, kind="stream", text="[stdout] moving...", session="console:bernina"),
|
|
]
|
|
|
|
|
|
def test_session_row_covers_every_distinct_stream():
|
|
app = QtWidgets.QApplication.instance() or QtWidgets.QApplication([])
|
|
viewer = LogTimelineQt(_multi_stream_entries(), title="test")
|
|
assert set(viewer._session_checkboxes.keys()) == {"desktop:bernina", "console:bernina"}
|
|
assert all(cb.isChecked() for cb in viewer._session_checkboxes.values())
|
|
|
|
|
|
def test_unchecking_a_stream_hides_only_its_rows_and_combines_with_kind_filter():
|
|
"""Kind and Stream filters are AND-combined -- separable but still
|
|
mergeable by default (both rows start fully checked)."""
|
|
app = QtWidgets.QApplication.instance() or QtWidgets.QApplication([])
|
|
viewer = LogTimelineQt(_multi_stream_entries(), title="test")
|
|
|
|
viewer._session_checkboxes["desktop:bernina"].setChecked(False)
|
|
visible_sessions = {
|
|
viewer.list.item(i).data(QtCore.Qt.UserRole).session
|
|
for i in range(viewer.list.count())
|
|
if viewer.list.item(i).data(QtCore.Qt.UserRole) is not None and not viewer.list.item(i).isHidden()
|
|
}
|
|
assert visible_sessions == {"console:bernina"}
|
|
# console:bernina has both "input" and "stream" entries -- both kinds
|
|
# still checked, so both are still visible once the desktop stream is
|
|
# filtered out.
|
|
assert _visible_kinds(viewer) == {"input", "stream"}
|
|
|
|
# AND-combine: also uncheck the "stream" kind -- only console's
|
|
# "input" row should remain visible.
|
|
viewer._kind_checkboxes["stream"].setChecked(False)
|
|
assert _visible_kinds(viewer) == {"input"}
|
|
|
|
|
|
def test_rechecking_shows_rows_again():
|
|
app = QtWidgets.QApplication.instance() or QtWidgets.QApplication([])
|
|
viewer = LogTimelineQt(_entries(), title="test")
|
|
|
|
viewer._kind_checkboxes["error"].setChecked(False)
|
|
assert "error" not in _visible_kinds(viewer)
|
|
viewer._kind_checkboxes["error"].setChecked(True)
|
|
assert "error" in _visible_kinds(viewer)
|
|
|
|
|
|
def test_list_selection_mode_allows_multi_select():
|
|
app = QtWidgets.QApplication.instance() or QtWidgets.QApplication([])
|
|
viewer = LogTimelineQt(_entries(), title="test")
|
|
assert viewer.list.selectionMode() == QtWidgets.QAbstractItemView.ExtendedSelection
|
|
|
|
|
|
def test_copy_selected_as_script_without_timing():
|
|
app = QtWidgets.QApplication.instance() or QtWidgets.QApplication([])
|
|
viewer = LogTimelineQt(_entries(), title="test")
|
|
_select_all_visible(viewer)
|
|
viewer.timing_checkbox.setChecked(False)
|
|
|
|
viewer._copy_selected_as_script()
|
|
script = QtWidgets.QApplication.clipboard().text()
|
|
|
|
assert "mono.get_current_value()" in script
|
|
assert "cam_west.widget()" in script
|
|
assert "att.set_target_value(0.5)" in script
|
|
assert "# [result]" in script
|
|
assert "# [stream]" in script
|
|
assert "# [error]" in script
|
|
assert "time.sleep" not in script
|
|
assert "import time" not in script
|
|
|
|
|
|
def test_copy_selected_as_script_with_timing_inserts_real_gaps():
|
|
app = QtWidgets.QApplication.instance() or QtWidgets.QApplication([])
|
|
viewer = LogTimelineQt(_entries(), title="test")
|
|
_select_all_visible(viewer)
|
|
viewer.timing_checkbox.setChecked(True)
|
|
|
|
viewer._copy_selected_as_script()
|
|
script = QtWidgets.QApplication.clipboard().text()
|
|
|
|
assert script.startswith("import time\n")
|
|
# mono -> cam_west is a 5s gap (1005.0 - 1000.0), skipping the 0.2s
|
|
# mono->result gap (below the 0.05s "negligible" threshold is not the
|
|
# point here -- 0.2s *is* above it, but result isn't code, so no sleep
|
|
# is inserted purely between two comment lines either; what matters is
|
|
# a real, selected code-to-code/comment gap shows up as a real sleep):
|
|
assert "time.sleep(5.000)" in script
|
|
|
|
|
|
def test_copy_selected_as_script_only_uses_visible_selection():
|
|
"""Hidden (filtered-out) rows can still technically be "selected" in
|
|
Qt's own model even while hidden -- must not leak into the script."""
|
|
app = QtWidgets.QApplication.instance() or QtWidgets.QApplication([])
|
|
viewer = LogTimelineQt(_entries(), title="test")
|
|
viewer._kind_checkboxes["error"].setChecked(False) # hide the error row
|
|
|
|
# select everything in the model, hidden or not
|
|
for i in range(viewer.list.count()):
|
|
item = viewer.list.item(i)
|
|
if item.data(QtCore.Qt.UserRole) is not None:
|
|
item.setSelected(True)
|
|
viewer.timing_checkbox.setChecked(False)
|
|
|
|
viewer._copy_selected_as_script()
|
|
script = QtWidgets.QApplication.clipboard().text()
|
|
|
|
assert "timeout" not in script
|
|
|
|
|
|
def test_copy_selected_as_script_with_nothing_selected_sets_status():
|
|
app = QtWidgets.QApplication.instance() or QtWidgets.QApplication([])
|
|
viewer = LogTimelineQt(_entries(), title="test")
|
|
viewer.list.clearSelection()
|
|
|
|
viewer._copy_selected_as_script()
|
|
|
|
assert "nothing selected" in viewer.script_status.text()
|
|
|
|
|
|
def test_copy_selected_as_script_status_reports_counts():
|
|
app = QtWidgets.QApplication.instance() or QtWidgets.QApplication([])
|
|
viewer = LogTimelineQt(_entries(), title="test")
|
|
_select_all_visible(viewer)
|
|
viewer.timing_checkbox.setChecked(False)
|
|
|
|
viewer._copy_selected_as_script()
|
|
|
|
assert "copied 6 entries" in viewer.script_status.text()
|