Skip to content

Commit cdaf83a

Browse files
committed
Gave each set of backend tests it's own AssertableController
This is needed since not all datatypes are supported on every backend.
1 parent 5327339 commit cdaf83a

File tree

11 files changed

+297
-187
lines changed

11 files changed

+297
-187
lines changed

src/fastcs/transport/epics/gui.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
from pvi._format.dls import DLSFormatter
32
from pvi.device import (
43
LED,

src/fastcs/transport/epics/util.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@
3939
"max_alarm": "HOPR",
4040
"znam": "ZNAM",
4141
"onam": "ONAM",
42-
"shape": "length",
4342
}
4443

4544

@@ -50,12 +49,23 @@ def get_record_metadata_from_attribute(
5049

5150

5251
def get_record_metadata_from_datatype(datatype: DataType[T]) -> dict[str, str]:
53-
return {
52+
arguments = {
5453
DATATYPE_FIELD_TO_RECORD_FIELD[field]: value
5554
for field, value in asdict(datatype).items()
5655
if field in DATATYPE_FIELD_TO_RECORD_FIELD
5756
}
5857

58+
match datatype:
59+
case WaveForm():
60+
if len(datatype.shape) != 1:
61+
raise TypeError(
62+
f"Unsupported shape {datatype.shape}, the EPICS backend only "
63+
"supports to 1D arrays"
64+
)
65+
arguments["length"] = datatype.shape[0]
66+
67+
return arguments
68+
5969

6070
def get_cast_method_to_epics_type(datatype: DataType[T]) -> Callable[[T], object]:
6171
match datatype:

tests/__init__.py

Whitespace-only changes.

tests/assertable_controller.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import copy
2+
from contextlib import contextmanager
3+
from typing import Literal
4+
5+
from pytest_mock import MockerFixture
6+
7+
from fastcs.attributes import AttrR, Handler, Sender, Updater
8+
from fastcs.controller import Controller, SubController
9+
from fastcs.datatypes import Int
10+
from fastcs.wrappers import command, scan
11+
12+
13+
class TestUpdater(Updater):
14+
update_period = 1
15+
16+
async def update(self, controller, attr):
17+
print(f"{controller} update {attr}")
18+
19+
20+
class TestSender(Sender):
21+
async def put(self, controller, attr, value):
22+
print(f"{controller}: {attr} = {value}")
23+
24+
25+
class TestHandler(Handler, TestUpdater, TestSender):
26+
pass
27+
28+
29+
class TestSubController(SubController):
30+
read_int: AttrR = AttrR(Int(), handler=TestUpdater())
31+
32+
33+
class TestController(Controller):
34+
def __init__(self) -> None:
35+
super().__init__()
36+
37+
self._sub_controllers: list[TestSubController] = []
38+
for index in range(1, 3):
39+
controller = TestSubController()
40+
self._sub_controllers.append(controller)
41+
self.register_sub_controller(f"SubController{index:02d}", controller)
42+
43+
initialised = False
44+
connected = False
45+
count = 0
46+
47+
async def initialise(self) -> None:
48+
self.initialised = True
49+
50+
async def connect(self) -> None:
51+
self.connected = True
52+
53+
@command()
54+
async def go(self):
55+
pass
56+
57+
@scan(0.01)
58+
async def counter(self):
59+
self.count += 1
60+
61+
62+
class AssertableController(TestController):
63+
def __init__(self, mocker: MockerFixture) -> None:
64+
self.mocker = mocker
65+
super().__init__()
66+
67+
@contextmanager
68+
def assert_read_here(self, path: list[str]):
69+
yield from self._assert_method(path, "get")
70+
71+
@contextmanager
72+
def assert_write_here(self, path: list[str]):
73+
yield from self._assert_method(path, "process")
74+
75+
@contextmanager
76+
def assert_execute_here(self, path: list[str]):
77+
yield from self._assert_method(path, "")
78+
79+
def _assert_method(self, path: list[str], method: Literal["get", "process", ""]):
80+
"""
81+
This context manager can be used to confirm that a fastcs
82+
controller's respective attribute or command methods are called
83+
a single time within a context block
84+
"""
85+
queue = copy.deepcopy(path)
86+
87+
# Navigate to subcontroller
88+
controller = self
89+
item_name = queue.pop(-1)
90+
for item in queue:
91+
controllers = controller.get_sub_controllers()
92+
controller = controllers[item]
93+
94+
# create probe
95+
if method:
96+
attr = getattr(controller, item_name)
97+
spy = self.mocker.spy(attr, method)
98+
else:
99+
spy = self.mocker.spy(controller, item_name)
100+
initial = spy.call_count
101+
102+
try:
103+
yield # Enter context
104+
except Exception as e:
105+
raise e
106+
else: # Exit context
107+
final = spy.call_count
108+
assert final == initial + 1, (
109+
f"Expected {'.'.join(path + [method] if method else path)} "
110+
f"to be called once, but it was called {final - initial} times."
111+
)

tests/conftest.py

Lines changed: 28 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,45 @@
1-
import copy
2-
import enum
31
import os
42
import random
53
import string
64
import subprocess
75
import time
8-
from contextlib import contextmanager
96
from pathlib import Path
10-
from typing import Any, Literal
7+
from typing import Any
118

12-
import numpy as np
139
import pytest
1410
from aioca import purge_channel_caches
15-
from pytest_mock import MockerFixture
1611

17-
from fastcs.attributes import AttrR, AttrRW, AttrW, Handler, Sender, Updater
18-
from fastcs.controller import Controller, SubController
19-
from fastcs.datatypes import Bool, Enum, Float, Int, String, WaveForm
20-
from fastcs.wrappers import command, scan
12+
from fastcs.attributes import AttrR, AttrRW, AttrW
13+
from fastcs.datatypes import Bool, Float, Int, String
14+
from tests.assertable_controller import (
15+
TestController,
16+
TestHandler,
17+
TestSender,
18+
TestUpdater,
19+
)
2120

2221
DATA_PATH = Path(__file__).parent / "data"
2322

2423

24+
class BackendTestController(TestController):
25+
read_int: AttrR = AttrR(Int(), handler=TestUpdater())
26+
read_write_int: AttrRW = AttrRW(Int(), handler=TestHandler())
27+
read_write_float: AttrRW = AttrRW(Float())
28+
read_bool: AttrR = AttrR(Bool())
29+
write_bool: AttrW = AttrW(Bool(), handler=TestSender())
30+
read_string: AttrRW = AttrRW(String())
31+
big_enum: AttrR = AttrR(
32+
Int(
33+
allowed_values=list(range(17)),
34+
),
35+
)
36+
37+
38+
@pytest.fixture
39+
def controller():
40+
return BackendTestController()
41+
42+
2543
@pytest.fixture
2644
def data() -> Path:
2745
return DATA_PATH
@@ -45,131 +63,6 @@ def pytest_internalerror(excinfo: pytest.ExceptionInfo[Any]):
4563
raise excinfo.value
4664

4765

48-
class TestUpdater(Updater):
49-
update_period = 1
50-
51-
async def update(self, controller, attr):
52-
print(f"{controller} update {attr}")
53-
54-
55-
class TestSender(Sender):
56-
async def put(self, controller, attr, value):
57-
print(f"{controller}: {attr} = {value}")
58-
59-
60-
class TestHandler(Handler, TestUpdater, TestSender):
61-
pass
62-
63-
64-
class TestSubController(SubController):
65-
read_int: AttrR = AttrR(Int(), handler=TestUpdater())
66-
67-
68-
class TestController(Controller):
69-
def __init__(self) -> None:
70-
super().__init__()
71-
72-
self._sub_controllers: list[TestSubController] = []
73-
for index in range(1, 3):
74-
controller = TestSubController()
75-
self._sub_controllers.append(controller)
76-
self.register_sub_controller(f"SubController{index:02d}", controller)
77-
78-
read_int: AttrR = AttrR(Int(), handler=TestUpdater())
79-
read_write_int: AttrRW = AttrRW(Int(), handler=TestHandler())
80-
read_write_float: AttrRW = AttrRW(Float())
81-
read_bool: AttrR = AttrR(Bool())
82-
write_bool: AttrW = AttrW(Bool(), handler=TestSender())
83-
read_string: AttrRW = AttrRW(String())
84-
enum: AttrRW = AttrRW(Enum(enum.IntEnum("Enum", {"RED": 0, "GREEN": 1, "BLUE": 2})))
85-
one_d_waveform: AttrRW = AttrRW(WaveForm(np.int32, (10,)))
86-
two_d_waveform: AttrRW = AttrRW(WaveForm(np.int32, (10, 10)))
87-
big_enum: AttrR = AttrR(
88-
Int(
89-
allowed_values=list(range(17)),
90-
),
91-
)
92-
93-
initialised = False
94-
connected = False
95-
count = 0
96-
97-
async def initialise(self) -> None:
98-
self.initialised = True
99-
100-
async def connect(self) -> None:
101-
self.connected = True
102-
103-
@command()
104-
async def go(self):
105-
pass
106-
107-
@scan(0.01)
108-
async def counter(self):
109-
self.count += 1
110-
111-
112-
class AssertableController(TestController):
113-
def __init__(self, mocker: MockerFixture) -> None:
114-
super().__init__()
115-
self.mocker = mocker
116-
117-
@contextmanager
118-
def assert_read_here(self, path: list[str]):
119-
yield from self._assert_method(path, "get")
120-
121-
@contextmanager
122-
def assert_write_here(self, path: list[str]):
123-
yield from self._assert_method(path, "process")
124-
125-
@contextmanager
126-
def assert_execute_here(self, path: list[str]):
127-
yield from self._assert_method(path, "")
128-
129-
def _assert_method(self, path: list[str], method: Literal["get", "process", ""]):
130-
"""
131-
This context manager can be used to confirm that a fastcs
132-
controller's respective attribute or command methods are called
133-
a single time within a context block
134-
"""
135-
queue = copy.deepcopy(path)
136-
137-
# Navigate to subcontroller
138-
controller = self
139-
item_name = queue.pop(-1)
140-
for item in queue:
141-
controllers = controller.get_sub_controllers()
142-
controller = controllers[item]
143-
144-
# create probe
145-
if method:
146-
attr = getattr(controller, item_name)
147-
spy = self.mocker.spy(attr, method)
148-
else:
149-
spy = self.mocker.spy(controller, item_name)
150-
initial = spy.call_count
151-
try:
152-
yield # Enter context
153-
except Exception as e:
154-
raise e
155-
else: # Exit context
156-
final = spy.call_count
157-
assert final == initial + 1, (
158-
f"Expected {'.'.join(path + [method] if method else path)} "
159-
f"to be called once, but it was called {final - initial} times."
160-
)
161-
162-
163-
@pytest.fixture
164-
def controller():
165-
return TestController()
166-
167-
168-
@pytest.fixture(scope="class")
169-
def assertable_controller(class_mocker: MockerFixture):
170-
return AssertableController(class_mocker)
171-
172-
17366
PV_PREFIX = "".join(random.choice(string.ascii_lowercase) for _ in range(12))
17467
HERE = Path(os.path.dirname(os.path.abspath(__file__)))
17568

tests/test_launch.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ def test_over_defined_schema():
9292
error = (
9393
""
9494
"Expected no more than 2 arguments for 'ManyArgs.__init__' "
95-
"but received 3 as `(self, arg: test_launch.SomeConfig, too_many)`"
95+
"but received 3 as `(self, arg: tests.test_launch.SomeConfig, too_many)`"
9696
)
9797

9898
with pytest.raises(LaunchError) as exc_info:

tests/transport/epics/test_gui.py

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
from pvi.device import (
22
LED,
33
ButtonPanel,
4-
ComboBox,
54
Group,
65
SignalR,
76
SignalRW,
87
SignalW,
98
SignalX,
109
SubScreen,
11-
TextFormat,
1210
TextRead,
1311
TextWrite,
1412
ToggleButton,
@@ -47,19 +45,12 @@ def test_get_components(controller):
4745
children=[
4846
SignalR(
4947
name="ReadInt",
50-
read_pv="DEVICE:SubController01:ReadInt",
48+
read_pv="DEVICE:SubController02:ReadInt",
5149
read_widget=TextRead(),
5250
)
5351
],
5452
),
5553
SignalR(name="BigEnum", read_pv="DEVICE:BigEnum", read_widget=TextRead()),
56-
SignalRW(
57-
name="Enum",
58-
read_pv="DEVICE:Enum_RBV",
59-
read_widget=TextRead(format=TextFormat.string),
60-
write_pv="DEVICE:Enum",
61-
write_widget=ComboBox(choices=["RED", "GREEN", "BLUE"]),
62-
),
6354
SignalR(name="ReadBool", read_pv="DEVICE:ReadBool", read_widget=LED()),
6455
SignalR(
6556
name="ReadInt",

0 commit comments

Comments
 (0)