From 426b4200ad40acf3e21afae7d14484bbc5fbc45b Mon Sep 17 00:00:00 2001 From: olliesilvester <122091460+olliesilvester@users.noreply.github.com> Date: Wed, 8 May 2024 15:11:15 +0100 Subject: [PATCH 1/7] Add logging messages to signals (#275) * Add debug logging messages to signals on connections, making and closing subscriptions, subscriptions changing values, reading from a signal, and putting to a signal --- src/ophyd_async/core/device.py | 13 ++++++++ src/ophyd_async/core/signal.py | 24 +++++++++++--- src/ophyd_async/epics/_backend/_p4p.py | 1 - tests/core/test_device.py | 7 ++++ tests/core/test_signal.py | 46 +++++++++++++++++++++++++- tests/core/test_utils.py | 22 ++++++++---- tests/epics/demo/test_demo.py | 1 + 7 files changed, 102 insertions(+), 12 deletions(-) diff --git a/src/ophyd_async/core/device.py b/src/ophyd_async/core/device.py index 07145ae8f2..ce7beaeecb 100644 --- a/src/ophyd_async/core/device.py +++ b/src/ophyd_async/core/device.py @@ -3,6 +3,8 @@ from __future__ import annotations import sys +from functools import cached_property +from logging import LoggerAdapter, getLogger from typing import ( Any, Coroutine, @@ -39,6 +41,12 @@ def name(self) -> str: """Return the name of the Device""" return self._name + @cached_property + def log(self): + return LoggerAdapter( + getLogger("ophyd_async.devices"), {"ophyd_async_device_name": self.name} + ) + def children(self) -> Iterator[Tuple[str, Device]]: for attr_name, attr in self.__dict__.items(): if attr_name != "parent" and isinstance(attr, Device): @@ -52,6 +60,11 @@ def set_name(self, name: str): name: New name to set """ + + # Ensure self.log is recreated after a name change + if hasattr(self, "log"): + del self.log + self._name = name for attr_name, child in self.children(): child_name = f"{name}-{attr_name.rstrip('_')}" if name else "" diff --git a/src/ophyd_async/core/signal.py b/src/ophyd_async/core/signal.py index d1fe17546a..d0d50c8999 100644 --- a/src/ophyd_async/core/signal.py +++ b/src/ophyd_async/core/signal.py @@ -61,9 +61,9 @@ def __init__( timeout: Optional[float] = DEFAULT_TIMEOUT, name: str = "", ) -> None: - super().__init__(name) self._timeout = timeout self._init_backend = self._backend = backend + super().__init__(name) async def connect(self, sim=False, timeout=DEFAULT_TIMEOUT): if sim: @@ -72,6 +72,7 @@ async def connect(self, sim=False, timeout=DEFAULT_TIMEOUT): else: self._backend = self._init_backend _sim_backends.pop(self, None) + self.log.debug(f"Connecting to {self.source}") await self._backend.connect(timeout=timeout) @property @@ -96,10 +97,12 @@ def __init__(self, backend: SignalBackend[T], signal: Signal): self._value: Optional[T] = None self.backend = backend + signal.log.debug(f"Making subscription on source {signal.source}") backend.set_callback(self._callback) def close(self): self.backend.set_callback(None) + self._signal.log.debug(f"Closing subscription on source {self._signal.source}") async def get_reading(self) -> Reading: await self._valid.wait() @@ -112,6 +115,10 @@ async def get_value(self) -> T: return self._value def _callback(self, reading: Reading, value: T): + self._signal.log.debug( + f"Updated subscription: reading of source {self._signal.source} changed" + f"from {self._reading} to {reading}" + ) self._reading = reading self._value = value self._valid.set() @@ -178,7 +185,9 @@ async def describe(self) -> Dict[str, DataKey]: @_add_timeout async def get_value(self, cached: Optional[bool] = None) -> T: """The current value""" - return await self._backend_or_cache(cached).get_value() + value = await self._backend_or_cache(cached).get_value() + self.log.debug(f"get_value() on source {self.source} returned {value}") + return value def subscribe_value(self, function: Callback[T]): """Subscribe to updates in value of a device""" @@ -213,8 +222,15 @@ def set(self, value: T, wait=True, timeout=USE_DEFAULT_TIMEOUT) -> AsyncStatus: """Set the value and return a status saying when it's done""" if timeout is USE_DEFAULT_TIMEOUT: timeout = self._timeout - coro = self._backend.put(value, wait=wait, timeout=timeout) - return AsyncStatus(coro) + + async def do_set(): + self.log.debug(f"Putting value {value} to backend at source {self.source}") + await self._backend.put(value, wait=wait, timeout=timeout) + self.log.debug( + f"Successfully put value {value} to backend at source {self.source}" + ) + + return AsyncStatus(do_set()) class SignalRW(SignalR[T], SignalW[T], Locatable): diff --git a/src/ophyd_async/epics/_backend/_p4p.py b/src/ophyd_async/epics/_backend/_p4p.py index 807b74e696..816f1f5e68 100644 --- a/src/ophyd_async/epics/_backend/_p4p.py +++ b/src/ophyd_async/epics/_backend/_p4p.py @@ -244,7 +244,6 @@ def __init__(self, datatype: Optional[Type[T]], read_pv: str, write_pv: str): self.converter: PvaConverter = DisconnectedPvaConverter() self.subscription: Optional[Subscription] = None - @property def source(self, name: str): return f"pva://{self.read_pv}" diff --git a/tests/core/test_device.py b/tests/core/test_device.py index 482666c18c..97184aed54 100644 --- a/tests/core/test_device.py +++ b/tests/core/test_device.py @@ -110,3 +110,10 @@ async def test_wait_for_connection_propagates_error( with pytest.raises(NotConnected) as e: await wait_for_connection(**failing_coros) assert traceback.extract_tb(e.__traceback__)[-1].name == "failing_coroutine" + + +async def test_device_log_has_correct_name(): + device = DummyBaseDevice() + assert device.log.extra["ophyd_async_device_name"] == "" + device.set_name("device") + assert device.log.extra["ophyd_async_device_name"] == "device" diff --git a/tests/core/test_signal.py b/tests/core/test_signal.py index 5b3037f7b7..ccd3540133 100644 --- a/tests/core/test_signal.py +++ b/tests/core/test_signal.py @@ -1,7 +1,8 @@ import asyncio +import logging import re import time -from unittest.mock import ANY +from unittest.mock import ANY, AsyncMock import numpy import pytest @@ -12,6 +13,7 @@ DeviceCollector, HintedSignal, Signal, + SignalBackend, SignalR, SignalRW, SimSignalBackend, @@ -26,6 +28,7 @@ soft_signal_rw, wait_for_value, ) +from ophyd_async.core.signal import _SignalCache from ophyd_async.core.utils import DEFAULT_TIMEOUT from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw @@ -39,6 +42,19 @@ async def connect(self, sim=False, timeout=DEFAULT_TIMEOUT): pass +class MockSignalRW(SignalRW): + def __init__(self, backend, timeout, name): + super().__init__(backend, timeout, name) + self._backend = AsyncMock() + + @property + def source(self) -> str: + return "source" + + async def connect(self): + pass + + def test_signals_equality_raises(): sim_backend = SimSignalBackend(str) @@ -228,3 +244,31 @@ async def test_assert_configuration(sim_readable: DummyReadable): }, } await assert_configuration(sim_readable, dummy_config_reading) + + +async def test_signal_connect_logs(caplog): + caplog.set_level(logging.DEBUG) + sim_signal = Signal(SimSignalBackend(str, "test"), timeout=1, name="test_signal") + await sim_signal.connect(sim=True) + assert caplog.text.endswith("Connecting to soft://test_signal\n") + + +async def test_signal_get_and_set_logging(caplog): + caplog.set_level(logging.DEBUG) + mock_signal_rw = MockSignalRW(SignalBackend, timeout=1, name="mock_signal") + await mock_signal_rw.set(value=0) + assert "Putting value 0 to backend at source" in caplog.text + assert "Successfully put value 0 to backend at source" in caplog.text + await mock_signal_rw.get_value() + assert "get_value() on source" in caplog.text + + +def test_subscription_logs(caplog): + caplog.set_level(logging.DEBUG) + cache = _SignalCache( + SignalBackend(), + signal=MockSignalRW(SignalBackend, timeout=1, name="mock_signal"), + ) + assert "Making subscription" in caplog.text + cache.close() + assert "Closing subscription on source" in caplog.text diff --git a/tests/core/test_utils.py b/tests/core/test_utils.py index 74ea8bc787..b2e22a46c0 100644 --- a/tests/core/test_utils.py +++ b/tests/core/test_utils.py @@ -1,3 +1,5 @@ +from unittest.mock import patch + import pytest from ophyd_async.core import ( @@ -123,9 +125,9 @@ async def test_error_handling_connection_timeout(caplog): assert str(e.value) == str(ONE_WORKING_ONE_TIMEOUT_OUTPUT) logs = caplog.get_records("call") - assert len(logs) == 1 - assert "signal ca://A_NON_EXISTENT_SIGNAL timed out" == logs[0].message - assert logs[0].levelname == "DEBUG" + assert len(logs) == 3 + assert "signal ca://A_NON_EXISTENT_SIGNAL timed out" == logs[-1].message + assert logs[-1].levelname == "DEBUG" async def test_error_handling_value_errors(caplog): @@ -134,7 +136,7 @@ async def test_error_handling_value_errors(caplog): caplog.set_level(10) dummy_device_two_working_one_timeout_two_value_error = ( - DummyDeviceTwoWorkingTwoTimeOutTwoValueError() + DummyDeviceTwoWorkingTwoTimeOutTwoValueError("dsf") ) # This should fail since the error is a ValueError @@ -147,7 +149,11 @@ async def test_error_handling_value_errors(caplog): assert str(e.value) == str(TWO_WORKING_TWO_TIMEOUT_TWO_VALUE_ERROR_OUTPUT) logs = caplog.get_records("call") - logs = [log for log in logs if "ophyd_async" in log.pathname] + logs = [ + log + for log in logs + if "ophyd_async" in log.pathname and "signal" not in log.pathname + ] assert len(logs) == 4 for i in range(0, 2): @@ -184,7 +190,11 @@ async def test_error_handling_device_collector(caplog): assert str(expected_output) == str(e.value) logs = caplog.get_records("call") - logs = [log for log in logs if "ophyd_async" in log.pathname] + logs = [ + log + for log in logs + if "ophyd_async" in log.pathname and "signal" not in log.pathname + ] assert len(logs) == 5 assert ( logs[0].message diff --git a/tests/epics/demo/test_demo.py b/tests/epics/demo/test_demo.py index 35ff880191..f6cddb4ad1 100644 --- a/tests/epics/demo/test_demo.py +++ b/tests/epics/demo/test_demo.py @@ -192,6 +192,7 @@ async def test_sensor_disconnected(caplog): async with DeviceCollector(timeout=0.1): s = demo.Sensor("ca://PRE:", name="sensor") logs = caplog.get_records("call") + logs = [log for log in logs if "signal" not in log.pathname] assert len(logs) == 2 assert logs[0].message == ("signal ca://PRE:Value timed out") From 09b48675619151c35654b0620a7d1e14590f6c13 Mon Sep 17 00:00:00 2001 From: olliesilvester <122091460+olliesilvester@users.noreply.github.com> Date: Thu, 9 May 2024 11:32:28 +0100 Subject: [PATCH 2/7] Move colorlog to a regular dependency (#292) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1e2846b081..6715226ab3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ dependencies = [ "event-model<1.21.0", "p4p", "pyyaml", + "colorlog", ] dynamic = ["version"] license.file = "LICENSE" @@ -64,7 +65,6 @@ dev = [ "tox-direct", "types-mock", "types-pyyaml", - "colorlog" ] [project.scripts] From ce5d58509265d0315439dc3c6783f2c86c2ee5bf Mon Sep 17 00:00:00 2001 From: Zoheb Shaikh Date: Thu, 9 May 2024 15:53:21 +0100 Subject: [PATCH 3/7] replace-ad_rw (#274) * replace-ad_rw * replaced ad_r * added _RBV for ad_rw and ad_r * code formatting * removed _RBV where not required * removed suffix from epics_signal_rw_rbv --------- Co-authored-by: Tom C (DLS) <101418278+coretl@users.noreply.github.com> --- .../epics/areadetector/__init__.py | 4 -- .../epics/areadetector/drivers/ad_base.py | 22 +++++----- .../areadetector/drivers/aravis_driver.py | 12 +++--- .../areadetector/drivers/kinetix_driver.py | 11 +++-- .../areadetector/drivers/pilatus_driver.py | 7 +++- .../areadetector/drivers/vimba_driver.py | 19 +++++---- src/ophyd_async/epics/areadetector/utils.py | 14 +------ .../epics/areadetector/writers/nd_file_hdf.py | 40 ++++++++++--------- .../epics/areadetector/writers/nd_plugin.py | 11 +++-- 9 files changed, 71 insertions(+), 69 deletions(-) diff --git a/src/ophyd_async/epics/areadetector/__init__.py b/src/ophyd_async/epics/areadetector/__init__.py index 309827cb5e..7678cd384c 100644 --- a/src/ophyd_async/epics/areadetector/__init__.py +++ b/src/ophyd_async/epics/areadetector/__init__.py @@ -7,8 +7,6 @@ ImageMode, NDAttributeDataType, NDAttributesXML, - ad_r, - ad_rw, ) from .vimba import VimbaDetector @@ -19,8 +17,6 @@ "SingleTriggerDet", "FileWriteMode", "ImageMode", - "ad_r", - "ad_rw", "NDAttributeDataType", "NDAttributesXML", "PilatusDetector", diff --git a/src/ophyd_async/epics/areadetector/drivers/ad_base.py b/src/ophyd_async/epics/areadetector/drivers/ad_base.py index b42a45f66b..6c0f17433d 100644 --- a/src/ophyd_async/epics/areadetector/drivers/ad_base.py +++ b/src/ophyd_async/epics/areadetector/drivers/ad_base.py @@ -9,8 +9,8 @@ set_and_wait_for_value, ) -from ...signal.signal import epics_signal_rw -from ..utils import ImageMode, ad_r, ad_rw +from ...signal.signal import epics_signal_r, epics_signal_rw, epics_signal_rw_rbv +from ..utils import ImageMode from ..writers.nd_plugin import NDArrayBase @@ -43,14 +43,16 @@ class DetectorState(str, Enum): class ADBase(NDArrayBase): def __init__(self, prefix: str, name: str = "") -> None: # Define some signals - self.acquire = ad_rw(bool, prefix + "Acquire") - self.acquire_time = ad_rw(float, prefix + "AcquireTime") - self.num_images = ad_rw(int, prefix + "NumImages") - self.image_mode = ad_rw(ImageMode, prefix + "ImageMode") - self.array_counter = ad_rw(int, prefix + "ArrayCounter") - self.array_size_x = ad_r(int, prefix + "ArraySizeX") - self.array_size_y = ad_r(int, prefix + "ArraySizeY") - self.detector_state = ad_r(DetectorState, prefix + "DetectorState") + self.acquire = epics_signal_rw_rbv(bool, prefix + "Acquire") + self.acquire_time = epics_signal_rw_rbv(float, prefix + "AcquireTime") + self.num_images = epics_signal_rw_rbv(int, prefix + "NumImages") + self.image_mode = epics_signal_rw_rbv(ImageMode, prefix + "ImageMode") + self.array_counter = epics_signal_rw_rbv(int, prefix + "ArrayCounter") + self.array_size_x = epics_signal_r(int, prefix + "ArraySizeX_RBV") + self.array_size_y = epics_signal_r(int, prefix + "ArraySizeY_RBV") + self.detector_state = epics_signal_r( + DetectorState, prefix + "DetectorState_RBV" + ) # There is no _RBV for this one self.wait_for_plugins = epics_signal_rw(bool, prefix + "WaitForPlugins") super().__init__(prefix, name=name) diff --git a/src/ophyd_async/epics/areadetector/drivers/aravis_driver.py b/src/ophyd_async/epics/areadetector/drivers/aravis_driver.py index b2f89d8007..e69fc609b0 100644 --- a/src/ophyd_async/epics/areadetector/drivers/aravis_driver.py +++ b/src/ophyd_async/epics/areadetector/drivers/aravis_driver.py @@ -2,7 +2,7 @@ from typing import Callable, Dict, Literal, Optional, Tuple from ophyd_async.epics.areadetector.drivers import ADBase -from ophyd_async.epics.areadetector.utils import ad_r, ad_rw +from ophyd_async.epics.signal.signal import epics_signal_r, epics_signal_rw_rbv class AravisTriggerMode(str, Enum): @@ -138,10 +138,12 @@ class AravisDriver(ADBase): """ def __init__(self, prefix: str, name: str = "") -> None: - self.trigger_mode = ad_rw(AravisTriggerMode, prefix + "TriggerMode") - self.trigger_source = ad_rw(str, prefix + "TriggerSource") - self.model = ad_r(str, prefix + "Model") - self.pixel_format = ad_rw(str, prefix + "PixelFormat") + self.trigger_mode = epics_signal_rw_rbv( + AravisTriggerMode, prefix + "TriggerMode" + ) + self.trigger_source = epics_signal_rw_rbv(str, prefix + "TriggerSource") + self.model = epics_signal_r(str, prefix + "Model_RBV") + self.pixel_format = epics_signal_rw_rbv(str, prefix + "PixelFormat") self.dead_time: Optional[float] = None super().__init__(prefix, name=name) diff --git a/src/ophyd_async/epics/areadetector/drivers/kinetix_driver.py b/src/ophyd_async/epics/areadetector/drivers/kinetix_driver.py index ab0bd01af4..b3497bee0b 100644 --- a/src/ophyd_async/epics/areadetector/drivers/kinetix_driver.py +++ b/src/ophyd_async/epics/areadetector/drivers/kinetix_driver.py @@ -1,6 +1,7 @@ from enum import Enum -from ..utils import ad_rw +from ophyd_async.epics.signal.signal import epics_signal_rw_rbv + from .ad_base import ADBase @@ -18,7 +19,9 @@ class KinetixReadoutMode(str, Enum): class KinetixDriver(ADBase): def __init__(self, prefix: str, name: str = "") -> None: - # self.pixel_format = ad_rw(PixelFormat, prefix + "PixelFormat") - self.trigger_mode = ad_rw(KinetixTriggerMode, prefix + "TriggerMode") - self.mode = ad_rw(KinetixReadoutMode, prefix + "ReadoutPortIdx") + # self.pixel_format = epics_signal_rw_rbv(PixelFormat, prefix + "PixelFormat") + self.trigger_mode = epics_signal_rw_rbv( + KinetixTriggerMode, prefix + "TriggerMode" + ) + self.mode = epics_signal_rw_rbv(KinetixReadoutMode, prefix + "ReadoutPortIdx") super().__init__(prefix, name) diff --git a/src/ophyd_async/epics/areadetector/drivers/pilatus_driver.py b/src/ophyd_async/epics/areadetector/drivers/pilatus_driver.py index c0ffeffdfc..c1bb602e72 100644 --- a/src/ophyd_async/epics/areadetector/drivers/pilatus_driver.py +++ b/src/ophyd_async/epics/areadetector/drivers/pilatus_driver.py @@ -1,6 +1,7 @@ from enum import Enum -from ..utils import ad_rw +from ophyd_async.epics.signal.signal import epics_signal_rw_rbv + from .ad_base import ADBase @@ -14,5 +15,7 @@ class PilatusTriggerMode(str, Enum): class PilatusDriver(ADBase): def __init__(self, prefix: str, name: str = "") -> None: - self.trigger_mode = ad_rw(PilatusTriggerMode, prefix + "TriggerMode") + self.trigger_mode = epics_signal_rw_rbv( + PilatusTriggerMode, prefix + "TriggerMode" + ) super().__init__(prefix, name) diff --git a/src/ophyd_async/epics/areadetector/drivers/vimba_driver.py b/src/ophyd_async/epics/areadetector/drivers/vimba_driver.py index 4cec75dd2e..0f2b69ebc2 100644 --- a/src/ophyd_async/epics/areadetector/drivers/vimba_driver.py +++ b/src/ophyd_async/epics/areadetector/drivers/vimba_driver.py @@ -1,6 +1,7 @@ from enum import Enum -from ..utils import ad_rw +from ophyd_async.epics.signal.signal import epics_signal_rw_rbv + from .ad_base import ADBase @@ -47,12 +48,16 @@ class VimbaExposeOutMode(str, Enum): class VimbaDriver(ADBase): def __init__(self, prefix: str, name: str = "") -> None: - # self.pixel_format = ad_rw(PixelFormat, prefix + "PixelFormat") - self.convert_format = ad_rw( + # self.pixel_format = epics_signal_rw_rbv(PixelFormat, prefix + "PixelFormat") + self.convert_format = epics_signal_rw_rbv( VimbaConvertFormat, prefix + "ConvertPixelFormat" ) # Pixel format of data outputted to AD - self.trig_source = ad_rw(VimbaTriggerSource, prefix + "TriggerSource") - self.trigger_mode = ad_rw(VimbaOnOff, prefix + "TriggerMode") - self.overlap = ad_rw(VimbaOverlap, prefix + "TriggerOverlap") - self.expose_mode = ad_rw(VimbaExposeOutMode, prefix + "ExposureMode") + self.trig_source = epics_signal_rw_rbv( + VimbaTriggerSource, prefix + "TriggerSource" + ) + self.trigger_mode = epics_signal_rw_rbv(VimbaOnOff, prefix + "TriggerMode") + self.overlap = epics_signal_rw_rbv(VimbaOverlap, prefix + "TriggerOverlap") + self.expose_mode = epics_signal_rw_rbv( + VimbaExposeOutMode, prefix + "ExposureMode" + ) super().__init__(prefix, name) diff --git a/src/ophyd_async/epics/areadetector/utils.py b/src/ophyd_async/epics/areadetector/utils.py index c8e35cded8..2aa1a4efde 100644 --- a/src/ophyd_async/epics/areadetector/utils.py +++ b/src/ophyd_async/epics/areadetector/utils.py @@ -1,18 +1,8 @@ from enum import Enum -from typing import Optional, Type +from typing import Optional from xml.etree import cElementTree as ET -from ophyd_async.core import DEFAULT_TIMEOUT, SignalR, SignalRW, T, wait_for_value - -from ..signal.signal import epics_signal_r, epics_signal_rw - - -def ad_rw(datatype: Type[T], prefix: str) -> SignalRW[T]: - return epics_signal_rw(datatype, prefix + "_RBV", prefix) - - -def ad_r(datatype: Type[T], prefix: str) -> SignalR[T]: - return epics_signal_r(datatype, prefix + "_RBV") +from ophyd_async.core import DEFAULT_TIMEOUT, SignalRW, T, wait_for_value class FileWriteMode(str, Enum): diff --git a/src/ophyd_async/epics/areadetector/writers/nd_file_hdf.py b/src/ophyd_async/epics/areadetector/writers/nd_file_hdf.py index 9689bc4678..77598985ee 100644 --- a/src/ophyd_async/epics/areadetector/writers/nd_file_hdf.py +++ b/src/ophyd_async/epics/areadetector/writers/nd_file_hdf.py @@ -1,7 +1,7 @@ from enum import Enum -from ...signal.signal import epics_signal_rw -from ..utils import FileWriteMode, ad_r, ad_rw +from ...signal.signal import epics_signal_r, epics_signal_rw, epics_signal_rw_rbv +from ..utils import FileWriteMode from .nd_plugin import NDPluginBase @@ -19,22 +19,24 @@ class Compression(str, Enum): class NDFileHDF(NDPluginBase): def __init__(self, prefix: str, name="") -> None: # Define some signals - self.position_mode = ad_rw(bool, prefix + "PositionMode") - self.compression = ad_rw(Compression, prefix + "Compression") - self.num_extra_dims = ad_rw(int, prefix + "NumExtraDims") - self.file_path = ad_rw(str, prefix + "FilePath") - self.file_name = ad_rw(str, prefix + "FileName") - self.file_path_exists = ad_r(bool, prefix + "FilePathExists") - self.file_template = ad_rw(str, prefix + "FileTemplate") - self.full_file_name = ad_r(str, prefix + "FullFileName") - self.file_write_mode = ad_rw(FileWriteMode, prefix + "FileWriteMode") - self.num_capture = ad_rw(int, prefix + "NumCapture") - self.num_captured = ad_r(int, prefix + "NumCaptured") - self.swmr_mode = ad_rw(bool, prefix + "SWMRMode") - self.lazy_open = ad_rw(bool, prefix + "LazyOpen") - self.capture = ad_rw(bool, prefix + "Capture") + self.position_mode = epics_signal_rw_rbv(bool, prefix + "PositionMode") + self.compression = epics_signal_rw_rbv(Compression, prefix + "Compression") + self.num_extra_dims = epics_signal_rw_rbv(int, prefix + "NumExtraDims") + self.file_path = epics_signal_rw_rbv(str, prefix + "FilePath") + self.file_name = epics_signal_rw_rbv(str, prefix + "FileName") + self.file_path_exists = epics_signal_r(bool, prefix + "FilePathExists_RBV") + self.file_template = epics_signal_rw_rbv(str, prefix + "FileTemplate") + self.full_file_name = epics_signal_r(str, prefix + "FullFileName_RBV") + self.file_write_mode = epics_signal_rw_rbv( + FileWriteMode, prefix + "FileWriteMode" + ) + self.num_capture = epics_signal_rw_rbv(int, prefix + "NumCapture") + self.num_captured = epics_signal_r(int, prefix + "NumCaptured_RBV") + self.swmr_mode = epics_signal_rw_rbv(bool, prefix + "SWMRMode") + self.lazy_open = epics_signal_rw_rbv(bool, prefix + "LazyOpen") + self.capture = epics_signal_rw_rbv(bool, prefix + "Capture") self.flush_now = epics_signal_rw(bool, prefix + "FlushNow") - self.array_size0 = ad_r(int, prefix + "ArraySize0") - self.array_size1 = ad_r(int, prefix + "ArraySize1") - self.xml_file_name = ad_rw(str, prefix + "XMLFileName") + self.array_size0 = epics_signal_r(int, prefix + "ArraySize0_RBV") + self.array_size1 = epics_signal_r(int, prefix + "ArraySize1_RBV") + self.xml_file_name = epics_signal_rw_rbv(str, prefix + "XMLFileName") super().__init__(prefix, name) diff --git a/src/ophyd_async/epics/areadetector/writers/nd_plugin.py b/src/ophyd_async/epics/areadetector/writers/nd_plugin.py index 815c2a8e0d..29268153ae 100644 --- a/src/ophyd_async/epics/areadetector/writers/nd_plugin.py +++ b/src/ophyd_async/epics/areadetector/writers/nd_plugin.py @@ -2,8 +2,7 @@ from ophyd_async.core import Device from ophyd_async.epics.signal import epics_signal_rw - -from ..utils import ad_r, ad_rw +from ophyd_async.epics.signal.signal import epics_signal_r, epics_signal_rw_rbv class Callback(str, Enum): @@ -13,16 +12,16 @@ class Callback(str, Enum): class NDArrayBase(Device): def __init__(self, prefix: str, name: str = "") -> None: - self.unique_id = ad_r(int, prefix + "UniqueId") + self.unique_id = epics_signal_r(int, prefix + "UniqueId_RBV") self.nd_attributes_file = epics_signal_rw(str, prefix + "NDAttributesFile") super().__init__(name) class NDPluginBase(NDArrayBase): def __init__(self, prefix: str, name: str = "") -> None: - self.nd_array_port = ad_rw(str, prefix + "NDArrayPort") - self.enable_callback = ad_rw(Callback, prefix + "EnableCallbacks") - self.nd_array_address = ad_rw(int, prefix + "NDArrayAddress") + self.nd_array_port = epics_signal_rw_rbv(str, prefix + "NDArrayPort") + self.enable_callback = epics_signal_rw_rbv(Callback, prefix + "EnableCallbacks") + self.nd_array_address = epics_signal_rw_rbv(int, prefix + "NDArrayAddress") super().__init__(prefix, name) From 989856201fe8889fdc69cd3b12f62ef3e2d4ce22 Mon Sep 17 00:00:00 2001 From: Abigail Emery Date: Thu, 9 May 2024 16:59:12 +0100 Subject: [PATCH 4/7] Remove cov line to allow vscode debugging (#295) --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6715226ab3..29377c6faa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,7 +86,6 @@ write_to = "src/ophyd_async/_version.py" addopts = """ --tb=native -vv --strict-markers --doctest-modules --doctest-glob="*.rst" --doctest-glob="*.md" --ignore=docs/examples - --cov=src/ophyd_async --cov-report term --cov-report xml:cov.xml """ # https://iscinumpy.gitlab.io/post/bound-version-constraints/#watch-for-warnings filterwarnings = "error" From 5fd7928de15b6f4f721196ea32206e572c63b76d Mon Sep 17 00:00:00 2001 From: DiamondJoseph <53935796+DiamondJoseph@users.noreply.github.com> Date: Fri, 10 May 2024 13:33:18 +0100 Subject: [PATCH 5/7] Fix support for Str datatype Enum signals (#289) Co-authored-by: Tom C (DLS) <101418278+coretl@users.noreply.github.com> --- docs/how-to/make-a-simple-device.rst | 3 ++- src/ophyd_async/epics/_backend/_aioca.py | 18 ++++++++----- src/ophyd_async/epics/_backend/_p4p.py | 17 +++++++----- src/ophyd_async/epics/_backend/common.py | 34 ++++++++++++------------ tests/epics/_backend/test_common.py | 30 +++++++++------------ tests/epics/test_signals.py | 27 ++++++++++++++++++- 6 files changed, 79 insertions(+), 50 deletions(-) diff --git a/docs/how-to/make-a-simple-device.rst b/docs/how-to/make-a-simple-device.rst index 01f12c3e53..76137e63da 100644 --- a/docs/how-to/make-a-simple-device.rst +++ b/docs/how-to/make-a-simple-device.rst @@ -30,7 +30,8 @@ its Python type, which could be: - A primitive (`str`, `int`, `float`) - An array (`numpy.typing.NDArray` or ``Sequence[str]``) -- An enum (`enum.Enum`). +- An enum (`enum.Enum`) which **must** also extend `str` + - `str` and ``EnumClass(str, Enum)`` are the only valid ``datatype`` for an enumerated signal. The rest of the arguments are PV connection information, in this case the PV suffix. diff --git a/src/ophyd_async/epics/_backend/_aioca.py b/src/ophyd_async/epics/_backend/_aioca.py index b96f0e9a98..87e3395dc9 100644 --- a/src/ophyd_async/epics/_backend/_aioca.py +++ b/src/ophyd_async/epics/_backend/_aioca.py @@ -28,7 +28,7 @@ ) from ophyd_async.core.utils import DEFAULT_TIMEOUT, NotConnected -from .common import get_supported_enum_class +from .common import get_supported_values dbr_to_dtype: Dict[Dbr, Dtype] = { dbr.DBR_STRING: "string", @@ -79,7 +79,7 @@ def get_datakey(self, source: str, value: AugmentedValue) -> DataKey: @dataclass class CaEnumConverter(CaConverter): - enum_class: Type[Enum] + choices: dict[str, str] def write_value(self, value: Union[Enum, str]): if isinstance(value, Enum): @@ -88,11 +88,15 @@ def write_value(self, value: Union[Enum, str]): return value def value(self, value: AugmentedValue): - return self.enum_class(value) + return self.choices[value] def get_datakey(self, source: str, value: AugmentedValue) -> DataKey: - choices = [e.value for e in self.enum_class] - return {"source": source, "dtype": "string", "shape": [], "choices": choices} + return { + "source": source, + "dtype": "string", + "shape": [], + "choices": list(self.choices), + } class DisconnectedCaConverter(CaConverter): @@ -138,8 +142,8 @@ def make_converter( pv_choices = get_unique( {k: tuple(v.enums) for k, v in values.items()}, "choices" ) - enum_class = get_supported_enum_class(pv, datatype, pv_choices) - return CaEnumConverter(dbr.DBR_STRING, None, enum_class) + supported_values = get_supported_values(pv, datatype, pv_choices) + return CaEnumConverter(dbr.DBR_STRING, None, supported_values) else: value = list(values.values())[0] # Done the dbr check, so enough to check one of the values diff --git a/src/ophyd_async/epics/_backend/_p4p.py b/src/ophyd_async/epics/_backend/_p4p.py index 816f1f5e68..d6e2b49b1c 100644 --- a/src/ophyd_async/epics/_backend/_p4p.py +++ b/src/ophyd_async/epics/_backend/_p4p.py @@ -20,7 +20,7 @@ ) from ophyd_async.core.utils import DEFAULT_TIMEOUT, NotConnected -from .common import get_supported_enum_class +from .common import get_supported_values # https://mdavidsaver.github.io/p4p/values.html specifier_to_dtype: Dict[str, Dtype] = { @@ -109,7 +109,8 @@ def write_value(self, value): @dataclass class PvaEnumConverter(PvaConverter): - enum_class: Type[Enum] + def __init__(self, choices: dict[str, str]): + self.choices = tuple(choices.values()) def write_value(self, value: Union[Enum, str]): if isinstance(value, Enum): @@ -118,11 +119,15 @@ def write_value(self, value: Union[Enum, str]): return value def value(self, value): - return list(self.enum_class)[value["value"]["index"]] + return self.choices[value["value"]["index"]] def get_datakey(self, source: str, value) -> DataKey: - choices = [e.value for e in self.enum_class] - return {"source": source, "dtype": "string", "shape": [], "choices": choices} + return { + "source": source, + "dtype": "string", + "shape": [], + "choices": list(self.choices), + } class PvaEnumBoolConverter(PvaConverter): @@ -214,7 +219,7 @@ def make_converter(datatype: Optional[Type], values: Dict[str, Any]) -> PvaConve pv_choices = get_unique( {k: tuple(v["value"]["choices"]) for k, v in values.items()}, "choices" ) - return PvaEnumConverter(get_supported_enum_class(pv, datatype, pv_choices)) + return PvaEnumConverter(get_supported_values(pv, datatype, pv_choices)) elif "NTScalar" in typeid: if ( datatype diff --git a/src/ophyd_async/epics/_backend/common.py b/src/ophyd_async/epics/_backend/common.py index ff7c23a9e9..bb89dfa756 100644 --- a/src/ophyd_async/epics/_backend/common.py +++ b/src/ophyd_async/epics/_backend/common.py @@ -1,25 +1,25 @@ from enum import Enum -from typing import Any, Optional, Tuple, Type +from typing import Dict, Optional, Tuple, Type -def get_supported_enum_class( +def get_supported_values( pv: str, - datatype: Optional[Type[Enum]], - pv_choices: Tuple[Any, ...], -) -> Type[Enum]: + datatype: Optional[Type[str]], + pv_choices: Tuple[str, ...], +) -> Dict[str, str]: if not datatype: - return Enum("GeneratedChoices", {x or "_": x for x in pv_choices}, type=str) # type: ignore + return {x: x or "_" for x in pv_choices} - if not issubclass(datatype, Enum): - raise TypeError(f"{pv} has type Enum not {datatype.__name__}") if not issubclass(datatype, str): - raise TypeError(f"{pv} has type Enum but doesn't inherit from String") - choices = tuple(v.value for v in datatype) - if set(choices) != set(pv_choices): - raise TypeError( - ( - f"{pv} has choices {pv_choices}, " - f"which do not match {datatype}, which has {choices}" + raise TypeError(f"{pv} is type Enum but doesn't inherit from String") + if issubclass(datatype, Enum): + choices = tuple(v.value for v in datatype) + if set(choices) != set(pv_choices): + raise TypeError( + ( + f"{pv} has choices {pv_choices}, " + f"which do not match {datatype}, which has {choices}" + ) ) - ) - return datatype + return {x: datatype(x) for x in pv_choices} + return {x: x for x in pv_choices} diff --git a/tests/epics/_backend/test_common.py b/tests/epics/_backend/test_common.py index 99801b3b80..2958179821 100644 --- a/tests/epics/_backend/test_common.py +++ b/tests/epics/_backend/test_common.py @@ -2,12 +2,12 @@ import pytest -from ophyd_async.epics._backend.common import get_supported_enum_class +from ophyd_async.epics._backend.common import get_supported_values def test_given_a_non_enum_passed_to_get_supported_enum_then_raises(): with pytest.raises(TypeError): - get_supported_enum_class("", int, ("test",)) + get_supported_values("", int, ("test",)) def test_given_an_enum_but_not_str_passed_to_get_supported_enum_then_raises(): @@ -15,7 +15,7 @@ class MyEnum(Enum): TEST = "test" with pytest.raises(TypeError): - get_supported_enum_class("", MyEnum, ("test",)) + get_supported_values("", MyEnum, ("test",)) def test_given_pv_has_choices_not_in_supplied_enum_then_raises(): @@ -23,7 +23,7 @@ class MyEnum(str, Enum): TEST = "test" with pytest.raises(TypeError): - get_supported_enum_class("", MyEnum, ("test", "unexpected_choice")) + get_supported_values("", MyEnum, ("test", "unexpected_choice")) def test_given_supplied_enum_has_choices_not_in_pv_then_raises(): @@ -32,16 +32,13 @@ class MyEnum(str, Enum): OTHER = "unexpected_choice" with pytest.raises(TypeError): - get_supported_enum_class("", MyEnum, ("test",)) + get_supported_values("", MyEnum, ("test",)) def test_given_no_supplied_enum_then_returns_generated_choices_enum_with_pv_choices(): - enum_class = get_supported_enum_class("", None, ("test",)) - - assert isinstance(enum_class, type(Enum("GeneratedChoices", {}))) - all_values = [e.value for e in enum_class] # type: ignore - assert len(all_values) == 1 - assert "test" in all_values + supported_vals = get_supported_values("", None, ("test",)) + assert len(supported_vals) == 1 + assert "test" in supported_vals def test_given_a_supplied_enum_that_matches_the_pv_choices_then_enum_type_is_returned(): @@ -49,10 +46,7 @@ class MyEnum(str, Enum): TEST_1 = "test_1" TEST_2 = "test_2" - enum_class = get_supported_enum_class("", MyEnum, ("test_1", "test_2")) - - assert isinstance(enum_class, type(MyEnum)) - all_values = [e.value for e in enum_class] # type: ignore - assert len(all_values) == 2 - assert "test_1" in all_values - assert "test_2" in all_values + supported_vals = get_supported_values("", MyEnum, ("test_1", "test_2")) + assert len(supported_vals) == 2 + assert "test_1" in supported_vals + assert "test_2" in supported_vals diff --git a/tests/epics/test_signals.py b/tests/epics/test_signals.py index c88a77b240..ef1afa481e 100644 --- a/tests/epics/test_signals.py +++ b/tests/epics/test_signals.py @@ -202,6 +202,7 @@ def waveform_d(value): (float, "float", 3.141, 43.5, number_d), (str, "str", "hello", "goodbye", string_d), (MyEnum, "enum", MyEnum.b, MyEnum.c, enum_d), + (str, "enum", "Bbb", "Ccc", enum_d), (npt.NDArray[np.int8], "int8a", [-128, 127], [-8, 3, 44], waveform_d), (npt.NDArray[np.uint8], "uint8a", [0, 255], [218], waveform_d), (npt.NDArray[np.int16], "int16a", [-32768, 32767], [-855], waveform_d), @@ -338,7 +339,7 @@ class EnumNoString(Enum): (str, "float", "has type float not str"), (str, "stra", "has type [str] not str"), (int, "uint8a", "has type [uint8] not int"), - (float, "enum", "has type Enum not float"), + (float, "enum", "is type Enum but doesn't inherit from String"), (npt.NDArray[np.int32], "float64a", "has type [float64] not [int32]"), ], ) @@ -535,3 +536,27 @@ def test_signal_helpers(): execute = epics_signal_x("Execute") assert execute._backend.write_pv == "Execute" + + +async def test_str_enum_returns_enum(ioc: IOC): + await ioc.make_backend(MyEnum, "enum") + pv_name = f"{ioc.protocol}://{PV_PREFIX}:{ioc.protocol}:enum" + + sig = epics_signal_rw(MyEnum, pv_name) + await sig.connect() + val = await sig.get_value() + assert repr(val) == "" + assert val is MyEnum.b + assert val == "Bbb" + + +async def test_str_returns_enum(ioc: IOC): + await ioc.make_backend(str, "enum") + pv_name = f"{ioc.protocol}://{PV_PREFIX}:{ioc.protocol}:enum" + + sig = epics_signal_rw(str, pv_name) + await sig.connect() + val = await sig.get_value() + assert val == MyEnum.b + assert val == "Bbb" + assert val is not MyEnum.b From 1e8423347249b2ab34f2abbe04f9790818cac3b6 Mon Sep 17 00:00:00 2001 From: Eva Lott Date: Fri, 10 May 2024 14:59:22 +0100 Subject: [PATCH 6/7] Create mock signal backend (#251) * added `SoftSignalBackend` and `MockSignalBackend` also added utility functions for `MockSignalBackend` in tests * changed to give `MockSignalBackend` a reference to a `SoftSignalBackend` * fixed flakey ioc test fixture * use real panda controller in HDFPanda test --- docs/how-to/write-tests-for-devices.rst | 18 +- docs/tutorials/using-existing-devices.rst | 2 +- src/ophyd_async/core/__init__.py | 34 +- src/ophyd_async/core/async_status.py | 2 + src/ophyd_async/core/device.py | 18 +- src/ophyd_async/core/mock_signal_backend.py | 86 +++++ src/ophyd_async/core/mock_signal_utils.py | 149 ++++++++ src/ophyd_async/core/signal.py | 56 +-- ...gnal_backend.py => soft_signal_backend.py} | 56 ++- .../epics/areadetector/writers/nd_plugin.py | 2 +- src/ophyd_async/epics/motion/motor.py | 6 +- src/ophyd_async/epics/pvi/pvi.py | 22 +- src/ophyd_async/panda/_hdf_panda.py | 6 +- src/ophyd_async/sim/demo/sim_motor.py | 8 +- src/ophyd_async/sim/pattern_generator.py | 10 +- tests/core/test_device.py | 6 +- tests/core/test_device_collector.py | 36 +- tests/core/test_device_save_loader.py | 2 +- tests/core/test_flyer.py | 9 +- tests/core/test_mock_signal_backend.py | 341 ++++++++++++++++++ tests/core/test_signal.py | 213 +++++------ ...est_sim.py => test_soft_signal_backend.py} | 29 +- tests/core/test_standard_readable.py | 4 +- tests/core/test_utils.py | 16 +- tests/epics/areadetector/test_aravis.py | 40 +- tests/epics/areadetector/test_controllers.py | 4 +- tests/epics/areadetector/test_drivers.py | 10 +- tests/epics/areadetector/test_kinetix.py | 26 +- tests/epics/areadetector/test_pilatus.py | 6 +- tests/epics/areadetector/test_scans.py | 10 +- .../areadetector/test_single_trigger_det.py | 14 +- tests/epics/areadetector/test_vimba.py | 30 +- tests/epics/areadetector/test_writers.py | 8 +- tests/epics/demo/test_demo.py | 191 +++++----- tests/epics/demo/test_demo_ad_sim_detector.py | 44 +-- tests/epics/motion/test_motor.py | 40 +- tests/epics/test_pvi.py | 22 +- tests/epics/test_signals.py | 9 + tests/panda/test_hdf_panda.py | 123 +++---- tests/panda/test_panda_connect.py | 34 +- tests/panda/test_panda_controller.py | 23 +- tests/panda/test_panda_utils.py | 24 +- tests/panda/test_trigger.py | 18 +- tests/panda/test_writer.py | 150 ++++---- tests/protocols/test_protocols.py | 11 +- tests/sim/conftest.py | 2 +- tests/sim/test_sim_detector.py | 2 +- tests/sim/test_sim_writer.py | 2 +- tests/sim/test_streaming_plan.py | 1 - tests/test_flyer_with_panda.py | 32 +- 50 files changed, 1265 insertions(+), 742 deletions(-) create mode 100644 src/ophyd_async/core/mock_signal_backend.py create mode 100644 src/ophyd_async/core/mock_signal_utils.py rename src/ophyd_async/core/{sim_signal_backend.py => soft_signal_backend.py} (79%) create mode 100644 tests/core/test_mock_signal_backend.py rename tests/core/{test_sim.py => test_soft_signal_backend.py} (82%) diff --git a/docs/how-to/write-tests-for-devices.rst b/docs/how-to/write-tests-for-devices.rst index 35169ff5c4..a492fa230a 100644 --- a/docs/how-to/write-tests-for-devices.rst +++ b/docs/how-to/write-tests-for-devices.rst @@ -21,19 +21,19 @@ Async Tests ... asyncio_mode = "auto" -Sim Backend ------------ +Mock Backend +------------ -Ophyd devices initialized with a sim backend behave in a similar way to mocks, without requiring you to mock out all the dependencies and internals. The `DeviceCollector` can initialize any number of devices, and their signals and sub-devices (recursively), with a sim backend. +Ophyd devices initialized with a mock backend behave in a similar way to mocks, without requiring you to mock out all the dependencies and internals. The `DeviceCollector` can initialize any number of devices, and their signals and sub-devices (recursively), with a mock backend. .. literalinclude:: ../../tests/epics/demo/test_demo.py - :pyobject: sim_sensor + :pyobject: mock_sensor -Sim Utility Functions ---------------------- +Mock Utility Functions +---------------------- -Sim signals behave as simply as possible, holding a sensible default value when initialized and retaining any value (in memory) to which they are set. This model breaks down in the case of read-only signals, which cannot be set because there is an expectation of some external device setting them in the real world. There is a utility function, ``set_sim_value``, to mock-set values for sim signals, including read-only ones. +Mock signals behave as simply as possible, holding a sensible default value when initialized and retaining any value (in memory) to which they are set. This model breaks down in the case of read-only signals, which cannot be set because there is an expectation of some external device setting them in the real world. There is a utility function, ``set_mock_value``, to mock-set values for mock signals, including read-only ones. In addition this example also utilizes helper functions like ``assert_reading`` and ``assert_value`` to ensure the validity of device readings and values. For more information see: :doc:`API.core<../generated/ophyd_async.core>` @@ -41,7 +41,9 @@ In addition this example also utilizes helper functions like ``assert_reading`` :pyobject: test_sensor_reading_shows_value -There is another utility function, ``set_sim_callback``, for hooking in logic when a sim value changes (e.g. because someone puts to it). +There are several other test utility functions: + +Use ``callback_on_mock_put``, for hooking in logic when a mock value changes (e.g. because someone puts to it). This can be called directly, or used as a context, with the callbacks ending after exit. .. literalinclude:: ../../tests/epics/demo/test_demo.py :pyobject: test_mover_stopped diff --git a/docs/tutorials/using-existing-devices.rst b/docs/tutorials/using-existing-devices.rst index 6ac638eefb..f9e2cc6c8d 100644 --- a/docs/tutorials/using-existing-devices.rst +++ b/docs/tutorials/using-existing-devices.rst @@ -57,7 +57,7 @@ and run the following: - If ``connect=True`` (the default), then call `Device.connect` in parallel for all top level Devices, waiting for up to ``timeout`` seconds. For example, here we call ``asyncio.wait([det.connect(), samp.connect()])`` -- If ``sim=True`` is passed, then don't connect to PVs, but set Devices into +- If ``mock=True`` is passed, then don't connect to PVs, but set Devices into simulation mode The Devices we create in this example are a "sample stage" with a couple of diff --git a/src/ophyd_async/core/__init__.py b/src/ophyd_async/core/__init__.py index 8d0493801c..9577d7c6a2 100644 --- a/src/ophyd_async/core/__init__.py +++ b/src/ophyd_async/core/__init__.py @@ -24,6 +24,18 @@ walk_rw_signals, ) from .flyer import HardwareTriggeredFlyable, TriggerLogic +from .mock_signal_backend import ( + MockSignalBackend, +) +from .mock_signal_utils import ( + assert_mock_put_called_with, + callback_on_mock_put, + mock_puts_blocked, + reset_mock_put_calls, + set_mock_put_proceeds, + set_mock_value, + set_mock_values, +) from .signal import ( Signal, SignalR, @@ -36,15 +48,12 @@ assert_value, observe_value, set_and_wait_for_value, - set_sim_callback, - set_sim_put_proceeds, - set_sim_value, - soft_signal_r_and_backend, + soft_signal_r_and_setter, soft_signal_rw, wait_for_value, ) from .signal_backend import SignalBackend -from .sim_signal_backend import SimSignalBackend +from .soft_signal_backend import SoftSignalBackend from .standard_readable import ConfigSignal, HintedSignal, StandardReadable from .utils import ( DEFAULT_TIMEOUT, @@ -59,9 +68,15 @@ ) __all__ = [ + "assert_mock_put_called_with", + "callback_on_mock_put", + "mock_puts_blocked", + "set_mock_values", + "reset_mock_put_calls", "SignalBackend", - "SimSignalBackend", + "SoftSignalBackend", "DetectorControl", + "MockSignalBackend", "DetectorTrigger", "DetectorWriter", "StandardDetector", @@ -73,13 +88,12 @@ "SignalW", "SignalRW", "SignalX", - "soft_signal_r_and_backend", + "soft_signal_r_and_setter", "soft_signal_rw", "observe_value", "set_and_wait_for_value", - "set_sim_callback", - "set_sim_put_proceeds", - "set_sim_value", + "set_mock_put_proceeds", + "set_mock_value", "wait_for_value", "AsyncStatus", "DirectoryInfo", diff --git a/src/ophyd_async/core/async_status.py b/src/ophyd_async/core/async_status.py index 201c7162b2..2cdd3e804c 100644 --- a/src/ophyd_async/core/async_status.py +++ b/src/ophyd_async/core/async_status.py @@ -21,7 +21,9 @@ def __init__( self.task = awaitable else: self.task = asyncio.create_task(awaitable) # type: ignore + self.task.add_done_callback(self._run_callbacks) + self._callbacks = cast(List[Callback[Status]], []) self._watchers = watchers diff --git a/src/ophyd_async/core/device.py b/src/ophyd_async/core/device.py index ce7beaeecb..06a00bf547 100644 --- a/src/ophyd_async/core/device.py +++ b/src/ophyd_async/core/device.py @@ -71,20 +71,20 @@ def set_name(self, name: str): child.set_name(child_name) child.parent = self - async def connect(self, sim: bool = False, timeout: float = DEFAULT_TIMEOUT): + async def connect(self, mock: bool = False, timeout: float = DEFAULT_TIMEOUT): """Connect self and all child Devices. Contains a timeout that gets propagated to child.connect methods. Parameters ---------- - sim: - If True then connect in simulation mode. + mock: + If True then use ``MockSignalBackend`` for all Signals timeout: Time to wait before failing with a TimeoutError. """ coros = { - name: child_device.connect(sim, timeout=timeout) + name: child_device.connect(mock=mock, timeout=timeout) for name, child_device in self.children() } if coros: @@ -118,9 +118,9 @@ class DeviceCollector: If True, call ``device.set_name(variable_name)`` on all collected Devices connect: - If True, call ``device.connect(sim)`` in parallel on all + If True, call ``device.connect(mock)`` in parallel on all collected Devices - sim: + mock: If True, connect Signals in simulation mode timeout: How long to wait for connect before logging an exception @@ -142,12 +142,12 @@ def __init__( self, set_name=True, connect=True, - sim=False, + mock=False, timeout: float = 10.0, ): self._set_name = set_name self._connect = connect - self._sim = sim + self._mock = mock self._timeout = timeout self._names_on_enter: Set[str] = set() self._objects_on_exit: Dict[str, Any] = {} @@ -181,7 +181,7 @@ async def _on_exit(self) -> None: obj.set_name(name) if self._connect: connect_coroutines[name] = obj.connect( - self._sim, timeout=self._timeout + self._mock, timeout=self._timeout ) # Connect to all the devices diff --git a/src/ophyd_async/core/mock_signal_backend.py b/src/ophyd_async/core/mock_signal_backend.py new file mode 100644 index 0000000000..a00fd62967 --- /dev/null +++ b/src/ophyd_async/core/mock_signal_backend.py @@ -0,0 +1,86 @@ +import asyncio +from typing import Optional, Type +from unittest.mock import MagicMock + +from bluesky.protocols import Descriptor, Reading + +from ophyd_async.core.signal_backend import SignalBackend +from ophyd_async.core.soft_signal_backend import SoftSignalBackend +from ophyd_async.core.utils import DEFAULT_TIMEOUT, ReadingValueCallback, T + + +class MockSignalBackend(SignalBackend): + def __init__( + self, + datatype: Optional[Type[T]] = None, + initial_backend: Optional[SignalBackend[T]] = None, + ) -> None: + if isinstance(initial_backend, MockSignalBackend): + raise ValueError("Cannot make a MockSignalBackend for a MockSignalBackends") + + self.initial_backend = initial_backend + + if datatype is None: + assert ( + self.initial_backend + ), "Must supply either initial_backend or datatype" + datatype = self.initial_backend.datatype + + self.datatype = datatype + + if not isinstance(self.initial_backend, SoftSignalBackend): + # If the backend is a hard signal backend, or not provided, + # then we create a soft signal to mimick it + + self.soft_backend = SoftSignalBackend(datatype=datatype) + else: + self.soft_backend = initial_backend + + self.mock = MagicMock() + + self.put_proceeds = asyncio.Event() + self.put_proceeds.set() + + def source(self, name: str) -> str: + self.mock.source(name) + if self.initial_backend: + return f"mock+{self.initial_backend.source(name)}" + return f"mock+{name}" + + async def connect(self, timeout: float = DEFAULT_TIMEOUT) -> None: + self.mock.connect(timeout=timeout) + + async def put(self, value: Optional[T], wait=True, timeout=None): + self.mock.put(value, wait=wait, timeout=timeout) + await self.soft_backend.put(value, wait=wait, timeout=timeout) + + if wait: + await asyncio.wait_for(self.put_proceeds.wait(), timeout=timeout) + + def set_value(self, value: T): + self.mock.set_value(value) + self.soft_backend.set_value(value) + + async def get_descriptor(self, source: str) -> Descriptor: + self.mock.get_descriptor(source) + return await self.soft_backend.get_descriptor(source) + + async def get_reading(self) -> Reading: + self.mock.get_reading() + return await self.soft_backend.get_reading() + + async def get_value(self) -> T: + self.mock.get_value() + return await self.soft_backend.get_value() + + async def get_setpoint(self) -> T: + """For a soft signal, the setpoint and readback values are the same.""" + self.mock.get_setpoint() + return await self.soft_backend.get_setpoint() + + async def get_datakey(self, source: str) -> Descriptor: + return await self.soft_backend.get_datakey(source) + + def set_callback(self, callback: Optional[ReadingValueCallback[T]]) -> None: + self.mock.set_callback(callback) + self.soft_backend.set_callback(callback) diff --git a/src/ophyd_async/core/mock_signal_utils.py b/src/ophyd_async/core/mock_signal_utils.py new file mode 100644 index 0000000000..bf93187e28 --- /dev/null +++ b/src/ophyd_async/core/mock_signal_utils.py @@ -0,0 +1,149 @@ +from contextlib import asynccontextmanager, contextmanager +from typing import Any, Callable, Generator, Iterable, Iterator, List +from unittest.mock import ANY + +from ophyd_async.core.signal import Signal +from ophyd_async.core.utils import T + +from .mock_signal_backend import MockSignalBackend + + +def _get_mock_signal_backend(signal: Signal) -> MockSignalBackend: + assert isinstance(signal._backend, MockSignalBackend), ( + "Expected to receive a `MockSignalBackend`, instead " + f" received {type(signal._backend)}. " + ) + return signal._backend + + +def set_mock_value(signal: Signal[T], value: T): + """Set the value of a signal that is in mock mode.""" + backend = _get_mock_signal_backend(signal) + backend.set_value(value) + + +def set_mock_put_proceeds(signal: Signal[T], proceeds: bool): + """Allow or block a put with wait=True from proceeding""" + backend = _get_mock_signal_backend(signal) + + if proceeds: + backend.put_proceeds.set() + else: + backend.put_proceeds.clear() + + +@asynccontextmanager +async def mock_puts_blocked(*signals: List[Signal]): + for signal in signals: + set_mock_put_proceeds(signal, False) + yield + for signal in signals: + set_mock_put_proceeds(signal, True) + + +def assert_mock_put_called_with(signal: Signal, value: Any, wait=ANY, timeout=ANY): + backend = _get_mock_signal_backend(signal) + backend.mock.put.assert_called_with(value, wait=wait, timeout=timeout) + + +def reset_mock_put_calls(signal: Signal): + backend = _get_mock_signal_backend(signal) + backend.mock.put.reset_mock() + + +class _SetValuesIterator: + # Garbage collected by the time __del__ is called unless we put it as a + # global attrbute here. + require_all_consumed: bool = False + + def __init__( + self, + signal: Signal, + values: Iterable[Any], + require_all_consumed: bool = False, + ): + self.signal = signal + self.values = values + self.require_all_consumed = require_all_consumed + self.index = 0 + + self.iterator = enumerate(values, start=1) + + def __iter__(self): + return self + + def __next__(self): + # Will propogate StopIteration + self.index, next_value = next(self.iterator) + set_mock_value(self.signal, next_value) + return next_value + + def __del__(self): + if self.require_all_consumed and self.index != len(self.values): + raise AssertionError("Not all values have been consumed.") + + +def set_mock_values( + signal: Signal, + values: Iterable[Any], + require_all_consumed: bool = False, +) -> Iterator[Any]: + """Iterator to set a signal to a sequence of values, optionally repeating the + sequence. + + Parameters + ---------- + signal: + A signal with a `MockSignalBackend` backend. + values: + An iterable of the values to set the signal to, on each iteration + the value will be set. + require_all_consumed: + If True, an AssertionError will be raised if the iterator is deleted before + all values have been consumed. + + Notes + ----- + Example usage:: + + for value_set in set_mock_values(signal, [1, 2, 3]): + # do something + + cm = set_mock_values(signal, 1, 2, 3, require_all_consumed=True): + next(cm) + # do something + """ + + return _SetValuesIterator( + signal, + values, + require_all_consumed=require_all_consumed, + ) + + +@contextmanager +def _unset_side_effect_cm(mock): + yield + mock.put.side_effect = None + + +# linting isn't smart enought to realize @contextmanager will give use a +# ContextManager[None] +def callback_on_mock_put( + signal: Signal, callback: Callable[[T], None] +) -> Generator[None, None, None]: + """For setting a callback when a backend is put to. + + Can either be used in a context, with the callback being + unset on exit, or as an ordinary function. + + Parameters + ---------- + signal: + A signal with a `MockSignalBackend` backend. + callback: + The callback to call when the backend is put to during the context. + """ + backend = _get_mock_signal_backend(signal) + backend.mock.put.side_effect = callback + return _unset_side_effect_cm(backend.mock) diff --git a/src/ophyd_async/core/signal.py b/src/ophyd_async/core/signal.py index d0d50c8999..d41048a815 100644 --- a/src/ophyd_async/core/signal.py +++ b/src/ophyd_async/core/signal.py @@ -24,15 +24,14 @@ Subscribable, ) +from ophyd_async.core.mock_signal_backend import MockSignalBackend from ophyd_async.protocols import AsyncConfigurable, AsyncReadable, AsyncStageable from .async_status import AsyncStatus from .device import Device from .signal_backend import SignalBackend -from .sim_signal_backend import SimSignalBackend -from .utils import DEFAULT_TIMEOUT, Callback, ReadingValueCallback, T - -_sim_backends: Dict[Signal, SimSignalBackend] = {} +from .soft_signal_backend import SoftSignalBackend +from .utils import DEFAULT_TIMEOUT, Callback, T def _add_timeout(func): @@ -62,16 +61,15 @@ def __init__( name: str = "", ) -> None: self._timeout = timeout - self._init_backend = self._backend = backend + self._initial_backend = self._backend = backend super().__init__(name) - async def connect(self, sim=False, timeout=DEFAULT_TIMEOUT): - if sim: - self._backend = SimSignalBackend(datatype=self._init_backend.datatype) - _sim_backends[self] = self._backend - else: - self._backend = self._init_backend - _sim_backends.pop(self, None) + async def connect(self, mock=False, timeout=DEFAULT_TIMEOUT): + if mock and not isinstance(self._backend, MockSignalBackend): + # Using a soft backend, look to the initial value + self._backend = MockSignalBackend( + initial_backend=self._initial_backend, + ) self.log.debug(f"Connecting to {self.source}") await self._backend.connect(timeout=timeout) @@ -255,47 +253,29 @@ def trigger(self, wait=True, timeout=USE_DEFAULT_TIMEOUT) -> AsyncStatus: return AsyncStatus(coro) -def set_sim_value(signal: Signal[T], value: T): - """Set the value of a signal that is in sim mode.""" - _sim_backends[signal]._set_value(value) - - -def set_sim_put_proceeds(signal: Signal[T], proceeds: bool): - """Allow or block a put with wait=True from proceeding""" - event = _sim_backends[signal].put_proceeds - if proceeds: - event.set() - else: - event.clear() - - -def set_sim_callback(signal: Signal[T], callback: ReadingValueCallback[T]) -> None: - """Monitor the value of a signal that is in sim mode""" - return _sim_backends[signal].set_callback(callback) - - def soft_signal_rw( datatype: Optional[Type[T]] = None, initial_value: Optional[T] = None, name: str = "", ) -> SignalRW[T]: - """Creates a read-writable Signal with a SimSignalBackend""" - signal = SignalRW(SimSignalBackend(datatype, initial_value), name=name) + """Creates a read-writable Signal with a SoftSignalBackend""" + signal = SignalRW(SoftSignalBackend(datatype, initial_value), name=name) return signal -def soft_signal_r_and_backend( +def soft_signal_r_and_setter( datatype: Optional[Type[T]] = None, initial_value: Optional[T] = None, name: str = "", -) -> Tuple[SignalR[T], SimSignalBackend]: - """Returns a tuple of a read-only Signal and its SimSignalBackend through +) -> Tuple[SignalR[T], Callable[[T]]]: + """Returns a tuple of a read-only Signal and a callable through which the signal can be internally modified within the device. Use soft_signal_rw if you want a device that is externally modifiable """ - backend = SimSignalBackend(datatype, initial_value) + backend = SoftSignalBackend(datatype, initial_value) signal = SignalR(backend, name=name) - return (signal, backend) + + return (signal, backend.set_value) async def assert_value(signal: SignalR[T], value: Any) -> None: diff --git a/src/ophyd_async/core/sim_signal_backend.py b/src/ophyd_async/core/soft_signal_backend.py similarity index 79% rename from src/ophyd_async/core/sim_signal_backend.py rename to src/ophyd_async/core/soft_signal_backend.py index 52fecedfed..47eecf4ca2 100644 --- a/src/ophyd_async/core/sim_signal_backend.py +++ b/src/ophyd_async/core/soft_signal_backend.py @@ -1,12 +1,11 @@ from __future__ import annotations -import asyncio import inspect import time from collections import abc from dataclasses import dataclass from enum import Enum -from typing import Any, Dict, Generic, Optional, Type, Union, cast, get_origin +from typing import Dict, Generic, Optional, Type, Union, cast, get_origin import numpy as np from bluesky.protocols import DataKey, Dtype, Reading @@ -22,7 +21,7 @@ } -class SimConverter(Generic[T]): +class SoftConverter(Generic[T]): def value(self, value: T) -> T: return value @@ -55,7 +54,7 @@ def make_initial_value(self, datatype: Optional[Type[T]]) -> T: return datatype() -class SimArrayConverter(SimConverter): +class SoftArrayConverter(SoftConverter): def get_datakey(self, source: str, value) -> DataKey: return {"source": source, "dtype": "array", "shape": [len(value)]} @@ -70,7 +69,7 @@ def make_initial_value(self, datatype: Optional[Type[T]]) -> T: @dataclass -class SimEnumConverter(SimConverter): +class SoftEnumConverter(SoftConverter): enum_class: Type[Enum] def write_value(self, value: Union[Enum, str]) -> Enum: @@ -90,26 +89,21 @@ def make_initial_value(self, datatype: Optional[Type[T]]) -> T: return cast(T, list(datatype.__members__.values())[0]) # type: ignore -class DisconnectedSimConverter(SimConverter): - def __getattribute__(self, __name: str) -> Any: - raise NotImplementedError("No PV has been set as connect() has not been called") - - def make_converter(datatype): is_array = get_dtype(datatype) is not None is_sequence = get_origin(datatype) == abc.Sequence is_enum = issubclass(datatype, Enum) if inspect.isclass(datatype) else False if is_array or is_sequence: - return SimArrayConverter() + return SoftArrayConverter() if is_enum: - return SimEnumConverter(datatype) + return SoftEnumConverter(datatype) - return SimConverter() + return SoftConverter() -class SimSignalBackend(SignalBackend[T]): - """An simulated backend to a Signal, created with ``Signal.connect(sim=True)``""" +class SoftSignalBackend(SignalBackend[T]): + """An backend to a soft Signal, for test signals see ``MockSignalBackend``.""" _value: T _initial_value: Optional[T] @@ -122,25 +116,23 @@ def __init__( initial_value: Optional[T] = None, ) -> None: self.datatype = datatype - self.converter: SimConverter = DisconnectedSimConverter() self._initial_value = initial_value - self.put_proceeds = asyncio.Event() - self.put_proceeds.set() - self.callback: Optional[ReadingValueCallback[T]] = None - - def source(self, name: str) -> str: - return f"soft://{name}" - - async def connect(self, timeout: float = DEFAULT_TIMEOUT) -> None: - self.converter = make_converter(self.datatype) + self.converter: SoftConverter = make_converter(datatype) if self._initial_value is None: self._initial_value = self.converter.make_initial_value(self.datatype) else: - # convert potentially unconverted initial value passed to init method self._initial_value = self.converter.write_value(self._initial_value) + + self.callback: Optional[ReadingValueCallback[T]] = None self._severity = 0 + self.set_value(self._initial_value) - await self.put(None) + def source(self, name: str) -> str: + return f"soft://{name}" + + async def connect(self, timeout: float = DEFAULT_TIMEOUT) -> None: + """Connection isn't required for soft signals.""" + pass async def put(self, value: Optional[T], wait=True, timeout=None): write_value = ( @@ -148,13 +140,11 @@ async def put(self, value: Optional[T], wait=True, timeout=None): if value is not None else self._initial_value ) - self._set_value(write_value) - if wait: - await asyncio.wait_for(self.put_proceeds.wait(), timeout) + self.set_value(write_value) - def _set_value(self, value: T): - """Method to bypass asynchronous logic, designed to only be used in tests.""" + def set_value(self, value: T): + """Method to bypass asynchronous logic.""" self._value = value self._timestamp = time.monotonic() reading: Reading = self.converter.reading( @@ -174,7 +164,7 @@ async def get_value(self) -> T: return self.converter.value(self._value) async def get_setpoint(self) -> T: - """For a simulated backend, the setpoint and readback values are the same.""" + """For a soft signal, the setpoint and readback values are the same.""" return await self.get_value() def set_callback(self, callback: Optional[ReadingValueCallback[T]]) -> None: diff --git a/src/ophyd_async/epics/areadetector/writers/nd_plugin.py b/src/ophyd_async/epics/areadetector/writers/nd_plugin.py index 29268153ae..fcb71e4c2a 100644 --- a/src/ophyd_async/epics/areadetector/writers/nd_plugin.py +++ b/src/ophyd_async/epics/areadetector/writers/nd_plugin.py @@ -14,7 +14,7 @@ class NDArrayBase(Device): def __init__(self, prefix: str, name: str = "") -> None: self.unique_id = epics_signal_r(int, prefix + "UniqueId_RBV") self.nd_attributes_file = epics_signal_rw(str, prefix + "NDAttributesFile") - super().__init__(name) + super().__init__(name=name) class NDPluginBase(NDArrayBase): diff --git a/src/ophyd_async/epics/motion/motor.py b/src/ophyd_async/epics/motion/motor.py index 5746bafa74..33d3a41062 100644 --- a/src/ophyd_async/epics/motion/motor.py +++ b/src/ophyd_async/epics/motion/motor.py @@ -40,7 +40,11 @@ def set_name(self, name: str): # Readback should be named the same as its parent in read() self.user_readback.set_name(name) - async def _move(self, new_position: float, watchers: List[Callable] = []): + async def _move( + self, new_position: float, watchers: Optional[List[Callable]] = None + ): + if watchers is None: + watchers = [] self._set_success = True start = time.monotonic() old_position, units, precision = await asyncio.gather( diff --git a/src/ophyd_async/epics/pvi/pvi.py b/src/ophyd_async/epics/pvi/pvi.py index e68cf3b132..ccb5d1b92d 100644 --- a/src/ophyd_async/epics/pvi/pvi.py +++ b/src/ophyd_async/epics/pvi/pvi.py @@ -17,7 +17,7 @@ get_type_hints, ) -from ophyd_async.core import Device, DeviceVector, SimSignalBackend +from ophyd_async.core import Device, DeviceVector, SoftSignalBackend from ophyd_async.core.signal import Signal from ophyd_async.core.utils import DEFAULT_TIMEOUT from ophyd_async.epics._backend._p4p import PvaSignalBackend @@ -156,7 +156,7 @@ def _parse_type( return is_device_vector, is_signal, signal_dtype, device_cls -def _sim_common_blocks(device: Device, stripped_type: Optional[Type] = None): +def _mock_common_blocks(device: Device, stripped_type: Optional[Type] = None): device_t = stripped_type or type(device) sub_devices = ( (field, field_type) @@ -175,23 +175,23 @@ def _sim_common_blocks(device: Device, stripped_type: Optional[Type] = None): if is_device_vector: if is_signal: - sub_device_1 = device_cls(SimSignalBackend(signal_dtype)) - sub_device_2 = device_cls(SimSignalBackend(signal_dtype)) + sub_device_1 = device_cls(SoftSignalBackend(signal_dtype)) + sub_device_2 = device_cls(SoftSignalBackend(signal_dtype)) sub_device = DeviceVector({1: sub_device_1, 2: sub_device_2}) else: sub_device = DeviceVector({1: device_cls(), 2: device_cls()}) for sub_device_in_vector in sub_device.values(): - _sim_common_blocks(sub_device_in_vector, stripped_type=device_cls) + _mock_common_blocks(sub_device_in_vector, stripped_type=device_cls) for value in sub_device.values(): value.parent = sub_device else: if is_signal: - sub_device = device_cls(SimSignalBackend(signal_dtype)) + sub_device = device_cls(SoftSignalBackend(signal_dtype)) else: sub_device = getattr(device, device_name, device_cls()) - _sim_common_blocks(sub_device, stripped_type=device_cls) + _mock_common_blocks(sub_device, stripped_type=device_cls) setattr(device, device_name, sub_device) sub_device.parent = device @@ -269,16 +269,16 @@ def _set_device_attributes(entry: PVIEntry): async def fill_pvi_entries( - device: Device, root_pv: str, timeout=DEFAULT_TIMEOUT, sim=False + device: Device, root_pv: str, timeout=DEFAULT_TIMEOUT, mock=False ): """ Fills a ``device`` with signals from a the ``root_pvi:PVI`` table. If the device names match with parent devices of ``device`` then types are used. """ - if sim: - # set up sim signals for the common annotations - _sim_common_blocks(device) + if mock: + # set up mock signals for the common annotations + _mock_common_blocks(device) else: # check the pvi table for devices and fill the device with them root_entry = PVIEntry( diff --git a/src/ophyd_async/panda/_hdf_panda.py b/src/ophyd_async/panda/_hdf_panda.py index 75c483e031..4dbf725f2e 100644 --- a/src/ophyd_async/panda/_hdf_panda.py +++ b/src/ophyd_async/panda/_hdf_panda.py @@ -42,7 +42,7 @@ def __init__( ) async def connect( - self, sim: bool = False, timeout: float = DEFAULT_TIMEOUT + self, mock: bool = False, timeout: float = DEFAULT_TIMEOUT ) -> None: - await fill_pvi_entries(self, self._prefix + "PVI", timeout=timeout, sim=sim) - await super().connect(sim=sim, timeout=timeout) + await fill_pvi_entries(self, self._prefix + "PVI", timeout=timeout, mock=mock) + await super().connect(mock=mock, timeout=timeout) diff --git a/src/ophyd_async/sim/demo/sim_motor.py b/src/ophyd_async/sim/demo/sim_motor.py index b0d2a2d4ca..ce41354ac7 100644 --- a/src/ophyd_async/sim/demo/sim_motor.py +++ b/src/ophyd_async/sim/demo/sim_motor.py @@ -6,7 +6,7 @@ from ophyd_async.core import StandardReadable from ophyd_async.core.async_status import AsyncStatus -from ophyd_async.core.signal import soft_signal_r_and_backend, soft_signal_rw +from ophyd_async.core.signal import soft_signal_r_and_setter, soft_signal_rw from ophyd_async.core.standard_readable import ConfigSignal, HintedSignal @@ -21,7 +21,7 @@ def __init__(self, name="", instant=True) -> None: - instant: bool: whether to move instantly, or with a delay """ with self.add_children_as_readables(HintedSignal): - self.user_readback, self._user_readback = soft_signal_r_and_backend( + self.user_readback, self._user_readback_set = soft_signal_r_and_setter( float, 0 ) @@ -84,7 +84,7 @@ async def update_position(): # update position based on time elapsed if time_elapsed >= travel_time: # successfully reached our target position - await self._user_readback.put(new_position) + self._user_readback_set(new_position) self._set_success = True break else: @@ -92,7 +92,7 @@ async def update_position(): old_position + distance * time_elapsed / travel_time ) - await self._user_readback.put(current_position) + self._user_readback_set(current_position) # notify watchers of the new position for watcher in watchers: diff --git a/src/ophyd_async/sim/pattern_generator.py b/src/ophyd_async/sim/pattern_generator.py index f85880dc25..22c96e01ea 100644 --- a/src/ophyd_async/sim/pattern_generator.py +++ b/src/ophyd_async/sim/pattern_generator.py @@ -23,8 +23,8 @@ ) from ophyd_async.core import DirectoryInfo, DirectoryProvider +from ophyd_async.core.mock_signal_backend import MockSignalBackend from ophyd_async.core.signal import SignalR, observe_value -from ophyd_async.core.sim_signal_backend import SimSignalBackend from ophyd_async.core.utils import DEFAULT_TIMEOUT # raw data path @@ -158,8 +158,8 @@ def __init__( self.written_images_counter: int = 0 # it automatically initializes to 0 - self.signal_backend = SimSignalBackend(int) - self.sim_signal = SignalR(self.signal_backend) + self.signal_backend = MockSignalBackend(int) + self.mock_signal = SignalR(self.signal_backend) blob = np.array( generate_gaussian_blob(width=detector_width, height=detector_height) * MAX_UINT8_VALUE @@ -220,7 +220,7 @@ def set_y(self, value: float) -> None: async def open_file( self, directory: DirectoryProvider, multiplier: int = 1 ) -> Dict[str, DataKey]: - await self.sim_signal.connect() + await self.mock_signal.connect() self.target_path = self._get_new_path(directory) @@ -314,5 +314,5 @@ def close(self) -> None: async def observe_indices_written( self, timeout=DEFAULT_TIMEOUT ) -> AsyncGenerator[int, None]: - async for num_captured in observe_value(self.sim_signal, timeout=timeout): + async for num_captured in observe_value(self.mock_signal, timeout=timeout): yield num_captured // self.multiplier diff --git a/tests/core/test_device.py b/tests/core/test_device.py index 97184aed54..720128c7d1 100644 --- a/tests/core/test_device.py +++ b/tests/core/test_device.py @@ -17,7 +17,7 @@ class DummyBaseDevice(Device): def __init__(self) -> None: self.connected = False - async def connect(self, sim=False, timeout=DEFAULT_TIMEOUT): + async def connect(self, mock=False, timeout=DEFAULT_TIMEOUT): self.connected = True @@ -71,7 +71,7 @@ async def test_children_of_device_have_set_names_and_get_connected( async def test_device_with_device_collector(): - async with DeviceCollector(sim=True): + async with DeviceCollector(mock=True): parent = DummyDeviceGroup("parent") assert parent.name == "parent" @@ -88,7 +88,7 @@ class DummyDeviceWithSleep(DummyBaseDevice): def __init__(self, name) -> None: self.set_name(name) - async def connect(self, sim=False, timeout=DEFAULT_TIMEOUT): + async def connect(self, mock=False, timeout=DEFAULT_TIMEOUT): await asyncio.sleep(0.01) self.connected = True diff --git a/tests/core/test_device_collector.py b/tests/core/test_device_collector.py index 7ad92fecd6..cc187a6be8 100644 --- a/tests/core/test_device_collector.py +++ b/tests/core/test_device_collector.py @@ -9,16 +9,16 @@ class FailingDevice(Device): - async def connect(self, sim: bool = False, timeout=DEFAULT_TIMEOUT): + async def connect(self, mock: bool = False, timeout=DEFAULT_TIMEOUT): raise AttributeError() class WorkingDevice(Device): connected = False - async def connect(self, sim: bool = True, timeout=DEFAULT_TIMEOUT): + async def connect(self, mock: bool = True, timeout=DEFAULT_TIMEOUT): self.connected = True - return await super().connect(sim=True) + return await super().connect(mock=True) async def set(self, new_position: float): ... @@ -83,25 +83,25 @@ def clean_event_loop(): def test_async_device_connector_run_engine_same_event_loop(): async def set_up_device(): - async with DeviceCollector(sim=True): - sim_motor = motor.Motor("BLxxI-MO-TABLE-01:X") - return sim_motor + async with DeviceCollector(mock=True): + mock_motor = motor.Motor("BLxxI-MO-TABLE-01:X") + return mock_motor loop = asyncio.new_event_loop() checking_loop = asyncio.new_event_loop() try: - sim_motor = loop.run_until_complete(set_up_device()) + mock_motor = loop.run_until_complete(set_up_device()) RE = RunEngine(call_returns_result=True, loop=loop) def my_plan(): - yield from bps.mov(sim_motor, 3.14) + yield from bps.mov(mock_motor, 3.14) RE(my_plan()) assert ( - checking_loop.run_until_complete(sim_motor.user_setpoint.read())[ - "sim_motor-user_setpoint" + checking_loop.run_until_complete(mock_motor.user_setpoint.read())[ + "mock_motor-user_setpoint" ]["value"] == 3.14 ) @@ -121,33 +121,33 @@ def my_plan(): @pytest.mark.skip( reason=( - "SimSignalBackend currently allows a different event-" + "MockSignalBackend currently allows a different event-" "loop to set the value, unlike real signals." ) ) def test_async_device_connector_run_engine_different_event_loop(): async def set_up_device(): - async with DeviceCollector(sim=True): - sim_motor = motor.Motor("BLxxI-MO-TABLE-01:X") - return sim_motor + async with DeviceCollector(mock=True): + mock_motor = motor.Motor("BLxxI-MO-TABLE-01:X") + return mock_motor device_connector_loop = asyncio.new_event_loop() run_engine_loop = asyncio.new_event_loop() assert run_engine_loop is not device_connector_loop - sim_motor = device_connector_loop.run_until_complete(set_up_device()) + mock_motor = device_connector_loop.run_until_complete(set_up_device()) RE = RunEngine(loop=run_engine_loop) def my_plan(): - yield from bps.mov(sim_motor, 3.14) + yield from bps.mov(mock_motor, 3.14) RE(my_plan()) # The set should fail since the run engine is on a different event loop assert ( - device_connector_loop.run_until_complete(sim_motor.user_setpoint.read())[ - "sim_motor-user_setpoint" + device_connector_loop.run_until_complete(mock_motor.user_setpoint.read())[ + "mock_motor-user_setpoint" ]["value"] != 3.14 ) diff --git a/tests/core/test_device_save_loader.py b/tests/core/test_device_save_loader.py index 1d50def471..688c0cdfb5 100644 --- a/tests/core/test_device_save_loader.py +++ b/tests/core/test_device_save_loader.py @@ -51,7 +51,7 @@ def __init__(self, name: str): @pytest.fixture async def device() -> DummyDeviceGroup: device = DummyDeviceGroup("parent") - await device.connect(sim=True) + await device.connect(mock=True) return device diff --git a/tests/core/test_flyer.py b/tests/core/test_flyer.py index 723ed9ee2b..d5edfb6b86 100644 --- a/tests/core/test_flyer.py +++ b/tests/core/test_flyer.py @@ -15,13 +15,12 @@ DetectorTrigger, DetectorWriter, HardwareTriggeredFlyable, - SignalRW, - SimSignalBackend, TriggerInfo, TriggerLogic, ) from ophyd_async.core.detector import StandardDetector from ophyd_async.core.signal import observe_value +from ophyd_async.epics.signal.signal import epics_signal_rw class TriggerState(str, Enum): @@ -51,7 +50,7 @@ async def stop(self): class DummyWriter(DetectorWriter): def __init__(self, name: str, shape: Sequence[int]): - self.dummy_signal = SignalRW(backend=SimSignalBackend(int)) + self.dummy_signal = epics_signal_rw(int, "pva://read_pv") self._shape = shape self._name = name self._file: Optional[ComposeStreamResourceBundle] = None @@ -112,8 +111,8 @@ async def close(self) -> None: @pytest.fixture async def detector_list(RE: RunEngine) -> tuple[StandardDetector, StandardDetector]: writers = [DummyWriter("testa", (1, 1)), DummyWriter("testb", (1, 1))] - await writers[0].dummy_signal.connect(sim=True) - await writers[1].dummy_signal.connect(sim=True) + await writers[0].dummy_signal.connect(mock=True) + await writers[1].dummy_signal.connect(mock=True) async def dummy_arm_1(self=None, trigger=None, num=0, exposure=None): return writers[0].dummy_signal.set(1) diff --git a/tests/core/test_mock_signal_backend.py b/tests/core/test_mock_signal_backend.py new file mode 100644 index 0000000000..fae2ad81b2 --- /dev/null +++ b/tests/core/test_mock_signal_backend.py @@ -0,0 +1,341 @@ +import asyncio +import re +from itertools import repeat +from unittest.mock import MagicMock, call + +import pytest + +from ophyd_async.core import MockSignalBackend, SignalRW +from ophyd_async.core.device import Device, DeviceCollector +from ophyd_async.core.mock_signal_utils import ( + assert_mock_put_called_with, + callback_on_mock_put, + mock_puts_blocked, + reset_mock_put_calls, + set_mock_put_proceeds, + set_mock_value, + set_mock_values, +) +from ophyd_async.core.signal import ( + SignalW, + soft_signal_r_and_setter, + soft_signal_rw, +) +from ophyd_async.core.soft_signal_backend import SoftSignalBackend +from ophyd_async.epics.signal.signal import epics_signal_r, epics_signal_rw + + +@pytest.mark.parametrize("connect_mock_mode", [True, False]) +async def test_mock_signal_backend(connect_mock_mode): + mock_signal = SignalRW(MockSignalBackend(datatype=str)) + # If mock is false it will be handled like a normal signal, otherwise it will + # initalize a new backend from the one in the line above + await mock_signal.connect(mock=connect_mock_mode) + + assert await mock_signal._backend.get_value() == "" + await mock_signal._backend.put("test") + assert await mock_signal._backend.get_value() == "test" + + mock_signal._backend.mock.get_value.assert_called_once + + mock_signal._backend.mock["get_value"].assert_called_once + assert mock_signal._backend.mock.put.call_args_list == [ + call("test", wait=True, timeout=None), + ] + + +@pytest.mark.parametrize("epics_protocol", ["ca", "pva"]) +async def test_mock_signal_backend_source(epics_protocol): + mock_signal_rw = epics_signal_rw( + str, + f"{epics_protocol}://READ_PV", + f"{epics_protocol}://WRITE_PV", + name="mock_name", + ) + mock_signal_r = epics_signal_r( + str, + f"{epics_protocol}://READ_PV", + name="mock_name", + ) + await mock_signal_rw.connect(mock=True) + await mock_signal_r.connect(mock=True) + + assert mock_signal_rw.source == f"mock+{epics_protocol}://READ_PV" + assert mock_signal_r.source == f"mock+{epics_protocol}://READ_PV" + + +async def test_set_mock_value(): + mock_signal = SignalRW(SoftSignalBackend(int)) + await mock_signal.connect(mock=True) + assert await mock_signal.get_value() == 0 + assert await mock_signal._backend.get_value() == 0 + set_mock_value(mock_signal, 10) + assert await mock_signal.get_value() == 10 + assert await mock_signal._backend.get_value() == 10 + + +async def test_set_mock_put_proceeds(): + mock_signal = SignalW(SoftSignalBackend(str)) + await mock_signal.connect(mock=True) + + assert mock_signal._backend.put_proceeds.is_set() is True + + set_mock_put_proceeds(mock_signal, False) + assert mock_signal._backend.put_proceeds.is_set() is False + set_mock_put_proceeds(mock_signal, True) + assert mock_signal._backend.put_proceeds.is_set() is True + + +async def test_set_mock_put_proceeds_timeout(): + mock_signal = SignalRW(SoftSignalBackend(str)) + await mock_signal.connect(mock=True) + + set_mock_put_proceeds(mock_signal, False) + + with pytest.raises(asyncio.exceptions.TimeoutError): + await mock_signal.set("test", wait=True, timeout=1) + + +async def test_put_proceeds_timeout(): + mock_signal = SignalW(SoftSignalBackend(str)) + await mock_signal.connect(mock=True) + + assert mock_signal._backend.put_proceeds.is_set() is True + + set_mock_put_proceeds(mock_signal, False) + assert mock_signal._backend.put_proceeds.is_set() is False + set_mock_put_proceeds(mock_signal, True) + assert mock_signal._backend.put_proceeds.is_set() is True + + +async def test_mock_utils_throw_error_if_backend_isnt_mock_signal_backend(): + signal = SignalRW(SoftSignalBackend(int)) + + exc_msgs = [] + with pytest.raises(AssertionError) as exc: + set_mock_value(signal, 10) + exc_msgs.append(str(exc.value)) + with pytest.raises(AssertionError) as exc: + assert_mock_put_called_with(signal, 10) + exc_msgs.append(str(exc.value)) + with pytest.raises(AssertionError) as exc: + async with mock_puts_blocked(signal, 10): + ... + exc_msgs.append(str(exc.value)) + with pytest.raises(AssertionError) as exc: + with callback_on_mock_put(signal, 10): + ... + exc_msgs.append(str(exc.value)) + with pytest.raises(AssertionError) as exc: + set_mock_put_proceeds(signal, False) + exc_msgs.append(str(exc.value)) + with pytest.raises(AssertionError) as exc: + for _ in set_mock_values(signal, [10]): + ... + exc_msgs.append(str(exc.value)) + + for msg in exc_msgs: + assert msg == ( + "Expected to receive a `MockSignalBackend`, instead " + f" received {SoftSignalBackend}. " + ) + + +async def test_assert_mock_put_called_with(): + mock_signal = epics_signal_rw(str, "READ_PV", "WRITE_PV", name="mock_name") + await mock_signal.connect(mock=True) + await mock_signal.set("test_value", wait=True, timeout=100) + + # can leave out kwargs + assert_mock_put_called_with(mock_signal, "test_value") + assert_mock_put_called_with(mock_signal, "test_value", wait=True) + assert_mock_put_called_with(mock_signal, "test_value", timeout=100) + assert_mock_put_called_with(mock_signal, "test_value", wait=True, timeout=100) + + def err_text(text, wait, timeout): + return ( + f"Expected: put('{re.escape(str(text))}', wait={re.escape(str(wait))}," + f" timeout={re.escape(str(timeout))})", + "Actual: put('test_value', wait=True, timeout=100)", + ) + + for text, wait, timeout in [ + ("wrong_name", True, 100), # name wrong + ("test_value", False, 100), # wait wrong + ("test_value", True, 0), # timeout wrong + ("test_value", False, 0), # wait and timeout wrong + ]: + with pytest.raises(AssertionError) as exc: + assert_mock_put_called_with(mock_signal, text, wait=wait, timeout=timeout) + for err_substr in err_text(text, wait, timeout): + assert err_substr in str(exc.value) + + +@pytest.fixture +async def mock_signals(): + async with DeviceCollector(mock=True): + signal1 = epics_signal_rw(str, "READ_PV1", "WRITE_PV1", name="mock_name1") + signal2 = epics_signal_rw(str, "READ_PV2", "WRITE_PV2", name="mock_name2") + + await signal1.set("first_value", wait=True, timeout=1) + await signal2.set("first_value", wait=True, timeout=1) + assert await signal1.get_value() == "first_value" + assert await signal2.get_value() == "first_value" + return signal1, signal2 + + +async def test_blocks_during_put(mock_signals): + signal1, signal2 = mock_signals + + async with mock_puts_blocked(signal1, signal2): + status1 = signal1.set("second_value", wait=True, timeout=1) + status2 = signal2.set("second_value", wait=True, timeout=1) + assert await signal1.get_value() == "second_value" + assert await signal2.get_value() == "second_value" + assert not status1.done + assert not status2.done + + await asyncio.sleep(1e-4) + + assert status1.done + assert status2.done + assert await signal1._backend.get_value() == "second_value" + assert await signal2._backend.get_value() == "second_value" + + +async def test_callback_on_mock_put_ctxt(mock_signals): + signal1_callbacks = MagicMock() + signal2_callbacks = MagicMock() + signal1, signal2 = mock_signals + with callback_on_mock_put(signal1, signal1_callbacks): + await signal1.set("second_value", wait=True, timeout=1) + with callback_on_mock_put(signal2, signal2_callbacks): + await signal2.set("second_value", wait=True, timeout=1) + + signal1_callbacks.assert_called_once_with("second_value", wait=True, timeout=1) + signal2_callbacks.assert_called_once_with("second_value", wait=True, timeout=1) + + +async def test_callback_on_mock_put_no_ctx(): + mock_signal = SignalRW(SoftSignalBackend(float)) + await mock_signal.connect(mock=True) + calls = [] + ( + callback_on_mock_put( + mock_signal, lambda *args, **kwargs: calls.append({**kwargs, "_args": args}) + ), + ) + await mock_signal.set(10.0) + assert calls == [ + { + "_args": (10.0,), + "timeout": 10.0, + "wait": True, + } + ] + + +async def test_set_mock_values(mock_signals): + signal1, signal2 = mock_signals + + await signal2.get_value() == "first_value" + for value_set in set_mock_values(signal1, ["second_value", "third_value"]): + assert await signal1.get_value() == value_set + + iterator = set_mock_values(signal2, ["second_value", "third_value"]) + await signal2.get_value() == "first_value" + next(iterator) + await signal2.get_value() == "second_value" + next(iterator) + await signal2.get_value() == "third_value" + + +async def test_set_mock_values_exhausted_passes(mock_signals): + signal1, signal2 = mock_signals + for value_set in set_mock_values( + signal1, ["second_value", "third_value"], require_all_consumed=True + ): + assert await signal1.get_value() == value_set + + iterator = set_mock_values( + signal2, + repeat(iter(["second_value", "third_value"]), 6), + require_all_consumed=False, + ) + for calls, value_set in enumerate(iterator, start=1): + assert await signal2.get_value() == value_set + + assert calls == 6 + + +async def test_set_mock_values_exhausted_fails(mock_signals): + signal1, signal2 = mock_signals + + for value_set in ( + iterator := set_mock_values( + signal1, ["second_value", "third_value"], require_all_consumed=True + ) + ): + assert await signal1.get_value() == value_set + break + + with pytest.raises(AssertionError): + iterator.__del__() + + # Set so it doesn't raise the same error on teardown + iterator.require_all_consumed = False + + +async def test_reset_mock_put_calls(mock_signals): + signal1, signal2 = mock_signals + await signal1.set("test_value", wait=True, timeout=1) + assert_mock_put_called_with(signal1, "test_value") + reset_mock_put_calls(signal1) + with pytest.raises(AssertionError) as exc: + assert_mock_put_called_with(signal1, "test_value") + # Replacing spaces because they change between runners + # (e.g the github actions runner has more) + assert str(exc.value).replace(" ", "").replace("\n", "") == ( + "expectedcallnotfound." + "Expected:put('test_value',wait=,timeout=)" + "Actual:notcalled." + ) + + +async def test_mock_signal_of_soft_signal_backend_receives_intial_value(): + class SomeDevice(Device): + def __init__(self, name): + self.my_signal = soft_signal_rw( + datatype=int, + initial_value=10, + name=name, + ) + + mocked_device = SomeDevice("mocked_device") + await mocked_device.connect(mock=True) + soft_device = SomeDevice("soft_device") + await soft_device.connect(mock=False) + + assert await mocked_device.my_signal.get_value() == 10 + assert await soft_device.my_signal.get_value() == 10 + + +async def test_writing_to_soft_signals_in_mock(): + class MyDevice(Device): + def __init__(self, prefix: str, name: str = ""): + self.signal, self.backend_put = soft_signal_r_and_setter(int) + + async def set(self): + self.backend_put(1) + + device = MyDevice("-SOME-PREFIX", name="my_device") + await device.connect(mock=True) + assert await device.signal.get_value() == 0 + await device.set() + assert await device.signal.get_value() == 1 + + signal, backend_put = soft_signal_r_and_setter(int) + await signal.connect(mock=False) + assert await signal.get_value() == 0 + backend_put(100) + assert await signal.get_value() == 100 diff --git a/tests/core/test_signal.py b/tests/core/test_signal.py index ccd3540133..edb7d28535 100644 --- a/tests/core/test_signal.py +++ b/tests/core/test_signal.py @@ -2,7 +2,7 @@ import logging import re import time -from unittest.mock import ANY, AsyncMock +from unittest.mock import ANY import numpy import pytest @@ -12,54 +12,30 @@ ConfigSignal, DeviceCollector, HintedSignal, - Signal, - SignalBackend, SignalR, SignalRW, - SimSignalBackend, + SoftSignalBackend, StandardReadable, assert_configuration, assert_reading, assert_value, set_and_wait_for_value, - set_sim_put_proceeds, - set_sim_value, - soft_signal_r_and_backend, + set_mock_put_proceeds, + set_mock_value, + soft_signal_r_and_setter, soft_signal_rw, wait_for_value, ) from ophyd_async.core.signal import _SignalCache -from ophyd_async.core.utils import DEFAULT_TIMEOUT from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw -class MySignal(Signal): - @property - def source(self) -> str: - return "me" +async def test_signals_equality_raises(): + s1 = epics_signal_rw(int, "pva://pv1", name="signal") + s2 = epics_signal_rw(int, "pva://pv2", name="signal") + await s1.connect(mock=True) + await s2.connect(mock=True) - async def connect(self, sim=False, timeout=DEFAULT_TIMEOUT): - pass - - -class MockSignalRW(SignalRW): - def __init__(self, backend, timeout, name): - super().__init__(backend, timeout, name) - self._backend = AsyncMock() - - @property - def source(self) -> str: - return "source" - - async def connect(self): - pass - - -def test_signals_equality_raises(): - sim_backend = SimSignalBackend(str) - - s1 = MySignal(sim_backend) - s2 = MySignal(sim_backend) with pytest.raises( TypeError, match=re.escape( @@ -69,107 +45,102 @@ def test_signals_equality_raises(): s1 == s2 with pytest.raises( TypeError, - match=re.escape("'>' not supported between instances of 'MySignal' and 'int'"), + match=re.escape("'>' not supported between instances of 'SignalRW' and 'int'"), ): s1 > 4 -async def test_set_sim_put_proceeds(): - sim_signal = Signal(SimSignalBackend(str)) - await sim_signal.connect(sim=True) - - assert sim_signal._backend.put_proceeds.is_set() is True - - set_sim_put_proceeds(sim_signal, False) - assert sim_signal._backend.put_proceeds.is_set() is False - set_sim_put_proceeds(sim_signal, True) - assert sim_signal._backend.put_proceeds.is_set() is True - - async def time_taken_by(coro) -> float: start = time.monotonic() await coro return time.monotonic() - start +async def test_set_and_wait_for_value(): + signal = epics_signal_rw(int, "pva://pv", name="signal") + await signal.connect(mock=True) + assert await signal.get_value() == 0 + set_mock_put_proceeds(signal, False) + + async def wait_and_set_proceeds(): + await asyncio.sleep(0.1) + set_mock_put_proceeds(signal, True) + await asyncio.sleep(0.01) + + async def check_set_and_wait(): + st = await set_and_wait_for_value(signal, 1, timeout=100) + await st + await asyncio.sleep(0.01) + + assert ( + 0.1 + < await time_taken_by( + asyncio.gather(wait_and_set_proceeds(), check_set_and_wait()) + ) + < 0.15 + ) + assert await signal.get_value() == 1 + + async def test_wait_for_value_with_value(): - sim_signal = SignalRW(SimSignalBackend(str)) - sim_signal.set_name("sim_signal") - await sim_signal.connect(sim=True) - set_sim_value(sim_signal, "blah") + signal = epics_signal_rw(str, read_pv="pva://signal", name="signal") + await signal.connect(mock=True) + await signal.set("blah") with pytest.raises( TimeoutError, - match="sim_signal didn't match 'something' in 0.1s, last value 'blah'", + match="signal didn't match 'something' in 0.1s, last value 'blah'", ): - await wait_for_value(sim_signal, "something", timeout=0.1) - assert await time_taken_by(wait_for_value(sim_signal, "blah", timeout=2)) < 0.1 + await wait_for_value(signal, "something", timeout=0.1) + assert await time_taken_by(wait_for_value(signal, "blah", timeout=2)) < 0.1 t = asyncio.create_task( - time_taken_by(wait_for_value(sim_signal, "something else", timeout=2)) + time_taken_by(wait_for_value(signal, "something else", timeout=2)) ) await asyncio.sleep(0.2) assert not t.done() - set_sim_value(sim_signal, "something else") + set_mock_value(signal, "something else") assert 0.2 < await t < 1.0 async def test_wait_for_value_with_funcion(): - sim_signal = SignalRW(SimSignalBackend(float)) - sim_signal.set_name("sim_signal") - await sim_signal.connect(sim=True) - set_sim_value(sim_signal, 45.8) + signal = epics_signal_rw(float, read_pv="pva://signal", name="signal") + await signal.connect(mock=True) + set_mock_value(signal, 45.8) def less_than_42(v): return v < 42 with pytest.raises( TimeoutError, - match="sim_signal didn't match less_than_42 in 0.1s, last value 45.8", + match="signal didn't match less_than_42 in 0.1s, last value 45.8", ): - await wait_for_value(sim_signal, less_than_42, timeout=0.1) + await wait_for_value(signal, less_than_42, timeout=0.1) t = asyncio.create_task( - time_taken_by(wait_for_value(sim_signal, less_than_42, timeout=2)) + time_taken_by(wait_for_value(signal, less_than_42, timeout=2)) ) await asyncio.sleep(0.2) assert not t.done() - set_sim_value(sim_signal, 41) + set_mock_value(signal, 41) assert 0.2 < await t < 1.0 - assert ( - await time_taken_by(wait_for_value(sim_signal, less_than_42, timeout=2)) < 0.1 - ) - - -async def test_set_and_wait_for_value(): - sim_signal = SignalRW(SimSignalBackend(int)) - sim_signal.set_name("sim_signal") - await sim_signal.connect(sim=True) - set_sim_value(sim_signal, 0) - set_sim_put_proceeds(sim_signal, False) - st = await set_and_wait_for_value(sim_signal, 1) - assert not st.done - set_sim_put_proceeds(sim_signal, True) - assert await time_taken_by(st) < 0.1 + assert await time_taken_by(wait_for_value(signal, less_than_42, timeout=2)) < 0.1 @pytest.mark.parametrize( "signal_method,signal_class", - [(soft_signal_r_and_backend, SignalR), (soft_signal_rw, SignalRW)], + [(soft_signal_r_and_setter, SignalR), (soft_signal_rw, SignalRW)], ) async def test_create_soft_signal(signal_method, signal_class): SIGNAL_NAME = "TEST-PREFIX:SIGNAL" INITIAL_VALUE = "INITIAL" - if signal_method == soft_signal_r_and_backend: - signal, backend = signal_method(str, INITIAL_VALUE, SIGNAL_NAME) + if signal_method == soft_signal_r_and_setter: + signal, unused_backend_set = signal_method(str, INITIAL_VALUE, SIGNAL_NAME) elif signal_method == soft_signal_rw: signal = signal_method(str, INITIAL_VALUE, SIGNAL_NAME) - backend = signal._backend assert signal.source == f"soft://{SIGNAL_NAME}" assert isinstance(signal, signal_class) - assert isinstance(signal._backend, SimSignalBackend) + assert isinstance(signal._backend, SoftSignalBackend) await signal.connect() assert (await signal.get_value()) == INITIAL_VALUE - # connecting with sim=False uses existing SimSignalBackend - assert signal._backend is backend async def test_soft_signal_numpy(): @@ -182,24 +153,23 @@ async def test_soft_signal_numpy(): @pytest.fixture -async def sim_signal(): - sim_signal = SignalRW(SimSignalBackend(int, "test")) - sim_signal.set_name("sim_signal") - await sim_signal.connect(sim=True) - yield sim_signal +async def mock_signal(): + mock_signal = epics_signal_rw(int, "pva://mock_signal", name="mock_signal") + await mock_signal.connect(mock=True) + yield mock_signal -async def test_assert_value(sim_signal: SignalRW): - set_sim_value(sim_signal, 168) - await assert_value(sim_signal, 168) +async def test_assert_value(mock_signal: SignalRW): + set_mock_value(mock_signal, 168) + await assert_value(mock_signal, 168) -async def test_assert_reaading(sim_signal: SignalRW): - set_sim_value(sim_signal, 888) +async def test_assert_reaading(mock_signal: SignalRW): + set_mock_value(mock_signal, 888) dummy_reading = { - "sim_signal": Reading({"alarm_severity": 0, "timestamp": ANY, "value": 888}) + "mock_signal": Reading({"alarm_severity": 0, "timestamp": ANY, "value": 888}) } - await assert_reading(sim_signal, dummy_reading) + await assert_reading(mock_signal, dummy_reading) class DummyReadable(StandardReadable): @@ -217,45 +187,45 @@ def __init__(self, prefix: str, name="") -> None: @pytest.fixture -async def sim_readable(): - async with DeviceCollector(sim=True): - sim_readable = DummyReadable("SIM:READABLE:") - # Signals connected here - assert sim_readable.name == "sim_readable" - yield sim_readable - - -async def test_assert_configuration(sim_readable: DummyReadable): - set_sim_value(sim_readable.value, 123) - set_sim_value(sim_readable.mode, "super mode") - set_sim_value(sim_readable.mode2, "slow mode") +async def mock_readable(): + async with DeviceCollector(mock=True): + mock_readable = DummyReadable("SIM:READABLE:", name="mock_readable") + + yield mock_readable + + +async def test_assert_configuration(mock_readable: DummyReadable): + set_mock_value(mock_readable.value, 123) + set_mock_value(mock_readable.mode, "super mode") + set_mock_value(mock_readable.mode2, "slow mode") dummy_config_reading = { - "sim_readable-mode": ( + "mock_readable-mode": ( { "alarm_severity": 0, "timestamp": ANY, "value": "super mode", } ), - "sim_readable-mode2": { + "mock_readable-mode2": { "alarm_severity": 0, "timestamp": ANY, "value": "slow mode", }, } - await assert_configuration(sim_readable, dummy_config_reading) + await assert_configuration(mock_readable, dummy_config_reading) async def test_signal_connect_logs(caplog): caplog.set_level(logging.DEBUG) - sim_signal = Signal(SimSignalBackend(str, "test"), timeout=1, name="test_signal") - await sim_signal.connect(sim=True) - assert caplog.text.endswith("Connecting to soft://test_signal\n") + mock_signal_rw = epics_signal_rw(int, "pva://mock_signal", name="mock_signal") + await mock_signal_rw.connect(mock=True) + assert caplog.text.endswith("Connecting to mock+pva://mock_signal\n") async def test_signal_get_and_set_logging(caplog): caplog.set_level(logging.DEBUG) - mock_signal_rw = MockSignalRW(SignalBackend, timeout=1, name="mock_signal") + mock_signal_rw = epics_signal_rw(int, "pva://mock_signal", name="mock_signal") + await mock_signal_rw.connect(mock=True) await mock_signal_rw.set(value=0) assert "Putting value 0 to backend at source" in caplog.text assert "Successfully put value 0 to backend at source" in caplog.text @@ -263,12 +233,11 @@ async def test_signal_get_and_set_logging(caplog): assert "get_value() on source" in caplog.text -def test_subscription_logs(caplog): +async def test_subscription_logs(caplog): caplog.set_level(logging.DEBUG) - cache = _SignalCache( - SignalBackend(), - signal=MockSignalRW(SignalBackend, timeout=1, name="mock_signal"), - ) + mock_signal_rw = epics_signal_rw(int, "pva://mock_signal", name="mock_signal") + await mock_signal_rw.connect(mock=True) + cache = _SignalCache(mock_signal_rw._backend, signal=mock_signal_rw) assert "Making subscription" in caplog.text cache.close() assert "Closing subscription on source" in caplog.text diff --git a/tests/core/test_sim.py b/tests/core/test_soft_signal_backend.py similarity index 82% rename from tests/core/test_sim.py rename to tests/core/test_soft_signal_backend.py index 2f222c2fe3..1edbd6eced 100644 --- a/tests/core/test_sim.py +++ b/tests/core/test_soft_signal_backend.py @@ -8,7 +8,8 @@ import pytest from bluesky.protocols import Reading -from ophyd_async.core import Signal, SignalBackend, SimSignalBackend, T +from ophyd_async.core import Signal, SignalBackend, T +from ophyd_async.core.soft_signal_backend import SoftSignalBackend class MyEnum(str, Enum): @@ -87,13 +88,13 @@ def close(self): # (str, "longstr2.VAL$", ls1, ls2, string_d), ], ) -async def test_backend_get_put_monitor( +async def test_soft_signal_backend_get_put_monitor( datatype: Type[T], initial_value: T, put_value: T, descriptor: Callable[[Any], dict], ): - backend = SimSignalBackend(datatype) + backend = SoftSignalBackend(datatype) await backend.connect() q = MonitorQueue(backend) @@ -114,27 +115,21 @@ async def test_backend_get_put_monitor( q.close() -async def test_sim_backend_if_disconnected(): - sim_backend = SimSignalBackend(npt.NDArray[np.float64]) - with pytest.raises(NotImplementedError): - await sim_backend.get_value() +async def test_soft_signal_backend_with_numpy_typing(): + soft_backend = SoftSignalBackend(npt.NDArray[np.float64]) + await soft_backend.connect() - -async def test_sim_backend_with_numpy_typing(): - sim_backend = SimSignalBackend(npt.NDArray[np.float64]) - await sim_backend.connect() - - array = await sim_backend.get_value() + array = await soft_backend.get_value() assert array.shape == (0,) -async def test_sim_backend_descriptor_fails_for_invalid_class(): +async def test_soft_signal_descriptor_fails_for_invalid_class(): class myClass: def __init__(self) -> None: pass - sim_signal = Signal(SimSignalBackend(myClass)) - await sim_signal.connect(sim=True) + soft_signal = Signal(SoftSignalBackend(myClass)) + await soft_signal.connect() with pytest.raises(AssertionError): - await sim_signal._backend.get_datakey("") + await soft_signal._backend.get_datakey("") diff --git a/tests/core/test_standard_readable.py b/tests/core/test_standard_readable.py index ecab6afaa6..8074f6672a 100644 --- a/tests/core/test_standard_readable.py +++ b/tests/core/test_standard_readable.py @@ -7,8 +7,8 @@ from ophyd_async.core import ConfigSignal, HintedSignal, StandardReadable from ophyd_async.core.device import Device, DeviceVector +from ophyd_async.core.mock_signal_backend import MockSignalBackend from ophyd_async.core.signal import SignalR -from ophyd_async.core.sim_signal_backend import SimSignalBackend from ophyd_async.protocols import AsyncConfigurable, AsyncReadable, AsyncStageable @@ -136,7 +136,7 @@ def test_standard_readable_add_children_cm_filters_non_devices(): sr.b = MagicMock(spec=Device) sr.c = 1.0 sr.d = "abc" - sr.e = MagicMock(spec=SimSignalBackend) + sr.e = MagicMock(spec=MockSignalBackend) # Can't use assert_called_once_with() as the order of items returned from # internal dict comprehension is not guaranteed diff --git a/tests/core/test_utils.py b/tests/core/test_utils.py index b2e22a46c0..8298eec7c4 100644 --- a/tests/core/test_utils.py +++ b/tests/core/test_utils.py @@ -6,19 +6,17 @@ DEFAULT_TIMEOUT, Device, DeviceCollector, - DirectoryInfo, + MockSignalBackend, NotConnected, SignalRW, - SimSignalBackend, - StaticDirectoryProvider, ) from ophyd_async.epics.signal import epics_signal_rw -class ValueErrorBackend(SimSignalBackend): +class ValueErrorBackend(MockSignalBackend): def __init__(self, exc_text=""): self.exc_text = exc_text - super().__init__(int, "VALUE_ERROR_SIGNAL") + super().__init__(datatype=int, initial_backend=None) async def connect(self, timeout: float = DEFAULT_TIMEOUT): raise ValueError(self.exc_text) @@ -26,7 +24,7 @@ async def connect(self, timeout: float = DEFAULT_TIMEOUT): class WorkingDummyChildDevice(Device): def __init__(self, name: str = "working_dummy_child_device") -> None: - self.working_signal = SignalRW(backend=SimSignalBackend(int)) + self.working_signal = SignalRW(backend=MockSignalBackend(datatype=int)) super().__init__(name=name) @@ -174,8 +172,8 @@ async def test_error_handling_value_errors(caplog): async def test_error_handling_device_collector(caplog): caplog.set_level(10) with pytest.raises(NotConnected) as e: + # flake8: noqa async with DeviceCollector(timeout=0.1): - # flake8: noqa dummy_device_two_working_one_timeout_two_value_error = ( DummyDeviceTwoWorkingTwoTimeOutTwoValueError() ) @@ -183,7 +181,9 @@ async def test_error_handling_device_collector(caplog): expected_output = NotConnected( { - "dummy_device_two_working_one_timeout_two_value_error": TWO_WORKING_TWO_TIMEOUT_TWO_VALUE_ERROR_OUTPUT, + "dummy_device_two_working_one_timeout_two_value_error": ( + TWO_WORKING_TWO_TIMEOUT_TWO_VALUE_ERROR_OUTPUT + ), "dummy_device_one_working_one_timeout": ONE_WORKING_ONE_TIMEOUT_OUTPUT, } ) diff --git a/tests/epics/areadetector/test_aravis.py b/tests/epics/areadetector/test_aravis.py index 9b61f50ff8..5c514617b4 100644 --- a/tests/epics/areadetector/test_aravis.py +++ b/tests/epics/areadetector/test_aravis.py @@ -8,7 +8,7 @@ DeviceCollector, DirectoryProvider, TriggerInfo, - set_sim_value, + set_mock_value, ) from ophyd_async.epics.areadetector.aravis import AravisDetector @@ -18,7 +18,7 @@ async def adaravis( RE: RunEngine, static_directory_provider: DirectoryProvider, ) -> AravisDetector: - async with DeviceCollector(sim=True): + async with DeviceCollector(mock=True): adaravis = AravisDetector("ADARAVIS:", static_directory_provider) return adaravis @@ -46,8 +46,8 @@ async def test_deadtime_fetched( deadtime: float, adaravis: AravisDetector, ): - set_sim_value(adaravis.drv.model, model) - set_sim_value(adaravis.drv.pixel_format, pixel_format) + set_mock_value(adaravis.drv.model, model) + set_mock_value(adaravis.drv.pixel_format, pixel_format) await adaravis.drv.fetch_deadtime() # deadtime invariant with exposure time @@ -58,7 +58,7 @@ async def test_deadtime_fetched( async def test_unknown_model_deadtime( adaravis: AravisDetector, ): - set_sim_value(adaravis.drv.model, "FOO") + set_mock_value(adaravis.drv.model, "FOO") with pytest.raises(ValueError, match="Model FOO does not have defined deadtimes"): await adaravis.drv.fetch_deadtime() @@ -67,8 +67,8 @@ async def test_unknown_model_deadtime( async def test_unknown_pixel_format_deadtime( adaravis: AravisDetector, ): - set_sim_value(adaravis.drv.model, "Manta G-235") - set_sim_value(adaravis.drv.pixel_format, "BAR") + set_mock_value(adaravis.drv.model, "Manta G-235") + set_mock_value(adaravis.drv.pixel_format, "BAR") with pytest.raises( ValueError, @@ -79,12 +79,12 @@ async def test_unknown_pixel_format_deadtime( async def test_trigger_source_set_to_gpio_line(adaravis: AravisDetector): - set_sim_value(adaravis.drv.trigger_source, "Freerun") + set_mock_value(adaravis.drv.trigger_source, "Freerun") async def trigger_and_complete(): await adaravis.controller.arm(num=1, trigger=DetectorTrigger.edge_trigger) # Prevent timeouts - set_sim_value(adaravis.drv.acquire, True) + set_mock_value(adaravis.drv.acquire, True) # Default TriggerSource assert (await adaravis.drv.trigger_source.get_value()) == "Freerun" @@ -127,14 +127,14 @@ async def test_can_read(adaravis: AravisDetector): async def test_decribe_describes_writer_dataset(adaravis: AravisDetector): - set_sim_value(adaravis._writer.hdf.file_path_exists, True) - set_sim_value(adaravis._writer.hdf.capture, True) + set_mock_value(adaravis._writer.hdf.file_path_exists, True) + set_mock_value(adaravis._writer.hdf.capture, True) assert await adaravis.describe() == {} await adaravis.stage() assert await adaravis.describe() == { "adaravis": { - "source": "soft://adaravis-hdf-full_file_name", + "source": "mock+ca://ADARAVIS:HDF1:FullFileName_RBV", "shape": (0, 0), "dtype": "array", "external": "STREAM:", @@ -147,9 +147,9 @@ async def test_can_collect( ): directory_info = static_directory_provider() full_file_name = directory_info.root / directory_info.resource_dir / "foo.h5" - set_sim_value(adaravis.hdf.full_file_name, str(full_file_name)) - set_sim_value(adaravis._writer.hdf.file_path_exists, True) - set_sim_value(adaravis._writer.hdf.capture, True) + set_mock_value(adaravis.hdf.full_file_name, str(full_file_name)) + set_mock_value(adaravis._writer.hdf.file_path_exists, True) + set_mock_value(adaravis._writer.hdf.capture, True) await adaravis.stage() docs = [(name, doc) async for name, doc in adaravis.collect_asset_docs(1)] assert len(docs) == 2 @@ -176,13 +176,13 @@ async def test_can_collect( async def test_can_decribe_collect(adaravis: AravisDetector): - set_sim_value(adaravis._writer.hdf.file_path_exists, True) - set_sim_value(adaravis._writer.hdf.capture, True) + set_mock_value(adaravis._writer.hdf.file_path_exists, True) + set_mock_value(adaravis._writer.hdf.capture, True) assert (await adaravis.describe_collect()) == {} await adaravis.stage() assert (await adaravis.describe_collect()) == { "adaravis": { - "source": "soft://adaravis-hdf-full_file_name", + "source": "mock+ca://ADARAVIS:HDF1:FullFileName_RBV", "shape": (0, 0), "dtype": "array", "external": "STREAM:", @@ -191,8 +191,8 @@ async def test_can_decribe_collect(adaravis: AravisDetector): async def test_unsupported_trigger_excepts(adaravis: AravisDetector): - set_sim_value(adaravis.drv.model, "Manta G-125") - set_sim_value(adaravis.drv.pixel_format, "Mono12Packed") + set_mock_value(adaravis.drv.model, "Manta G-125") + set_mock_value(adaravis.drv.pixel_format, "Mono12Packed") with pytest.raises( ValueError, # str(EnumClass.value) handling changed in Python 3.11 diff --git a/tests/epics/areadetector/test_controllers.py b/tests/epics/areadetector/test_controllers.py index faa5df5d15..99962fe12c 100644 --- a/tests/epics/areadetector/test_controllers.py +++ b/tests/epics/areadetector/test_controllers.py @@ -14,7 +14,7 @@ @pytest.fixture async def pilatus(RE) -> PilatusController: - async with DeviceCollector(sim=True): + async with DeviceCollector(mock=True): drv = PilatusDriver("DRIVER:") controller = PilatusController(drv) @@ -23,7 +23,7 @@ async def pilatus(RE) -> PilatusController: @pytest.fixture async def ad(RE) -> ADSimController: - async with DeviceCollector(sim=True): + async with DeviceCollector(mock=True): drv = ADBase("DRIVER:") controller = ADSimController(drv) diff --git a/tests/epics/areadetector/test_drivers.py b/tests/epics/areadetector/test_drivers.py index 2de55e7c71..ecdcd57074 100644 --- a/tests/epics/areadetector/test_drivers.py +++ b/tests/epics/areadetector/test_drivers.py @@ -2,7 +2,7 @@ import pytest -from ophyd_async.core import DeviceCollector, set_sim_value +from ophyd_async.core import DeviceCollector, set_mock_value from ophyd_async.epics.areadetector.drivers import ( ADBase, DetectorState, @@ -12,7 +12,7 @@ @pytest.fixture def driver(RE) -> ADBase: - with DeviceCollector(sim=True): + with DeviceCollector(mock=True): driver = ADBase("DRV:", name="drv") return driver @@ -20,7 +20,7 @@ def driver(RE) -> ADBase: async def test_start_acquiring_driver_and_ensure_status_flags_immediate_failure( driver: ADBase, ): - set_sim_value(driver.detector_state, DetectorState.Error) + set_mock_value(driver.detector_state, DetectorState.Error) acquiring = await start_acquiring_driver_and_ensure_status(driver, timeout=0.01) with pytest.raises(ValueError): await acquiring @@ -34,11 +34,11 @@ async def test_start_acquiring_driver_and_ensure_status_fails_after_some_time( Real world application; it takes some time to start acquiring, and during that time the detector gets itself into a bad state. """ - set_sim_value(driver.detector_state, DetectorState.Idle) + set_mock_value(driver.detector_state, DetectorState.Idle) async def wait_then_fail(): await asyncio.sleep(0) - set_sim_value(driver.detector_state, DetectorState.Disconnected) + set_mock_value(driver.detector_state, DetectorState.Disconnected) acquiring = await start_acquiring_driver_and_ensure_status(driver, timeout=0.1) await wait_then_fail() diff --git a/tests/epics/areadetector/test_kinetix.py b/tests/epics/areadetector/test_kinetix.py index a5513a9ab7..ef367b9b0a 100644 --- a/tests/epics/areadetector/test_kinetix.py +++ b/tests/epics/areadetector/test_kinetix.py @@ -5,7 +5,7 @@ DetectorTrigger, DeviceCollector, DirectoryProvider, - set_sim_value, + set_mock_value, ) from ophyd_async.epics.areadetector.kinetix import KinetixDetector @@ -15,7 +15,7 @@ async def adkinetix( RE: RunEngine, static_directory_provider: DirectoryProvider, ) -> KinetixDetector: - async with DeviceCollector(sim=True): + async with DeviceCollector(mock=True): adkinetix = KinetixDetector("KINETIX:", static_directory_provider) return adkinetix @@ -29,12 +29,12 @@ async def test_get_deadtime( async def test_trigger_modes(adkinetix: KinetixDetector): - set_sim_value(adkinetix.drv.trigger_mode, "Internal") + set_mock_value(adkinetix.drv.trigger_mode, "Internal") async def setup_trigger_mode(trig_mode: DetectorTrigger): await adkinetix.controller.arm(num=1, trigger=trig_mode) # Prevent timeouts - set_sim_value(adkinetix.drv.acquire, True) + set_mock_value(adkinetix.drv.acquire, True) # Default TriggerSource assert (await adkinetix.drv.trigger_mode.get_value()) == "Internal" @@ -62,14 +62,14 @@ async def test_can_read(adkinetix: KinetixDetector): async def test_decribe_describes_writer_dataset(adkinetix: KinetixDetector): - set_sim_value(adkinetix._writer.hdf.file_path_exists, True) - set_sim_value(adkinetix._writer.hdf.capture, True) + set_mock_value(adkinetix._writer.hdf.file_path_exists, True) + set_mock_value(adkinetix._writer.hdf.capture, True) assert await adkinetix.describe() == {} await adkinetix.stage() assert await adkinetix.describe() == { "adkinetix": { - "source": "soft://adkinetix-hdf-full_file_name", + "source": "mock+ca://KINETIX:HDF1:FullFileName_RBV", "shape": (0, 0), "dtype": "array", "external": "STREAM:", @@ -82,9 +82,9 @@ async def test_can_collect( ): directory_info = static_directory_provider() full_file_name = directory_info.root / directory_info.resource_dir / "foo.h5" - set_sim_value(adkinetix.hdf.full_file_name, str(full_file_name)) - set_sim_value(adkinetix._writer.hdf.file_path_exists, True) - set_sim_value(adkinetix._writer.hdf.capture, True) + set_mock_value(adkinetix.hdf.full_file_name, str(full_file_name)) + set_mock_value(adkinetix._writer.hdf.file_path_exists, True) + set_mock_value(adkinetix._writer.hdf.capture, True) await adkinetix.stage() docs = [(name, doc) async for name, doc in adkinetix.collect_asset_docs(1)] assert len(docs) == 2 @@ -111,13 +111,13 @@ async def test_can_collect( async def test_can_decribe_collect(adkinetix: KinetixDetector): - set_sim_value(adkinetix._writer.hdf.file_path_exists, True) - set_sim_value(adkinetix._writer.hdf.capture, True) + set_mock_value(adkinetix._writer.hdf.file_path_exists, True) + set_mock_value(adkinetix._writer.hdf.capture, True) assert (await adkinetix.describe_collect()) == {} await adkinetix.stage() assert (await adkinetix.describe_collect()) == { "adkinetix": { - "source": "soft://adkinetix-hdf-full_file_name", + "source": "mock+ca://KINETIX:HDF1:FullFileName_RBV", "shape": (0, 0), "dtype": "array", "external": "STREAM:", diff --git a/tests/epics/areadetector/test_pilatus.py b/tests/epics/areadetector/test_pilatus.py index ac34a9e4e8..eb30b691bb 100644 --- a/tests/epics/areadetector/test_pilatus.py +++ b/tests/epics/areadetector/test_pilatus.py @@ -6,7 +6,7 @@ DeviceCollector, DirectoryProvider, TriggerInfo, - set_sim_value, + set_mock_value, ) from ophyd_async.epics.areadetector.drivers.pilatus_driver import PilatusTriggerMode from ophyd_async.epics.areadetector.pilatus import PilatusDetector @@ -17,7 +17,7 @@ async def pilatus( RE: RunEngine, static_directory_provider: DirectoryProvider, ) -> PilatusDetector: - async with DeviceCollector(sim=True): + async with DeviceCollector(mock=True): adpilatus = PilatusDetector("PILATUS:", static_directory_provider) return adpilatus @@ -48,7 +48,7 @@ async def test_trigger_mode_set( async def trigger_and_complete(): await pilatus.controller.arm(num=1, trigger=detector_trigger) # Prevent timeouts - set_sim_value(pilatus.controller._drv.acquire, True) + set_mock_value(pilatus.controller._drv.acquire, True) # Default TriggerMode assert (await pilatus.drv.trigger_mode.get_value()) == PilatusTriggerMode.internal diff --git a/tests/epics/areadetector/test_scans.py b/tests/epics/areadetector/test_scans.py index 3e13186d29..b9bd70268c 100644 --- a/tests/epics/areadetector/test_scans.py +++ b/tests/epics/areadetector/test_scans.py @@ -18,7 +18,7 @@ StaticDirectoryProvider, TriggerInfo, TriggerLogic, - set_sim_value, + set_mock_value, ) from ophyd_async.epics.areadetector.controllers import ADSimController from ophyd_async.epics.areadetector.drivers import ADBase @@ -57,7 +57,7 @@ def get_deadtime(self, exposure: float) -> float: @pytest.fixture def controller(RE) -> ADSimController: - with DeviceCollector(sim=True): + with DeviceCollector(mock=True): drv = ADBase("DRV") return ADSimController(drv) @@ -65,7 +65,7 @@ def controller(RE) -> ADSimController: @pytest.fixture def writer(RE, tmp_path: Path) -> HDFWriter: - with DeviceCollector(sim=True): + with DeviceCollector(mock=True): hdf = NDFileHDF("HDF") return HDFWriter( @@ -81,7 +81,7 @@ async def test_hdf_writer_fails_on_timeout_with_stepscan( writer: HDFWriter, controller: ADSimController, ): - set_sim_value(writer.hdf.file_path_exists, True) + set_mock_value(writer.hdf.file_path_exists, True) detector: StandardDetector[Any] = StandardDetector( controller, writer, name="detector", writer_timeout=0.01 ) @@ -94,7 +94,7 @@ async def test_hdf_writer_fails_on_timeout_with_stepscan( def test_hdf_writer_fails_on_timeout_with_flyscan(RE: RunEngine, writer: HDFWriter): controller = DummyController() - set_sim_value(writer.hdf.file_path_exists, True) + set_mock_value(writer.hdf.file_path_exists, True) detector: StandardDetector[Optional[TriggerInfo]] = StandardDetector( controller, writer, writer_timeout=0.01 diff --git a/tests/epics/areadetector/test_single_trigger_det.py b/tests/epics/areadetector/test_single_trigger_det.py index 684f72c3cf..86d76afd27 100644 --- a/tests/epics/areadetector/test_single_trigger_det.py +++ b/tests/epics/areadetector/test_single_trigger_det.py @@ -2,7 +2,7 @@ import pytest from bluesky import RunEngine -from ophyd_async.core import DeviceCollector, set_sim_value +from ophyd_async.core import DeviceCollector, set_mock_value from ophyd_async.epics.areadetector import ImageMode, SingleTriggerDet from ophyd_async.epics.areadetector.drivers import ADBase from ophyd_async.epics.areadetector.writers import NDPluginStats @@ -10,7 +10,7 @@ @pytest.fixture async def single_trigger_det(): - async with DeviceCollector(sim=True): + async with DeviceCollector(mock=True): stats = NDPluginStats("PREFIX:STATS") det = SingleTriggerDet( drv=ADBase("PREFIX:DRV"), stats=stats, read_uncached=[stats.unique_id] @@ -19,12 +19,12 @@ async def single_trigger_det(): assert det.name == "det" assert stats.name == "det-stats" # Set non-default values to check they are set back - # These are using set_sim_value to simulate the backend IOC being setup + # These are using set_mock_value to simulate the backend IOC being setup # in a particular way, rather than values being set by the Ophyd signals - set_sim_value(det.drv.acquire_time, 0.5) - set_sim_value(det.drv.array_counter, 1) - set_sim_value(det.drv.image_mode, ImageMode.continuous) - set_sim_value(stats.unique_id, 3) + set_mock_value(det.drv.acquire_time, 0.5) + set_mock_value(det.drv.array_counter, 1) + set_mock_value(det.drv.image_mode, ImageMode.continuous) + set_mock_value(stats.unique_id, 3) yield det diff --git a/tests/epics/areadetector/test_vimba.py b/tests/epics/areadetector/test_vimba.py index fbb1f88bf0..f8eccb3624 100644 --- a/tests/epics/areadetector/test_vimba.py +++ b/tests/epics/areadetector/test_vimba.py @@ -5,7 +5,7 @@ DetectorTrigger, DeviceCollector, DirectoryProvider, - set_sim_value, + set_mock_value, ) from ophyd_async.epics.areadetector.vimba import VimbaDetector @@ -15,7 +15,7 @@ async def advimba( RE: RunEngine, static_directory_provider: DirectoryProvider, ) -> VimbaDetector: - async with DeviceCollector(sim=True): + async with DeviceCollector(mock=True): advimba = VimbaDetector("VIMBA:", static_directory_provider) return advimba @@ -29,14 +29,14 @@ async def test_get_deadtime( async def test_arming_trig_modes(advimba: VimbaDetector): - set_sim_value(advimba.drv.trig_source, "Freerun") - set_sim_value(advimba.drv.trigger_mode, "Off") - set_sim_value(advimba.drv.expose_mode, "Timed") + set_mock_value(advimba.drv.trig_source, "Freerun") + set_mock_value(advimba.drv.trigger_mode, "Off") + set_mock_value(advimba.drv.expose_mode, "Timed") async def setup_trigger_mode(trig_mode: DetectorTrigger): await advimba.controller.arm(num=1, trigger=trig_mode) # Prevent timeouts - set_sim_value(advimba.drv.acquire, True) + set_mock_value(advimba.drv.acquire, True) # Default TriggerSource assert (await advimba.drv.trig_source.get_value()) == "Freerun" @@ -74,14 +74,14 @@ async def test_can_read(advimba: VimbaDetector): async def test_decribe_describes_writer_dataset(advimba: VimbaDetector): - set_sim_value(advimba._writer.hdf.file_path_exists, True) - set_sim_value(advimba._writer.hdf.capture, True) + set_mock_value(advimba._writer.hdf.file_path_exists, True) + set_mock_value(advimba._writer.hdf.capture, True) assert await advimba.describe() == {} await advimba.stage() assert await advimba.describe() == { "advimba": { - "source": "soft://advimba-hdf-full_file_name", + "source": "mock+ca://VIMBA:HDF1:FullFileName_RBV", "shape": (0, 0), "dtype": "array", "external": "STREAM:", @@ -94,9 +94,9 @@ async def test_can_collect( ): directory_info = static_directory_provider() full_file_name = directory_info.root / directory_info.resource_dir / "foo.h5" - set_sim_value(advimba.hdf.full_file_name, str(full_file_name)) - set_sim_value(advimba._writer.hdf.file_path_exists, True) - set_sim_value(advimba._writer.hdf.capture, True) + set_mock_value(advimba.hdf.full_file_name, str(full_file_name)) + set_mock_value(advimba._writer.hdf.file_path_exists, True) + set_mock_value(advimba._writer.hdf.capture, True) await advimba.stage() docs = [(name, doc) async for name, doc in advimba.collect_asset_docs(1)] assert len(docs) == 2 @@ -123,13 +123,13 @@ async def test_can_collect( async def test_can_decribe_collect(advimba: VimbaDetector): - set_sim_value(advimba._writer.hdf.file_path_exists, True) - set_sim_value(advimba._writer.hdf.capture, True) + set_mock_value(advimba._writer.hdf.file_path_exists, True) + set_mock_value(advimba._writer.hdf.capture, True) assert (await advimba.describe_collect()) == {} await advimba.stage() assert (await advimba.describe_collect()) == { "advimba": { - "source": "soft://advimba-hdf-full_file_name", + "source": "mock+ca://VIMBA:HDF1:FullFileName_RBV", "shape": (0, 0), "dtype": "array", "external": "STREAM:", diff --git a/tests/epics/areadetector/test_writers.py b/tests/epics/areadetector/test_writers.py index dda8b74d44..b8ab2cbe76 100644 --- a/tests/epics/areadetector/test_writers.py +++ b/tests/epics/areadetector/test_writers.py @@ -7,7 +7,7 @@ DeviceCollector, ShapeProvider, StaticDirectoryProvider, - set_sim_value, + set_mock_value, ) from ophyd_async.epics.areadetector.writers import HDFWriter, NDFileHDF @@ -22,7 +22,7 @@ async def __call__(self) -> Sequence[int]: @pytest.fixture async def hdf_writer(RE) -> HDFWriter: - async with DeviceCollector(sim=True): + async with DeviceCollector(mock=True): hdf = NDFileHDF("HDF:") return HDFWriter( @@ -34,13 +34,13 @@ async def hdf_writer(RE) -> HDFWriter: async def test_correct_descriptor_doc_after_open(hdf_writer: HDFWriter): - set_sim_value(hdf_writer.hdf.file_path_exists, True) + set_mock_value(hdf_writer.hdf.file_path_exists, True) with patch("ophyd_async.core.signal.wait_for_value", return_value=None): descriptor = await hdf_writer.open() assert descriptor == { "test": { - "source": "soft://hdf-full_file_name", + "source": "mock+ca://HDF:FullFileName_RBV", "shape": (10, 10), "dtype": "array", "external": "STREAM:", diff --git a/tests/epics/demo/test_demo.py b/tests/epics/demo/test_demo.py index f6cddb4ad1..fa76d34892 100644 --- a/tests/epics/demo/test_demo.py +++ b/tests/epics/demo/test_demo.py @@ -15,8 +15,8 @@ assert_emitted, assert_reading, assert_value, - set_sim_callback, - set_sim_value, + callback_on_mock_put, + set_mock_value, ) from ophyd_async.epics import demo @@ -26,36 +26,46 @@ @pytest.fixture -async def sim_mover() -> demo.Mover: - async with DeviceCollector(sim=True): - sim_mover = demo.Mover("BLxxI-MO-TABLE-01:X:") +async def mock_mover() -> demo.Mover: + async with DeviceCollector(mock=True): + mock_mover = demo.Mover("BLxxI-MO-TABLE-01:X:") # Signals connected here - assert sim_mover.name == "sim_mover" - set_sim_value(sim_mover.units, "mm") - set_sim_value(sim_mover.precision, 3) - set_sim_value(sim_mover.velocity, 1) - return sim_mover + assert mock_mover.name == "mock_mover" + set_mock_value(mock_mover.units, "mm") + set_mock_value(mock_mover.precision, 3) + set_mock_value(mock_mover.velocity, 1) + return mock_mover @pytest.fixture -async def sim_sensor() -> demo.Sensor: - async with DeviceCollector(sim=True): - sim_sensor = demo.Sensor("SIM:SENSOR:") +async def mock_sensor() -> demo.Sensor: + async with DeviceCollector(mock=True): + mock_sensor = demo.Sensor("MOCK:SENSOR:") # Signals connected here - assert sim_sensor.name == "sim_sensor" - return sim_sensor + assert mock_sensor.name == "mock_sensor" + return mock_sensor @pytest.fixture -async def sim_sensor_group() -> demo.SensorGroup: - async with DeviceCollector(sim=True): - sim_sensor_group = demo.SensorGroup("SIM:SENSOR:") +async def mock_sensor_group() -> demo.SensorGroup: + async with DeviceCollector(mock=True): + mock_sensor_group = demo.SensorGroup("MOCK:SENSOR:") # Signals connected here - assert sim_sensor_group.name == "sim_sensor_group" - return sim_sensor_group + assert mock_sensor_group.name == "mock_sensor_group" + return mock_sensor_group + + +async def test_mover_stopped(mock_mover: demo.Mover): + callbacks = [] + callback_on_mock_put( + mock_mover.stop_, lambda v, *args, **kwargs: callbacks.append(v) + ) + + await mock_mover.stop() + assert callbacks == [None] class Watcher: @@ -75,14 +85,14 @@ async def wait_for_call(self, *args, **kwargs): self._event.clear() -async def test_mover_moving_well(sim_mover: demo.Mover) -> None: - s = sim_mover.set(0.55) +async def test_mover_moving_well(mock_mover: demo.Mover) -> None: + s = mock_mover.set(0.55) watcher = Watcher() s.watch(watcher) done = Mock() s.add_callback(done) await watcher.wait_for_call( - name="sim_mover", + name="mock_mover", current=0.0, initial=0.0, target=0.55, @@ -91,13 +101,13 @@ async def test_mover_moving_well(sim_mover: demo.Mover) -> None: time_elapsed=pytest.approx(0.0, abs=0.05), ) - await assert_value(sim_mover.setpoint, 0.55) + await assert_value(mock_mover.setpoint, 0.55) assert not s.done done.assert_not_called() await asyncio.sleep(0.1) - set_sim_value(sim_mover.readback, 0.1) + set_mock_value(mock_mover.readback, 0.1) await watcher.wait_for_call( - name="sim_mover", + name="mock_mover", current=0.1, initial=0.0, target=0.55, @@ -105,7 +115,7 @@ async def test_mover_moving_well(sim_mover: demo.Mover) -> None: precision=3, time_elapsed=pytest.approx(0.1, abs=0.05), ) - set_sim_value(sim_mover.readback, 0.5499999) + set_mock_value(mock_mover.readback, 0.5499999) await asyncio.sleep(A_WHILE) assert s.done assert s.success @@ -115,14 +125,14 @@ async def test_mover_moving_well(sim_mover: demo.Mover) -> None: done2.assert_called_once_with(s) -async def test_sensor_reading_shows_value(sim_sensor: demo.Sensor): +async def test_sensor_reading_shows_value(mock_sensor: demo.Sensor): # Check default value - await assert_value(sim_sensor.value, pytest.approx(0.0)) - assert (await sim_sensor.value.get_value()) == pytest.approx(0.0) + await assert_value(mock_sensor.value, pytest.approx(0.0)) + assert (await mock_sensor.value.get_value()) == pytest.approx(0.0) await assert_reading( - sim_sensor, + mock_sensor, { - "sim_sensor-value": { + "mock_sensor-value": { "value": 0.0, "alarm_severity": 0, "timestamp": ANY, @@ -130,11 +140,11 @@ async def test_sensor_reading_shows_value(sim_sensor: demo.Sensor): }, ) # Check different value - set_sim_value(sim_sensor.value, 5.0) + set_mock_value(mock_sensor.value, 5.0) await assert_reading( - sim_sensor, + mock_sensor, { - "sim_sensor-value": { + "mock_sensor-value": { "value": 5.0, "timestamp": ANY, "alarm_severity": 0, @@ -143,39 +153,32 @@ async def test_sensor_reading_shows_value(sim_sensor: demo.Sensor): ) -async def test_mover_stopped(sim_mover: demo.Mover): - callbacks = [] - set_sim_callback(sim_mover.stop_, lambda r, v: callbacks.append(v)) - - assert callbacks == [None] - await sim_mover.stop() - assert callbacks == [None, None] - - -async def test_read_mover(sim_mover: demo.Mover): - await sim_mover.stage() - assert (await sim_mover.read())["sim_mover"]["value"] == 0.0 - assert (await sim_mover.read_configuration())["sim_mover-velocity"]["value"] == 1 - assert (await sim_mover.describe_configuration())["sim_mover-units"]["shape"] == [] - set_sim_value(sim_mover.readback, 0.5) - assert (await sim_mover.read())["sim_mover"]["value"] == 0.5 - await sim_mover.unstage() +async def test_read_mover(mock_mover: demo.Mover): + await mock_mover.stage() + assert (await mock_mover.read())["mock_mover"]["value"] == 0.0 + assert (await mock_mover.read_configuration())["mock_mover-velocity"]["value"] == 1 + assert (await mock_mover.describe_configuration())["mock_mover-units"][ + "shape" + ] == [] + set_mock_value(mock_mover.readback, 0.5) + assert (await mock_mover.read())["mock_mover"]["value"] == 0.5 + await mock_mover.unstage() # Check we can still read and describe when not staged - set_sim_value(sim_mover.readback, 0.1) - assert (await sim_mover.read())["sim_mover"]["value"] == 0.1 - assert await sim_mover.describe() + set_mock_value(mock_mover.readback, 0.1) + assert (await mock_mover.read())["mock_mover"]["value"] == 0.1 + assert await mock_mover.describe() -async def test_set_velocity(sim_mover: demo.Mover) -> None: - v = sim_mover.velocity +async def test_set_velocity(mock_mover: demo.Mover) -> None: + v = mock_mover.velocity q: asyncio.Queue[Dict[str, Reading]] = asyncio.Queue() v.subscribe(q.put_nowait) - assert (await q.get())["sim_mover-velocity"]["value"] == 1.0 + assert (await q.get())["mock_mover-velocity"]["value"] == 1.0 await v.set(2.0) - assert (await q.get())["sim_mover-velocity"]["value"] == 2.0 + assert (await q.get())["mock_mover-velocity"]["value"] == 2.0 v.clear_sub(q.put_nowait) await v.set(3.0) - assert (await v.read())["sim_mover-velocity"]["value"] == 3.0 + assert (await v.read())["mock_mover-velocity"]["value"] == 3.0 assert q.empty() @@ -200,24 +203,24 @@ async def test_sensor_disconnected(caplog): assert s.name == "sensor" -async def test_read_sensor(sim_sensor: demo.Sensor): - sim_sensor.stage() - assert (await sim_sensor.read())["sim_sensor-value"]["value"] == 0 - assert (await sim_sensor.read_configuration())["sim_sensor-mode"][ +async def test_read_sensor(mock_sensor: demo.Sensor): + mock_sensor.stage() + assert (await mock_sensor.read())["mock_sensor-value"]["value"] == 0 + assert (await mock_sensor.read_configuration())["mock_sensor-mode"][ "value" ] == demo.EnergyMode.low - desc = (await sim_sensor.describe_configuration())["sim_sensor-mode"] + desc = (await mock_sensor.describe_configuration())["mock_sensor-mode"] assert desc["dtype"] == "string" assert desc["choices"] == ["Low Energy", "High Energy"] # type: ignore - set_sim_value(sim_sensor.mode, demo.EnergyMode.high) - assert (await sim_sensor.read_configuration())["sim_sensor-mode"][ + set_mock_value(mock_sensor.mode, demo.EnergyMode.high) + assert (await mock_sensor.read_configuration())["mock_sensor-mode"][ "value" ] == demo.EnergyMode.high - await sim_sensor.unstage() + await mock_sensor.unstage() -async def test_sensor_in_plan(RE: RunEngine, sim_sensor: demo.Sensor): - """Tests sim sensor behavior within a RunEngine plan. +async def test_sensor_in_plan(RE: RunEngine, mock_sensor: demo.Sensor): + """Tests mock sensor behavior within a RunEngine plan. This test verifies that the sensor emits the expected documents when used in plan(count). @@ -227,13 +230,13 @@ async def test_sensor_in_plan(RE: RunEngine, sim_sensor: demo.Sensor): def capture_emitted(name, doc): docs[name].append(doc) - RE(bp.count([sim_sensor], num=2), capture_emitted) + RE(bp.count([mock_sensor], num=2), capture_emitted) assert_emitted(docs, start=1, descriptor=1, event=2, stop=1) async def test_assembly_renaming() -> None: thing = demo.SampleStage("PRE") - await thing.connect(sim=True) + await thing.connect(mock=True) assert thing.x.name == "" assert thing.x.velocity.name == "" assert thing.x.stop_.name == "" @@ -245,11 +248,11 @@ async def test_assembly_renaming() -> None: assert thing.x.stop_.name == "foo-x-stop" -def test_mover_in_re(sim_mover: demo.Mover, RE) -> None: - sim_mover.move(0) +def test_mover_in_re(mock_mover: demo.Mover, RE) -> None: + mock_mover.move(0) def my_plan(): - sim_mover.move(0) + mock_mover.move(0) return yield @@ -260,36 +263,36 @@ def my_plan(): async def test_dynamic_sensor_group_disconnected(): with pytest.raises(NotConnected): async with DeviceCollector(timeout=0.1): - sim_sensor_group_dynamic = demo.SensorGroup("SIM:SENSOR:") + mock_sensor_group_dynamic = demo.SensorGroup("MOCK:SENSOR:") - assert sim_sensor_group_dynamic.name == "sim_sensor_group_dynamic" + assert mock_sensor_group_dynamic.name == "mock_sensor_group_dynamic" async def test_dynamic_sensor_group_read_and_describe( - sim_sensor_group: demo.SensorGroup, + mock_sensor_group: demo.SensorGroup, ): - set_sim_value(sim_sensor_group.sensors[1].value, 0.0) - set_sim_value(sim_sensor_group.sensors[2].value, 0.5) - set_sim_value(sim_sensor_group.sensors[3].value, 1.0) + set_mock_value(mock_sensor_group.sensors[1].value, 0.0) + set_mock_value(mock_sensor_group.sensors[2].value, 0.5) + set_mock_value(mock_sensor_group.sensors[3].value, 1.0) - await sim_sensor_group.stage() - description = await sim_sensor_group.describe() + await mock_sensor_group.stage() + description = await mock_sensor_group.describe() - await sim_sensor_group.unstage() + await mock_sensor_group.unstage() await assert_reading( - sim_sensor_group, + mock_sensor_group, { - "sim_sensor_group-sensors-1-value": { + "mock_sensor_group-sensors-1-value": { "value": 0.0, "timestamp": ANY, "alarm_severity": 0, }, - "sim_sensor_group-sensors-2-value": { + "mock_sensor_group-sensors-2-value": { "value": 0.5, "timestamp": ANY, "alarm_severity": 0, }, - "sim_sensor_group-sensors-3-value": { + "mock_sensor_group-sensors-3-value": { "value": 1.0, "timestamp": ANY, "alarm_severity": 0, @@ -297,20 +300,20 @@ async def test_dynamic_sensor_group_read_and_describe( }, ) assert description == { - "sim_sensor_group-sensors-1-value": { + "mock_sensor_group-sensors-1-value": { "dtype": "number", "shape": [], - "source": "soft://sim_sensor_group-sensors-1-value", + "source": "mock+ca://MOCK:SENSOR:1:Value", }, - "sim_sensor_group-sensors-2-value": { + "mock_sensor_group-sensors-2-value": { "dtype": "number", "shape": [], - "source": "soft://sim_sensor_group-sensors-2-value", + "source": "mock+ca://MOCK:SENSOR:2:Value", }, - "sim_sensor_group-sensors-3-value": { + "mock_sensor_group-sensors-3-value": { "dtype": "number", "shape": [], - "source": "soft://sim_sensor_group-sensors-3-value", + "source": "mock+ca://MOCK:SENSOR:3:Value", }, } diff --git a/tests/epics/demo/test_demo_ad_sim_detector.py b/tests/epics/demo/test_demo_ad_sim_detector.py index 32e0908de0..ac143fd024 100644 --- a/tests/epics/demo/test_demo_ad_sim_detector.py +++ b/tests/epics/demo/test_demo_ad_sim_detector.py @@ -14,8 +14,8 @@ DeviceCollector, StandardDetector, StaticDirectoryProvider, - set_sim_callback, - set_sim_value, + callback_on_mock_put, + set_mock_value, ) from ophyd_async.epics.areadetector.controllers import ADSimController from ophyd_async.epics.areadetector.drivers import ADBase @@ -27,17 +27,17 @@ async def make_detector(prefix: str, name: str, tmp_path: Path): dp = StaticDirectoryProvider(tmp_path, f"test-{new_uid()}") - async with DeviceCollector(sim=True): - drv = ADBase(f"{prefix}DRV:") + async with DeviceCollector(mock=True): + drv = ADBase(f"{prefix}DRV:", name="drv") hdf = NDFileHDF(f"{prefix}HDF:") det = DemoADSimDetector( drv, hdf, dp, config_sigs=[drv.acquire_time, drv.acquire], name=name ) - def _set_full_file_name(_, val): - set_sim_value(hdf.full_file_name, str(tmp_path / val)) + def _set_full_file_name(val, *args, **kwargs): + set_mock_value(hdf.full_file_name, str(tmp_path / val)) - set_sim_callback(hdf.file_name, _set_full_file_name) + callback_on_mock_put(hdf.file_name, _set_full_file_name) return det @@ -60,7 +60,7 @@ def count_sim(dets: List[StandardDetector], times: int = 1): yield from bps.sleep(0.001) [ - set_sim_value( + set_mock_value( cast(HDFWriter, det.writer).hdf.num_captured, read_values[det] + 1 ) for det in dets @@ -82,8 +82,8 @@ def count_sim(dets: List[StandardDetector], times: int = 1): async def single_detector(RE: RunEngine, tmp_path: Path) -> StandardDetector: detector = await make_detector(prefix="TEST:", name="test", tmp_path=tmp_path) - set_sim_value(detector._controller.driver.array_size_x, 10) - set_sim_value(detector._controller.driver.array_size_y, 20) + set_mock_value(detector._controller.driver.array_size_x, 10) + set_mock_value(detector._controller.driver.array_size_y, 20) return detector @@ -98,12 +98,12 @@ async def two_detectors(tmp_path: Path): controller = det._controller writer = det._writer - set_sim_value(controller.driver.acquire_time, 0.8 + i) - set_sim_value(controller.driver.image_mode, ImageMode.continuous) - set_sim_value(writer.hdf.num_capture, 1000) - set_sim_value(writer.hdf.num_captured, 0) - set_sim_value(controller.driver.array_size_x, 1024 + i) - set_sim_value(controller.driver.array_size_y, 768 + i) + set_mock_value(controller.driver.acquire_time, 0.8 + i) + set_mock_value(controller.driver.image_mode, ImageMode.continuous) + set_mock_value(writer.hdf.num_capture, 1000) + set_mock_value(writer.hdf.num_captured, 0) + set_mock_value(controller.driver.array_size_x, 1024 + i) + set_mock_value(controller.driver.array_size_y, 768 + i) yield deta, detb @@ -116,7 +116,7 @@ async def test_two_detectors_step( RE.subscribe(lambda name, _: names.append(name)) RE.subscribe(lambda _, doc: docs.append(doc)) [ - set_sim_value(cast(HDFWriter, det._writer).hdf.file_path_exists, True) + set_mock_value(cast(HDFWriter, det._writer).hdf.file_path_exists, True) for det in two_detectors ] @@ -184,7 +184,7 @@ async def test_detector_writes_to_file( docs = [] RE.subscribe(lambda name, _: names.append(name)) RE.subscribe(lambda _, doc: docs.append(doc)) - set_sim_value(cast(HDFWriter, single_detector._writer).hdf.file_path_exists, True) + set_mock_value(cast(HDFWriter, single_detector._writer).hdf.file_path_exists, True) RE(count_sim([single_detector], times=3)) @@ -214,12 +214,12 @@ async def test_read_and_describe_detector(single_detector: StandardDetector): read = await single_detector.read_configuration() assert describe == { "test-drv-acquire_time": { - "source": "soft://test-drv-acquire_time", + "source": "mock+ca://TEST:DRV:AcquireTime_RBV", "dtype": "number", "shape": [], }, "test-drv-acquire": { - "source": "soft://test-drv-acquire", + "source": "mock+ca://TEST:DRV:Acquire_RBV", "dtype": "boolean", "shape": [], }, @@ -252,7 +252,7 @@ async def test_trigger_logic(): detector.writer.hdf. Then, mock out set_and_wait_for_value in ophyd_async.epics.DemoADSimDetector.controllers.standard_controller.ADSimController so that, as well as setting detector.controller.driver.acquire to True, it sets - detector.writer.hdf.num_captured to 1, using set_sim_value + detector.writer.hdf.num_captured to 1, using set_mock_value """ ... @@ -263,7 +263,7 @@ async def test_detector_with_unnamed_or_disconnected_config_sigs(RE, tmp_path: P some_other_driver = ADBase("TEST") - async with DeviceCollector(sim=True): + async with DeviceCollector(mock=True): hdf = NDFileHDF("FOO:HDF:") det = DemoADSimDetector( drv, diff --git a/tests/epics/motion/test_motor.py b/tests/epics/motion/test_motor.py index c91b11aa0c..0654661bb9 100644 --- a/tests/epics/motion/test_motor.py +++ b/tests/epics/motion/test_motor.py @@ -5,29 +5,31 @@ import pytest from bluesky.protocols import Reading -from ophyd_async.core import DeviceCollector, set_sim_put_proceeds, set_sim_value +from ophyd_async.core import ( + DeviceCollector, + set_mock_put_proceeds, + set_mock_value, +) from ophyd_async.epics.motion import motor # Long enough for multiple asyncio event loop cycles to run so # all the tasks have a chance to run -A_BIT = 0.001 +A_BIT = 0.01 @pytest.fixture async def sim_motor(): - async with DeviceCollector(sim=True): - sim_motor = motor.Motor("BLxxI-MO-TABLE-01:X") - # Signals connected here - - assert sim_motor.name == "sim_motor" - set_sim_value(sim_motor.motor_egu, "mm") - set_sim_value(sim_motor.precision, 3) - set_sim_value(sim_motor.velocity, 1) + async with DeviceCollector(mock=True): + sim_motor = motor.Motor("BLxxI-MO-TABLE-01:X", name="sim_motor") + + set_mock_value(sim_motor.motor_egu, "mm") + set_mock_value(sim_motor.precision, 3) + set_mock_value(sim_motor.velocity, 1) yield sim_motor async def test_motor_moving_well(sim_motor: motor.Motor) -> None: - set_sim_put_proceeds(sim_motor.user_setpoint, False) + set_mock_put_proceeds(sim_motor.user_setpoint, False) s = sim_motor.set(0.55) watcher = Mock() s.watch(watcher) @@ -48,7 +50,7 @@ async def test_motor_moving_well(sim_motor: motor.Motor) -> None: assert 0.55 == await sim_motor.user_setpoint.get_value() assert not s.done await asyncio.sleep(0.1) - set_sim_value(sim_motor.user_readback, 0.1) + set_mock_value(sim_motor.user_readback, 0.1) assert watcher.call_count == 1 assert watcher.call_args == call( name="sim_motor", @@ -59,37 +61,37 @@ async def test_motor_moving_well(sim_motor: motor.Motor) -> None: precision=3, time_elapsed=pytest.approx(0.1, abs=0.05), ) - set_sim_put_proceeds(sim_motor.user_setpoint, True) + set_mock_put_proceeds(sim_motor.user_setpoint, True) await asyncio.sleep(A_BIT) assert s.done done.assert_called_once_with(s) async def test_motor_moving_stopped(sim_motor: motor.Motor): - set_sim_put_proceeds(sim_motor.user_setpoint, False) + set_mock_put_proceeds(sim_motor.user_setpoint, False) s = sim_motor.set(1.5) s.add_callback(Mock()) await asyncio.sleep(0.2) assert not s.done await sim_motor.stop() - set_sim_put_proceeds(sim_motor.user_setpoint, True) + set_mock_put_proceeds(sim_motor.user_setpoint, True) await asyncio.sleep(A_BIT) assert s.done assert s.success is False async def test_read_motor(sim_motor: motor.Motor): - sim_motor.stage() + await sim_motor.stage() assert (await sim_motor.read())["sim_motor"]["value"] == 0.0 assert (await sim_motor.read_configuration())["sim_motor-velocity"]["value"] == 1 assert (await sim_motor.describe_configuration())["sim_motor-motor_egu"][ "shape" ] == [] - set_sim_value(sim_motor.user_readback, 0.5) + set_mock_value(sim_motor.user_readback, 0.5) assert (await sim_motor.read())["sim_motor"]["value"] == 0.5 - sim_motor.unstage() + await sim_motor.unstage() # Check we can still read and describe when not staged - set_sim_value(sim_motor.user_readback, 0.1) + set_mock_value(sim_motor.user_readback, 0.1) assert (await sim_motor.read())["sim_motor"]["value"] == 0.1 assert await sim_motor.describe() diff --git a/tests/epics/test_pvi.py b/tests/epics/test_pvi.py index 65574f14dc..63a2f5ee77 100644 --- a/tests/epics/test_pvi.py +++ b/tests/epics/test_pvi.py @@ -45,17 +45,19 @@ def __init__(self, prefix: str, name: str = ""): super().__init__(name) async def connect( - self, sim: bool = False, timeout: float = DEFAULT_TIMEOUT + self, mock: bool = False, timeout: float = DEFAULT_TIMEOUT ) -> None: - await fill_pvi_entries(self, self._prefix + "PVI", timeout=timeout, sim=sim) + await fill_pvi_entries( + self, self._prefix + "PVI", timeout=timeout, mock=mock + ) - await super().connect(sim) + await super().connect(mock=mock) yield TestDevice -async def test_fill_pvi_entries_sim_mode(pvi_test_device_t): - async with DeviceCollector(sim=True): +async def test_fill_pvi_entries_mock_mode(pvi_test_device_t): + async with DeviceCollector(mock=True): test_device = pvi_test_device_t("PREFIX:") # device vectors are typed @@ -107,11 +109,13 @@ def __init__(self, prefix: str, name: str = ""): create_children_from_annotations(self) async def connect( - self, sim: bool = False, timeout: float = DEFAULT_TIMEOUT + self, mock: bool = False, timeout: float = DEFAULT_TIMEOUT ) -> None: - await fill_pvi_entries(self, self._prefix + "PVI", timeout=timeout, sim=sim) + await fill_pvi_entries( + self, self._prefix + "PVI", timeout=timeout, mock=mock + ) - await super().connect(sim) + await super().connect(mock=mock) yield TestDevice @@ -134,7 +138,7 @@ async def test_device_create_children_from_annotations( assert not hasattr(device, "signal_rw") assert not hasattr(top_block_1_device, "signal_rw") - await device.connect(sim=True) + await device.connect(mock=True) # The memory addresses have not changed assert device.device is block_2_device diff --git a/tests/epics/test_signals.py b/tests/epics/test_signals.py index ef1afa481e..ff6fce24f6 100644 --- a/tests/epics/test_signals.py +++ b/tests/epics/test_signals.py @@ -83,7 +83,16 @@ def ioc(request): stderr=subprocess.STDOUT, universal_newlines=True, ) + + start_time = time.monotonic() + while "iocRun: All initialization complete" not in ( + process.stdout.readline().strip() + ): + if time.monotonic() - start_time > 10: + raise TimeoutError("IOC did not start in time") + yield IOC(process, protocol) + # close backend caches before the event loop purge_channel_caches() try: diff --git a/tests/panda/test_hdf_panda.py b/tests/panda/test_hdf_panda.py index 092340594b..626e272bd3 100644 --- a/tests/panda/test_hdf_panda.py +++ b/tests/panda/test_hdf_panda.py @@ -1,19 +1,16 @@ -import asyncio -from typing import Dict, Optional +from typing import Dict import pytest from bluesky import plan_stubs as bps from bluesky.run_engine import RunEngine -from ophyd_async.core import StaticDirectoryProvider, set_sim_value -from ophyd_async.core.async_status import AsyncStatus -from ophyd_async.core.detector import DetectorControl, DetectorTrigger +from ophyd_async.core import StaticDirectoryProvider, set_mock_value from ophyd_async.core.device import Device from ophyd_async.core.flyer import HardwareTriggeredFlyable -from ophyd_async.core.signal import SignalR, assert_emitted, wait_for_value -from ophyd_async.core.sim_signal_backend import SimSignalBackend -from ophyd_async.core.utils import DEFAULT_TIMEOUT -from ophyd_async.panda import HDFPanda, PcapBlock +from ophyd_async.core.mock_signal_utils import callback_on_mock_put +from ophyd_async.core.signal import SignalR, assert_emitted +from ophyd_async.epics.signal.signal import epics_signal_r +from ophyd_async.panda import HDFPanda from ophyd_async.panda._trigger import StaticSeqTableTriggerLogic from ophyd_async.panda.writers._hdf_writer import Capture from ophyd_async.planstubs.prepare_trigger_and_dets import ( @@ -21,73 +18,46 @@ ) -class MockPandaPcapController(DetectorControl): - def __init__(self, pcap: PcapBlock) -> None: - self.pcap = pcap - - def get_deadtime(self, exposure: float) -> float: - return 0.000000008 - - async def arm( - self, - num: int, - trigger: DetectorTrigger = DetectorTrigger.constant_gate, - exposure: Optional[float] = None, - timeout=DEFAULT_TIMEOUT, - ) -> AsyncStatus: - assert trigger in ( - DetectorTrigger.constant_gate, - trigger == DetectorTrigger.variable_gate, - ), ( - f"Receieved trigger {trigger}. Only constant_gate and " - "variable_gate triggering is supported on the PandA" - ) - await self.pcap.arm.set(True, wait=True, timeout=timeout) - await wait_for_value(self.pcap.active, True, timeout=timeout) - await asyncio.sleep(0.2) - await self.pcap.arm.set(False, wait=False, timeout=timeout) - return AsyncStatus(wait_for_value(self.pcap.active, False, timeout=None)) - - async def disarm(self, timeout=DEFAULT_TIMEOUT) -> AsyncStatus: - await self.pcap.arm.set(False, wait=True, timeout=timeout) - await wait_for_value(self.pcap.active, False, timeout=timeout) - await asyncio.sleep(0.2) - set_sim_value(self.pcap.active, True) - return AsyncStatus(wait_for_value(self.pcap.active, False, timeout=None)) - - @pytest.fixture -async def sim_hdf_panda(tmp_path): +async def mock_hdf_panda(tmp_path): class CaptureBlock(Device): test_capture: SignalR directory_provider = StaticDirectoryProvider(str(tmp_path), filename_prefix="test") - sim_hdf_panda = HDFPanda( + mock_hdf_panda = HDFPanda( "HDFPANDA:", directory_provider=directory_provider, name="panda" ) - sim_hdf_panda._controller = MockPandaPcapController(sim_hdf_panda.pcap) block_a = CaptureBlock(name="block_a") block_b = CaptureBlock(name="block_b") - block_a.test_capture = SignalR(backend=SimSignalBackend(Capture)) - block_b.test_capture = SignalR(backend=SimSignalBackend(Capture)) + block_a.test_capture = epics_signal_r( + Capture, "pva://test_capture_a", name="test_capture_a" + ) + block_b.test_capture = epics_signal_r( + Capture, "pva://test_capture_b", name="test_capture_b" + ) + + setattr(mock_hdf_panda, "block_a", block_a) + setattr(mock_hdf_panda, "block_b", block_b) + await mock_hdf_panda.connect(mock=True) + + def link_function(value, **kwargs): + set_mock_value(mock_hdf_panda.pcap.active, value) - setattr(sim_hdf_panda, "block_a", block_a) - setattr(sim_hdf_panda, "block_b", block_b) - await sim_hdf_panda.connect(sim=True) - set_sim_value(block_a.test_capture, Capture.Min) - set_sim_value(block_b.test_capture, Capture.Diff) + callback_on_mock_put(mock_hdf_panda.pcap.arm, link_function) + set_mock_value(block_a.test_capture, Capture.Min) + set_mock_value(block_b.test_capture, Capture.Diff) - yield sim_hdf_panda + yield mock_hdf_panda -async def test_hdf_panda_passes_blocks_to_controller(sim_hdf_panda: HDFPanda): - assert hasattr(sim_hdf_panda.controller, "pcap") - assert sim_hdf_panda.controller.pcap is sim_hdf_panda.pcap +async def test_hdf_panda_passes_blocks_to_controller(mock_hdf_panda: HDFPanda): + assert hasattr(mock_hdf_panda.controller, "pcap") + assert mock_hdf_panda.controller.pcap is mock_hdf_panda.pcap async def test_hdf_panda_hardware_triggered_flyable( RE: RunEngine, - sim_hdf_panda, + mock_hdf_panda, ): docs = {} @@ -101,40 +71,36 @@ def append_and_print(name, doc): shutter_time = 0.004 exposure = 1 - trigger_logic = StaticSeqTableTriggerLogic(sim_hdf_panda.seq[1]) + trigger_logic = StaticSeqTableTriggerLogic(mock_hdf_panda.seq[1]) flyer = HardwareTriggeredFlyable(trigger_logic, [], name="flyer") def flying_plan(): - yield from bps.stage_all(sim_hdf_panda, flyer) + yield from bps.stage_all(mock_hdf_panda, flyer) yield from prepare_static_seq_table_flyer_and_detectors_with_same_trigger( flyer, - [sim_hdf_panda], + [mock_hdf_panda], num=1, width=exposure, - deadtime=sim_hdf_panda.controller.get_deadtime(1), + deadtime=mock_hdf_panda.controller.get_deadtime(1), shutter_time=shutter_time, ) - # sim_hdf_panda.controller.disarm.assert_called_once # type: ignore + # mock_hdf_panda.controller.disarm.assert_called_once # type: ignore yield from bps.open_run() - yield from bps.declare_stream(sim_hdf_panda, name="main_stream", collect=True) + yield from bps.declare_stream(mock_hdf_panda, name="main_stream", collect=True) - set_sim_value(flyer.trigger_logic.seq.active, 1) + set_mock_value(flyer.trigger_logic.seq.active, 1) yield from bps.kickoff(flyer, wait=True) - yield from bps.kickoff(sim_hdf_panda) + yield from bps.kickoff(mock_hdf_panda) yield from bps.complete(flyer, wait=False, group="complete") - yield from bps.complete(sim_hdf_panda, wait=False, group="complete") + yield from bps.complete(mock_hdf_panda, wait=False, group="complete") # Manually incremenet the index as if a frame was taken - set_sim_value( - sim_hdf_panda.data.num_captured, - sim_hdf_panda.data.num_captured._backend._value + 1, - ) - - set_sim_value(flyer.trigger_logic.seq.active, 0) + set_mock_value(mock_hdf_panda.data.num_captured, 1) + set_mock_value(flyer.trigger_logic.seq.active, 0) done = False while not done: @@ -145,15 +111,16 @@ def flying_plan(): else: done = True yield from bps.collect( - sim_hdf_panda, + mock_hdf_panda, return_payload=False, name="main_stream", ) yield from bps.wait(group="complete") yield from bps.close_run() - yield from bps.unstage_all(flyer, sim_hdf_panda) - # assert sim_hdf_panda.controller.disarm.called # type: ignore + yield from bps.unstage_all(flyer, mock_hdf_panda) + yield from bps.wait_for([lambda: mock_hdf_panda.controller.disarm()]) + # assert mock_hdf_panda.controller.disarm.called # type: ignore # fly scan RE(flying_plan()) @@ -171,7 +138,7 @@ def flying_plan(): for data_key_name in data_key_names: assert ( docs["descriptor"][0]["data_keys"][data_key_name]["source"] - == "soft://panda-data-hdf_directory" + == "mock+soft://panda-data-hdf_directory" ) # test stream resources diff --git a/tests/panda/test_panda_connect.py b/tests/panda/test_panda_connect.py index 7dcd9c5b51..60ac635e93 100644 --- a/tests/panda/test_panda_connect.py +++ b/tests/panda/test_panda_connect.py @@ -50,25 +50,27 @@ def __init__(self, prefix: str, name: str = ""): create_children_from_annotations(self) super().__init__(name) - async def connect(self, sim: bool = False, timeout: float = DEFAULT_TIMEOUT): - await fill_pvi_entries(self, self._prefix + "PVI", timeout=timeout, sim=sim) - await super().connect(sim, timeout) + async def connect(self, mock: bool = False, timeout: float = DEFAULT_TIMEOUT): + await fill_pvi_entries( + self, self._prefix + "PVI", timeout=timeout, mock=mock + ) + await super().connect(mock=mock, timeout=timeout) yield Panda @pytest.fixture -async def sim_panda(panda_t): - async with DeviceCollector(sim=True): - sim_panda = panda_t("PANDAQSRV:", "sim_panda") +async def mock_panda(panda_t): + async with DeviceCollector(mock=True): + mock_panda = panda_t("PANDAQSRV:", "mock_panda") - assert sim_panda.name == "sim_panda" - yield sim_panda + assert mock_panda.name == "mock_panda" + yield mock_panda -def test_panda_names_correct(sim_panda): - assert sim_panda.seq[1].name == "sim_panda-seq-1" - assert sim_panda.pulse[1].name == "sim_panda-pulse-1" +def test_panda_names_correct(mock_panda): + assert mock_panda.seq[1].name == "mock_panda-seq-1" + assert mock_panda.pulse[1].name == "mock_panda-pulse-1" def test_panda_name_set(panda_t): @@ -76,7 +78,7 @@ def test_panda_name_set(panda_t): assert panda.name == "panda" -async def test_panda_children_connected(sim_panda): +async def test_panda_children_connected(mock_panda): # try to set and retrieve from simulated values... table = SeqTable( repeats=np.array([1, 1, 1, 32]).astype(np.uint16), @@ -102,11 +104,11 @@ async def test_panda_children_connected(sim_panda): oute2=np.array([1, 0, 1, 0]).astype(np.bool_), outf2=np.array([1, 0, 0, 0]).astype(np.bool_), ) - await sim_panda.pulse[1].delay.set(20.0) - await sim_panda.seq[1].table.set(table) + await mock_panda.pulse[1].delay.set(20.0) + await mock_panda.seq[1].table.set(table) - readback_pulse = await sim_panda.pulse[1].delay.get_value() - readback_seq = await sim_panda.seq[1].table.get_value() + readback_pulse = await mock_panda.pulse[1].delay.get_value() + readback_seq = await mock_panda.seq[1].table.get_value() assert readback_pulse == 20.0 assert readback_seq == table diff --git a/tests/panda/test_panda_controller.py b/tests/panda/test_panda_controller.py index 875a4158b6..7cb64cdf8c 100644 --- a/tests/panda/test_panda_controller.py +++ b/tests/panda/test_panda_controller.py @@ -11,21 +11,22 @@ @pytest.fixture -async def sim_panda(): +async def mock_panda(): class Panda(CommonPandaBlocks): def __init__(self, prefix: str, name: str = ""): self._prefix = prefix super().__init__(name) - async def connect(self, sim: bool = False, timeout: float = DEFAULT_TIMEOUT): - await fill_pvi_entries(self, self._prefix + "PVI", timeout=timeout, sim=sim) - await super().connect(sim, timeout) + async def connect(self, mock: bool = False, timeout: float = DEFAULT_TIMEOUT): + await fill_pvi_entries( + self, self._prefix + "PVI", timeout=timeout, mock=mock + ) + await super().connect(mock=mock, timeout=timeout) - async with DeviceCollector(sim=True): - sim_panda = Panda("PANDACONTROLLER:", "sim_panda") - sim_panda.phase_1_signal_units = epics_signal_rw(int, "") - assert sim_panda.name == "sim_panda" - yield sim_panda + async with DeviceCollector(mock=True): + mock_panda = Panda("PANDACONTROLLER:", name="mock_panda") + mock_panda.phase_1_signal_units = epics_signal_rw(int, "") + yield mock_panda async def test_panda_controller_not_filled_blocks(): @@ -39,8 +40,8 @@ class PcapBlock(Device): assert ("'PcapBlock' object has no attribute 'arm'") in str(exc.value) -async def test_panda_controller_arm_disarm(sim_panda): - pandaController = PandaPcapController(sim_panda.pcap) +async def test_panda_controller_arm_disarm(mock_panda): + pandaController = PandaPcapController(mock_panda.pcap) with patch("ophyd_async.panda._panda_controller.wait_for_value", return_value=None): await pandaController.arm(num=1, trigger=DetectorTrigger.constant_gate) await pandaController.disarm() diff --git a/tests/panda/test_panda_utils.py b/tests/panda/test_panda_utils.py index 35e53a07b3..4bae8a0a1c 100644 --- a/tests/panda/test_panda_utils.py +++ b/tests/panda/test_panda_utils.py @@ -14,7 +14,7 @@ @pytest.fixture -async def sim_panda(): +async def mock_panda(): class Panda(CommonPandaBlocks): data: DataBlock @@ -22,20 +22,22 @@ def __init__(self, prefix: str, name: str = ""): self._prefix = prefix super().__init__(name) - async def connect(self, sim: bool = False, timeout: float = DEFAULT_TIMEOUT): - await fill_pvi_entries(self, self._prefix + "PVI", timeout=timeout, sim=sim) - await super().connect(sim, timeout) + async def connect(self, mock: bool = False, timeout: float = DEFAULT_TIMEOUT): + await fill_pvi_entries( + self, self._prefix + "PVI", timeout=timeout, mock=mock + ) + await super().connect(mock=mock, timeout=timeout) - async with DeviceCollector(sim=True): - sim_panda = Panda("PANDA") - sim_panda.phase_1_signal_units = epics_signal_rw(int, "") - assert sim_panda.name == "sim_panda" - yield sim_panda + async with DeviceCollector(mock=True): + mock_panda = Panda("PANDA") + mock_panda.phase_1_signal_units = epics_signal_rw(int, "") + assert mock_panda.name == "mock_panda" + yield mock_panda @patch("ophyd_async.core.device_save_loader.save_to_yaml") -async def test_save_panda(mock_save_to_yaml, sim_panda, RE: RunEngine): - RE(save_device(sim_panda, "path", sorter=phase_sorter)) +async def test_save_panda(mock_save_to_yaml, mock_panda, RE: RunEngine): + RE(save_device(mock_panda, "path", sorter=phase_sorter)) mock_save_to_yaml.assert_called_once() assert mock_save_to_yaml.call_args[0] == ( [ diff --git a/tests/panda/test_trigger.py b/tests/panda/test_trigger.py index ac23aeba7c..cd6317ab7b 100644 --- a/tests/panda/test_trigger.py +++ b/tests/panda/test_trigger.py @@ -13,18 +13,20 @@ def __init__(self, prefix: str, name: str = ""): self._prefix = prefix super().__init__(name) - async def connect(self, sim: bool = False, timeout: float = DEFAULT_TIMEOUT): - await fill_pvi_entries(self, self._prefix + "PVI", timeout=timeout, sim=sim) - await super().connect(sim, timeout) + async def connect(self, mock: bool = False, timeout: float = DEFAULT_TIMEOUT): + await fill_pvi_entries( + self, self._prefix + "PVI", timeout=timeout, mock=mock + ) + await super().connect(mock=mock, timeout=timeout) - async with DeviceCollector(sim=True): - sim_panda = Panda("PANDAQSRV:", "sim_panda") + async with DeviceCollector(mock=True): + mock_panda = Panda("PANDAQSRV:", "mock_panda") - assert sim_panda.name == "sim_panda" - yield sim_panda + assert mock_panda.name == "mock_panda" + yield mock_panda -def test_trigger_logic_has_given_methods(panda): +async def test_trigger_logic_has_given_methods(panda): trigger_logic = StaticSeqTableTriggerLogic(panda.seq[1]) assert hasattr(trigger_logic, "prepare") assert hasattr(trigger_logic, "kickoff") diff --git a/tests/panda/test_writer.py b/tests/panda/test_writer.py index 29687a3b74..0492cc6f0e 100644 --- a/tests/panda/test_writer.py +++ b/tests/panda/test_writer.py @@ -8,11 +8,11 @@ Device, DeviceCollector, SignalR, - SimSignalBackend, StaticDirectoryProvider, - set_sim_value, + set_mock_value, ) from ophyd_async.epics.pvi import create_children_from_annotations, fill_pvi_entries +from ophyd_async.epics.signal.signal import epics_signal_r from ophyd_async.panda import CommonPandaBlocks from ophyd_async.panda.writers._hdf_writer import ( Capture, @@ -38,58 +38,64 @@ def __init__(self, prefix: str, name: str = ""): create_children_from_annotations(self) super().__init__(name) - async def connect(self, sim: bool = False, timeout: float = DEFAULT_TIMEOUT): - await fill_pvi_entries(self, self._prefix + "PVI", timeout=timeout, sim=sim) - await super().connect(sim, timeout) + async def connect(self, mock: bool = False, timeout: float = DEFAULT_TIMEOUT): + await fill_pvi_entries( + self, self._prefix + "PVI", timeout=timeout, mock=mock + ) + await super().connect(mock=mock, timeout=timeout) yield Panda @pytest.fixture -async def sim_panda(panda_t): - async with DeviceCollector(sim=True): - sim_panda = panda_t("SIM_PANDA", name="sim_panda") +async def mock_panda(panda_t): + async with DeviceCollector(mock=True): + mock_panda = panda_t("mock_PANDA", name="mock_panda") - set_sim_value( - sim_panda.block_a.test_capture, + set_mock_value( + mock_panda.block_a.test_capture, Capture.MinMaxMean, # type: ignore[attr-defined] ) - set_sim_value( - sim_panda.block_b.test_capture, + set_mock_value( + mock_panda.block_b.test_capture, Capture.No, # type: ignore[attr-defined] ) - return sim_panda + return mock_panda @pytest.fixture -async def sim_writer(tmp_path, sim_panda) -> PandaHDFWriter: +async def mock_writer(tmp_path, mock_panda) -> PandaHDFWriter: dir_prov = StaticDirectoryProvider( directory_path=str(tmp_path), filename_prefix="", filename_suffix="/data.h5" ) - async with DeviceCollector(sim=True): + async with DeviceCollector(mock=True): writer = PandaHDFWriter( prefix="TEST-PANDA", directory_provider=dir_prov, name_provider=lambda: "test-panda", - panda_device=sim_panda, + panda_device=mock_panda, ) return writer -async def test_get_capture_signals_gets_all_signals(sim_panda): - async with DeviceCollector(sim=True): - sim_panda.test_seq = Device("seq") - sim_panda.test_seq.seq1_capture = SignalR(backend=SimSignalBackend(str)) - sim_panda.test_seq.seq2_capture = SignalR(backend=SimSignalBackend(str)) +async def test_get_capture_signals_gets_all_signals(mock_panda): + async with DeviceCollector(mock=True): + mock_panda.test_seq = Device("seq") + mock_panda.test_seq.seq1_capture = epics_signal_r( + str, "pva://read_pv_1", name="seq1_capture" + ) + mock_panda.test_seq.seq2_capture = epics_signal_r( + str, "pva://read_pv_2", name="seq2_capture" + ) await asyncio.gather( - sim_panda.test_seq.connect(), - sim_panda.test_seq.seq1_capture.connect(), - sim_panda.test_seq.seq2_capture.connect(), + mock_panda.test_seq.connect(mock=True), + mock_panda.test_seq.seq1_capture.connect(mock=True), + mock_panda.test_seq.seq2_capture.connect(mock=True), ) - capture_signals = get_capture_signals(sim_panda) + capture_signals = get_capture_signals(mock_panda) expected_signals = [ "block_a.test_capture", "block_b.test_capture", @@ -100,23 +106,23 @@ async def test_get_capture_signals_gets_all_signals(sim_panda): assert signal in capture_signals.keys() -async def test_get_signals_marked_for_capture(sim_panda): +async def test_get_signals_marked_for_capture(mock_panda): capture_signals = { - "block_a.test_capture": sim_panda.block_a.test_capture, - "block_b.test_capture": sim_panda.block_b.test_capture, + "block_a.test_capture": mock_panda.block_a.test_capture, + "block_b.test_capture": mock_panda.block_b.test_capture, } signals_marked_for_capture = await get_signals_marked_for_capture(capture_signals) assert len(signals_marked_for_capture) == 1 assert signals_marked_for_capture["block_a.test"].capture_type == Capture.MinMaxMean -async def test_open_returns_correct_descriptors(sim_writer: PandaHDFWriter): - assert hasattr(sim_writer.panda_device, "data") - cap1 = sim_writer.panda_device.block_a.test_capture # type: ignore[attr-defined] - cap2 = sim_writer.panda_device.block_b.test_capture # type: ignore[attr-defined] - set_sim_value(cap1, Capture.MinMaxMean) - set_sim_value(cap2, Capture.Value) - description = await sim_writer.open() # to make capturing status not time out +async def test_open_returns_correct_descriptors(mock_writer: PandaHDFWriter): + assert hasattr(mock_writer.panda_device, "data") + cap1 = mock_writer.panda_device.block_a.test_capture # type: ignore[attr-defined] + cap2 = mock_writer.panda_device.block_b.test_capture # type: ignore[attr-defined] + set_mock_value(cap1, Capture.MinMaxMean) + set_mock_value(cap2, Capture.Value) + description = await mock_writer.open() # to make capturing status not time out assert len(description) == 4 for key, entry in description.items(): assert entry.get("shape") == [1] @@ -134,63 +140,63 @@ async def test_open_returns_correct_descriptors(sim_writer: PandaHDFWriter): assert key in description -async def test_open_close_sets_capture(sim_writer: PandaHDFWriter): - assert isinstance(await sim_writer.open(), dict) - assert await sim_writer.panda_device.data.capture.get_value() - await sim_writer.close() - assert not await sim_writer.panda_device.data.capture.get_value() +async def test_open_close_sets_capture(mock_writer: PandaHDFWriter): + assert isinstance(await mock_writer.open(), dict) + assert await mock_writer.panda_device.data.capture.get_value() + await mock_writer.close() + assert not await mock_writer.panda_device.data.capture.get_value() -async def test_open_sets_file_path_and_name(sim_writer: PandaHDFWriter, tmp_path): - await sim_writer.open() - path = await sim_writer.panda_device.data.hdf_directory.get_value() +async def test_open_sets_file_path_and_name(mock_writer: PandaHDFWriter, tmp_path): + await mock_writer.open() + path = await mock_writer.panda_device.data.hdf_directory.get_value() assert path == str(tmp_path) - name = await sim_writer.panda_device.data.hdf_file_name.get_value() - assert name == "sim_panda/data.h5" + name = await mock_writer.panda_device.data.hdf_file_name.get_value() + assert name == "mock_panda/data.h5" -async def test_open_errors_when_multiplier_not_one(sim_writer: PandaHDFWriter): +async def test_open_errors_when_multiplier_not_one(mock_writer: PandaHDFWriter): with pytest.raises(ValueError): - await sim_writer.open(2) + await mock_writer.open(2) -async def test_get_indices_written(sim_writer: PandaHDFWriter): - await sim_writer.open() - set_sim_value(sim_writer.panda_device.data.num_captured, 4) - written = await sim_writer.get_indices_written() +async def test_get_indices_written(mock_writer: PandaHDFWriter): + await mock_writer.open() + set_mock_value(mock_writer.panda_device.data.num_captured, 4) + written = await mock_writer.get_indices_written() assert written == 4 -async def test_wait_for_index(sim_writer: PandaHDFWriter): - await sim_writer.open() - set_sim_value(sim_writer.panda_device.data.num_captured, 3) - await sim_writer.wait_for_index(3, timeout=1) - set_sim_value(sim_writer.panda_device.data.num_captured, 2) +async def test_wait_for_index(mock_writer: PandaHDFWriter): + await mock_writer.open() + set_mock_value(mock_writer.panda_device.data.num_captured, 3) + await mock_writer.wait_for_index(3, timeout=1) + set_mock_value(mock_writer.panda_device.data.num_captured, 2) with pytest.raises(TimeoutError): - await sim_writer.wait_for_index(3, timeout=0.1) + await mock_writer.wait_for_index(3, timeout=0.1) -async def test_collect_stream_docs(sim_writer: PandaHDFWriter): - # Give the sim writer datasets - cap1 = sim_writer.panda_device.block_a.test_capture # type: ignore[attr-defined] - cap2 = sim_writer.panda_device.block_b.test_capture # type: ignore[attr-defined] - set_sim_value(cap1, Capture.MinMaxMean) - set_sim_value(cap2, Capture.Value) - await sim_writer.open() +async def test_collect_stream_docs(mock_writer: PandaHDFWriter): + # Give the mock writer datasets + cap1 = mock_writer.panda_device.block_a.test_capture # type: ignore[attr-defined] + cap2 = mock_writer.panda_device.block_b.test_capture # type: ignore[attr-defined] + set_mock_value(cap1, Capture.MinMaxMean) + set_mock_value(cap2, Capture.Value) + await mock_writer.open() - [item async for item in sim_writer.collect_stream_docs(1)] - assert type(sim_writer._file) is _HDFFile - assert sim_writer._file._last_emitted == 1 - resource_doc = sim_writer._file._bundles[0].stream_resource_doc + [item async for item in mock_writer.collect_stream_docs(1)] + assert type(mock_writer._file) is _HDFFile + assert mock_writer._file._last_emitted == 1 + resource_doc = mock_writer._file._bundles[0].stream_resource_doc assert resource_doc["data_key"] == "test-panda-block_a-test-Min" - assert "sim_panda/data.h5" in resource_doc["resource_path"] + assert "mock_panda/data.h5" in resource_doc["resource_path"] -async def test_numeric_blocks_correctly_formated(sim_writer: PandaHDFWriter): +async def test_numeric_blocks_correctly_formated(mock_writer: PandaHDFWriter): async def get_numeric_signal(_): return { "device.block.1": CaptureSignalWrapper( - SignalR(backend=SimSignalBackend(str)), + epics_signal_r(str, "pva://read_pv", name="Capture.Value"), Capture.Value, ) } @@ -199,4 +205,4 @@ async def get_numeric_signal(_): "ophyd_async.panda.writers._hdf_writer.get_signals_marked_for_capture", get_numeric_signal, ): - assert "test-panda-block-1-Capture.Value" in await sim_writer.open() + assert "test-panda-block-1-Capture.Value" in await mock_writer.open() diff --git a/tests/protocols/test_protocols.py b/tests/protocols/test_protocols.py index d75940fb55..82838a4a42 100644 --- a/tests/protocols/test_protocols.py +++ b/tests/protocols/test_protocols.py @@ -6,8 +6,6 @@ from ophyd_async.core import ( DeviceCollector, StaticDirectoryProvider, - set_sim_callback, - set_sim_value, ) from ophyd_async.core.flyer import HardwareTriggeredFlyable from ophyd_async.epics.areadetector.drivers import ADBase @@ -19,23 +17,18 @@ async def make_detector(prefix: str, name: str, tmp_path: Path): dp = StaticDirectoryProvider(tmp_path, f"test-{new_uid()}") - async with DeviceCollector(sim=True): + async with DeviceCollector(mock=True): drv = ADBase(f"{prefix}DRV:") hdf = NDFileHDF(f"{prefix}HDF:") det = DemoADSimDetector( drv, hdf, dp, config_sigs=[drv.acquire_time, drv.acquire], name=name ) - def _set_full_file_name(_, val): - set_sim_value(hdf.full_file_name, str(tmp_path / val)) - - set_sim_callback(hdf.file_name, _set_full_file_name) - return det async def test_readable(): - async with DeviceCollector(sim=True): + async with DeviceCollector(mock=True): det = await make_detector("test", "test det", Path("/tmp")) assert isinstance(SimMotor, bs_protocols.AsyncReadable) assert isinstance(det, bs_protocols.AsyncReadable) diff --git a/tests/sim/conftest.py b/tests/sim/conftest.py index b3ba9301dd..fae8a7de08 100644 --- a/tests/sim/conftest.py +++ b/tests/sim/conftest.py @@ -9,7 +9,7 @@ @pytest.fixture async def sim_pattern_detector(tmp_path_factory) -> SimPatternDetector: path: Path = tmp_path_factory.mktemp("tmp") - async with DeviceCollector(sim=True): + async with DeviceCollector(mock=True): sim_pattern_detector = SimPatternDetector(name="PATTERN1", path=path) return sim_pattern_detector diff --git a/tests/sim/test_sim_detector.py b/tests/sim/test_sim_detector.py index 338cbf342a..b6fb01266f 100644 --- a/tests/sim/test_sim_detector.py +++ b/tests/sim/test_sim_detector.py @@ -7,7 +7,7 @@ @pytest.fixture async def sim_motor(): - async with DeviceCollector(sim=True): + async with DeviceCollector(mock=True): sim_motor = motor.Motor("test") return sim_motor diff --git a/tests/sim/test_sim_writer.py b/tests/sim/test_sim_writer.py index d26fd3beaa..c6acef7ecb 100644 --- a/tests/sim/test_sim_writer.py +++ b/tests/sim/test_sim_writer.py @@ -10,7 +10,7 @@ @pytest.fixture async def writer(tmp_path) -> SimPatternDetectorWriter: - async with DeviceCollector(sim=True): + async with DeviceCollector(mock=True): driver = PatternGenerator() directory = StaticDirectoryProvider(tmp_path) diff --git a/tests/sim/test_streaming_plan.py b/tests/sim/test_streaming_plan.py index ac44051224..e5369354a7 100644 --- a/tests/sim/test_streaming_plan.py +++ b/tests/sim/test_streaming_plan.py @@ -22,7 +22,6 @@ def append_and_print(name, doc): RE(bp.count([sim_pattern_detector], num=1)) - print(names) # NOTE - double resource because double stream assert names == [ "start", diff --git a/tests/test_flyer_with_panda.py b/tests/test_flyer_with_panda.py index 0260e1e62f..ef386471c9 100644 --- a/tests/test_flyer_with_panda.py +++ b/tests/test_flyer_with_panda.py @@ -13,13 +13,13 @@ DetectorControl, DetectorWriter, HardwareTriggeredFlyable, - SignalRW, - SimSignalBackend, + observe_value, + set_mock_value, ) from ophyd_async.core.detector import StandardDetector from ophyd_async.core.device import DeviceCollector -from ophyd_async.core.signal import observe_value, set_sim_value from ophyd_async.epics.pvi.pvi import fill_pvi_entries +from ophyd_async.epics.signal.signal import epics_signal_rw from ophyd_async.panda import CommonPandaBlocks from ophyd_async.panda._trigger import StaticSeqTableTriggerLogic from ophyd_async.planstubs import ( @@ -29,7 +29,7 @@ class DummyWriter(DetectorWriter): def __init__(self, name: str, shape: Sequence[int]): - self.dummy_signal = SignalRW(backend=SimSignalBackend(int)) + self.dummy_signal = epics_signal_rw(int, "pva://read_pv") self._shape = shape self._name = name self._file: Optional[ComposeStreamResourceBundle] = None @@ -90,8 +90,8 @@ async def close(self) -> None: @pytest.fixture async def detector_list(RE: RunEngine) -> tuple[StandardDetector, StandardDetector]: writers = [DummyWriter("testa", (1, 1)), DummyWriter("testb", (1, 1))] - await writers[0].dummy_signal.connect(sim=True) - await writers[1].dummy_signal.connect(sim=True) + await writers[0].dummy_signal.connect(mock=True) + await writers[1].dummy_signal.connect(mock=True) async def dummy_arm_1(self=None, trigger=None, num=0, exposure=None): return writers[0].dummy_signal.set(1) @@ -121,15 +121,17 @@ def __init__(self, prefix: str, name: str = ""): self._prefix = prefix super().__init__(name) - async def connect(self, sim: bool = False, timeout: float = DEFAULT_TIMEOUT): - await fill_pvi_entries(self, self._prefix + "PVI", timeout=timeout, sim=sim) - await super().connect(sim, timeout) + async def connect(self, mock: bool = False, timeout: float = DEFAULT_TIMEOUT): + await fill_pvi_entries( + self, self._prefix + "PVI", timeout=timeout, mock=mock + ) + await super().connect(mock, timeout) - async with DeviceCollector(sim=True): - sim_panda = Panda("PANDAQSRV:", "sim_panda") + async with DeviceCollector(mock=True): + mock_panda = Panda("PANDAQSRV:", "mock_panda") - assert sim_panda.name == "sim_panda" - yield sim_panda + assert mock_panda.name == "mock_panda" + yield mock_panda async def test_hardware_triggered_flyable_with_static_seq_table_logic( @@ -180,7 +182,7 @@ def flying_plan(): yield from bps.open_run() yield from bps.declare_stream(*detector_list, name="main_stream", collect=True) - set_sim_value(flyer.trigger_logic.seq.active, 1) + set_mock_value(flyer.trigger_logic.seq.active, 1) yield from bps.kickoff(flyer, wait=True) for detector in detector_list: @@ -194,7 +196,7 @@ def flying_plan(): for detector in detector_list: detector.writer.index += 1 - set_sim_value(flyer.trigger_logic.seq.active, 0) + set_mock_value(flyer.trigger_logic.seq.active, 0) done = False while not done: From 54a46b96dc855074a0dab8de71c2d012c96c2bf6 Mon Sep 17 00:00:00 2001 From: Callum Forrester Date: Wed, 15 May 2024 08:54:56 +0100 Subject: [PATCH 7/7] Export epics_signal_rw_rbv (#306) Export epics_signal_rw_rbv so a user can import it from ophyd_async.epics.signal rather than ophyd_async.epics.signal.signal --- src/ophyd_async/epics/signal/__init__.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/ophyd_async/epics/signal/__init__.py b/src/ophyd_async/epics/signal/__init__.py index 2bbcff867a..05870d33e4 100644 --- a/src/ophyd_async/epics/signal/__init__.py +++ b/src/ophyd_async/epics/signal/__init__.py @@ -1,8 +1,15 @@ -from .signal import epics_signal_r, epics_signal_rw, epics_signal_w, epics_signal_x +from .signal import ( + epics_signal_r, + epics_signal_rw, + epics_signal_rw_rbv, + epics_signal_w, + epics_signal_x, +) __all__ = [ "epics_signal_r", "epics_signal_rw", + "epics_signal_rw_rbv", "epics_signal_w", "epics_signal_x", ]