Skip to content

Commit

Permalink
Remove tango polling (#66)
Browse files Browse the repository at this point in the history
* Remove tango polling

* Process review comments

* Fix epics tests

* Fix typo

* Expose assertions in tests

* Do read after write test
  • Loading branch information
marcelldls authored Nov 19, 2024
1 parent 76cf387 commit d1fc038
Show file tree
Hide file tree
Showing 4 changed files with 224 additions and 67 deletions.
46 changes: 17 additions & 29 deletions src/fastcs/backends/tango/dsr.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from types import MethodType
from typing import Any

import tango
Expand All @@ -16,7 +15,6 @@
@dataclass
class TangoDSROptions:
dev_name: str = "MY/DEVICE/NAME"
dev_class: str = "FAST_CS_DEVICE"
dsr_instance: str = "MY_SERVER_INSTANCE"
debug: bool = False

Expand All @@ -25,23 +23,12 @@ def _wrap_updater_fget(
attr_name: str, attribute: AttrR, controller: BaseController
) -> Callable[[Any], Any]:
async def fget(tango_device: Device):
assert attribute.updater is not None

await attribute.updater.update(controller, attribute)
tango_device.info_stream(f"called fget method: {attr_name}")
return attribute.get()

return fget


def _tango_polling_period(attribute: AttrR) -> int:
if attribute.updater is not None:
# Convert to integer milliseconds
return int(attribute.updater.update_period * 1000)

return -1 # `tango.server.attribute` default for `polling_period`


def _tango_display_format(attribute: Attribute) -> str:
match attribute.datatype:
case Float(prec):
Expand All @@ -54,10 +41,8 @@ def _wrap_updater_fset(
attr_name: str, attribute: AttrW, controller: BaseController
) -> Callable[[Any, Any], Any]:
async def fset(tango_device: Device, val):
assert attribute.sender is not None

await attribute.sender.put(controller, attribute, val)
tango_device.info_stream(f"called fset method: {attr_name}")
await attribute.process(val)

return fset

Expand All @@ -84,7 +69,6 @@ def _collect_dev_attributes(mapping: Mapping) -> dict[str, Any]:
),
access=AttrWriteType.READ_WRITE,
format=_tango_display_format(attribute),
polling_period=_tango_polling_period(attribute),
)
case AttrR():
collection[d_attr_name] = server.attribute(
Expand All @@ -95,7 +79,6 @@ def _collect_dev_attributes(mapping: Mapping) -> dict[str, Any]:
attr_name, attribute, single_mapping.controller
),
format=_tango_display_format(attribute),
polling_period=_tango_polling_period(attribute),
)
case AttrW():
collection[d_attr_name] = server.attribute(
Expand All @@ -115,8 +98,10 @@ def _wrap_command_f(
method_name: str, method: Callable, controller: BaseController
) -> Callable[..., Awaitable[None]]:
async def _dynamic_f(tango_device: Device) -> None:
tango_device.info_stream(f"called {controller} f method: {method_name}")
return await MethodType(method, controller)()
tango_device.info_stream(
f"called {'_'.join(controller.path)} f method: {method_name}"
)
return await getattr(controller, method.__name__)()

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

return {"init_device": init_device}

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

def run(self, options: TangoDSROptions | None = None) -> None:
if options is None:
options = TangoDSROptions()

def _create_device(self):
class_dict: dict = {
**_collect_dev_attributes(self._mapping),
**_collect_dev_commands(self._mapping),
Expand All @@ -185,14 +168,19 @@ def run(self, options: TangoDSROptions | None = None) -> None:
}

class_bases = (server.Device,)
pytango_class = type(options.dev_class, class_bases, class_dict)
register_dev(options.dev_name, options.dev_class, options.dsr_instance)
pytango_class = type(self.dev_class, class_bases, class_dict)
return pytango_class

def run(self, options: TangoDSROptions | None = None) -> None:
if options is None:
options = TangoDSROptions()

dsr_args = _collect_dsr_args(options)

server.run(
(pytango_class,),
[options.dev_class, options.dsr_instance, *dsr_args],
(self._device,),
[self.dev_class, options.dsr_instance, *dsr_args],
green_mode=server.GreenMode.Asyncio,
)


Expand Down
24 changes: 24 additions & 0 deletions tests/backends/epics/test_gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
LED,
ButtonPanel,
ComboBox,
Group,
SignalR,
SignalRW,
SignalW,
SignalX,
SubScreen,
TextFormat,
TextRead,
TextWrite,
Expand All @@ -30,6 +32,28 @@ def test_get_components(mapping):

components = gui.extract_mapping_components(mapping.get_controller_mappings()[0])
assert components == [
Group(
name="SubController01",
layout=SubScreen(labelled=True),
children=[
SignalR(
name="ReadInt",
read_pv="DEVICE:SubController01:ReadInt",
read_widget=TextRead(),
)
],
),
Group(
name="SubController02",
layout=SubScreen(labelled=True),
children=[
SignalR(
name="ReadInt",
read_pv="DEVICE:SubController01:ReadInt",
read_widget=TextRead(),
)
],
),
SignalR(name="BigEnum", read_pv="DEVICE:BigEnum", read_widget=TextRead()),
SignalR(name="ReadBool", read_pv="DEVICE:ReadBool", read_widget=LED()),
SignalR(
Expand Down
147 changes: 111 additions & 36 deletions tests/backends/tango/test_dsr.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,112 @@
import pytest
from pytest_mock import MockerFixture
from tango._tango import AttrWriteType, CmdArgType

from fastcs.backends.tango.dsr import _collect_dev_attributes, _collect_dev_commands


def test_collect_attributes(mapping):
attributes = _collect_dev_attributes(mapping)

# Check that attributes are created and of expected type
assert list(attributes.keys()) == [
"BigEnum",
"ReadBool",
"ReadInt",
"ReadWriteFloat",
"ReadWriteInt",
"StringEnum",
"WriteBool",
]
assert attributes["ReadInt"].attr_write == AttrWriteType.READ
assert attributes["ReadInt"].attr_type == CmdArgType.DevLong64
assert attributes["StringEnum"].attr_write == AttrWriteType.READ_WRITE
assert attributes["StringEnum"].attr_type == CmdArgType.DevString
assert attributes["ReadWriteFloat"].attr_write == AttrWriteType.READ_WRITE
assert attributes["ReadWriteFloat"].attr_type == CmdArgType.DevDouble
assert attributes["WriteBool"].attr_write == AttrWriteType.WRITE
assert attributes["WriteBool"].attr_type == CmdArgType.DevBoolean


@pytest.mark.asyncio
async def test_collect_commands(mapping, mocker: MockerFixture):
commands = _collect_dev_commands(mapping)

# Check that command is created and it can be called
assert list(commands.keys()) == ["Go"]
await commands["Go"](mocker.MagicMock())
from tango import DevState
from tango.test_context import DeviceTestContext

from fastcs.backends.tango.backend import TangoBackend


class TestTangoDevice:
@pytest.fixture(scope="class")
def tango_context(self, assertable_controller):
# https://tango-controls.readthedocs.io/projects/pytango/en/v9.5.1/testing/test_context.html
device = TangoBackend(assertable_controller)._dsr._device
with DeviceTestContext(device) as proxy:
yield proxy

def test_list_attributes(self, tango_context):
assert list(tango_context.get_attribute_list()) == [
"BigEnum",
"ReadBool",
"ReadInt",
"ReadWriteFloat",
"ReadWriteInt",
"StringEnum",
"WriteBool",
"SubController01_ReadInt",
"SubController02_ReadInt",
"State",
"Status",
]

def test_list_commands(self, tango_context):
assert list(tango_context.get_command_list()) == [
"Go",
"Init",
"State",
"Status",
]

def test_state(self, tango_context):
assert tango_context.command_inout("State") == DevState.ON

def test_status(self, tango_context):
expect = "The device is in ON state."
assert tango_context.command_inout("Status") == expect

def test_read_int(self, assertable_controller, tango_context):
expect = 0
with assertable_controller.assert_read_here(["read_int"]):
result = tango_context.read_attribute("ReadInt").value
assert result == expect

def test_read_write_int(self, assertable_controller, tango_context):
expect = 0
with assertable_controller.assert_read_here(["read_write_int"]):
result = tango_context.read_attribute("ReadWriteInt").value
assert result == expect
new = 9
with assertable_controller.assert_write_here(["read_write_int"]):
tango_context.write_attribute("ReadWriteInt", new)
assert tango_context.read_attribute("ReadWriteInt").value == new

def test_read_write_float(self, assertable_controller, tango_context):
expect = 0.0
with assertable_controller.assert_read_here(["read_write_float"]):
result = tango_context.read_attribute("ReadWriteFloat").value
assert result == expect
new = 0.5
with assertable_controller.assert_write_here(["read_write_float"]):
tango_context.write_attribute("ReadWriteFloat", new)
assert tango_context.read_attribute("ReadWriteFloat").value == new

def test_read_bool(self, assertable_controller, tango_context):
expect = False
with assertable_controller.assert_read_here(["read_bool"]):
result = tango_context.read_attribute("ReadBool").value
assert result == expect

def test_write_bool(self, assertable_controller, tango_context):
with assertable_controller.assert_write_here(["write_bool"]):
tango_context.write_attribute("WriteBool", True)

def test_string_enum(self, assertable_controller, tango_context):
expect = ""
with assertable_controller.assert_read_here(["string_enum"]):
result = tango_context.read_attribute("StringEnum").value
assert result == expect
new = "new"
with assertable_controller.assert_write_here(["string_enum"]):
tango_context.write_attribute("StringEnum", new)
assert tango_context.read_attribute("StringEnum").value == new

def test_big_enum(self, assertable_controller, tango_context):
expect = 0
with assertable_controller.assert_read_here(["big_enum"]):
result = tango_context.read_attribute("BigEnum").value
assert result == expect

def test_go(self, assertable_controller, tango_context):
with assertable_controller.assert_execute_here(["go"]):
tango_context.command_inout("Go")

def test_read_child1(self, assertable_controller, tango_context):
expect = 0
with assertable_controller.assert_read_here(["SubController01", "read_int"]):
result = tango_context.read_attribute("SubController01_ReadInt").value
assert result == expect

def test_read_child2(self, assertable_controller, tango_context):
expect = 0
with assertable_controller.assert_read_here(["SubController02", "read_int"]):
result = tango_context.read_attribute("SubController02_ReadInt").value
assert result == expect
Loading

0 comments on commit d1fc038

Please sign in to comment.