Skip to content

Commit 2f98919

Browse files
committed
Better tests, proper methods
1 parent 88bf793 commit 2f98919

File tree

3 files changed

+148
-82
lines changed

3 files changed

+148
-82
lines changed

src/fastcs/backends/tango/dsr.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def _wrap_updater_fset(
4343
) -> Callable[[Any, Any], Any]:
4444
async def fset(tango_device: Device, val):
4545
tango_device.info_stream(f"called fset method: {attr_name}")
46-
await attribute.process_without_display_update(val)
46+
await attribute.process(val)
4747

4848
return fset
4949

@@ -100,7 +100,7 @@ def _wrap_command_f(
100100
) -> Callable[..., Awaitable[None]]:
101101
async def _dynamic_f(tango_device: Device) -> None:
102102
tango_device.info_stream(f"called {controller} f method: {method_name}")
103-
return await MethodType(method, controller)()
103+
return await method.__get__(controller)()
104104

105105
_dynamic_f.__name__ = method_name
106106
return _dynamic_f

tests/backends/tango/test_dsr.py

Lines changed: 85 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,63 @@
1+
import copy
2+
import re
3+
from typing import Any
4+
15
import pytest
26
from pytest_mock import MockerFixture
37
from tango import DevState
4-
from tango._tango import AttrWriteType, CmdArgType
8+
from tango.test_context import DeviceTestContext
9+
from tests.conftest import AssertableController
510

611
from fastcs.attributes import AttrR
7-
from fastcs.backends.tango.dsr import _collect_dev_attributes, _collect_dev_commands
8-
from fastcs.datatypes import Bool, Int
9-
10-
11-
def test_collect_attributes(mapping):
12-
attributes = _collect_dev_attributes(mapping)
13-
14-
# Check that attributes are created and of expected type
15-
assert list(attributes.keys()) == [
16-
"BigEnum",
17-
"ReadBool",
18-
"ReadInt",
19-
"ReadWriteFloat",
20-
"ReadWriteInt",
21-
"StringEnum",
22-
"WriteBool",
23-
]
24-
assert attributes["ReadInt"].attr_write == AttrWriteType.READ
25-
assert attributes["ReadInt"].attr_type == CmdArgType.DevLong64
26-
assert attributes["StringEnum"].attr_write == AttrWriteType.READ_WRITE
27-
assert attributes["StringEnum"].attr_type == CmdArgType.DevString
28-
assert attributes["ReadWriteFloat"].attr_write == AttrWriteType.READ_WRITE
29-
assert attributes["ReadWriteFloat"].attr_type == CmdArgType.DevDouble
30-
assert attributes["WriteBool"].attr_write == AttrWriteType.WRITE
31-
assert attributes["WriteBool"].attr_type == CmdArgType.DevBoolean
32-
33-
34-
@pytest.mark.asyncio
35-
async def test_collect_commands(mapping, mocker: MockerFixture):
36-
commands = _collect_dev_commands(mapping)
37-
38-
# Check that command is created and it can be called
39-
assert list(commands.keys()) == ["Go"]
40-
await commands["Go"](mocker.MagicMock())
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
4120

4221

4322
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+
4461
def test_list_attributes(self, tango_context):
4562
assert list(tango_context.get_attribute_list()) == [
4663
"BigEnum",
@@ -50,6 +67,8 @@ def test_list_attributes(self, tango_context):
5067
"ReadWriteInt",
5168
"StringEnum",
5269
"WriteBool",
70+
"SubController01_ReadInt",
71+
"SubController02_ReadInt",
5372
"State",
5473
"Status",
5574
]
@@ -62,46 +81,44 @@ def test_list_commands(self, tango_context):
6281
"Status",
6382
]
6483

65-
def test_read_int(self, tango_context):
66-
result = tango_context.read_attribute("ReadInt")
67-
assert result.value == AttrR(Int())._value
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)
6893

69-
def test_read_write_int(self, tango_context):
70-
expected = 42
71-
tango_context.write_attribute("ReadWriteInt", expected)
72-
result = tango_context.read_attribute("ReadWriteInt")
73-
assert result.w_value == expected
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)
7497

