diff --git a/src/fastcs_odin/frame_processor.py b/src/fastcs_odin/frame_processor.py index 84a1b70..108ed46 100644 --- a/src/fastcs_odin/frame_processor.py +++ b/src/fastcs_odin/frame_processor.py @@ -1,14 +1,19 @@ +import asyncio import re from collections.abc import Sequence -from functools import partial +from functools import cached_property, partial from fastcs.attributes import AttrR from fastcs.cs_methods import Command from fastcs.datatypes import Bool, Int from fastcs.logging import bind_logger +from fastcs.wrappers import command from pydantic import ValidationError -from fastcs_odin.io.status_summary_attribute_io import StatusSummaryAttributeIORef +from fastcs_odin.io.status_summary_attribute_io import ( + StatusSummaryAttributeIORef, + _filter_sub_controllers, +) from fastcs_odin.odin_data import OdinDataAdapterController from fastcs_odin.odin_subcontroller import OdinSubController from fastcs_odin.util import ( @@ -99,6 +104,46 @@ class FrameProcessorAdapterController(OdinDataAdapterController): _subcontroller_label = "FP" _subcontroller_cls = FrameProcessorController + def _collect_commands( + self, + path_filter: Sequence[str | tuple[str, ...] | re.Pattern], + command_name: str, + ): + commands = [] + + controllers = list(_filter_sub_controllers(self, path_filter)) + + for controller in controllers: + try: + cmd = getattr(controller, command_name) + commands.append(cmd) + except AttributeError as err: + raise AttributeError( + f"Sub controller {controller} does not have command " + f"'{command_name}' required by {self} command fan out {path_filter}" + ) from err + return commands + + @cached_property + def _start_writing_commands(self): + return self._collect_commands((re.compile(r"[0-9]+"), "HDF"), "start_writing") + + @cached_property + def _stop_writing_commands(self): + return self._collect_commands((re.compile(r"[0-9]+"), "HDF"), "stop_writing") + + @command() + async def start_writing(self) -> None: + await asyncio.gather( + *(start_writing() for start_writing in self._start_writing_commands) + ) + + @command() + async def stop_writing(self) -> None: + await asyncio.gather( + *(stop_writing() for stop_writing in self._stop_writing_commands) + ) + class FrameProcessorPluginController(OdinSubController): """SubController for a plugin in a frameProcessor application.""" diff --git a/src/fastcs_odin/io/status_summary_attribute_io.py b/src/fastcs_odin/io/status_summary_attribute_io.py index 23dded3..fa7e249 100644 --- a/src/fastcs_odin/io/status_summary_attribute_io.py +++ b/src/fastcs_odin/io/status_summary_attribute_io.py @@ -58,16 +58,20 @@ def initialise_summary_attributes(controller): for attribute in controller.attributes.values(): if isinstance(attribute.io_ref, StatusSummaryAttributeIORef): - attributes = [ - attr - for sub_controller in _filter_sub_controllers( - controller, attribute.io_ref.path_filter - ) - if isinstance( - attr := sub_controller.attributes[attribute.io_ref.attribute_name], - AttrR, - ) - ] + attributes: Sequence[AttrR] = [] + for sub_controller in _filter_sub_controllers( + controller, attribute.io_ref.path_filter + ): + try: + attr = sub_controller.attributes[attribute.io_ref.attribute_name] + except KeyError as err: + raise KeyError( + f"Sub controller {sub_controller} does not have attribute " + f"'{attribute.io_ref.attribute_name}' required by {controller} " + f"status summary {attribute.io_ref.path_filter}" + ) from err + if isinstance(attr, AttrR): + attributes.append(attr) attribute.io_ref.set_attributes(attributes) diff --git a/tests/test_controllers.py b/tests/test_controllers.py index 96cfaf1..1dcd4b6 100644 --- a/tests/test_controllers.py +++ b/tests/test_controllers.py @@ -381,7 +381,7 @@ async def test_status_summary_attribute_io(): @pytest.mark.asyncio @pytest.mark.parametrize("mock_sub_controller", ("FP", ("FP",), re.compile("FP"))) -async def test_status_summary_updater_raise_exception( +async def test_status_summary_updater_raise_exception_if_controller_not_found( mock_sub_controller, mocker: MockerFixture ): controller = Controller() @@ -444,3 +444,59 @@ async def test_frame_reciever_controllers(): assert invalid_decoder_parameter not in decoder_controller.parameters # index, status, decoder parts removed from path assert decoder_controller.parameters[0]._path == ["packets_dropped"] + + +@pytest.mark.asyncio +async def test_frame_processor_start_and_stop_writing(mocker: MockerFixture): + fpac = FrameProcessorAdapterController( + mocker.AsyncMock(), mocker.AsyncMock(), "api/0.1", [] + ) + fpc = FrameProcessorController( + mocker.AsyncMock(), mocker.AsyncMock(), "api/0.1", [] + ) + await fpc._create_plugin_sub_controllers(["hdf"]) + + # Mock the commands to check calls + hdf = fpc.sub_controllers["HDF"] + hdf.start_writing = mocker.AsyncMock() # type: ignore + hdf.stop_writing = mocker.AsyncMock() # type: ignore + + fpac[0] = fpc + + # Top level FP commands should collect and call lower level commands + await fpac.start_writing() + await fpac.stop_writing() + assert len(hdf.start_writing.mock_calls) == 1 # type: ignore + assert len(hdf.stop_writing.mock_calls) == 1 # type: ignore + + +@pytest.mark.asyncio +async def test_top_level_frame_processor_commands_raise_exception( + mocker: MockerFixture, +): + fpac = FrameProcessorAdapterController( + mocker.AsyncMock(), mocker.AsyncMock(), "api/0.1", [] + ) + + fpc = FrameProcessorController( + mocker.AsyncMock(), mocker.AsyncMock(), "api/0.1", [] + ) + await fpc._create_plugin_sub_controllers(["hdf"]) + fpac[0] = fpc + + with pytest.raises(AttributeError, match="does not have"): + await fpac.start_writing() + + +@pytest.mark.asyncio +async def test_status_summary_updater_raises_exception_if_attribute_not_found(): + controller = Controller() + sub_controller = Controller() + + controller.add_sub_controller("OD", sub_controller) + + controller.writing = AttrR( + Bool(), StatusSummaryAttributeIORef(["OD"], "some_attribute", any) + ) + with pytest.raises(KeyError, match=r"Sub controller .* does not have attribute"): + initialise_summary_attributes(controller)