Skip to content

Commit 5d50efe

Browse files
committed
Implement ensure_connected configuration option
DeviceManagerSource now accepts ensure_connected boolean option. When enabled any failure to build or connect a device will result in an exception.
1 parent 038fe03 commit 5d50efe

5 files changed

Lines changed: 63 additions & 2 deletions

File tree

helm/blueapi/config_schema.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,12 @@
9191
"description": "Name of the device manager in the module",
9292
"title": "Name",
9393
"type": "string"
94+
},
95+
"ensure_connected": {
96+
"default": false,
97+
"description": "If true, all devices must be successfully connected at startup.",
98+
"title": "Ensure Connected",
99+
"type": "boolean"
94100
}
95101
},
96102
"required": [

helm/blueapi/values.schema.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,12 @@
510510
"module"
511511
],
512512
"properties": {
513+
"ensure_connected": {
514+
"title": "Ensure Connected",
515+
"description": "If true, all devices must be successfully connected at startup.",
516+
"default": false,
517+
"type": "boolean"
518+
},
513519
"kind": {
514520
"title": "Kind",
515521
"default": "deviceManager",

src/blueapi/config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ class DeviceManagerSource(Source):
8383
name: str = Field(
8484
default="devices", description="Name of the device manager in the module"
8585
)
86+
ensure_connected: bool = Field(
87+
default=False,
88+
description="If true, all devices must be successfully connected at startup.",
89+
)
8690

8791

8892
class TcpUrl(AnyUrl):

src/blueapi/core/context.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,13 +214,21 @@ def with_config(self, config: EnvironmentConfig) -> None:
214214
self.with_device_module(mod)
215215
case DodalSource(mock=mock):
216216
self.with_dodal_module(mod, mock=mock)
217-
case DeviceManagerSource(mock=mock, name=name):
217+
case DeviceManagerSource(
218+
mock=mock, name=name, ensure_connected=ensure_connected
219+
):
218220
manager = getattr(mod, name)
219221
if not isinstance(manager, DeviceManager):
220222
raise ValueError(
221223
f"{name} in module {mod} is not a device manager"
222224
)
223-
self.with_device_manager(manager, mock)
225+
device_map, error_map = self.with_device_manager(manager, mock)
226+
if ensure_connected and error_map:
227+
raise ExceptionGroup(
228+
"Errors occurred while connecting the following devices: "
229+
f"{', '.join(error_map.keys())}",
230+
list(error_map.values()),
231+
)
224232

225233
def with_plan_module(self, module: ModuleType) -> None:
226234
"""

tests/unit_tests/core/test_context.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from dodal.common import PlanGenerator, inject
2121
from ophyd import Device
2222
from ophyd_async.core import (
23+
NotConnectedError,
2324
PathProvider,
2425
StandardDetector,
2526
StaticPathProvider,
@@ -153,6 +154,20 @@ def devicey_context(sim_motor: Motor, sim_detector: StandardDetector) -> Bluesky
153154
return ctx
154155

155156

157+
@pytest.fixture
158+
def beamline_with_connection_and_build_errors():
159+
stm = StaticDeviceManager(
160+
devices={},
161+
build_errors={"foo": RuntimeError("Simulated Build Error")},
162+
connection_errors={"bar": NotConnectedError("Simulated Connection Error")},
163+
)
164+
dev_mod = Mock(spec=ModuleType)
165+
dev_mod.devices = stm
166+
with patch("blueapi.core.context.import_module") as imp_mod:
167+
imp_mod.side_effect = lambda mod: dev_mod if mod == "foo.bar" else None
168+
yield
169+
170+
156171
class SomeConfigurable:
157172
def read_configuration(self) -> SyncOrAsync[dict[str, Reading]]:
158173
return {}
@@ -425,6 +440,28 @@ def test_with_config_passes_mock_to_with_dodal_module(
425440
mock_with_dodal_module.assert_called_once_with(ANY, mock=mock)
426441

427442

443+
def test_with_config_raises_exception_group_on_connection_errors_when_ensure_connected(
444+
empty_context: BlueskyContext, beamline_with_connection_and_build_errors: None
445+
):
446+
with pytest.raises(ExceptionGroup, match="Errors occurred while connecting.*") as e:
447+
empty_context.with_config(
448+
EnvironmentConfig(
449+
sources=[DeviceManagerSource(module="foo.bar", ensure_connected=True)]
450+
)
451+
)
452+
453+
assert e.value.exceptions[0].args[0] == "Simulated Build Error"
454+
assert e.value.exceptions[1].args[0] == "Simulated Connection Error"
455+
456+
457+
def test_with_config_ignores_build_connect_exceptions_by_default(
458+
empty_context: BlueskyContext, beamline_with_connection_and_build_errors: None
459+
):
460+
empty_context.with_config(
461+
EnvironmentConfig(sources=[DeviceManagerSource(module="foo.bar")])
462+
)
463+
464+
428465
def test_function_spec(empty_context: BlueskyContext):
429466
spec = empty_context._type_spec_for_function(has_some_params)
430467
assert spec["foo"][0] is int

0 commit comments

Comments
 (0)