Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 47 additions & 2 deletions src/fastcs_odin/frame_processor.py
Original file line number Diff line number Diff line change
@@ -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 (
Expand Down Expand Up @@ -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."""
Expand Down
24 changes: 14 additions & 10 deletions src/fastcs_odin/io/status_summary_attribute_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
58 changes: 57 additions & 1 deletion tests/test_controllers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Loading