ophyd-async 0.9.0a1__py3-none-any.whl → 0.10.0a1__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 +102 -74
- ophyd_async/core/_derived_signal.py +271 -0
- ophyd_async/core/_derived_signal_backend.py +300 -0
- ophyd_async/core/_detector.py +158 -153
- ophyd_async/core/_device.py +143 -115
- ophyd_async/core/_device_filler.py +82 -9
- ophyd_async/core/_flyer.py +16 -7
- 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 +74 -58
- ophyd_async/core/_settings.py +113 -0
- ophyd_async/core/_signal.py +304 -174
- ophyd_async/core/_signal_backend.py +60 -14
- ophyd_async/core/_soft_signal_backend.py +18 -12
- ophyd_async/core/_status.py +72 -24
- ophyd_async/core/_table.py +54 -17
- ophyd_async/core/_utils.py +101 -52
- ophyd_async/core/_yaml_settings.py +66 -0
- ophyd_async/epics/__init__.py +1 -0
- ophyd_async/epics/adandor/__init__.py +9 -0
- ophyd_async/epics/adandor/_andor.py +45 -0
- ophyd_async/epics/adandor/_andor_controller.py +51 -0
- ophyd_async/epics/adandor/_andor_io.py +34 -0
- ophyd_async/epics/adaravis/__init__.py +8 -1
- ophyd_async/epics/adaravis/_aravis.py +23 -41
- ophyd_async/epics/adaravis/_aravis_controller.py +23 -55
- ophyd_async/epics/adaravis/_aravis_io.py +13 -28
- ophyd_async/epics/adcore/__init__.py +36 -14
- ophyd_async/epics/adcore/_core_detector.py +81 -0
- ophyd_async/epics/adcore/_core_io.py +145 -95
- ophyd_async/epics/adcore/_core_logic.py +179 -88
- ophyd_async/epics/adcore/_core_writer.py +223 -0
- ophyd_async/epics/adcore/_hdf_writer.py +51 -92
- ophyd_async/epics/adcore/_jpeg_writer.py +26 -0
- ophyd_async/epics/adcore/_single_trigger.py +6 -5
- ophyd_async/epics/adcore/_tiff_writer.py +26 -0
- ophyd_async/epics/adcore/_utils.py +3 -2
- ophyd_async/epics/adkinetix/__init__.py +2 -1
- ophyd_async/epics/adkinetix/_kinetix.py +32 -27
- ophyd_async/epics/adkinetix/_kinetix_controller.py +11 -21
- ophyd_async/epics/adkinetix/_kinetix_io.py +12 -13
- ophyd_async/epics/adpilatus/__init__.py +7 -2
- ophyd_async/epics/adpilatus/_pilatus.py +28 -40
- ophyd_async/epics/adpilatus/_pilatus_controller.py +25 -22
- ophyd_async/epics/adpilatus/_pilatus_io.py +11 -9
- ophyd_async/epics/adsimdetector/__init__.py +8 -1
- ophyd_async/epics/adsimdetector/_sim.py +22 -16
- ophyd_async/epics/adsimdetector/_sim_controller.py +9 -43
- ophyd_async/epics/adsimdetector/_sim_io.py +10 -0
- ophyd_async/epics/advimba/__init__.py +10 -1
- ophyd_async/epics/advimba/_vimba.py +26 -25
- ophyd_async/epics/advimba/_vimba_controller.py +12 -24
- ophyd_async/epics/advimba/_vimba_io.py +23 -28
- ophyd_async/epics/core/_aioca.py +66 -30
- ophyd_async/epics/core/_epics_connector.py +4 -0
- ophyd_async/epics/core/_epics_device.py +2 -0
- ophyd_async/epics/core/_p4p.py +50 -18
- ophyd_async/epics/core/_pvi_connector.py +65 -8
- ophyd_async/epics/core/_signal.py +51 -51
- ophyd_async/epics/core/_util.py +5 -5
- ophyd_async/epics/demo/__init__.py +11 -49
- 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/demo/{mover.db → 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 +83 -38
- ophyd_async/epics/signal.py +4 -1
- ophyd_async/epics/testing/__init__.py +14 -14
- ophyd_async/epics/testing/_example_ioc.py +68 -73
- ophyd_async/epics/testing/_utils.py +19 -44
- ophyd_async/epics/testing/test_records.db +16 -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 -8
- ophyd_async/fastcs/panda/_block.py +29 -9
- ophyd_async/fastcs/panda/_control.py +12 -2
- ophyd_async/fastcs/panda/_hdf_panda.py +5 -1
- ophyd_async/fastcs/panda/_table.py +13 -7
- ophyd_async/fastcs/panda/_trigger.py +23 -9
- ophyd_async/fastcs/panda/_writer.py +27 -30
- ophyd_async/plan_stubs/__init__.py +16 -0
- ophyd_async/plan_stubs/_ensure_connected.py +12 -17
- ophyd_async/plan_stubs/_fly.py +3 -5
- ophyd_async/plan_stubs/_nd_attributes.py +9 -5
- ophyd_async/plan_stubs/_panda.py +14 -0
- ophyd_async/plan_stubs/_settings.py +152 -0
- ophyd_async/plan_stubs/_utils.py +3 -0
- ophyd_async/plan_stubs/_wait_for_awaitable.py +13 -0
- ophyd_async/sim/__init__.py +29 -0
- 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 +21 -33
- ophyd_async/tango/core/_tango_readable.py +2 -19
- ophyd_async/tango/core/_tango_transport.py +148 -74
- ophyd_async/tango/core/_utils.py +47 -0
- ophyd_async/tango/demo/_counter.py +2 -0
- ophyd_async/tango/demo/_detector.py +2 -0
- ophyd_async/tango/demo/_mover.py +10 -6
- ophyd_async/tango/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 +48 -7
- ophyd_async/testing/__pytest_assert_rewrite.py +4 -0
- ophyd_async/testing/_assert.py +200 -96
- ophyd_async/testing/_mock_signal_utils.py +59 -73
- ophyd_async/testing/_one_of_everything.py +146 -0
- ophyd_async/testing/_single_derived.py +87 -0
- ophyd_async/testing/_utils.py +3 -0
- {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info}/METADATA +25 -26
- ophyd_async-0.10.0a1.dist-info/RECORD +149 -0
- {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info}/WHEEL +1 -1
- ophyd_async/core/_device_save_loader.py +0 -274
- ophyd_async/epics/demo/_mover.py +0 -95
- ophyd_async/epics/demo/_sensor.py +0 -37
- ophyd_async/epics/demo/sensor.db +0 -19
- ophyd_async/fastcs/panda/_utils.py +0 -16
- ophyd_async/sim/demo/__init__.py +0 -19
- ophyd_async/sim/demo/_pattern_detector/__init__.py +0 -13
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector.py +0 -42
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +0 -62
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector_writer.py +0 -41
- ophyd_async/sim/demo/_pattern_detector/_pattern_generator.py +0 -207
- ophyd_async/sim/demo/_sim_motor.py +0 -107
- ophyd_async/sim/testing/__init__.py +0 -0
- ophyd_async-0.9.0a1.dist-info/RECORD +0 -119
- ophyd_async-0.9.0a1.dist-info/entry_points.txt +0 -2
- {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info/licenses}/LICENSE +0 -0
- {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.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,5 +1,13 @@
|
|
|
1
|
+
"""Utilities for testing devices."""
|
|
2
|
+
|
|
3
|
+
from . import __pytest_assert_rewrite # noqa: F401
|
|
1
4
|
from ._assert import (
|
|
5
|
+
ApproxTable,
|
|
6
|
+
MonitorQueue,
|
|
7
|
+
StatusWatcher,
|
|
8
|
+
approx_value,
|
|
2
9
|
assert_configuration,
|
|
10
|
+
assert_describe_signal,
|
|
3
11
|
assert_emitted,
|
|
4
12
|
assert_reading,
|
|
5
13
|
assert_value,
|
|
@@ -9,25 +17,58 @@ from ._mock_signal_utils import (
|
|
|
9
17
|
get_mock,
|
|
10
18
|
get_mock_put,
|
|
11
19
|
mock_puts_blocked,
|
|
12
|
-
reset_mock_put_calls,
|
|
13
20
|
set_mock_put_proceeds,
|
|
14
21
|
set_mock_value,
|
|
15
22
|
set_mock_values,
|
|
16
23
|
)
|
|
24
|
+
from ._one_of_everything import (
|
|
25
|
+
ExampleEnum,
|
|
26
|
+
ExampleTable,
|
|
27
|
+
OneOfEverythingDevice,
|
|
28
|
+
ParentOfEverythingDevice,
|
|
29
|
+
float_array_value,
|
|
30
|
+
int_array_value,
|
|
31
|
+
)
|
|
32
|
+
from ._single_derived import (
|
|
33
|
+
BeamstopPosition,
|
|
34
|
+
Exploder,
|
|
35
|
+
MovableBeamstop,
|
|
36
|
+
ReadOnlyBeamstop,
|
|
37
|
+
)
|
|
17
38
|
from ._wait_for_pending import wait_for_pending_wakeups
|
|
18
39
|
|
|
40
|
+
# The order of this list determines the order of the documentation,
|
|
41
|
+
# so does not match the alphabetical order of the imports
|
|
19
42
|
__all__ = [
|
|
43
|
+
"approx_value",
|
|
44
|
+
# Assert functions
|
|
45
|
+
"assert_value",
|
|
46
|
+
"assert_reading",
|
|
20
47
|
"assert_configuration",
|
|
48
|
+
"assert_describe_signal",
|
|
21
49
|
"assert_emitted",
|
|
22
|
-
|
|
23
|
-
"assert_value",
|
|
24
|
-
"callback_on_mock_put",
|
|
50
|
+
# Mocking utilities
|
|
25
51
|
"get_mock",
|
|
52
|
+
"set_mock_value",
|
|
53
|
+
"set_mock_values",
|
|
26
54
|
"get_mock_put",
|
|
55
|
+
"callback_on_mock_put",
|
|
27
56
|
"mock_puts_blocked",
|
|
28
|
-
"reset_mock_put_calls",
|
|
29
57
|
"set_mock_put_proceeds",
|
|
30
|
-
|
|
31
|
-
"set_mock_values",
|
|
58
|
+
# Wait for pending wakeups
|
|
32
59
|
"wait_for_pending_wakeups",
|
|
60
|
+
"ExampleEnum",
|
|
61
|
+
"ExampleTable",
|
|
62
|
+
"OneOfEverythingDevice",
|
|
63
|
+
"ParentOfEverythingDevice",
|
|
64
|
+
"MonitorQueue",
|
|
65
|
+
"ApproxTable",
|
|
66
|
+
"StatusWatcher",
|
|
67
|
+
"int_array_value",
|
|
68
|
+
"float_array_value",
|
|
69
|
+
# Derived examples
|
|
70
|
+
"BeamstopPosition",
|
|
71
|
+
"Exploder",
|
|
72
|
+
"MovableBeamstop",
|
|
73
|
+
"ReadOnlyBeamstop",
|
|
33
74
|
]
|
ophyd_async/testing/_assert.py
CHANGED
|
@@ -1,128 +1,232 @@
|
|
|
1
|
-
|
|
1
|
+
import asyncio
|
|
2
|
+
from contextlib import AbstractContextManager
|
|
2
3
|
from typing import Any
|
|
4
|
+
from unittest.mock import Mock, call
|
|
3
5
|
|
|
6
|
+
import pytest
|
|
4
7
|
from bluesky.protocols import Reading
|
|
8
|
+
from event_model import DataKey
|
|
5
9
|
|
|
6
|
-
from ophyd_async.core import
|
|
10
|
+
from ophyd_async.core import (
|
|
11
|
+
AsyncConfigurable,
|
|
12
|
+
AsyncReadable,
|
|
13
|
+
SignalDatatypeT,
|
|
14
|
+
SignalR,
|
|
15
|
+
Table,
|
|
16
|
+
WatchableAsyncStatus,
|
|
17
|
+
Watcher,
|
|
18
|
+
)
|
|
7
19
|
|
|
20
|
+
from ._utils import T
|
|
8
21
|
|
|
9
|
-
def _generate_assert_error_msg(name: str, expected_result, actual_result) -> str:
|
|
10
|
-
WARNING = "\033[93m"
|
|
11
|
-
FAIL = "\033[91m"
|
|
12
|
-
ENDC = "\033[0m"
|
|
13
|
-
return (
|
|
14
|
-
f"Expected {WARNING}{name}{ENDC} to produce"
|
|
15
|
-
+ f"\n{FAIL}{expected_result}{ENDC}"
|
|
16
|
-
+ f"\nbut actually got \n{FAIL}{actual_result}{ENDC}"
|
|
17
|
-
)
|
|
18
22
|
|
|
23
|
+
def approx_value(value: Any):
|
|
24
|
+
"""Allow any value to be compared to another in tests.
|
|
19
25
|
|
|
20
|
-
|
|
21
|
-
|
|
26
|
+
This is needed because numpy arrays give a numpy array back when compared,
|
|
27
|
+
not a bool. This means that you can't ``assert array1==array2``. Numpy
|
|
28
|
+
arrays can be wrapped with `pytest.approx`, but this doesn't work for
|
|
29
|
+
`Table` instances: in this case we use `ApproxTable`.
|
|
30
|
+
"""
|
|
31
|
+
return ApproxTable(value) if isinstance(value, Table) else pytest.approx(value)
|
|
22
32
|
|
|
23
|
-
Parameters
|
|
24
|
-
----------
|
|
25
|
-
signal:
|
|
26
|
-
signal with get_value.
|
|
27
|
-
value:
|
|
28
|
-
The expected value from the signal.
|
|
29
33
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
Example usage::
|
|
33
|
-
await assert_value(signal, value)
|
|
34
|
+
async def assert_value(signal: SignalR[SignalDatatypeT], value: Any) -> None:
|
|
35
|
+
"""Assert that a Signal has the given value.
|
|
34
36
|
|
|
37
|
+
:param signal: Signal with get_value.
|
|
38
|
+
:param value: The expected value from the signal.
|
|
35
39
|
"""
|
|
36
40
|
actual_value = await signal.get_value()
|
|
37
|
-
assert
|
|
38
|
-
name=signal.name,
|
|
39
|
-
expected_result=value,
|
|
40
|
-
actual_result=actual_value,
|
|
41
|
-
)
|
|
41
|
+
assert approx_value(value) == actual_value
|
|
42
42
|
|
|
43
43
|
|
|
44
44
|
async def assert_reading(
|
|
45
|
-
readable: AsyncReadable,
|
|
45
|
+
readable: AsyncReadable,
|
|
46
|
+
expected_reading: dict[str, dict[str, Any]],
|
|
46
47
|
) -> None:
|
|
47
|
-
"""Assert
|
|
48
|
-
|
|
49
|
-
Parameters
|
|
50
|
-
----------
|
|
51
|
-
readable:
|
|
52
|
-
Callable with readable.read function that generate readings.
|
|
53
|
-
|
|
54
|
-
reading:
|
|
55
|
-
The expected readings from the readable.
|
|
56
|
-
|
|
57
|
-
Notes
|
|
58
|
-
-----
|
|
59
|
-
Example usage::
|
|
60
|
-
await assert_reading(readable, reading)
|
|
48
|
+
"""Assert that a readable Device has the given reading.
|
|
61
49
|
|
|
50
|
+
:param readable: Device with an async ``read()`` method to get the reading from.
|
|
51
|
+
:param expected_reading: The expected reading from the readable.
|
|
62
52
|
"""
|
|
63
53
|
actual_reading = await readable.read()
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
)
|
|
54
|
+
_assert_readings_approx_equal(expected_reading, actual_reading)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _assert_readings_approx_equal(expected, actual):
|
|
58
|
+
assert expected.keys() == actual.keys()
|
|
59
|
+
approx_expected_reading = {
|
|
60
|
+
k: dict(
|
|
61
|
+
v,
|
|
62
|
+
value=approx_value(v["value"]),
|
|
63
|
+
timestamp=pytest.approx(v["timestamp"], rel=0.1)
|
|
64
|
+
if "timestamp" in v
|
|
65
|
+
else actual[k]["timestamp"],
|
|
66
|
+
alarm_severity=v.get("alarm_severity", actual[k]["alarm_severity"]),
|
|
67
|
+
)
|
|
68
|
+
for k, v in expected.items()
|
|
69
|
+
}
|
|
70
|
+
assert actual == approx_expected_reading
|
|
69
71
|
|
|
70
72
|
|
|
71
73
|
async def assert_configuration(
|
|
72
74
|
configurable: AsyncConfigurable,
|
|
73
|
-
configuration:
|
|
75
|
+
configuration: dict[str, dict[str, Any]],
|
|
74
76
|
) -> None:
|
|
75
|
-
"""Assert
|
|
77
|
+
"""Assert that a configurable Device has the given configuration.
|
|
76
78
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
:param configurable:
|
|
80
|
+
Device with an async ``read_configuration()`` method to get the
|
|
81
|
+
configuration from.
|
|
82
|
+
:param configuration: The expected configuration from the configurable.
|
|
83
|
+
"""
|
|
84
|
+
actual_configuration = await configurable.read_configuration()
|
|
85
|
+
_assert_readings_approx_equal(configuration, actual_configuration)
|
|
81
86
|
|
|
82
|
-
configuration:
|
|
83
|
-
The expected readings from configurable.
|
|
84
87
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
Example usage::
|
|
88
|
-
await assert_configuration(configurable configuration)
|
|
88
|
+
async def assert_describe_signal(signal: SignalR, /, **metadata):
|
|
89
|
+
"""Assert the describe of a signal matches the expected metadata.
|
|
89
90
|
|
|
91
|
+
:param signal: The signal to describe.
|
|
92
|
+
:param metadata: The expected metadata.
|
|
90
93
|
"""
|
|
91
|
-
|
|
92
|
-
assert
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
-----
|
|
112
|
-
Example usage::
|
|
113
|
-
docs = defaultdict(list)
|
|
114
|
-
RE.subscribe(lambda name, doc: docs[name].append(doc))
|
|
115
|
-
RE(my_plan())
|
|
116
|
-
assert_emitted(docs, start=1, descriptor=1, event=1, stop=1)
|
|
94
|
+
actual_describe = await signal.describe()
|
|
95
|
+
assert list(actual_describe) == [signal.name]
|
|
96
|
+
(actual_datakey,) = actual_describe.values()
|
|
97
|
+
expected_datakey = DataKey(source=signal.source, **metadata)
|
|
98
|
+
assert actual_datakey == expected_datakey
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def assert_emitted(docs: dict[str, list[dict]], **numbers: int):
|
|
102
|
+
"""Assert emitted document generated by running a Bluesky plan.
|
|
103
|
+
|
|
104
|
+
:param docs: A mapping of document type -> list of documents that have been emitted.
|
|
105
|
+
:param numbers: The number of each document type expected.
|
|
106
|
+
|
|
107
|
+
:example:
|
|
108
|
+
```python
|
|
109
|
+
docs = defaultdict(list)
|
|
110
|
+
RE.subscribe(lambda name, doc: docs[name].append(doc))
|
|
111
|
+
RE(my_plan())
|
|
112
|
+
assert_emitted(docs, start=1, descriptor=1, event=1, stop=1)
|
|
113
|
+
```
|
|
117
114
|
"""
|
|
118
|
-
assert list(docs) == list(numbers)
|
|
119
|
-
name="documents",
|
|
120
|
-
expected_result=list(numbers),
|
|
121
|
-
actual_result=list(docs),
|
|
122
|
-
)
|
|
115
|
+
assert list(docs) == list(numbers)
|
|
123
116
|
actual_numbers = {name: len(d) for name, d in docs.items()}
|
|
124
|
-
assert actual_numbers == numbers
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
117
|
+
assert actual_numbers == numbers
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class ApproxTable:
|
|
121
|
+
"""For approximating two tables are equivalent.
|
|
122
|
+
|
|
123
|
+
:param expected: The expected table.
|
|
124
|
+
:param rel: The relative tolerance.
|
|
125
|
+
:param abs: The absolute tolerance.
|
|
126
|
+
:param nan_ok: Whether NaNs are allowed.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
def __init__(self, expected: Table, rel=None, abs=None, nan_ok: bool = False):
|
|
130
|
+
self.expected = expected
|
|
131
|
+
self.rel = rel
|
|
132
|
+
self.abs = abs
|
|
133
|
+
self.nan_ok = nan_ok
|
|
134
|
+
|
|
135
|
+
def __eq__(self, value):
|
|
136
|
+
approx_fields = {
|
|
137
|
+
k: pytest.approx(v, self.rel, self.abs, self.nan_ok)
|
|
138
|
+
for k, v in self.expected
|
|
139
|
+
}
|
|
140
|
+
expected = type(self.expected).model_construct(**approx_fields) # type: ignore
|
|
141
|
+
return expected == value
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class MonitorQueue(AbstractContextManager):
|
|
145
|
+
"""Monitors a `Signal` and stores its updates."""
|
|
146
|
+
|
|
147
|
+
def __init__(self, signal: SignalR):
|
|
148
|
+
self.signal = signal
|
|
149
|
+
self.updates: asyncio.Queue[dict[str, Reading]] = asyncio.Queue()
|
|
150
|
+
|
|
151
|
+
async def assert_updates(self, expected_value):
|
|
152
|
+
# Get an update, value and reading
|
|
153
|
+
update = await asyncio.wait_for(self.updates.get(), timeout=5)
|
|
154
|
+
await assert_value(self.signal, expected_value)
|
|
155
|
+
expected_reading = {
|
|
156
|
+
self.signal.name: {
|
|
157
|
+
"value": expected_value,
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
await assert_reading(self.signal, expected_reading)
|
|
161
|
+
_assert_readings_approx_equal(expected_reading, update)
|
|
162
|
+
|
|
163
|
+
def __enter__(self):
|
|
164
|
+
self.signal.subscribe(self.updates.put_nowait)
|
|
165
|
+
return self
|
|
166
|
+
|
|
167
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
168
|
+
self.signal.clear_sub(self.updates.put_nowait)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class StatusWatcher(Watcher[T]):
|
|
172
|
+
"""Watches an `AsyncStatus`, storing the calls within."""
|
|
173
|
+
|
|
174
|
+
def __init__(self, status: WatchableAsyncStatus) -> None:
|
|
175
|
+
self._event = asyncio.Event()
|
|
176
|
+
self.mock = Mock()
|
|
177
|
+
"""Mock that stores watcher updates from the status."""
|
|
178
|
+
status.watch(self)
|
|
179
|
+
|
|
180
|
+
def __call__(
|
|
181
|
+
self,
|
|
182
|
+
current: T | None = None,
|
|
183
|
+
initial: T | None = None,
|
|
184
|
+
target: T | None = None,
|
|
185
|
+
name: str | None = None,
|
|
186
|
+
unit: str | None = None,
|
|
187
|
+
precision: int | None = None,
|
|
188
|
+
fraction: float | None = None,
|
|
189
|
+
time_elapsed: float | None = None,
|
|
190
|
+
time_remaining: float | None = None,
|
|
191
|
+
) -> Any:
|
|
192
|
+
self.mock(
|
|
193
|
+
current=current,
|
|
194
|
+
initial=initial,
|
|
195
|
+
target=target,
|
|
196
|
+
name=name,
|
|
197
|
+
unit=unit,
|
|
198
|
+
precision=precision,
|
|
199
|
+
fraction=fraction,
|
|
200
|
+
time_elapsed=time_elapsed,
|
|
201
|
+
time_remaining=time_remaining,
|
|
202
|
+
)
|
|
203
|
+
self._event.set()
|
|
204
|
+
|
|
205
|
+
async def wait_for_call(
|
|
206
|
+
self,
|
|
207
|
+
current: T | None = None,
|
|
208
|
+
initial: T | None = None,
|
|
209
|
+
target: T | None = None,
|
|
210
|
+
name: str | None = None,
|
|
211
|
+
unit: str | None = None,
|
|
212
|
+
precision: int | None = None,
|
|
213
|
+
fraction: float | None = None,
|
|
214
|
+
# Any so we can use pytest.approx
|
|
215
|
+
time_elapsed: float | Any = None,
|
|
216
|
+
time_remaining: float | Any = None,
|
|
217
|
+
):
|
|
218
|
+
await asyncio.wait_for(self._event.wait(), timeout=1)
|
|
219
|
+
assert self.mock.call_count == 1
|
|
220
|
+
assert self.mock.call_args == call(
|
|
221
|
+
current=current,
|
|
222
|
+
initial=initial,
|
|
223
|
+
target=target,
|
|
224
|
+
name=name,
|
|
225
|
+
unit=unit,
|
|
226
|
+
precision=precision,
|
|
227
|
+
fraction=fraction,
|
|
228
|
+
time_elapsed=time_elapsed,
|
|
229
|
+
time_remaining=time_remaining,
|
|
230
|
+
)
|
|
231
|
+
self.mock.reset_mock()
|
|
232
|
+
self._event.clear()
|