Files
bec_widgets/tests/unit_tests/test_bec_queue.py
T

265 lines
9.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"]