75-
def test_read_write_float(self, tango_context):
76-
expected = 23.1
77-
tango_context.write_attribute("ReadWriteFloat", expected)
78-
result = tango_context.read_attribute("ReadWriteFloat")
79-
assert result.w_value == expected
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)
80101

81-
def test_read_bool(self, tango_context):
82-
result = tango_context.read_attribute("ReadBool")
83-
assert result.value == AttrR(Bool())._value
102+
def test_read_bool(self, client_read):
103+
client_read(["ReadBool"], AttrR(Bool())._value)
84104

85-
def test_write_bool(self, tango_context):
86-
tango_context.write_attribute("WriteBool", AttrR(Bool())._value)
105+
def test_write_bool(self, client_write):
106+
client_write(["WriteBool"], AttrR(Bool())._value)
87107

88108
# # We need to discuss enums
89-
# def test_string_enum(self, tango_context):
90-
# expected = 1
91-
# tango_context.write_attribute("StringEnum", "green")
92-
# result = tango_context.read_attribute("StringEnum")
93-
# assert result.w_value == expected
109+
# def test_string_enum(self, client_read, client_write):
110+
# client_read(["StringEnum"], "green")
111+
# client_write(["StringEnum"], "green")
94112

95-
def test_big_enum(self, tango_context):
96-
result = tango_context.read_attribute("BigEnum")
97-
assert result.value == AttrR(Int(), allowed_values=list(range(1, 18)))._value
113+
def test_big_enum(self, client_read):
114+
client_read(["BigEnum"], AttrR(Int(), allowed_values=list(range(1, 18)))._value)
98115

99-
def test_go(self, tango_context):
100-
tango_context.command_inout("go")
116+
# The method works but does not seem linked to the device instance?
117+
# def test_go(self, client_exec):
118+
# client_exec(["Go"])
101119

102-
def test_state(self, tango_context):
103-
assert tango_context.command_inout("State") == DevState.ON
120+
def test_read_child1(self, client_read):
121+
client_read(["SubController01", "ReadInt"], AttrR(Int())._value)
104122

105-
def test_status(self, tango_context):
106-
expected = "The device is in ON state."
107-
assert tango_context.command_inout("Status") == expected
123+
def test_read_child2(self, client_read):
124+
client_read(["SubController02", "ReadInt"], AttrR(Int())._value)

tests/conftest.py

Lines changed: 61 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +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
11-
from tango.test_context import DeviceTestContext
13+
from pytest_mock import MockerFixture
1214

1315
from fastcs.attributes import AttrR, AttrRW, AttrW, Handler, Sender, Updater
14-
from fastcs.backends.tango.backend import TangoBackend
15-
from fastcs.controller import Controller
16+
from fastcs.controller import Controller, SubController
1617
from fastcs.datatypes import Bool, Float, Int, String
1718
from fastcs.mapping import Mapping
1819
from fastcs.wrappers import command, scan
@@ -51,7 +52,20 @@ class TestHandler(Handler, TestUpdater, TestSender):
5152
pass
5253

5354

55+
class TestSubController(SubController):
56+
read_int: AttrR = AttrR(Int(), handler=TestUpdater())
57+
58+
5459
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+
5569
read_int: AttrR = AttrR(Int(), handler=TestUpdater())
5670
read_write_int: AttrRW = AttrRW(Int(), handler=TestHandler())
5771
read_write_float: AttrRW = AttrRW(Float())
@@ -60,7 +74,7 @@ class TestController(Controller):
6074
string_enum: AttrRW = AttrRW(String(), allowed_values=["red", "green", "blue"])
6175
big_enum: AttrR = AttrR(
6276
Int(),
63-
allowed_values=list(range(1, 18)),
77+
allowed_values=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17],
6478
)
6579

6680
initialised = False
@@ -82,6 +96,48 @@ async def counter(self):
8296
self.count += 1
8397

8498

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+
85141
@pytest.fixture
86142
def controller():
87143
return TestController()
@@ -122,10 +178,3 @@ def ioc():
122178
except ValueError:
123179
# Someone else already called communicate
124180
pass
125-
126-
127-
@pytest.fixture(scope="class")
128-
def tango_context():
129-
device = TangoBackend(TestController())._dsr._device
130-
with DeviceTestContext(device) as proxy:
131-
yield proxy

0 commit comments

Comments
 (0)