ophyd-async 0.9.0a2__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 +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 +37 -8
- 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 +137 -81
- ophyd_async/testing/_mock_signal_utils.py +56 -70
- ophyd_async/testing/_one_of_everything.py +41 -21
- ophyd_async/testing/_single_derived.py +87 -0
- ophyd_async/testing/_utils.py +3 -0
- {ophyd_async-0.9.0a2.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.0a2.dist-info → ophyd_async-0.10.0a1.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.0a1.dist-info/licenses}/LICENSE +0 -0
- {ophyd_async-0.9.0a2.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,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,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import time
|
|
3
2
|
from contextlib import AbstractContextManager
|
|
4
3
|
from typing import Any
|
|
4
|
+
from unittest.mock import Mock, call
|
|
5
5
|
|
|
6
6
|
import pytest
|
|
7
7
|
from bluesky.protocols import Reading
|
|
@@ -13,89 +13,84 @@ from ophyd_async.core import (
|
|
|
13
13
|
SignalDatatypeT,
|
|
14
14
|
SignalR,
|
|
15
15
|
Table,
|
|
16
|
+
WatchableAsyncStatus,
|
|
17
|
+
Watcher,
|
|
16
18
|
)
|
|
17
19
|
|
|
20
|
+
from ._utils import T
|
|
21
|
+
|
|
18
22
|
|
|
19
23
|
def approx_value(value: Any):
|
|
24
|
+
"""Allow any value to be compared to another in tests.
|
|
25
|
+
|
|
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
|
+
"""
|
|
20
31
|
return ApproxTable(value) if isinstance(value, Table) else pytest.approx(value)
|
|
21
32
|
|
|
22
33
|
|
|
23
34
|
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)
|
|
35
|
+
"""Assert that a Signal has the given value.
|
|
37
36
|
|
|
37
|
+
:param signal: Signal with get_value.
|
|
38
|
+
:param value: The expected value from the signal.
|
|
38
39
|
"""
|
|
39
40
|
actual_value = await signal.get_value()
|
|
40
41
|
assert approx_value(value) == actual_value
|
|
41
42
|
|
|
42
43
|
|
|
43
44
|
async def assert_reading(
|
|
44
|
-
readable: AsyncReadable,
|
|
45
|
+
readable: AsyncReadable,
|
|
46
|
+
expected_reading: dict[str, dict[str, Any]],
|
|
45
47
|
) -> 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)
|
|
48
|
+
"""Assert that a readable Device has the given reading.
|
|
60
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.
|
|
61
52
|
"""
|
|
62
53
|
actual_reading = await readable.read()
|
|
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()
|
|
63
59
|
approx_expected_reading = {
|
|
64
|
-
k: dict(
|
|
65
|
-
|
|
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()
|
|
66
69
|
}
|
|
67
|
-
assert
|
|
70
|
+
assert actual == approx_expected_reading
|
|
68
71
|
|
|
69
72
|
|
|
70
73
|
async def assert_configuration(
|
|
71
74
|
configurable: AsyncConfigurable,
|
|
72
|
-
configuration: dict[str,
|
|
75
|
+
configuration: dict[str, dict[str, Any]],
|
|
73
76
|
) -> 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)
|
|
77
|
+
"""Assert that a configurable Device has the given configuration.
|
|
88
78
|
|
|
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.
|
|
89
83
|
"""
|
|
90
84
|
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
|
|
85
|
+
_assert_readings_approx_equal(configuration, actual_configuration)
|
|
96
86
|
|
|
97
87
|
|
|
98
88
|
async def assert_describe_signal(signal: SignalR, /, **metadata):
|
|
89
|
+
"""Assert the describe of a signal matches the expected metadata.
|
|
90
|
+
|
|
91
|
+
:param signal: The signal to describe.
|
|
92
|
+
:param metadata: The expected metadata.
|
|
93
|
+
"""
|
|
99
94
|
actual_describe = await signal.describe()
|
|
100
95
|
assert list(actual_describe) == [signal.name]
|
|
101
96
|
(actual_datakey,) = actual_describe.values()
|
|
@@ -104,23 +99,18 @@ async def assert_describe_signal(signal: SignalR, /, **metadata):
|
|
|
104
99
|
|
|
105
100
|
|
|
106
101
|
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)
|
|
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
|
+
```
|
|
124
114
|
"""
|
|
125
115
|
assert list(docs) == list(numbers)
|
|
126
116
|
actual_numbers = {name: len(d) for name, d in docs.items()}
|
|
@@ -128,6 +118,14 @@ def assert_emitted(docs: dict[str, list[dict]], **numbers: int):
|
|
|
128
118
|
|
|
129
119
|
|
|
130
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
|
+
|
|
131
129
|
def __init__(self, expected: Table, rel=None, abs=None, nan_ok: bool = False):
|
|
132
130
|
self.expected = expected
|
|
133
131
|
self.rel = rel
|
|
@@ -144,29 +142,23 @@ class ApproxTable:
|
|
|
144
142
|
|
|
145
143
|
|
|
146
144
|
class MonitorQueue(AbstractContextManager):
|
|
145
|
+
"""Monitors a `Signal` and stores its updates."""
|
|
146
|
+
|
|
147
147
|
def __init__(self, signal: SignalR):
|
|
148
148
|
self.signal = signal
|
|
149
149
|
self.updates: asyncio.Queue[dict[str, Reading]] = asyncio.Queue()
|
|
150
|
-
self.signal.subscribe(self.updates.put_nowait)
|
|
151
150
|
|
|
152
151
|
async def assert_updates(self, expected_value):
|
|
153
152
|
# 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
|
|
153
|
+
update = await asyncio.wait_for(self.updates.get(), timeout=5)
|
|
154
|
+
await assert_value(self.signal, expected_value)
|
|
162
155
|
expected_reading = {
|
|
163
156
|
self.signal.name: {
|
|
164
157
|
"value": expected_value,
|
|
165
|
-
"timestamp": pytest.approx(time.time(), rel=0.1),
|
|
166
|
-
"alarm_severity": 0,
|
|
167
158
|
}
|
|
168
159
|
}
|
|
169
|
-
|
|
160
|
+
await assert_reading(self.signal, expected_reading)
|
|
161
|
+
_assert_readings_approx_equal(expected_reading, update)
|
|
170
162
|
|
|
171
163
|
def __enter__(self):
|
|
172
164
|
self.signal.subscribe(self.updates.put_nowait)
|
|
@@ -174,3 +166,67 @@ class MonitorQueue(AbstractContextManager):
|
|
|
174
166
|
|
|
175
167
|
def __exit__(self, exc_type, exc_value, traceback):
|
|
176
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()
|