ophyd-async 0.9.0a2__py3-none-any.whl → 0.10.0a2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ophyd_async/__init__.py +5 -8
- ophyd_async/_docs_parser.py +12 -0
- ophyd_async/_version.py +9 -4
- ophyd_async/core/__init__.py +97 -62
- ophyd_async/core/_derived_signal.py +271 -0
- ophyd_async/core/_derived_signal_backend.py +300 -0
- ophyd_async/core/_detector.py +106 -125
- ophyd_async/core/_device.py +69 -63
- ophyd_async/core/_device_filler.py +65 -1
- ophyd_async/core/_flyer.py +14 -5
- ophyd_async/core/_hdf_dataset.py +29 -22
- ophyd_async/core/_log.py +14 -23
- ophyd_async/core/_mock_signal_backend.py +11 -3
- ophyd_async/core/_protocol.py +65 -45
- ophyd_async/core/_providers.py +28 -9
- ophyd_async/core/_readable.py +44 -35
- ophyd_async/core/_settings.py +36 -27
- ophyd_async/core/_signal.py +262 -170
- ophyd_async/core/_signal_backend.py +56 -13
- ophyd_async/core/_soft_signal_backend.py +16 -11
- ophyd_async/core/_status.py +72 -24
- ophyd_async/core/_table.py +41 -11
- ophyd_async/core/_utils.py +96 -49
- ophyd_async/core/_yaml_settings.py +2 -0
- ophyd_async/epics/__init__.py +1 -0
- ophyd_async/epics/adandor/_andor.py +2 -2
- ophyd_async/epics/adandor/_andor_controller.py +4 -2
- ophyd_async/epics/adandor/_andor_io.py +2 -4
- ophyd_async/epics/adaravis/__init__.py +5 -0
- ophyd_async/epics/adaravis/_aravis.py +4 -8
- ophyd_async/epics/adaravis/_aravis_controller.py +20 -43
- ophyd_async/epics/adaravis/_aravis_io.py +13 -28
- ophyd_async/epics/adcore/__init__.py +23 -8
- ophyd_async/epics/adcore/_core_detector.py +42 -2
- ophyd_async/epics/adcore/_core_io.py +124 -99
- ophyd_async/epics/adcore/_core_logic.py +106 -27
- ophyd_async/epics/adcore/_core_writer.py +12 -8
- ophyd_async/epics/adcore/_hdf_writer.py +21 -38
- ophyd_async/epics/adcore/_single_trigger.py +2 -2
- ophyd_async/epics/adcore/_utils.py +2 -2
- ophyd_async/epics/adkinetix/__init__.py +2 -1
- ophyd_async/epics/adkinetix/_kinetix.py +3 -3
- ophyd_async/epics/adkinetix/_kinetix_controller.py +4 -2
- ophyd_async/epics/adkinetix/_kinetix_io.py +12 -13
- ophyd_async/epics/adpilatus/__init__.py +5 -0
- ophyd_async/epics/adpilatus/_pilatus.py +1 -1
- ophyd_async/epics/adpilatus/_pilatus_controller.py +5 -24
- ophyd_async/epics/adpilatus/_pilatus_io.py +11 -9
- ophyd_async/epics/adsimdetector/__init__.py +8 -1
- ophyd_async/epics/adsimdetector/_sim.py +4 -14
- ophyd_async/epics/adsimdetector/_sim_controller.py +17 -0
- ophyd_async/epics/adsimdetector/_sim_io.py +10 -0
- ophyd_async/epics/advimba/__init__.py +10 -1
- ophyd_async/epics/advimba/_vimba.py +3 -2
- ophyd_async/epics/advimba/_vimba_controller.py +4 -2
- ophyd_async/epics/advimba/_vimba_io.py +23 -28
- ophyd_async/epics/core/_aioca.py +35 -16
- ophyd_async/epics/core/_epics_connector.py +4 -0
- ophyd_async/epics/core/_epics_device.py +2 -0
- ophyd_async/epics/core/_p4p.py +10 -2
- ophyd_async/epics/core/_pvi_connector.py +65 -8
- ophyd_async/epics/core/_signal.py +51 -51
- ophyd_async/epics/core/_util.py +4 -4
- ophyd_async/epics/demo/__init__.py +16 -0
- ophyd_async/epics/demo/__main__.py +31 -0
- ophyd_async/epics/demo/_ioc.py +32 -0
- ophyd_async/epics/demo/_motor.py +82 -0
- ophyd_async/epics/demo/_point_detector.py +42 -0
- ophyd_async/epics/demo/_point_detector_channel.py +22 -0
- ophyd_async/epics/demo/_stage.py +15 -0
- ophyd_async/epics/{sim/mover.db → demo/motor.db} +2 -1
- ophyd_async/epics/demo/point_detector.db +59 -0
- ophyd_async/epics/demo/point_detector_channel.db +21 -0
- ophyd_async/epics/eiger/_eiger.py +1 -3
- ophyd_async/epics/eiger/_eiger_controller.py +11 -4
- ophyd_async/epics/eiger/_eiger_io.py +2 -0
- ophyd_async/epics/eiger/_odin_io.py +1 -2
- ophyd_async/epics/motor.py +65 -28
- ophyd_async/epics/signal.py +4 -1
- ophyd_async/epics/testing/_example_ioc.py +21 -9
- ophyd_async/epics/testing/_utils.py +3 -0
- ophyd_async/epics/testing/test_records.db +8 -0
- ophyd_async/epics/testing/test_records_pva.db +17 -16
- ophyd_async/fastcs/__init__.py +1 -0
- ophyd_async/fastcs/core.py +6 -0
- ophyd_async/fastcs/odin/__init__.py +1 -0
- ophyd_async/fastcs/panda/__init__.py +8 -6
- ophyd_async/fastcs/panda/_block.py +29 -9
- ophyd_async/fastcs/panda/_control.py +5 -0
- ophyd_async/fastcs/panda/_hdf_panda.py +2 -0
- ophyd_async/fastcs/panda/_table.py +9 -6
- ophyd_async/fastcs/panda/_trigger.py +23 -9
- ophyd_async/fastcs/panda/_writer.py +27 -30
- ophyd_async/plan_stubs/__init__.py +2 -0
- ophyd_async/plan_stubs/_ensure_connected.py +1 -0
- ophyd_async/plan_stubs/_fly.py +2 -4
- ophyd_async/plan_stubs/_nd_attributes.py +2 -0
- ophyd_async/plan_stubs/_panda.py +1 -0
- ophyd_async/plan_stubs/_settings.py +43 -16
- ophyd_async/plan_stubs/_utils.py +3 -0
- ophyd_async/plan_stubs/_wait_for_awaitable.py +1 -1
- ophyd_async/sim/__init__.py +24 -14
- ophyd_async/sim/__main__.py +43 -0
- ophyd_async/sim/_blob_detector.py +33 -0
- ophyd_async/sim/_blob_detector_controller.py +48 -0
- ophyd_async/sim/_blob_detector_writer.py +105 -0
- ophyd_async/sim/_mirror_horizontal.py +46 -0
- ophyd_async/sim/_mirror_vertical.py +74 -0
- ophyd_async/sim/_motor.py +233 -0
- ophyd_async/sim/_pattern_generator.py +124 -0
- ophyd_async/sim/_point_detector.py +86 -0
- ophyd_async/sim/_stage.py +19 -0
- ophyd_async/tango/__init__.py +1 -0
- ophyd_async/tango/core/__init__.py +6 -1
- ophyd_async/tango/core/_base_device.py +41 -33
- ophyd_async/tango/core/_converters.py +81 -0
- ophyd_async/tango/core/_signal.py +18 -32
- ophyd_async/tango/core/_tango_readable.py +2 -19
- ophyd_async/tango/core/_tango_transport.py +136 -60
- ophyd_async/tango/core/_utils.py +47 -0
- ophyd_async/tango/{sim → demo}/_counter.py +2 -0
- ophyd_async/tango/{sim → demo}/_detector.py +2 -0
- ophyd_async/tango/{sim → demo}/_mover.py +5 -4
- ophyd_async/tango/{sim → demo}/_tango/_servers.py +4 -0
- ophyd_async/tango/testing/__init__.py +6 -0
- ophyd_async/tango/testing/_one_of_everything.py +200 -0
- ophyd_async/testing/__init__.py +29 -7
- ophyd_async/testing/_assert.py +145 -83
- ophyd_async/testing/_mock_signal_utils.py +56 -70
- ophyd_async/testing/_one_of_everything.py +41 -21
- ophyd_async/testing/_single_derived.py +89 -0
- ophyd_async/testing/_utils.py +3 -0
- {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a2.dist-info}/METADATA +25 -26
- ophyd_async-0.10.0a2.dist-info/RECORD +149 -0
- {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a2.dist-info}/WHEEL +1 -1
- ophyd_async/epics/sim/__init__.py +0 -54
- ophyd_async/epics/sim/_ioc.py +0 -29
- ophyd_async/epics/sim/_mover.py +0 -101
- ophyd_async/epics/sim/_sensor.py +0 -37
- ophyd_async/epics/sim/sensor.db +0 -19
- ophyd_async/sim/_pattern_detector/__init__.py +0 -13
- ophyd_async/sim/_pattern_detector/_pattern_detector.py +0 -42
- ophyd_async/sim/_pattern_detector/_pattern_detector_controller.py +0 -69
- ophyd_async/sim/_pattern_detector/_pattern_detector_writer.py +0 -41
- ophyd_async/sim/_pattern_detector/_pattern_generator.py +0 -214
- ophyd_async/sim/_sim_motor.py +0 -107
- ophyd_async-0.9.0a2.dist-info/RECORD +0 -129
- /ophyd_async/tango/{sim → demo}/__init__.py +0 -0
- /ophyd_async/tango/{sim → demo}/_tango/__init__.py +0 -0
- {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a2.dist-info/licenses}/LICENSE +0 -0
- {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import textwrap
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Any, Generic, TypeVar
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from ophyd_async.core import (
|
|
8
|
+
Array1D,
|
|
9
|
+
DTypeScalar_co,
|
|
10
|
+
StrictEnum,
|
|
11
|
+
)
|
|
12
|
+
from ophyd_async.testing import float_array_value, int_array_value
|
|
13
|
+
from tango import AttrDataFormat, AttrWriteType, DevState
|
|
14
|
+
from tango.server import Device, attribute, command
|
|
15
|
+
|
|
16
|
+
T = TypeVar("T")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ExampleStrEnum(StrictEnum):
|
|
20
|
+
A = "AAA"
|
|
21
|
+
B = "BBB"
|
|
22
|
+
C = "CCC"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def int_image_value(
|
|
26
|
+
dtype: type[DTypeScalar_co],
|
|
27
|
+
):
|
|
28
|
+
# how do we type this?
|
|
29
|
+
array_1d = int_array_value(dtype)
|
|
30
|
+
return np.vstack((array_1d, array_1d))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def float_image_value(
|
|
34
|
+
dtype: type[DTypeScalar_co],
|
|
35
|
+
):
|
|
36
|
+
# how do we type this?
|
|
37
|
+
array_1d = float_array_value(dtype)
|
|
38
|
+
return np.vstack((array_1d, array_1d))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _valid_command(dformat: AttrDataFormat, dtype: str):
|
|
42
|
+
if dtype == "DevUChar":
|
|
43
|
+
return False
|
|
44
|
+
if dformat != AttrDataFormat.SCALAR and dtype in ["DevState", "DevEnum"]:
|
|
45
|
+
return False
|
|
46
|
+
return True
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class AttributeData(Generic[T]):
|
|
51
|
+
name: str
|
|
52
|
+
tango_type: str
|
|
53
|
+
initial_scalar: T
|
|
54
|
+
initial_spectrum: Array1D
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
_all_attribute_definitions = [
|
|
58
|
+
AttributeData(
|
|
59
|
+
"str",
|
|
60
|
+
"DevString",
|
|
61
|
+
"test_string",
|
|
62
|
+
np.array(["one", "two", "three"], dtype=str),
|
|
63
|
+
),
|
|
64
|
+
AttributeData(
|
|
65
|
+
"bool",
|
|
66
|
+
"DevBoolean",
|
|
67
|
+
True,
|
|
68
|
+
np.array([False, True], dtype=bool),
|
|
69
|
+
),
|
|
70
|
+
AttributeData("strenum", "DevEnum", 1, np.array([0, 1, 2])),
|
|
71
|
+
AttributeData("int8", "DevShort", 1, int_array_value(np.int8)),
|
|
72
|
+
AttributeData("uint8", "DevUChar", 1, int_array_value(np.uint8)),
|
|
73
|
+
AttributeData("int16", "DevShort", 1, int_array_value(np.int16)),
|
|
74
|
+
AttributeData("uint16", "DevUShort", 1, int_array_value(np.uint16)),
|
|
75
|
+
AttributeData("int32", "DevLong", 1, int_array_value(np.int32)),
|
|
76
|
+
AttributeData("uint32", "DevULong", 1, int_array_value(np.uint32)),
|
|
77
|
+
AttributeData("int64", "DevLong64", 1, int_array_value(np.int64)),
|
|
78
|
+
AttributeData("uint64", "DevULong64", 1, int_array_value(np.uint64)),
|
|
79
|
+
AttributeData("float32", "DevFloat", 1.234, float_array_value(np.float32)),
|
|
80
|
+
AttributeData("float64", "DevDouble", 1.234, float_array_value(np.float64)),
|
|
81
|
+
AttributeData(
|
|
82
|
+
"my_state",
|
|
83
|
+
"DevState",
|
|
84
|
+
DevState.INIT,
|
|
85
|
+
np.array([DevState.INIT, DevState.ON, DevState.MOVING], dtype=DevState),
|
|
86
|
+
),
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class OneOfEverythingTangoDevice(Device):
|
|
91
|
+
attr_values = {}
|
|
92
|
+
initial_values = {}
|
|
93
|
+
|
|
94
|
+
def _add_attr(self, attr: attribute, initial_value):
|
|
95
|
+
self.attr_values[attr.name] = initial_value
|
|
96
|
+
self.initial_values[attr.name] = initial_value
|
|
97
|
+
self.add_attribute(attr)
|
|
98
|
+
self.set_change_event(attr.name, True, False)
|
|
99
|
+
|
|
100
|
+
def add_scalar_attr(self, name: str, dtype: str, initial_value: Any):
|
|
101
|
+
attr = attribute(
|
|
102
|
+
name=name,
|
|
103
|
+
dtype=dtype,
|
|
104
|
+
dformat=AttrDataFormat.SCALAR,
|
|
105
|
+
access=AttrWriteType.READ_WRITE,
|
|
106
|
+
fget=self.read,
|
|
107
|
+
fset=self.write,
|
|
108
|
+
enum_labels=[e.value for e in ExampleStrEnum],
|
|
109
|
+
)
|
|
110
|
+
self._add_attr(attr, initial_value)
|
|
111
|
+
|
|
112
|
+
def add_array_attrs(self, name: str, dtype: str, initial_value: np.ndarray):
|
|
113
|
+
spectrum_name = f"{name}_spectrum"
|
|
114
|
+
spectrum_attr = attribute(
|
|
115
|
+
name=spectrum_name,
|
|
116
|
+
dtype=dtype,
|
|
117
|
+
dformat=AttrDataFormat.SPECTRUM,
|
|
118
|
+
access=AttrWriteType.READ_WRITE,
|
|
119
|
+
fget=self.read,
|
|
120
|
+
fset=self.write,
|
|
121
|
+
max_dim_x=initial_value.shape[-1],
|
|
122
|
+
enum_labels=[e.value for e in ExampleStrEnum],
|
|
123
|
+
)
|
|
124
|
+
image_name = f"{name}_image"
|
|
125
|
+
image_attr = attribute(
|
|
126
|
+
name=image_name,
|
|
127
|
+
dtype=dtype,
|
|
128
|
+
dformat=AttrDataFormat.IMAGE,
|
|
129
|
+
access=AttrWriteType.READ_WRITE,
|
|
130
|
+
fget=self.read,
|
|
131
|
+
fset=self.write,
|
|
132
|
+
max_dim_x=initial_value.shape[-1],
|
|
133
|
+
max_dim_y=2,
|
|
134
|
+
enum_labels=[e.value for e in ExampleStrEnum],
|
|
135
|
+
)
|
|
136
|
+
self._add_attr(spectrum_attr, initial_value)
|
|
137
|
+
# have image just be 2 of the initial spectrum stacked
|
|
138
|
+
self._add_attr(image_attr, np.vstack((initial_value, initial_value)))
|
|
139
|
+
|
|
140
|
+
def add_scalar_command(self, name: str, dtype: str):
|
|
141
|
+
if _valid_command(AttrDataFormat.SCALAR, dtype):
|
|
142
|
+
self.add_command(
|
|
143
|
+
command(
|
|
144
|
+
f=getattr(self, f"{name}_cmd"),
|
|
145
|
+
dtype_in=dtype,
|
|
146
|
+
dtype_out=dtype,
|
|
147
|
+
dformat_in=AttrDataFormat.SCALAR,
|
|
148
|
+
dformat_out=AttrDataFormat.SCALAR,
|
|
149
|
+
),
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
def add_spectrum_command(self, name: str, dtype: str):
|
|
153
|
+
if _valid_command(AttrDataFormat.SPECTRUM, dtype):
|
|
154
|
+
self.add_command(
|
|
155
|
+
command(
|
|
156
|
+
f=getattr(self, f"{name}_spectrum_cmd"),
|
|
157
|
+
dtype_in=dtype,
|
|
158
|
+
dtype_out=dtype,
|
|
159
|
+
dformat_in=AttrDataFormat.SPECTRUM,
|
|
160
|
+
dformat_out=AttrDataFormat.SPECTRUM,
|
|
161
|
+
),
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
def initialize_dynamic_attributes(self):
|
|
165
|
+
for attr_data in _all_attribute_definitions:
|
|
166
|
+
self.add_scalar_attr(
|
|
167
|
+
attr_data.name, attr_data.tango_type, attr_data.initial_scalar
|
|
168
|
+
)
|
|
169
|
+
self.add_array_attrs(
|
|
170
|
+
attr_data.name, attr_data.tango_type, attr_data.initial_spectrum
|
|
171
|
+
)
|
|
172
|
+
self.add_scalar_command(attr_data.name, attr_data.tango_type)
|
|
173
|
+
self.add_spectrum_command(attr_data.name, attr_data.tango_type)
|
|
174
|
+
|
|
175
|
+
@command
|
|
176
|
+
def reset_values(self):
|
|
177
|
+
for attr_name in self.attr_values:
|
|
178
|
+
self.attr_values[attr_name] = self.initial_values[attr_name]
|
|
179
|
+
|
|
180
|
+
def read(self, attr):
|
|
181
|
+
value = self.attr_values[attr.get_name()]
|
|
182
|
+
attr.set_value(value)
|
|
183
|
+
|
|
184
|
+
def write(self, attr):
|
|
185
|
+
new_value = attr.get_write_value()
|
|
186
|
+
self.attr_values[attr.get_name()] = new_value
|
|
187
|
+
self.push_change_event(attr.get_name(), new_value)
|
|
188
|
+
|
|
189
|
+
echo_command_code = textwrap.dedent(
|
|
190
|
+
"""\
|
|
191
|
+
def {}(self, arg):
|
|
192
|
+
return arg
|
|
193
|
+
"""
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
for attr_data in _all_attribute_definitions:
|
|
197
|
+
if _valid_command(AttrDataFormat.SCALAR, attr_data.tango_type):
|
|
198
|
+
exec(echo_command_code.format(f"{attr_data.name}_cmd"))
|
|
199
|
+
if _valid_command(AttrDataFormat.SPECTRUM, attr_data.tango_type):
|
|
200
|
+
exec(echo_command_code.format(f"{attr_data.name}_spectrum_cmd"))
|
ophyd_async/testing/__init__.py
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
"""Utilities for testing devices."""
|
|
2
|
+
|
|
1
3
|
from . import __pytest_assert_rewrite # noqa: F401
|
|
2
4
|
from ._assert import (
|
|
3
5
|
ApproxTable,
|
|
4
6
|
MonitorQueue,
|
|
7
|
+
StatusWatcher,
|
|
5
8
|
approx_value,
|
|
6
9
|
assert_configuration,
|
|
7
10
|
assert_describe_signal,
|
|
@@ -14,7 +17,6 @@ from ._mock_signal_utils import (
|
|
|
14
17
|
get_mock,
|
|
15
18
|
get_mock_put,
|
|
16
19
|
mock_puts_blocked,
|
|
17
|
-
reset_mock_put_calls,
|
|
18
20
|
set_mock_put_proceeds,
|
|
19
21
|
set_mock_value,
|
|
20
22
|
set_mock_values,
|
|
@@ -24,24 +26,36 @@ from ._one_of_everything import (
|
|
|
24
26
|
ExampleTable,
|
|
25
27
|
OneOfEverythingDevice,
|
|
26
28
|
ParentOfEverythingDevice,
|
|
29
|
+
float_array_value,
|
|
30
|
+
int_array_value,
|
|
31
|
+
)
|
|
32
|
+
from ._single_derived import (
|
|
33
|
+
BeamstopPosition,
|
|
34
|
+
Exploder,
|
|
35
|
+
MovableBeamstop,
|
|
36
|
+
ReadOnlyBeamstop,
|
|
27
37
|
)
|
|
28
38
|
from ._wait_for_pending import wait_for_pending_wakeups
|
|
29
39
|
|
|
40
|
+
# The order of this list determines the order of the documentation,
|
|
41
|
+
# so does not match the alphabetical order of the imports
|
|
30
42
|
__all__ = [
|
|
31
43
|
"approx_value",
|
|
44
|
+
# Assert functions
|
|
45
|
+
"assert_value",
|
|
46
|
+
"assert_reading",
|
|
32
47
|
"assert_configuration",
|
|
33
48
|
"assert_describe_signal",
|
|
34
49
|
"assert_emitted",
|
|
35
|
-
|
|
36
|
-
"assert_value",
|
|
37
|
-
"callback_on_mock_put",
|
|
50
|
+
# Mocking utilities
|
|
38
51
|
"get_mock",
|
|
52
|
+
"set_mock_value",
|
|
53
|
+
"set_mock_values",
|
|
39
54
|
"get_mock_put",
|
|
55
|
+
"callback_on_mock_put",
|
|
40
56
|
"mock_puts_blocked",
|
|
41
|
-
"reset_mock_put_calls",
|
|
42
57
|
"set_mock_put_proceeds",
|
|
43
|
-
|
|
44
|
-
"set_mock_values",
|
|
58
|
+
# Wait for pending wakeups
|
|
45
59
|
"wait_for_pending_wakeups",
|
|
46
60
|
"ExampleEnum",
|
|
47
61
|
"ExampleTable",
|
|
@@ -49,4 +63,12 @@ __all__ = [
|
|
|
49
63
|
"ParentOfEverythingDevice",
|
|
50
64
|
"MonitorQueue",
|
|
51
65
|
"ApproxTable",
|
|
66
|
+
"StatusWatcher",
|
|
67
|
+
"int_array_value",
|
|
68
|
+
"float_array_value",
|
|
69
|
+
# Derived examples
|
|
70
|
+
"BeamstopPosition",
|
|
71
|
+
"Exploder",
|
|
72
|
+
"MovableBeamstop",
|
|
73
|
+
"ReadOnlyBeamstop",
|
|
52
74
|
]
|
ophyd_async/testing/_assert.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import
|
|
2
|
+
from collections.abc import Mapping
|
|
3
3
|
from contextlib import AbstractContextManager
|
|
4
|
-
from typing import Any
|
|
4
|
+
from typing import Any, cast
|
|
5
|
+
from unittest.mock import Mock, call
|
|
5
6
|
|
|
6
7
|
import pytest
|
|
7
8
|
from bluesky.protocols import Reading
|
|
@@ -13,89 +14,89 @@ from ophyd_async.core import (
|
|
|
13
14
|
SignalDatatypeT,
|
|
14
15
|
SignalR,
|
|
15
16
|
Table,
|
|
17
|
+
WatchableAsyncStatus,
|
|
18
|
+
Watcher,
|
|
16
19
|
)
|
|
17
20
|
|
|
21
|
+
from ._utils import T
|
|
22
|
+
|
|
18
23
|
|
|
19
24
|
def approx_value(value: Any):
|
|
25
|
+
"""Allow any value to be compared to another in tests.
|
|
26
|
+
|
|
27
|
+
This is needed because numpy arrays give a numpy array back when compared,
|
|
28
|
+
not a bool. This means that you can't ``assert array1==array2``. Numpy
|
|
29
|
+
arrays can be wrapped with `pytest.approx`, but this doesn't work for
|
|
30
|
+
`Table` instances: in this case we use `ApproxTable`.
|
|
31
|
+
"""
|
|
20
32
|
return ApproxTable(value) if isinstance(value, Table) else pytest.approx(value)
|
|
21
33
|
|
|
22
34
|
|
|
23
35
|
async def assert_value(signal: SignalR[SignalDatatypeT], value: Any) -> None:
|
|
24
|
-
"""Assert a
|
|
25
|
-
|
|
26
|
-
Parameters
|
|
27
|
-
----------
|
|
28
|
-
signal:
|
|
29
|
-
signal with get_value.
|
|
30
|
-
value:
|
|
31
|
-
The expected value from the signal.
|
|
32
|
-
|
|
33
|
-
Notes
|
|
34
|
-
-----
|
|
35
|
-
Example usage::
|
|
36
|
-
await assert_value(signal, value)
|
|
36
|
+
"""Assert that a Signal has the given value.
|
|
37
37
|
|
|
38
|
+
:param signal: Signal with get_value.
|
|
39
|
+
:param value: The expected value from the signal.
|
|
38
40
|
"""
|
|
39
41
|
actual_value = await signal.get_value()
|
|
40
42
|
assert approx_value(value) == actual_value
|
|
41
43
|
|
|
42
44
|
|
|
43
45
|
async def assert_reading(
|
|
44
|
-
readable: AsyncReadable,
|
|
46
|
+
readable: AsyncReadable,
|
|
47
|
+
expected_reading: Mapping[str, Mapping[str, Any]],
|
|
45
48
|
) -> None:
|
|
46
|
-
"""Assert
|
|
47
|
-
|
|
48
|
-
Parameters
|
|
49
|
-
----------
|
|
50
|
-
readable:
|
|
51
|
-
Callable with readable.read function that generate readings.
|
|
52
|
-
|
|
53
|
-
reading:
|
|
54
|
-
The expected readings from the readable.
|
|
55
|
-
|
|
56
|
-
Notes
|
|
57
|
-
-----
|
|
58
|
-
Example usage::
|
|
59
|
-
await assert_reading(readable, reading)
|
|
49
|
+
"""Assert that a readable Device has the given reading.
|
|
60
50
|
|
|
51
|
+
:param readable: Device with an async ``read()`` method to get the reading from.
|
|
52
|
+
:param expected_reading: The expected reading from the readable.
|
|
61
53
|
"""
|
|
62
54
|
actual_reading = await readable.read()
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
55
|
+
_assert_readings_approx_equal(expected_reading, actual_reading)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _approx_reading(expected: Mapping[str, Any], actual: Reading) -> Reading:
|
|
59
|
+
ret = dict(
|
|
60
|
+
expected,
|
|
61
|
+
value=approx_value(expected["value"]),
|
|
62
|
+
timestamp=pytest.approx(expected["timestamp"], rel=0.1)
|
|
63
|
+
if "timestamp" in expected
|
|
64
|
+
else actual["timestamp"],
|
|
65
|
+
)
|
|
66
|
+
if "alarm_severity" in actual and "alarm_severity" not in expected:
|
|
67
|
+
ret["alarm_severity"] = actual["alarm_severity"]
|
|
68
|
+
return cast(Reading, ret)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _assert_readings_approx_equal(
|
|
72
|
+
expected: Mapping[str, Mapping[str, Any]], actual: Mapping[str, Reading]
|
|
73
|
+
):
|
|
74
|
+
assert actual == {
|
|
75
|
+
k: _approx_reading(v, actual[k]) for k, v in expected.items() if k in actual
|
|
66
76
|
}
|
|
67
|
-
assert actual_reading == approx_expected_reading
|
|
68
77
|
|
|
69
78
|
|
|
70
79
|
async def assert_configuration(
|
|
71
80
|
configurable: AsyncConfigurable,
|
|
72
|
-
configuration: dict[str,
|
|
81
|
+
configuration: dict[str, dict[str, Any]],
|
|
73
82
|
) -> None:
|
|
74
|
-
"""Assert
|
|
75
|
-
|
|
76
|
-
Parameters
|
|
77
|
-
----------
|
|
78
|
-
configurable:
|
|
79
|
-
Configurable with Configurable.read function that generate readings.
|
|
80
|
-
|
|
81
|
-
configuration:
|
|
82
|
-
The expected readings from configurable.
|
|
83
|
-
|
|
84
|
-
Notes
|
|
85
|
-
-----
|
|
86
|
-
Example usage::
|
|
87
|
-
await assert_configuration(configurable configuration)
|
|
83
|
+
"""Assert that a configurable Device has the given configuration.
|
|
88
84
|
|
|
85
|
+
:param configurable:
|
|
86
|
+
Device with an async ``read_configuration()`` method to get the
|
|
87
|
+
configuration from.
|
|
88
|
+
:param configuration: The expected configuration from the configurable.
|
|
89
89
|
"""
|
|
90
90
|
actual_configuration = await configurable.read_configuration()
|
|
91
|
-
|
|
92
|
-
k: dict(v, value=approx_value(configuration[k]["value"]))
|
|
93
|
-
for k, v in configuration.items()
|
|
94
|
-
}
|
|
95
|
-
assert actual_configuration == approx_expected_configuration
|
|
91
|
+
_assert_readings_approx_equal(configuration, actual_configuration)
|
|
96
92
|
|
|
97
93
|
|
|
98
94
|
async def assert_describe_signal(signal: SignalR, /, **metadata):
|
|
95
|
+
"""Assert the describe of a signal matches the expected metadata.
|
|
96
|
+
|
|
97
|
+
:param signal: The signal to describe.
|
|
98
|
+
:param metadata: The expected metadata.
|
|
99
|
+
"""
|
|
99
100
|
actual_describe = await signal.describe()
|
|
100
101
|
assert list(actual_describe) == [signal.name]
|
|
101
102
|
(actual_datakey,) = actual_describe.values()
|
|
@@ -104,23 +105,18 @@ async def assert_describe_signal(signal: SignalR, /, **metadata):
|
|
|
104
105
|
|
|
105
106
|
|
|
106
107
|
def assert_emitted(docs: dict[str, list[dict]], **numbers: int):
|
|
107
|
-
"""Assert emitted document generated by running a Bluesky plan
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
Example usage::
|
|
120
|
-
docs = defaultdict(list)
|
|
121
|
-
RE.subscribe(lambda name, doc: docs[name].append(doc))
|
|
122
|
-
RE(my_plan())
|
|
123
|
-
assert_emitted(docs, start=1, descriptor=1, event=1, stop=1)
|
|
108
|
+
"""Assert emitted document generated by running a Bluesky plan.
|
|
109
|
+
|
|
110
|
+
:param docs: A mapping of document type -> list of documents that have been emitted.
|
|
111
|
+
:param numbers: The number of each document type expected.
|
|
112
|
+
|
|
113
|
+
:example:
|
|
114
|
+
```python
|
|
115
|
+
docs = defaultdict(list)
|
|
116
|
+
RE.subscribe(lambda name, doc: docs[name].append(doc))
|
|
117
|
+
RE(my_plan())
|
|
118
|
+
assert_emitted(docs, start=1, descriptor=1, event=1, stop=1)
|
|
119
|
+
```
|
|
124
120
|
"""
|
|
125
121
|
assert list(docs) == list(numbers)
|
|
126
122
|
actual_numbers = {name: len(d) for name, d in docs.items()}
|
|
@@ -128,6 +124,14 @@ def assert_emitted(docs: dict[str, list[dict]], **numbers: int):
|
|
|
128
124
|
|
|
129
125
|
|
|
130
126
|
class ApproxTable:
|
|
127
|
+
"""For approximating two tables are equivalent.
|
|
128
|
+
|
|
129
|
+
:param expected: The expected table.
|
|
130
|
+
:param rel: The relative tolerance.
|
|
131
|
+
:param abs: The absolute tolerance.
|
|
132
|
+
:param nan_ok: Whether NaNs are allowed.
|
|
133
|
+
"""
|
|
134
|
+
|
|
131
135
|
def __init__(self, expected: Table, rel=None, abs=None, nan_ok: bool = False):
|
|
132
136
|
self.expected = expected
|
|
133
137
|
self.rel = rel
|
|
@@ -144,29 +148,23 @@ class ApproxTable:
|
|
|
144
148
|
|
|
145
149
|
|
|
146
150
|
class MonitorQueue(AbstractContextManager):
|
|
151
|
+
"""Monitors a `Signal` and stores its updates."""
|
|
152
|
+
|
|
147
153
|
def __init__(self, signal: SignalR):
|
|
148
154
|
self.signal = signal
|
|
149
155
|
self.updates: asyncio.Queue[dict[str, Reading]] = asyncio.Queue()
|
|
150
|
-
self.signal.subscribe(self.updates.put_nowait)
|
|
151
156
|
|
|
152
157
|
async def assert_updates(self, expected_value):
|
|
153
158
|
# Get an update, value and reading
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
update = await self.updates.get()
|
|
157
|
-
value = await self.signal.get_value()
|
|
158
|
-
reading = await self.signal.read()
|
|
159
|
-
# Check they match what we expected
|
|
160
|
-
assert value == expected_value
|
|
161
|
-
assert type(value) is expected_type
|
|
159
|
+
update = await asyncio.wait_for(self.updates.get(), timeout=5)
|
|
160
|
+
await assert_value(self.signal, expected_value)
|
|
162
161
|
expected_reading = {
|
|
163
162
|
self.signal.name: {
|
|
164
163
|
"value": expected_value,
|
|
165
|
-
"timestamp": pytest.approx(time.time(), rel=0.1),
|
|
166
|
-
"alarm_severity": 0,
|
|
167
164
|
}
|
|
168
165
|
}
|
|
169
|
-
|
|
166
|
+
await assert_reading(self.signal, expected_reading)
|
|
167
|
+
_assert_readings_approx_equal(expected_reading, update)
|
|
170
168
|
|
|
171
169
|
def __enter__(self):
|
|
172
170
|
self.signal.subscribe(self.updates.put_nowait)
|
|
@@ -174,3 +172,67 @@ class MonitorQueue(AbstractContextManager):
|
|
|
174
172
|
|
|
175
173
|
def __exit__(self, exc_type, exc_value, traceback):
|
|
176
174
|
self.signal.clear_sub(self.updates.put_nowait)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class StatusWatcher(Watcher[T]):
|
|
178
|
+
"""Watches an `AsyncStatus`, storing the calls within."""
|
|
179
|
+
|
|
180
|
+
def __init__(self, status: WatchableAsyncStatus) -> None:
|
|
181
|
+
self._event = asyncio.Event()
|
|
182
|
+
self.mock = Mock()
|
|
183
|
+
"""Mock that stores watcher updates from the status."""
|
|
184
|
+
status.watch(self)
|
|
185
|
+
|
|
186
|
+
def __call__(
|
|
187
|
+
self,
|
|
188
|
+
current: T | None = None,
|
|
189
|
+
initial: T | None = None,
|
|
190
|
+
target: T | None = None,
|
|
191
|
+
name: str | None = None,
|
|
192
|
+
unit: str | None = None,
|
|
193
|
+
precision: int | None = None,
|
|
194
|
+
fraction: float | None = None,
|
|
195
|
+
time_elapsed: float | None = None,
|
|
196
|
+
time_remaining: float | None = None,
|
|
197
|
+
) -> Any:
|
|
198
|
+
self.mock(
|
|
199
|
+
current=current,
|
|
200
|
+
initial=initial,
|
|
201
|
+
target=target,
|
|
202
|
+
name=name,
|
|
203
|
+
unit=unit,
|
|
204
|
+
precision=precision,
|
|
205
|
+
fraction=fraction,
|
|
206
|
+
time_elapsed=time_elapsed,
|
|
207
|
+
time_remaining=time_remaining,
|
|
208
|
+
)
|
|
209
|
+
self._event.set()
|
|
210
|
+
|
|
211
|
+
async def wait_for_call(
|
|
212
|
+
self,
|
|
213
|
+
current: T | None = None,
|
|
214
|
+
initial: T | None = None,
|
|
215
|
+
target: T | None = None,
|
|
216
|
+
name: str | None = None,
|
|
217
|
+
unit: str | None = None,
|
|
218
|
+
precision: int | None = None,
|
|
219
|
+
fraction: float | None = None,
|
|
220
|
+
# Any so we can use pytest.approx
|
|
221
|
+
time_elapsed: float | Any = None,
|
|
222
|
+
time_remaining: float | Any = None,
|
|
223
|
+
):
|
|
224
|
+
await asyncio.wait_for(self._event.wait(), timeout=1)
|
|
225
|
+
assert self.mock.call_count == 1
|
|
226
|
+
assert self.mock.call_args == call(
|
|
227
|
+
current=current,
|
|
228
|
+
initial=initial,
|
|
229
|
+
target=target,
|
|
230
|
+
name=name,
|
|
231
|
+
unit=unit,
|
|
232
|
+
precision=precision,
|
|
233
|
+
fraction=fraction,
|
|
234
|
+
time_elapsed=time_elapsed,
|
|
235
|
+
time_remaining=time_remaining,
|
|
236
|
+
)
|
|
237
|
+
self.mock.reset_mock()
|
|
238
|
+
self._event.clear()
|