Skip to content

Commit 1a079b9

Browse files
committed
Work on tango tests
1 parent ae978a9 commit 1a079b9

File tree

4 files changed

+195
-69
lines changed

4 files changed

+195
-69
lines changed

src/fastcs/backends/tango/dsr.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
@dataclass
1717
class TangoDSROptions:
1818
dev_name: str = "MY/DEVICE/NAME"
19-
dev_class: str = "FAST_CS_DEVICE"
2019
dsr_instance: str = "MY_SERVER_INSTANCE"
2120
debug: bool = False
2221

@@ -44,7 +43,7 @@ def _wrap_updater_fset(
4443
) -> Callable[[Any, Any], Any]:
4544
async def fset(tango_device: Device, val):
4645
tango_device.info_stream(f"called fset method: {attr_name}")
47-
await attribute.process_without_display_update(val)
46+
await attribute.process(val)
4847

4948
return fset
5049

@@ -101,7 +100,7 @@ def _wrap_command_f(
101100
) -> Callable[..., Awaitable[None]]:
102101
async def _dynamic_f(tango_device: Device) -> None:
103102
tango_device.info_stream(f"called {controller} f method: {method_name}")
104-
return await MethodType(method, controller)()
103+
return await method.__get__(controller)()
105104

106105
_dynamic_f.__name__ = method_name
107106
return _dynamic_f
@@ -155,11 +154,10 @@ def _collect_dsr_args(options: TangoDSROptions) -> list[str]:
155154
class TangoDSR:
156155
def __init__(self, mapping: Mapping):
157156
self._mapping = mapping
157+
self.dev_class = self._mapping.controller.__class__.__name__
158+
self._device = self._create_device()
158159

159-
def run(self, options: TangoDSROptions | None = None) -> None:
160-
if options is None:
161-
options = TangoDSROptions()
162-
160+
def _create_device(self):
163161
class_dict: dict = {
164162
**_collect_dev_attributes(self._mapping),
165163
**_collect_dev_commands(self._mapping),
@@ -169,13 +167,18 @@ def run(self, options: TangoDSROptions | None = None) -> None:
169167
}
170168

171169
class_bases = (server.Device,)
172-
pytango_class = type(options.dev_class, class_bases, class_dict)
170+
pytango_class = type(self.dev_class, class_bases, class_dict)
171+
return pytango_class
172+
173+
def run(self, options: TangoDSROptions | None = None) -> None:
174+
if options is None:
175+
options = TangoDSROptions()
173176

174177
dsr_args = _collect_dsr_args(options)
175178

176179
server.run(
177-
(pytango_class,),
178-
[options.dev_class, options.dsr_instance, *dsr_args],
180+
(self._device,),
181+
[self.dev_class, options.dsr_instance, *dsr_args],
179182
green_mode=server.GreenMode.Asyncio,
180183
)
181184

tests/backends/tango/compose/compose.yaml

Lines changed: 0 additions & 22 deletions
This file was deleted.

tests/backends/tango/test_dsr.py

Lines changed: 122 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,124 @@
1+
import copy
2+
import re
3+
from typing import Any
4+
15
import pytest
26
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())
7+
from tango import DevState
8+
from tango.test_context import DeviceTestContext
9+
from tests.conftest import AssertableController
10+
11+
from fastcs.attributes import AttrR
12+
from fastcs.backends.tango.backend import TangoBackend
13+
from fastcs.datatypes import Bool, Float, Int
14+
15+
16+
def pascal_2_snake(input: list[str]) -> list[str]:
17+
snake_list = copy.deepcopy(input)
18+
snake_list[-1] = re.sub(r"(?<!^)(?=[A-Z])", "_", snake_list[-1]).lower()
19+
return snake_list
20+
21+
22+
class TestTangoDevice:
23+
@pytest.fixture(scope="class", autouse=True)
24+
def setup_class(self, class_mocker: MockerFixture):
25+
self.controller = AssertableController(class_mocker)
26+
27+
@pytest.fixture(scope="class")
28+
def tango_context(self):
29+
device = TangoBackend(self.controller)._dsr._device
30+
with DeviceTestContext(device) as proxy:
31+
yield proxy
32+
33+
@pytest.fixture(scope="class")
34+
def client_read(self, tango_context):
35+
def _read_attribute(path: list[str], expected: Any):
36+
attribute = "_".join(path)
37+
with self.controller.assertPerformed(pascal_2_snake(path), "READ"):
38+
result = tango_context.read_attribute(attribute).value
39+
assert result == expected
40+
41+
return _read_attribute
42+
43+
@pytest.fixture(scope="class")
44+
def client_write(self, tango_context):
45+
def _write_attribute(path: list[str], expected: Any):
46+
attribute = "_".join(path)
47+
with self.controller.assertPerformed(pascal_2_snake(path), "WRITE"):
48+
tango_context.write_attribute(attribute, expected)
49+
50+
return _write_attribute
51+
52+
@pytest.fixture(scope="class")
53+
def client_exec(self, tango_context):
54+
def _exec_command(path: list[str]):
55+
command = "_".join(path)
56+
with self.controller.assertPerformed(pascal_2_snake(path), "EXECUTE"):
57+
tango_context.command_inout(command)
58+
59+
return _exec_command
60+
61+
def test_list_attributes(self, tango_context):
62+
assert list(tango_context.get_attribute_list()) == [
63+
"BigEnum",
64+
"ReadBool",
65+
"ReadInt",
66+
"ReadWriteFloat",
67+
"ReadWriteInt",
68+
"StringEnum",
69+
"WriteBool",
70+
"SubController01_ReadInt",
71+
"SubController02_ReadInt",
72+
"State",
73+
"Status",
74+
]
75+
76+
def test_list_commands(self, tango_context):
77+
assert list(tango_context.get_command_list()) == [
78+
"Go",
79+
"Init",
80+
"State",
81+
"Status",
82+
]
83+
84+
def test_state(self, tango_context):
85+
assert tango_context.command_inout("State") == DevState.ON
86+
87+
def test_status(self, tango_context):
88+
expected = "The device is in ON state."
89+
assert tango_context.command_inout("Status") == expected
90+
91+
def test_read_int(self, client_read):
92+
client_read(["ReadInt"], AttrR(Int())._value)
93+
94+
def test_read_write_int(self, client_read, client_write):
95+
client_read(["ReadWriteInt"], AttrR(Int())._value)
96+
client_write(["ReadWriteInt"], AttrR(Int())._value)
97+
98+
def test_read_write_float(self, client_read, client_write):
99+
client_read(["ReadWriteFloat"], AttrR(Float())._value)
100+
client_write(["ReadWriteFloat"], AttrR(Float())._value)
101+
102+
def test_read_bool(self, client_read):
103+
client_read(["ReadBool"], AttrR(Bool())._value)
104+
105+
def test_write_bool(self, client_write):
106+
client_write(["WriteBool"], AttrR(Bool())._value)
107+
108+
# # We need to discuss enums
109+
# def test_string_enum(self, client_read, client_write):
110+
# client_read(["StringEnum"], "green")
111+
# client_write(["StringEnum"], "green")
112+
113+
def test_big_enum(self, client_read):
114+
client_read(["BigEnum"], AttrR(Int(), allowed_values=list(range(1, 18)))._value)
115+
116+
# The method works but does not seem linked to the device instance?
117+
# def test_go(self, client_exec):
118+
# client_exec(["Go"])
119+
120+
def test_read_child1(self, client_read):
121+
client_read(["SubController01", "ReadInt"], AttrR(Int())._value)
122+
123+
def test_read_child2(self, client_read):
124+
client_read(["SubController02", "ReadInt"], AttrR(Int())._value)

