mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-10-28 08:01:29 +01:00
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F2iSCsSSznEdY4HxoVaqoi
265 lines
9.6 KiB
Python
265 lines
9.6 KiB
Python
import uuid
|
||
from unittest import mock
|
||
|
||
import pytest
|
||
from bec_lib import messages
|
||
from bec_lib.endpoints import MessageEndpoints
|
||
from qtpy.QtQuickWidgets import QQuickWidget
|
||
|
||
from bec_widgets.tests.client_mocks import mocked_client
|
||
from bec_widgets.tests.utils import create_widget
|
||
from bec_widgets.widgets.services.bec_queue.bec_queue import BECQueue
|
||
from bec_widgets.widgets.services.bec_queue.queue_model import (
|
||
ProgressState,
|
||
QueueListModel,
|
||
QueueRow,
|
||
build_rows,
|
||
describe_parameters,
|
||
)
|
||
|
||
|
||
def _block(scan_type="line_scan", scan_number=None, user_metadata=None, args=None, kwargs=None):
|
||
rid = str(uuid.uuid4())
|
||
return {
|
||
"msg": messages.ScanQueueMessage(
|
||
scan_type=scan_type,
|
||
parameter={
|
||
"args": args if args is not None else {"samx": [-0.1, 0.1]},
|
||
"kwargs": kwargs if kwargs is not None else {"steps": 20, "exp_time": 0.5},
|
||
},
|
||
queue="primary",
|
||
metadata={"RID": rid, "user_metadata": user_metadata or {}},
|
||
),
|
||
"RID": rid,
|
||
"readout_priority": {"monitored": ["samx"], "baseline": [], "async": [], "on_request": []},
|
||
"is_scan": True,
|
||
"scan_number": scan_number,
|
||
"scan_id": str(uuid.uuid4()),
|
||
}
|
||
|
||
|
||
def _entry(status, scan_number=None, reason=None, **kwargs):
|
||
block = _block(scan_number=scan_number, **kwargs)
|
||
return messages.QueueInfoEntry(
|
||
queue_id=str(uuid.uuid4()),
|
||
scan_id=[block["scan_id"]],
|
||
is_scan=[True],
|
||
request_blocks=[block],
|
||
scan_number=[scan_number],
|
||
status=status,
|
||
reason=reason,
|
||
)
|
||
|
||
|
||
def _status_msg(entries, status="RUNNING", locks=None):
|
||
return messages.ScanQueueStatusMessage(
|
||
metadata={}, queue={"primary": {"info": entries, "status": status, "locks": locks or []}}
|
||
)
|
||
|
||
|
||
@pytest.fixture
|
||
def bec_queue_msg_full():
|
||
entry = _entry("COMPLETED", scan_number=1289, user_metadata={"sample_name": "testA"})
|
||
return _status_msg([entry])
|
||
|
||
|
||
@pytest.fixture
|
||
def busy_queue():
|
||
"""One running scan and three waiting ones."""
|
||
entries = [_entry("RUNNING", scan_number=1291, user_metadata={"sample_name": "LaB6"})]
|
||
entries += [_entry("PENDING") for _ in range(3)]
|
||
return _status_msg(entries)
|
||
|
||
|
||
@pytest.fixture
|
||
def bec_queue(qtbot, mocked_client):
|
||
widget = create_widget(qtbot, BECQueue, client=mocked_client, refresh_upon_start=False)
|
||
qtbot.waitExposed(widget)
|
||
yield widget
|
||
|
||
|
||
def _rows(widget: BECQueue) -> list[QueueRow]:
|
||
return widget.controller.model.rows()
|
||
|
||
|
||
def test_bec_queue_qml_loads(bec_queue):
|
||
assert bec_queue.view.status() == QQuickWidget.Status.Ready
|
||
assert not bec_queue.view.errors()
|
||
|
||
|
||
def test_bec_queue(bec_queue, bec_queue_msg_full):
|
||
bec_queue.update_queue(bec_queue_msg_full.content, {})
|
||
rows = _rows(bec_queue)
|
||
assert len(rows) == 1
|
||
assert rows[0].scan_number == "1289"
|
||
assert rows[0].title == "line_scan"
|
||
assert rows[0].sample == "testA"
|
||
assert rows[0].state_label == "Done"
|
||
assert rows[0].subtitle == "samx −0.1 → 0.1 · 20 steps · 0.5 s"
|
||
|
||
|
||
def test_bec_queue_empty(bec_queue, bec_queue_msg_full):
|
||
bec_queue.update_queue(bec_queue_msg_full.content, {})
|
||
bec_queue.update_queue({}, {})
|
||
assert _rows(bec_queue) == []
|
||
assert bec_queue.controller.state == "idle"
|
||
|
||
|
||
def test_bec_queue_sections_and_busy(qtbot, bec_queue, busy_queue):
|
||
with qtbot.waitSignal(bec_queue.queue_busy) as blocker:
|
||
bec_queue.update_queue(busy_queue.content, {})
|
||
assert blocker.args == [True]
|
||
rows = _rows(bec_queue)
|
||
assert [r.section for r in rows] == ["Now", "Waiting", "Waiting", "Waiting"]
|
||
assert bec_queue.controller.state == "running"
|
||
assert bec_queue.controller.runningCount == 1
|
||
assert bec_queue.controller.waitingCount == 3
|
||
waiting = rows[1:]
|
||
assert [(r.can_move_up, r.can_move_down) for r in waiting] == [
|
||
(False, True),
|
||
(True, True),
|
||
(True, False),
|
||
]
|
||
|
||
|
||
def test_bec_queue_paused_and_locked_state(bec_queue, busy_queue):
|
||
info = busy_queue.queue["primary"].info
|
||
bec_queue.update_queue(_status_msg(info, status="PAUSED").content, {})
|
||
assert bec_queue.controller.state == "paused"
|
||
lock = messages.ScanQueueLock(reason="Ring current low", identifier="beam")
|
||
bec_queue.update_queue(_status_msg(info, status="LOCKED", locks=[lock]).content, {})
|
||
assert bec_queue.controller.state == "locked"
|
||
assert bec_queue.controller.lockReason == "Ring current low"
|
||
|
||
|
||
def test_remove_marks_only_the_target_row(bec_queue, busy_queue):
|
||
"""Regression: removing the third row used to hide the first (running) row."""
|
||
bec_queue.update_queue(busy_queue.content, {})
|
||
target = _rows(bec_queue)[3]
|
||
bec_queue.controller.remove(target.request_id)
|
||
|
||
bec_queue.client.queue.request_scan_abortion.assert_called_once_with(
|
||
request_id=target.request_id
|
||
)
|
||
rows = _rows(bec_queue)
|
||
assert len(rows) == 4, "rows only disappear once the server confirms"
|
||
assert [r.removing for r in rows] == [False, False, False, True]
|
||
|
||
# the server answers with the item gone
|
||
bec_queue.update_queue(_status_msg(busy_queue.queue["primary"].info[:3]).content, {})
|
||
rows = _rows(bec_queue)
|
||
assert len(rows) == 3
|
||
assert not any(r.removing for r in rows)
|
||
|
||
|
||
def test_abort_current(bec_queue, busy_queue):
|
||
bec_queue.update_queue(busy_queue.content, {})
|
||
running = _rows(bec_queue)[0]
|
||
bec_queue.controller.abortCurrent()
|
||
bec_queue.client.queue.request_scan_abortion.assert_called_once_with(
|
||
request_id=running.request_id
|
||
)
|
||
|
||
|
||
def test_move_requests(bec_queue, busy_queue):
|
||
bec_queue.update_queue(busy_queue.content, {})
|
||
row = _rows(bec_queue)[2]
|
||
bec_queue.controller.moveUp(row.scan_id)
|
||
bec_queue.controller.moveDown(row.scan_id)
|
||
calls = bec_queue.client.queue.request_queue_order_modification.call_args_list
|
||
assert calls == [
|
||
mock.call(scan_id=row.scan_id, action="move_up"),
|
||
mock.call(scan_id=row.scan_id, action="move_down"),
|
||
]
|
||
|
||
|
||
def test_pause_and_resume(bec_queue):
|
||
with mock.patch.object(bec_queue.client.connector, "send") as send:
|
||
bec_queue.controller.pause()
|
||
endpoint, msg = send.call_args.args
|
||
assert endpoint == MessageEndpoints.scan_queue_modification_request()
|
||
assert msg.action == "deferred_pause"
|
||
bec_queue.controller.resume()
|
||
bec_queue.client.queue.request_scan_continuation.assert_called_once()
|
||
|
||
|
||
def test_halt_and_clear(bec_queue):
|
||
bec_queue.controller.halt()
|
||
bec_queue.client.queue.request_scan_halt.assert_called_once()
|
||
bec_queue.controller.clear()
|
||
bec_queue.client.queue.request_queue_reset.assert_called_once()
|
||
|
||
|
||
def test_progress_is_shown_on_running_row(bec_queue, busy_queue):
|
||
bec_queue.update_queue(busy_queue.content, {})
|
||
scan_id = busy_queue.queue["primary"].info[0].scan_id[0]
|
||
bec_queue.on_scan_progress({"value": 10, "max_value": 40, "done": False}, {"scan_id": scan_id})
|
||
running = _rows(bec_queue)[0]
|
||
assert running.progress == pytest.approx(0.25)
|
||
assert running.progress_text.startswith("10 / 40")
|
||
bec_queue.on_scan_progress({"value": 40, "max_value": 40, "done": True}, {"scan_id": scan_id})
|
||
assert _rows(bec_queue)[0].progress == -1
|
||
|
||
|
||
def test_history_is_shown_as_recent(qtbot, bec_queue, busy_queue):
|
||
finished = _entry("STOPPED", scan_number=1290, reason="user")
|
||
bec_queue.client.connector.lpush(
|
||
MessageEndpoints.scan_queue_history(),
|
||
messages.ScanQueueHistoryMessage(
|
||
status="STOPPED", queue_id=finished.queue_id, info=finished
|
||
),
|
||
)
|
||
bec_queue.update_queue(busy_queue.content, {})
|
||
# an item leaving the queue triggers a history read
|
||
bec_queue.update_queue(_status_msg(busy_queue.queue["primary"].info[1:]).content, {})
|
||
qtbot.waitUntil(lambda: any(r.section == "Recent" for r in _rows(bec_queue)), timeout=2000)
|
||
recent = [r for r in _rows(bec_queue) if r.section == "Recent"]
|
||
assert recent[0].scan_number == "1290"
|
||
assert recent[0].state_label == "Aborted · by user"
|
||
assert bec_queue.controller.lastFinished == "scan 1290 · aborted · by user"
|
||
|
||
bec_queue.controller.showRecent = False
|
||
assert not any(r.section == "Recent" for r in _rows(bec_queue))
|
||
|
||
|
||
def test_hide_toolbar(bec_queue):
|
||
bec_queue.hide_toolbar = True
|
||
assert bec_queue.controller.toolbarVisible is False
|
||
bec_queue.hide_toolbar = False
|
||
assert bec_queue.controller.toolbarVisible is True
|
||
|
||
|
||
def test_describe_parameters_grid():
|
||
msg = messages.ScanQueueMessage(
|
||
scan_type="grid_scan",
|
||
parameter={
|
||
"args": {"samx": [-5, 5, 41], "samy": [-2, 2, 11]},
|
||
"kwargs": {"exp_time": 0.1, "relative": True},
|
||
},
|
||
queue="primary",
|
||
metadata={},
|
||
)
|
||
assert describe_parameters(msg) == "samx −5 → 5 (41) · samy −2 → 2 (11) · 0.1 s · relative"
|
||
|
||
|
||
def test_build_rows_skips_history_still_in_queue():
|
||
entry = _entry("RUNNING", scan_number=5)
|
||
queue = messages.ScanQueueStatus(info=[entry], status="RUNNING")
|
||
history = [
|
||
messages.ScanQueueHistoryMessage(status="RUNNING", queue_id=entry.queue_id, info=entry)
|
||
]
|
||
rows = build_rows(queue, history, ProgressState())
|
||
assert [r.section for r in rows] == ["Now"]
|
||
|
||
|
||
def test_list_model_moves_rows_instead_of_resetting(qtbot):
|
||
model = QueueListModel()
|
||
rows = [QueueRow(key=k, section="Waiting", queue_id=k) for k in "abc"]
|
||
model.set_rows(rows)
|
||
with qtbot.assertNotEmitted(model.modelReset), qtbot.waitSignal(model.rowsMoved):
|
||
model.set_rows([rows[1], rows[0], rows[2]])
|
||
assert [r.key for r in model.rows()] == ["b", "a", "c"]
|
||
with qtbot.waitSignal(model.rowsRemoved):
|
||
model.set_rows([rows[0]])
|
||
assert [r.key for r in model.rows()] == ["a"]
|