From 4a61df59a4306ef63962841ccc1495e05b9d089b Mon Sep 17 00:00:00 2001 From: Callum Forrester Date: Thu, 6 Feb 2025 14:00:09 +0000 Subject: [PATCH 1/5] Refactor test logic --- tests/system_tests/test_plans.py | 103 ++++++++++++++++++++++++++++--- 1 file changed, 93 insertions(+), 10 deletions(-) diff --git a/tests/system_tests/test_plans.py b/tests/system_tests/test_plans.py index 36c2619..f9b29ec 100644 --- a/tests/system_tests/test_plans.py +++ b/tests/system_tests/test_plans.py @@ -1,3 +1,5 @@ +from dataclasses import dataclass + import pytest from blueapi.client.client import BlueapiClient from blueapi.client.event_bus import AnyEvent @@ -54,22 +56,103 @@ def test_device_present(client: BlueapiClient, device: str): @pytest.mark.parametrize( - "plan", ["step_scan_plan", "fly_and_collect_plan", "log_scan_plan"] + "task", + [ + Task( + name="step_scan_plan", + params={ + "detectors": "det", + "motor": "sample_stage.theta", + }, + ), + Task( + name="fly_and_collect_plan", + params={ + "panda": "panda", + "diff": "det", + }, + ), + Task( + name="log_scan_plan", + params={ + "detectors": "det", + "motor": "sample_stage.x", + }, + ), + ], ) -def test_spec_scan_task( +def test_scan( client: BlueapiClient, - task_definition: dict[str, Task], - plan: str, + task: Task, ): - assert client.get_plan(plan), f"In {plan} is available" + result = run_plan_test(client, task) + + assert result.complete_without_errors() + assert result.worker_idle_at_end() + assert result.worker_start_and_stop() + +@dataclass(frozen=True) +class PlanTestResult: + all_events: list[AnyEvent] + task_id: str + + def worker_idle_at_end(self) -> bool: + return self.final_event().state is WorkerState.IDLE + + def worker_start_and_stop(self) -> bool: + # First event is WorkerEvent + assert self.all_events[0] == WorkerEvent( + state=WorkerState.RUNNING, + task_status=TaskStatus( + task_id=self.task_id(), + task_complete=False, + task_failed=False, + ), + ) + + # Last 2 events are WorkerEvent + assert self.all_events[-2:] == [ + WorkerEvent( + state=WorkerState.IDLE, + task_status=TaskStatus( + task_id=self.task_id(), + task_complete=False, + task_failed=False, + ), + ), + WorkerEvent( + state=WorkerState.IDLE, + task_status=TaskStatus( + task_id=self.task_id(), + task_complete=True, + task_failed=False, + ), + ), + ] + + def complete_without_errors(self) -> bool: + final_event = self.final_event() + return final_event.is_complete() and not final_event.is_error() + + def task_id(self) -> str: + return self.final_event().task_status.task_id + + def final_event(self) -> WorkerEvent: + event = self.all_events[-1] + assert isinstance(event, WorkerEvent) + return event + + def data_events(self) -> list[DataEvent]: + return [event for event in self.all_events if isinstance(event, DataEvent)] + + +def run_plan_test(client: BlueapiClient, task: Task) -> PlanTestResult: all_events: list[AnyEvent] = [] def on_event(event: AnyEvent): all_events.append(event) - client.run_task(task_definition[plan], on_event=on_event) - - _check_all_events(all_events) - - assert client.get_state() is WorkerState.IDLE + final_event = client.run_task(task, on_event=on_event) + assert final_event == all_events[-1] + return PlanTestResult(all_events=all_events) From cc05d7b556e41b0816431be8090a9ee11168a434 Mon Sep 17 00:00:00 2001 From: Callum Forrester Date: Thu, 6 Feb 2025 14:02:03 +0000 Subject: [PATCH 2/5] Fix conftest --- tests/system_tests/conftest.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/tests/system_tests/conftest.py b/tests/system_tests/conftest.py index cf2ebd6..aaa9139 100644 --- a/tests/system_tests/conftest.py +++ b/tests/system_tests/conftest.py @@ -1,30 +1,11 @@ import pytest from blueapi.client.client import BlueapiClient from blueapi.config import ApplicationConfig, RestConfig, StompConfig -from blueapi.worker.task import Task from bluesky_stomp.models import BasicAuthentication from htss_rig_bluesky.names import BEAMLINE -@pytest.fixture -def task_definition() -> dict[str, Task]: - return { - "step_scan_plan": Task( - name="step_scan_plan", - params={"detectors": "det", "motor": "sample_stage.theta"}, - ), - "fly_and_collect_plan": Task( - name="fly_and_collect_plan", - params={"panda": "panda", "diff": "det"}, - ), - "log_scan_plan": Task( - name="log_scan_plan", - params={"detectors": "det", "motor": "sample_stage.x"}, - ), - } - - @pytest.fixture def config() -> ApplicationConfig: if BEAMLINE == "p46": From 4ee8a7299b2d1faa2995fae8c375ae2f2a1a20bd Mon Sep 17 00:00:00 2001 From: Callum Forrester Date: Thu, 6 Feb 2025 14:49:06 +0000 Subject: [PATCH 3/5] Create plan for exercising motor --- src/htss_rig_bluesky/plans/exercise.py | 154 +++++-------------------- tests/system_tests/test_plans.py | 6 + 2 files changed, 38 insertions(+), 122 deletions(-) diff --git a/src/htss_rig_bluesky/plans/exercise.py b/src/htss_rig_bluesky/plans/exercise.py index cc5b33b..9ddc5d7 100644 --- a/src/htss_rig_bluesky/plans/exercise.py +++ b/src/htss_rig_bluesky/plans/exercise.py @@ -7,46 +7,12 @@ import bluesky.plan_stubs as bps import bluesky.plans as bp -from dodal.beamlines.training_rig import TrainingRigSampleStage as SampleStage +import bluesky.preprocessors as bpp +from bluesky.protocols import Status +from bluesky.utils import MsgGenerator from ophyd_async.epics.adaravis import AravisDetector from ophyd_async.epics.motor import Motor -from .detector import ensure_detector_ready - - -def exercise_beamline(det: AravisDetector, sample: SampleStage) -> Generator: - """ - Perform all beamline exercise plans sequentially. - - Args: - det: Detector - sample: Sample stage - - Yields: - Plan - """ - - yield from exercise_motors(sample) - yield from exercise_detector(det) - yield from exercise_scan(det, sample) - - -def exercise_motors(sample: SampleStage) -> Generator: - """ - exercise the motors on the sample stage. - - Args: - sample: Sample stage - - Yields: - Plan - """ - - yield from exercise_motor(sample.x, -24.9, 14.0, tolerance=0.1) - yield from exercise_motor( - sample.theta, -1000.0, 1000.0, tolerance=0.1, check_limits=False - ) - def exercise_detector(det: AravisDetector) -> Generator: """ @@ -59,105 +25,49 @@ def exercise_detector(det: AravisDetector) -> Generator: Plan """ - print(f"Excercising {det}") - yield from ensure_detector_ready(det) yield from bp.count([det]) -def exercise_scan(det: AravisDetector, sample: SampleStage) -> Generator: +def exercise_motor(motor: Motor) -> Generator: """ - Perform a short scan to exercise the test rig. - - Args: - det (AravisDetector): Detector - sample (SampleStage): Sample stage - - Yields: - Plan - """ - - print("Excercising scan") - yield from ensure_detector_ready(det) - yield from bp.scan([det], sample.theta, -180.0, 180.0, 10) - - -def exercise_motor( - motor: Motor, - low_limit: float, - high_limit: float, - tolerance: float = 0.0, - check_limits: bool = True, -) -> Generator: - """ - exercise a motor by making sure it can traverse between a low point + Exercise a motor by making sure it can traverse between a low point and a high point. Args: motor: The motor - low_limit: Place to start - high_limit: Place to end - tolerance: Tolerance for checking motor position. Defaults to 0.0. - check_limits: Check whether the motor's limits fall within the bounds, - disable for limitless motors. Defaults to True. Yields: Plan """ - name = motor.name - print(f"Excercising {name}") - - if check_limits: - yield from assert_limits_within(motor, low_limit, high_limit) + low_limit, high_limit = yield from get_limits(motor, padding=0.2) + + @bpp.run_decorator() + @bpp.stage_decorator([motor]) + def move_and_monitor(): + status: Status = yield from bps.abs_set( + motor, + high_limit, + wait=False, + group=move_and_monitor.__name__, + ) + while not status.done: + yield from bps.trigger_and_read([motor]) + yield from bps.sleep(0.1) + yield from bps.wait(group=move_and_monitor.__name__) + + # Perform plan as max velocity + max_velocity = yield from bps.rd(motor.velocity) + yield from bps.abs_set(motor.velocity, max_velocity) + + # Move to start point yield from bps.abs_set(motor, low_limit, wait=True) - yield from assert_motor_at(motor, low_limit, tolerance) - yield from bps.abs_set(motor, high_limit, wait=True) - yield from assert_motor_at(motor, high_limit, tolerance) + # Traverse to high limit, taking a snapshot of motor's position at 10Hz + yield from move_and_monitor() -def assert_limits_within( - motor: Motor, low_limit: float, high_limit: float -) -> Generator: - """ - Check a motors limits fall within the bounds supplied. - Note this is not an exact check, just whether the real limits exceed - the "limit limits" supplied. - - Args: - motor: The motor with limits - low_limit: The lower bound - high_limit: The upper bound - """ - - name = motor.name - motor_high_limit: float = yield from bps.rd(motor.high_limit_travel) - motor_low_limit: float = yield from bps.rd(motor.low_limit_travel) - assert motor_high_limit >= high_limit, ( - f"{name}'s upper limit is {motor.high_limit_travel}, should be >= {high_limit}" - ) - assert motor_low_limit <= low_limit, ( - f"{name}'s lower limit is {motor_low_limit}, should be <= {low_limit}" - ) - - -def assert_motor_at(motor: Motor, pos: float, tolerance: float = 0.0) -> Generator: - """ - Check a motor has reached a required position - - Args: - motor: The motor to check - pos: The required position - tolerance: Plus or minus tolerance, useful for - less precise motors. Defaults to 0.0. - - Yields: - Plan - """ - actual_pos = yield from bps.rd(motor) - upper_bound = pos + (tolerance / 2.0) - lower_bound = pos - (tolerance / 2.0) - assert upper_bound >= actual_pos >= lower_bound, ( - f"{motor.name} is at {actual_pos}, " - ) - f"should be between {lower_bound} and {upper_bound}" +def get_limits(motor: Motor, padding: float = 0.0) -> MsgGenerator[tuple[float, float]]: + low_limit = yield from bps.rd(motor.low_limit_travel) + high_limit = yield from bps.rd(motor.high_limit_travel) + return low_limit + padding, high_limit - padding diff --git a/tests/system_tests/test_plans.py b/tests/system_tests/test_plans.py index f9b29ec..28a3879 100644 --- a/tests/system_tests/test_plans.py +++ b/tests/system_tests/test_plans.py @@ -55,6 +55,12 @@ def test_device_present(client: BlueapiClient, device: str): assert client.get_device(device), f"{device} is not available" +@pytest.mark.parametrize("motor", ["sample_stage.x", "sample_stage.theta"]) +def test_motor_behavoir(client: BlueapiClient, motor: str): + task = Task(name="exercise_motor", params={"motor": motor}) + run_plan_test(client, task) + + @pytest.mark.parametrize( "task", [ From b53c05246eb3e83de0580feb77aac5f7eced4336 Mon Sep 17 00:00:00 2001 From: Callum Forrester Date: Thu, 6 Feb 2025 14:50:04 +0000 Subject: [PATCH 4/5] Remove unused function --- tests/system_tests/test_plans.py | 40 -------------------------------- 1 file changed, 40 deletions(-) diff --git a/tests/system_tests/test_plans.py b/tests/system_tests/test_plans.py index 28a3879..b870b89 100644 --- a/tests/system_tests/test_plans.py +++ b/tests/system_tests/test_plans.py @@ -10,46 +10,6 @@ # Please export BEAMLINE=pXX before running the tests or add it in pyproject.toml -def _check_all_events(all_events: list[AnyEvent]): - assert ( - isinstance(all_events[0], WorkerEvent) and all_events[0].task_status is not None - ) - task_id = all_events[0].task_status.task_id - # First event is WorkerEvent - assert all_events[0] == WorkerEvent( - state=WorkerState.RUNNING, - task_status=TaskStatus( - task_id=task_id, - task_complete=False, - task_failed=False, - ), - ) - - assert all(isinstance(event, DataEvent) for event in all_events[1:-2]), ( - "Middle elements must be DataEvents." - ) - - # Last 2 events are WorkerEvent - assert all_events[-2:] == [ - WorkerEvent( - state=WorkerState.IDLE, - task_status=TaskStatus( - task_id=task_id, - task_complete=False, - task_failed=False, - ), - ), - WorkerEvent( - state=WorkerState.IDLE, - task_status=TaskStatus( - task_id=task_id, - task_complete=True, - task_failed=False, - ), - ), - ] - - @pytest.mark.parametrize("device", ["sample_stage", "panda", "det"]) def test_device_present(client: BlueapiClient, device: str): assert client.get_device(device), f"{device} is not available" From 6e9afb7f7455534e6f9125bd67ffbbbe6004b884 Mon Sep 17 00:00:00 2001 From: Callum Forrester Date: Tue, 11 Feb 2025 13:52:45 +0000 Subject: [PATCH 5/5] Start on hardware system tests --- tests/system_tests/test_plans.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/tests/system_tests/test_plans.py b/tests/system_tests/test_plans.py index b870b89..8b5f21d 100644 --- a/tests/system_tests/test_plans.py +++ b/tests/system_tests/test_plans.py @@ -1,3 +1,4 @@ +import tempfile from dataclasses import dataclass import pytest @@ -7,7 +8,20 @@ from blueapi.worker.event import TaskStatus, WorkerEvent, WorkerState from blueapi.worker.task import Task + # Please export BEAMLINE=pXX before running the tests or add it in pyproject.toml +@pytest.fixture +def tiled_client(): + from tiled.catalog import in_memory + from tiled.client import Context, from_context + from tiled.server.app import build_app + + with tempfile.TemporaryDirectory() as tempdir: + catalog = in_memory(writable_storage=tempdir, readable_storage=None) + app = build_app(catalog) + with Context.from_app(app) as context: + client = from_context(context) + yield client @pytest.mark.parametrize("device", ["sample_stage", "panda", "det"]) @@ -18,7 +32,9 @@ def test_device_present(client: BlueapiClient, device: str): @pytest.mark.parametrize("motor", ["sample_stage.x", "sample_stage.theta"]) def test_motor_behavoir(client: BlueapiClient, motor: str): task = Task(name="exercise_motor", params={"motor": motor}) - run_plan_test(client, task) + r = run_plan_test(client, task) + tiled = r.tiled() + print(tiled) @pytest.mark.parametrize( @@ -63,6 +79,12 @@ class PlanTestResult: all_events: list[AnyEvent] task_id: str + def tiled(self): + client = tiled_client() + for data_event in self.data_events(): + client.send(data_event.model_d) + return client + def worker_idle_at_end(self) -> bool: return self.final_event().state is WorkerState.IDLE