tests/conftest.py

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
1+
import copy
12
import os
23
import random
34
import string
45
import subprocess
56
import time
7+
from contextlib import contextmanager
68
from pathlib import Path
7-
from typing import Any
9+
from typing import Any, Literal
810

911
import pytest
1012
from aioca import purge_channel_caches
13+
from pytest_mock import MockerFixture
1114

1215
from fastcs.attributes import AttrR, AttrRW, AttrW, Handler, Sender, Updater
13-
from fastcs.controller import Controller
16+
from fastcs.controller import Controller, SubController
1417
from fastcs.datatypes import Bool, Float, Int, String
1518
from fastcs.mapping import Mapping
1619
from fastcs.wrappers import command, scan
@@ -49,7 +52,20 @@ class TestHandler(Handler, TestUpdater, TestSender):
4952
pass
5053

5154

55+
class TestSubController(SubController):
56+
read_int: AttrR = AttrR(Int(), handler=TestUpdater())
57+
58+
5259
class TestController(Controller):
60+
def __init__(self) -> None:
61+
super().__init__()
62+
63+
self._sub_controllers: list[TestSubController] = []
64+
for index in range(1, 3):
65+
controller = TestSubController()
66+
self._sub_controllers.append(controller)
67+
self.register_sub_controller(f"SubController{index:02d}", controller)
68+
5369
read_int: AttrR = AttrR(Int(), handler=TestUpdater())
5470
read_write_int: AttrRW = AttrRW(Int(), handler=TestHandler())
5571
read_write_float: AttrRW = AttrRW(Float())
@@ -80,6 +96,48 @@ async def counter(self):
8096
self.count += 1
8197

8298

99+
class AssertableController(TestController):
100+
def __init__(self, mocker: MockerFixture) -> None:
101+
super().__init__()
102+
self.mocker = mocker
103+
104+
@contextmanager
105+
def assertPerformed(
106+
self, path: list[str], action: Literal["READ", "WRITE", "EXECUTE"]
107+
):
108+
queue = copy.deepcopy(path)
109+
match action:
110+
case "READ":
111+
method = "get"
112+
case "WRITE":
113+
method = "process"
114+
case "EXECUTE":
115+
method = ""
116+
117+
# Navigate to subcontroller
118+
controller = self
119+
item_name = queue.pop(-1)
120+
for item in queue:
121+
controllers = controller.get_sub_controllers()
122+
controller = controllers[item]
123+
124+
# create probe
125+
if method:
126+
attr = getattr(controller, item_name)
127+
spy = self.mocker.spy(attr, method)
128+
else:
129+
spy = self.mocker.spy(controller, item_name)
130+
initial = spy.call_count
131+
try:
132+
yield # Enter context
133+
finally: # Exit context
134+
final = spy.call_count
135+
assert final == initial + 1, (
136+
f"Expected {'.'.join(path + [method] if method else path)} "
137+
f"to be called once, but it was called {final - initial} times."
138+
)
139+
140+
83141
@pytest.fixture
84142
def controller():
85143
return TestController()

0 commit comments

Comments
 (0)