Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
160 changes: 124 additions & 36 deletions tests/backends/tango/test_dsr.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,125 @@
import copy
import re
from typing import Any

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.attributes import AttrR
from fastcs.backends.tango.backend import TangoBackend
from fastcs.datatypes import Bool, Float, Int, String


def pascal_2_snake(input: list[str]) -> list[str]:
"""
Converts the last entry in a list of strings
"""
snake_list = copy.deepcopy(input)
snake_list[-1] = re.sub(r"(?<!^)(?=[A-Z])", "_", snake_list[-1]).lower()
return snake_list


class TestTangoDevice:
@pytest.fixture(scope="class", autouse=True)
def setup_class(self, assertable_controller):
self.controller = assertable_controller

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

@pytest.fixture(scope="class")
def client_read(self, tango_context):
def _read_attribute(path: list[str], expected: Any):
attribute = "_".join(path)
with self.controller.assertPerformed(pascal_2_snake(path), "READ"):
result = tango_context.read_attribute(attribute).value
assert result == expected

return _read_attribute

@pytest.fixture(scope="class")
def client_write(self, tango_context):
def _write_attribute(path: list[str], expected: Any):
attribute = "_".join(path)
with self.controller.assertPerformed(pascal_2_snake(path), "WRITE"):
tango_context.write_attribute(attribute, expected)

return _write_attribute

@pytest.fixture(scope="class")
def client_exec(self, tango_context):
def _exec_command(path: list[str]):
command = "_".join(path)
with self.controller.assertPerformed(pascal_2_snake(path), "EXECUTE"):
tango_context.command_inout(command)

return _exec_command

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):
expected = "The device is in ON state."
assert tango_context.command_inout("Status") == expected

def test_read_int(self, client_read):
client_read(["ReadInt"], AttrR(Int())._value)

def test_read_write_int(self, client_read, client_write):
client_read(["ReadWriteInt"], AttrR(Int())._value)
client_write(["ReadWriteInt"], AttrR(Int())._value)

def test_read_write_float(self, client_read, client_write):
client_read(["ReadWriteFloat"], AttrR(Float())._value)
client_write(["ReadWriteFloat"], AttrR(Float())._value)

def test_read_bool(self, client_read):
client_read(["ReadBool"], AttrR(Bool())._value)

def test_write_bool(self, client_write):
client_write(["WriteBool"], AttrR(Bool())._value)

def test_string_enum(self, client_read, client_write):
enum = AttrR(String(), allowed_values=["red", "green", "blue"])._value
client_read(["StringEnum"], enum)
client_write(["StringEnum"], enum)

def test_big_enum(self, client_read):
client_read(["BigEnum"], AttrR(Int(), allowed_values=list(range(1, 18)))._value)

def test_go(self, client_exec):
client_exec(["Go"])

def test_read_child1(self, client_read):
client_read(["SubController01", "ReadInt"], AttrR(Int())._value)

def test_read_child2(self, client_read):
client_read(["SubController02", "ReadInt"], AttrR(Int())._value)
Loading