Skip to content

Commit 766fc6d

Browse files
committed
add tests for UserClient
1 parent 8bddf46 commit 766fc6d

File tree

5 files changed

+232
-8
lines changed

5 files changed

+232
-8
lines changed

.vscode/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@
1414
},
1515
"[toml]": {
1616
"editor.defaultFormatter": "tamasfe.even-better-toml",
17-
}
17+
},
18+
"python-envs.pythonProjects": []
1819
}

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ dev = [
5050
"pre-commit>=3.8.0",
5151
"pydata-sphinx-theme>=0.15.4",
5252
"pytest",
53-
"pyright<1.1.407", # https://github.com/bluesky/scanspec/issues/190
53+
"pyright<1.1.407", # https://github.com/bluesky/scanspec/issues/190
5454
"pytest-cov",
5555
"pytest-asyncio",
5656
"responses",
@@ -69,6 +69,7 @@ dev = [
6969
"mock",
7070
"jwcrypto",
7171
"deepdiff",
72+
"dls-dodal",
7273
]
7374

7475
[project.scripts]

src/blueapi/client/client.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,10 @@ def __init__(
4848
self._rest = rest
4949
self._events = events
5050

51-
@classmethod
52-
def from_config(cls, config: ApplicationConfig) -> "BlueapiClient":
51+
@staticmethod
52+
def config_to_rest_and_events(
53+
config: ApplicationConfig,
54+
) -> tuple[BlueapiRestClient, EventBusClient | None]:
5355
session_manager: SessionManager | None = None
5456
try:
5557
session_manager = SessionManager.from_cache(config.auth_token_path)
@@ -67,9 +69,14 @@ def from_config(cls, config: ApplicationConfig) -> "BlueapiClient":
6769
)
6870
)
6971
events = EventBusClient(client)
70-
return cls(rest, events)
72+
return rest, events
7173
else:
72-
return cls(rest)
74+
return rest, None
75+
76+
@classmethod
77+
def from_config(cls, config: ApplicationConfig) -> "BlueapiClient":
78+
rest, events = BlueapiClient.config_to_rest_and_events(config)
79+
return cls(rest, events)
7380

7481
@start_as_current_span(TRACER)
7582
def get_plans(self) -> PlanResponse:

