Skip to content

Commit c33a21a

Browse files
committed
Better tests, proper methods
1 parent b0310db commit c33a21a

File tree

3 files changed

+124
-44
lines changed

3 files changed

+124
-44
lines changed

src/fastcs/backends/rest/rest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def _wrap_attr_put(
5454
attribute: AttrW[T],
5555
) -> Callable[[T], Coroutine[Any, Any, None]]:
5656
async def attr_set(request):
57-
await attribute.process_without_display_update(request.value)
57+
await attribute.process(request.value)
5858

5959
# Fast api uses type annotations for validation, schema, conversions
6060
attr_set.__annotations__["request"] = _put_request_body(attribute)
@@ -124,7 +124,7 @@ def _wrap_command(
124124
method: Callable, controller: BaseController
125125
) -> Callable[..., Awaitable[None]]:
126126
async def command() -> None:
127-
await MethodType(method, controller)()
127+
await method.__get__(controller)()
128128

129129
return command
130130

tests/backends/rest/test_rest.py

Lines changed: 62 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,79 @@
1+
import copy
2+
import re
3+
from typing import Any
4+
5+
import pytest
6+
from fastapi.testclient import TestClient
7+
from pytest_mock import MockerFixture
8+
from tests.conftest import AssertableController
9+
110
from fastcs.attributes import AttrR
11+
from fastcs.backends.rest.backend import RestBackend
212
from fastcs.datatypes import Bool, Float, Int
313

414

15+
def pascal_2_snake(input: list[str]) -> list[str]:
16+
snake_list = copy.deepcopy(input)
17+
snake_list[-1] = re.sub(r"(?<!^)(?=[A-Z])", "_", snake_list[-1]).lower()
18+
return snake_list
19+
20+
521
class TestRestServer:
6-
def test_read_int(self, rest_client):
7-
response = rest_client.get("/ReadInt")
8-
assert response.status_code == 200
9-
assert response.json() == {"value": AttrR(Int())._value}
22+
@pytest.fixture(autouse=True)
23+
def setup_tests(self, mocker: MockerFixture):
24+
self.controller = AssertableController(mocker)
25+
app = RestBackend(self.controller)._server._app
26+
self.client = TestClient(app)
1027

11-
def test_read_write_int(self, rest_client):
12-
response = rest_client.get("/ReadWriteInt")
28+
def client_read(self, path: list[str], expected: Any):
29+
route = "/" + "/".join(path)
30+
with self.controller.assertPerformed(pascal_2_snake(path), "READ"):
31+
response = self.client.get(route)
1332
assert response.status_code == 200
14-
assert response.json() == {"value": AttrR(Int())._value}
15-
response = rest_client.put("/ReadWriteInt", json={"value": AttrR(Int())._value})
33+
assert response.json()["value"] == expected
34+
35+
def client_write(self, path: list[str], value: Any):
36+
route = "/" + "/".join(path)
37+
with self.controller.assertPerformed(pascal_2_snake(path), "WRITE"):
38+
response = self.client.put(route, json={"value": value})
1639
assert response.status_code == 204
1740

18-
def test_read_write_float(self, rest_client):
19-
response = rest_client.get("/ReadWriteFloat")
20-
assert response.status_code == 200
21-
assert response.json() == {"value": AttrR(Float())._value}
22-
response = rest_client.put(
23-
"/ReadWriteFloat", json={"value": AttrR(Float())._value}
24-
)
41+
def client_exec(self, path: list[str]):
42+
route = "/" + "/".join(path)
43+
with self.controller.assertPerformed(pascal_2_snake(path), "EXECUTE"):
44+
response = self.client.put(route)
2545
assert response.status_code == 204
2646

27-
def test_read_bool(self, rest_client):
28-
response = rest_client.get("/ReadBool")
29-
assert response.status_code == 200
30-
assert response.json() == {"value": AttrR(Bool())._value}
47+
def test_read_int(self):
48+
self.client_read(["ReadInt"], AttrR(Int())._value)
3149

32-
def test_write_bool(self, rest_client):
33-
response = rest_client.put("/WriteBool", json={"value": AttrR(Bool())._value})
34-
assert response.status_code == 204
50+
def test_read_write_int(self):
51+
self.client_read(["ReadWriteInt"], AttrR(Int())._value)
52+
self.client_write(["ReadWriteInt"], AttrR(Int())._value)
53+
54+
def test_read_write_float(self):
55+
self.client_read(["ReadWriteFloat"], AttrR(Float())._value)
56+
self.client_write(["ReadWriteFloat"], AttrR(Float())._value)
57+
58+
def test_read_bool(self):
59+
self.client_read(["ReadBool"], AttrR(Bool())._value)
60+
61+
def test_write_bool(self):
62+
self.client_write(["WriteBool"], AttrR(Bool())._value)
3563

3664
# # We need to discuss enums
3765
# def test_string_enum(self, rest_client):
3866

39-
def test_big_enum(self, rest_client):
40-
response = rest_client.get("/BigEnum")
41-
assert response.status_code == 200
42-
assert response.json() == {
43-
"value": AttrR(Int(), allowed_values=list(range(1, 18)))._value
44-
}
67+
def test_big_enum(self):
68+
self.client_read(
69+
["BigEnum"], AttrR(Int(), allowed_values=list(range(1, 18)))._value
70+
)
4571

46-
def test_go(self, rest_client):
47-
response = rest_client.put("/Go")
48-
assert response.status_code == 204
72+
def test_go(self):
73+
self.client_exec(["Go"])
74+
75+
def test_read_child1(self):
76+
self.client_read(["SubController01", "ReadInt"], AttrR(Int())._value)
77+
78+
def test_read_child2(self):
79+
self.client_read(["SubController02", "ReadInt"], AttrR(Int())._value)

tests/conftest.py

Lines changed: 60 additions & 11 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 fastapi.testclient import TestClient
13+
from pytest_mock import MockerFixture
1214

1315
from fastcs.attributes import AttrR, AttrRW, AttrW, Handler, Sender, Updater
14-
from fastcs.backends.rest.backend import RestBackend
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())
@@ -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 rest_client():
129-
app = RestBackend(TestController())._server._app
130-
client = TestClient(app)
131-
return client

0 commit comments

Comments
 (0)