Skip to content

Commit d1fc038

Browse files
authored
Remove tango polling (#66)
* Remove tango polling * Process review comments * Fix epics tests * Fix typo * Expose assertions in tests * Do read after write test
1 parent 76cf387 commit d1fc038

File tree

4 files changed

+224
-67
lines changed

4 files changed

+224
-67
lines changed

src/fastcs/backends/tango/dsr.py

Lines changed: 17 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from collections.abc import Awaitable, Callable
22
from dataclasses import dataclass
3-
from types import MethodType
43
from typing import Any
54

65
import tango
@@ -16,7 +15,6 @@
1615
@dataclass
1716
class TangoDSROptions:
1817
dev_name: str = "MY/DEVICE/NAME"
19-
dev_class: str = "FAST_CS_DEVICE"
2018
dsr_instance: str = "MY_SERVER_INSTANCE"
2119
debug: bool = False
2220

@@ -25,23 +23,12 @@ def _wrap_updater_fget(
2523
attr_name: str, attribute: AttrR, controller: BaseController
2624
) -> Callable[[Any], Any]:
2725
async def fget(tango_device: Device):
28-
assert attribute.updater is not None
29-
30-
await attribute.updater.update(controller, attribute)
3126
tango_device.info_stream(f"called fget method: {attr_name}")
3227
return attribute.get()
3328

3429
return fget
3530

3631

37-
def _tango_polling_period(attribute: AttrR) -> int:
38-
if attribute.updater is not None:
39-
# Convert to integer milliseconds
40-
return int(attribute.updater.update_period * 1000)
41-
42-
return -1 # `tango.server.attribute` default for `polling_period`
43-
44-
4532
def _tango_display_format(attribute: Attribute) -> str:
4633
match attribute.datatype:
4734
case Float(prec):
@@ -54,10 +41,8 @@ def _wrap_updater_fset(
5441
attr_name: str, attribute: AttrW, controller: BaseController
5542
) -> Callable[[Any, Any], Any]:
5643
async def fset(tango_device: Device, val):
57-
assert attribute.sender is not None
58-
59-
await attribute.sender.put(controller, attribute, val)
6044
tango_device.info_stream(f"called fset method: {attr_name}")
45+
await attribute.process(val)
6146

6247
return fset
6348

@@ -84,7 +69,6 @@ def _collect_dev_attributes(mapping: Mapping) -> dict[str, Any]:
8469
),
8570
access=AttrWriteType.READ_WRITE,
8671
format=_tango_display_format(attribute),
87-
polling_period=_tango_polling_period(attribute),
8872
)
8973
case AttrR():
9074
collection[d_attr_name] = server.attribute(
@@ -95,7 +79,6 @@ def _collect_dev_attributes(mapping: Mapping) -> dict[str, Any]:
9579
attr_name, attribute, single_mapping.controller
9680
),
9781
format=_tango_display_format(attribute),
98-
polling_period=_tango_polling_period(attribute),
9982
)
10083
case AttrW():
10184
collection[d_attr_name] = server.attribute(
@@ -115,8 +98,10 @@ def _wrap_command_f(
11598
method_name: str, method: Callable, controller: BaseController
11699
) -> Callable[..., Awaitable[None]]:
117100
async def _dynamic_f(tango_device: Device) -> None:
118-
tango_device.info_stream(f"called {controller} f method: {method_name}")
119-
return await MethodType(method, controller)()
101+
tango_device.info_stream(
102+
f"called {'_'.join(controller.path)} f method: {method_name}"
103+
)
104+
return await getattr(controller, method.__name__)()
120105

121106
_dynamic_f.__name__ = method_name
122107
return _dynamic_f
@@ -146,7 +131,6 @@ def _collect_dev_init(mapping: Mapping) -> dict[str, Callable]:
146131
async def init_device(tango_device: Device):
147132
await server.Device.init_device(tango_device) # type: ignore
148133
tango_device.set_state(DevState.ON)
149-
await mapping.controller.connect()
150134

151135
return {"init_device": init_device}
152136

@@ -171,11 +155,10 @@ def _collect_dsr_args(options: TangoDSROptions) -> list[str]:
171155
class TangoDSR:
172156
def __init__(self, mapping: Mapping):
173157
self._mapping = mapping
158+
self.dev_class = self._mapping.controller.__class__.__name__
159+
self._device = self._create_device()
174160

175-
def run(self, options: TangoDSROptions | None = None) -> None:
176-
if options is None:
177-
options = TangoDSROptions()
178-
161+
def _create_device(self):
179162
class_dict: dict = {
180163
**_collect_dev_attributes(self._mapping),
181164
**_collect_dev_commands(self._mapping),
@@ -185,14 +168,19 @@ def run(self, options: TangoDSROptions | None = None) -> None:
185168
}
186169

187170
class_bases = (server.Device,)
188-
pytango_class = type(options.dev_class, class_bases, class_dict)
189-
register_dev(options.dev_name, options.dev_class, options.dsr_instance)
171+
pytango_class = type(self.dev_class, class_bases, class_dict)
172+
return pytango_class
173+
174+
def run(self, options: TangoDSROptions | None = None) -> None:
175+
if options is None:
176+
options = TangoDSROptions()
190177

191178
dsr_args = _collect_dsr_args(options)
192179

193180
server.run(
194-
(pytango_class,),
195-
[options.dev_class, options.dsr_instance, *dsr_args],
181+
(self._device,),
182+
[self.dev_class, options.dsr_instance, *dsr_args],
183+
green_mode=server.GreenMode.Asyncio,
196184
)
197185

198186

tests/backends/epics/test_gui.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
LED,
33
ButtonPanel,
44
ComboBox,
5+
Group,
56
SignalR,
67
SignalRW,
78
SignalW,
89
SignalX,
10+
SubScreen,
911
TextFormat,
1012
TextRead,
1113
TextWrite,
@@ -30,6 +32,28 @@ def test_get_components(mapping):
3032

3133
components = gui.extract_mapping_components(mapping.get_controller_mappings()[0])
3234
assert components == [
35+
Group(
36+
name="SubController01",
37+
layout=SubScreen(labelled=True),
38+
children=[
39+
SignalR(
40+
name="ReadInt",
41+
read_pv="DEVICE:SubController01:ReadInt",
42+
read_widget=TextRead(),
43+
)
44+
],
45+
),
46+
Group(
47+
name="SubController02",
48+
layout=SubScreen(labelled=True),
49+
children=[
50+
SignalR(
51+
name="ReadInt",
52+
read_pv="DEVICE:SubController01:ReadInt",
53+
read_widget=TextRead(),
54+
)
55+
],
56+
),
3357
SignalR(name="BigEnum", read_pv="DEVICE:BigEnum", read_widget=TextRead()),
3458
SignalR(name="ReadBool", read_pv="DEVICE:ReadBool", read_widget=LED()),
3559
SignalR(

tests/backends/tango/test_dsr.py

Lines changed: 111 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,112 @@
11
import pytest
2-
from pytest_mock import MockerFixture
3-
from tango._tango import AttrWriteType, CmdArgType
4-
5-
from fastcs.backends.tango.dsr import _collect_dev_attributes, _collect_dev_commands
6-
7-
8-
def test_collect_attributes(mapping):
9-
attributes = _collect_dev_attributes(mapping)
10-
11-
# Check that attributes are created and of expected type
12-
assert list(attributes.keys()) == [
13-
"BigEnum",
14-
"ReadBool",
15-
"ReadInt",
16-
"ReadWriteFloat",
17-
"ReadWriteInt",
18-
"StringEnum",
19-
"WriteBool",
20-
]
21-
assert attributes["ReadInt"].attr_write == AttrWriteType.READ
22-
assert attributes["ReadInt"].attr_type == CmdArgType.DevLong64
23-
assert attributes["StringEnum"].attr_write == AttrWriteType.READ_WRITE
24-
assert attributes["StringEnum"].attr_type == CmdArgType.DevString
25-
assert attributes["ReadWriteFloat"].attr_write == AttrWriteType.READ_WRITE
26-
assert attributes["ReadWriteFloat"].attr_type == CmdArgType.DevDouble
27-
assert attributes["WriteBool"].attr_write == AttrWriteType.WRITE
28-
assert attributes["WriteBool"].attr_type == CmdArgType.DevBoolean
29-
30-
31-
@pytest.mark.asyncio
32-
async def test_collect_commands(mapping, mocker: MockerFixture):
33-
commands = _collect_dev_commands(mapping)
34-
35-
# Check that command is created and it can be called
36-
assert list(commands.keys()) == ["Go"]
37-
await commands["Go"](mocker.MagicMock())
2+
from tango import DevState
3+
from tango.test_context import DeviceTestContext
4+
5+
from fastcs.backends.tango.backend import TangoBackend
6+
7+
8+
class TestTangoDevice:
9+
@pytest.fixture(scope="class")
10+
def tango_context(self, assertable_controller):
11+
# https://tango-controls.readthedocs.io/projects/pytango/en/v9.5.1/testing/test_context.html
12+
device = TangoBackend(assertable_controller)._dsr._device
13+
with DeviceTestContext(device) as proxy:
14+
yield proxy
15+
16+
def test_list_attributes(self, tango_context):
17+
assert list(tango_context.get_attribute_list()) == [
18+
"BigEnum",
19+
"ReadBool",
20+
"ReadInt",
21+
"ReadWriteFloat",
22+
"ReadWriteInt",
23+
"StringEnum",
24+
"WriteBool",
25+
"SubController01_ReadInt",
26+
"SubController02_ReadInt",
27+
"State",
28+
"Status",
29+
]
30+
31+
def test_list_commands(self, tango_context):
32+
assert list(tango_context.get_command_list()) == [
33+
"Go",
34+
"Init",
35+
"State",
36+
"Status",
37+
]
38+
39+
def test_state(self, tango_context):
40+
assert tango_context.command_inout("State") == DevState.ON
41+
42+
def test_status(self, tango_context):
43+
expect = "The device is in ON state."
44+
assert tango_context.command_inout("Status") == expect
45+
46+
def test_read_int(self, assertable_controller, tango_context):
47+
expect = 0
48+
with assertable_controller.assert_read_here(["read_int"]):
49+
result = tango_context.read_attribute("ReadInt").value
50+
assert result == expect
51+
52+
def test_read_write_int(self, assertable_controller, tango_context):
53+
expect = 0
54+
with assertable_controller.assert_read_here(["read_write_int"]):
55+
result = tango_context.read_attribute("ReadWriteInt").value
56+
assert result == expect
57+
new = 9
58+
with assertable_controller.assert_write_here(["read_write_int"]):
59+
tango_context.write_attribute("ReadWriteInt", new)
60+
assert tango_context.read_attribute("ReadWriteInt").value == new
61+
62+
def test_read_write_float(self, assertable_controller, tango_context):
63+
expect = 0.0
64+
with assertable_controller.assert_read_here(["read_write_float"]):
65+
result = tango_context.read_attribute("ReadWriteFloat").value
66+
assert result == expect
67+
new = 0.5
68+
with assertable_controller.assert_write_here(["read_write_float"]):
69+
tango_context.write_attribute("ReadWriteFloat", new)
70+
assert tango_context.read_attribute("ReadWriteFloat").value == new
71+
72+
def test_read_bool(self, assertable_controller, tango_context):
73+
expect = False
74+
with assertable_controller.assert_read_here(["read_bool"]):
75+
result = tango_context.read_attribute("ReadBool").value
76+
assert result == expect
77+
78+
def test_write_bool(self, assertable_controller, tango_context):
79+
with assertable_controller.assert_write_here(["write_bool"]):
80+
tango_context.write_attribute("WriteBool", True)
81+
82+
def test_string_enum(self, assertable_controller, tango_context):
83+
expect = ""
84+
with assertable_controller.assert_read_here(["string_enum"]):
85+
result = tango_context.read_attribute("StringEnum").value
86+
assert result == expect
87+
new = "new"
88+
with assertable_controller.assert_write_here(["string_enum"]):
89+
tango_context.write_attribute("StringEnum", new)
90+
assert tango_context.read_attribute("StringEnum").value == new
91+
92+
def test_big_enum(self, assertable_controller, tango_context):
93+
expect = 0
94+
with assertable_controller.assert_read_here(["big_enum"]):
95+
result = tango_context.read_attribute("BigEnum").value
96+
assert result == expect
97+
98+
def test_go(self, assertable_controller, tango_context):
99+
with assertable_controller.assert_execute_here(["go"]):
100+
tango_context.command_inout("Go")
101+
102+
def test_read_child1(self, assertable_controller, tango_context):
103+
expect = 0
104+
with assertable_controller.assert_read_here(["SubController01", "read_int"]):
105+
result = tango_context.read_attribute("SubController01_ReadInt").value
106+
assert result == expect
107+
108+
def test_read_child2(self, assertable_controller, tango_context):
109+
expect = 0
110+
with assertable_controller.assert_read_here(["SubController02", "read_int"]):
111+
result = tango_context.read_attribute("SubController02_ReadInt").value
112+
assert result == expect

0 commit comments

Comments
 (0)