src/blueapi/client/user_client.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@ def __init__(
6868
config_loader = ConfigLoader(ApplicationConfig)
6969
config_loader.use_values_from_yaml(blueapi_config_path)
7070
loaded_config = config_loader.load()
71-
blueapi_class = BlueapiClient.from_config(loaded_config)
72-
super().__init__(blueapi_class._rest, blueapi_class._events) # noqa
71+
rest, events = BlueapiClient.config_to_rest_and_events(loaded_config)
72+
super().__init__(rest, events)
7373

7474
def _convert_args_to_kwargs(self, plan: Callable, args: tuple) -> dict:
7575
"""Converts args to kwargs
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
from unittest.mock import Mock, patch
2+
3+
import pytest
4+
from dodal.plans.wrapped import count
5+
6+
from blueapi.client.client import BlueapiClient
7+
from blueapi.client.event_bus import EventBusClient
8+
from blueapi.client.rest import BlueapiRestClient
9+
from blueapi.client.user_client import UserClient
10+
from blueapi.service.model import DeviceResponse, PlanResponse
11+
12+
BLUEAPI_CONFIG_PATH = (
13+
"/workspaces/blueapi/tests/unit_tests/valid_example_config/client.yaml"
14+
)
15+
16+
17+
class MockDevice:
18+
def __init__(self, device: str):
19+
self.device = device
20+
self.name = device
21+
22+
23+
class MockResponse:
24+
def __init__(self, devices: list):
25+
self.devices = devices
26+
27+
28+
@pytest.fixture(autouse=True)
29+
def client():
30+
client = UserClient(BLUEAPI_CONFIG_PATH, "cm12345-1", callback=True)
31+
32+
client._rest = Mock(BlueapiRestClient)
33+
client._events = Mock(EventBusClient)
34+
35+
client._events.__enter__ = Mock(return_value=client._events)
36+
client._events.__exit__ = Mock(return_value=None)
37+
38+
return client
39+
40+
41+
@pytest.fixture(autouse=True)
42+
def client_without_callback():
43+
client_without_callback = UserClient(
44+
BLUEAPI_CONFIG_PATH, "cm12345-1", callback=False
45+
)
46+
47+
return client_without_callback
48+
49+
50+
def test_blueapi_python_client(client: UserClient):
51+
assert isinstance(client, BlueapiClient)
52+
assert isinstance(client, UserClient)
53+
54+
55+
def test_blueapi_python_client_change_session(client: UserClient):
56+
new_session = "cm54321-1"
57+
client.change_session(new_session)
58+
assert client.instrument_session == new_session
59+
60+
61+
def test_blueapi_python_client_run(client: UserClient):
62+
# Patch instance methods so run executes but no re calls happen.
63+
with (
64+
patch.object(client, "run_task", return_value=Mock()),
65+
patch.object(
66+
client, "create_and_start_task", return_value=Mock(task_id="t-fake")
67+
),
68+
patch.object(client, "create_task", return_value=Mock(task_id="t-fake")),
69+
patch.object(client, "start_task", return_value=Mock(task_id="t-fake")),
70+
):
71+
assert client._events is not None
72+
# Ensure the mocked event client can be used as a context manager if run uses it
73+
client._events.__enter__ = Mock(return_value=client._events)
74+
client._events.__exit__ = Mock(return_value=None)
75+
76+
# Call run while the instance methods are patched
77+
client.run(count)
78+
client.run("count")
79+
80+
81+
def test_blueapi_python_client_without_callback_run(
82+
client_without_callback: UserClient,
83+
):
84+
# Patch instance methods so run executes but no calls happen
85+
with (
86+
patch.object(client_without_callback, "run_task", return_value=Mock()),
87+
patch.object(
88+
client_without_callback,
89+
"create_and_start_task",
90+
return_value=Mock(task_id="t-fake"),
91+
),
92+
patch.object(
93+
client_without_callback, "create_task", return_value=Mock(task_id="t-fake")
94+
),
95+
patch.object(
96+
client_without_callback, "start_task", return_value=Mock(task_id="t-fake")
97+
),
98+
):
99+
# Ensure the mocked event client can be used as a context manager if run uses it
100+
client_without_callback._events = Mock(EventBusClient)
101+
client_without_callback._events.__enter__ = Mock(
102+
return_value=client_without_callback._events
103+
)
104+
client_without_callback._events.__exit__ = Mock(return_value=None)
105+
106+
client_without_callback.run(count)
107+
108+
109+
@pytest.mark.parametrize(
110+
"plan, args, kwargs",
111+
(
112+
["plan", (), {}],
113+
[count, ["det1", "det2"], {}],
114+
[count, ["det1", "det2"], {"num": 2}],
115+
[count, (), {"detectors": ["det1", "det2"]}],
116+
),
117+
)
118+
def test_run_with_valid_paraneters(client: UserClient, plan, args: tuple, kwargs: dict):
119+
# Patch instance methods so run executes but no re calls happen.
120+
with (
121+
patch.object(client, "run_task", return_value=Mock()),
122+
patch.object(
123+
client, "create_and_start_task", return_value=Mock(task_id="t-fake")
124+
),
125+
patch.object(client, "create_task", return_value=Mock(task_id="t-fake")),
126+
patch.object(client, "start_task", return_value=Mock(task_id="t-fake")),
127+
):
128+
assert client._events is not None
129+
# Ensure the mocked event client can be used as a context manager if run uses it
130+
client._events.__enter__ = Mock(return_value=client._events)
131+
client._events.__exit__ = Mock(return_value=None)
132+
133+
client.run(plan, *args, **kwargs)
134+
135+
136+
@pytest.mark.parametrize(
137+
"plan, args, kwargs",
138+
(
139+
[None, (), {}],
140+
["plan", ["det1", "det2"], {}],
141+
["plan", ["det1", "det2"], {"num": 2}],
142+
),
143+
)
144+
def test_run_fails_with_invalid_parameters(
145+
client: UserClient, plan, args: tuple, kwargs: dict
146+
):
147+
# Patch instance methods so run executes but no re calls happen.
148+
with (
149+
patch.object(client, "run_task", return_value=Mock()),
150+
patch.object(
151+
client, "create_and_start_task", return_value=Mock(task_id="t-fake")
152+
),
153+
patch.object(client, "create_task", return_value=Mock(task_id="t-fake")),
154+
patch.object(client, "start_task", return_value=Mock(task_id="t-fake")),
155+
):
156+
assert client._events is not None
157+
# Ensure the mocked event client can be used as a context manager if run uses it
158+
client._events.__enter__ = Mock(return_value=client._events)
159+
client._events.__exit__ = Mock(return_value=None)
160+
161+
# Call run while the instance methods are patched
162+
with pytest.raises(ValueError): # noqa
163+
client.run(plan, *args, **kwargs)
164+
165+
166+
def test_return_detectors(client: UserClient):
167+
# Mock the expected detector list response
168+
169+
# Create a method mock for get_detectors
170+
client.get_devices = Mock(
171+
DeviceResponse,
172+
return_value=MockResponse([MockDevice("det1"), MockDevice("det2")]),
173+
)
174+
175+
# Call the method under test
176+
result = client.return_detectors()
177+
178+
# Verify the result matches our expected data
179+
180+
# Verify the REST client was called correctly
181+
client.get_devices.assert_called_once()
182+
183+
assert isinstance(result, list)
184+
185+
186+
def test_show_devices(client: UserClient):
187+
# Create a method mock for get_detectors
188+
client.get_devices = Mock(
189+
DeviceResponse,
190+
return_value=MockResponse([MockDevice("det1"), MockDevice("det2")]),
191+
)
192+
193+
client.show_devices()
194+
client.get_devices.assert_called_once()
195+
196+
197+
class MockPlan:
198+
def __init__(self, device: str):
199+
self.name = device
200+
201+
202+
class MockPlanResponse:
203+
def __init__(self, plans: list):
204+
self.plans = plans
205+
206+
207+
def test_show_plans(client: UserClient):
208+
# Create a method mock for get_detectors
209+
client.get_plans = Mock(
210+
PlanResponse,
211+
return_value=MockPlanResponse([MockPlan("count"), MockPlan("test")]),
212+
)
213+
214+
client.show_plans()
215+
client.get_plans.assert_called_once()

0 commit comments

Comments
 (0)