diff --git a/src/fastcs/transports/epics/ca/ioc.py b/src/fastcs/transports/epics/ca/ioc.py index fbaaa993..887aa3af 100644 --- a/src/fastcs/transports/epics/ca/ioc.py +++ b/src/fastcs/transports/epics/ca/ioc.py @@ -7,18 +7,19 @@ from softioc.pythonSoftIoc import RecordWrapper from fastcs.attributes import AttrR, AttrRW, AttrW -from fastcs.datatypes import DataType, DType_T -from fastcs.datatypes.waveform import Waveform +from fastcs.datatypes import Bool, DataType, DType_T, Enum, Float, Int, String, Waveform +from fastcs.exceptions import FastCSError from fastcs.logging import bind_logger from fastcs.methods import Command from fastcs.tracer import Tracer from fastcs.transports.controller_api import ControllerAPI from fastcs.transports.epics import EpicsIOCOptions from fastcs.transports.epics.ca.util import ( - builder_callable_from_attribute, + DEFAULT_STRING_WAVEFORM_LENGTH, + MBB_MAX_CHOICES, cast_from_epics_type, cast_to_epics_type, - record_metadata_from_attribute, + create_state_keys, record_metadata_from_datatype, ) from fastcs.transports.epics.util import controller_pv_prefix @@ -187,36 +188,168 @@ async def async_record_set(value: DType_T): record.set(cast_to_epics_type(attribute.datatype, value)) - record = _make_record(pv, attribute) + record = _make_in_record(pv, attribute) _add_attr_pvi_info(record, pv_prefix, attr_name, "r") attribute.add_on_update_callback(async_record_set) -def _make_record( +def _make_in_record(pv: str, attribute: AttrR) -> RecordWrapper: + attribute_record_metadata = { + "DESC": attribute.description, + "initial_value": cast_to_epics_type(attribute.datatype, attribute.get()), + } + + match attribute.datatype: + case Bool(): + record = builder.boolIn( + pv, ZNAM="False", ONAM="True", **attribute_record_metadata + ) + case Int(): + record = builder.longIn( + pv, + LOPR=attribute.datatype.min_alarm, + HOPR=attribute.datatype.max_alarm, + EGU=attribute.datatype.units, + **attribute_record_metadata, + ) + case Float(): + record = builder.aIn( + pv, + LOPR=attribute.datatype.min_alarm, + HOPR=attribute.datatype.max_alarm, + EGU=attribute.datatype.units, + PREC=attribute.datatype.prec, + **attribute_record_metadata, + ) + case String(): + record = builder.longStringIn( + pv, + length=attribute.datatype.length or DEFAULT_STRING_WAVEFORM_LENGTH, + **attribute_record_metadata, + ) + case Enum(): + if len(attribute.datatype.members) > MBB_MAX_CHOICES: + record = builder.longStringIn( + pv, + **attribute_record_metadata, + ) + else: + attribute_record_metadata.update(create_state_keys(attribute.datatype)) + record = builder.mbbIn( + pv, + **attribute_record_metadata, + ) + case Waveform(): + record = builder.WaveformIn( + pv, length=attribute.datatype.shape[0], **attribute_record_metadata + ) + case _: + raise FastCSError( + f"EPICS unsupported datatype on {attribute}: {attribute.datatype}" + ) + + def datatype_updater(datatype: DataType): + for name, value in record_metadata_from_datatype(datatype).items(): + record.set_field(name, value) + + attribute.add_update_datatype_callback(datatype_updater) + return record + + +def _make_out_record( pv: str, - attribute: AttrR | AttrW | AttrRW, - on_update: Callable | None = None, - out_record: bool = False, + attribute: AttrW | AttrRW, + on_update: Callable, ) -> RecordWrapper: - builder_callable = builder_callable_from_attribute(attribute, on_update is None) - datatype_record_metadata = record_metadata_from_datatype( - attribute.datatype, out_record - ) - attribute_record_metadata = record_metadata_from_attribute(attribute) + attribute_record_metadata = { + "DESC": attribute.description, + "initial_value": cast_to_epics_type( + attribute.datatype, + attribute.get() + if isinstance(attribute, AttrR) + else attribute.datatype.initial_value, + ), + } - update = ( - {"on_update": on_update, "always_update": True, "blocking": True} - if on_update - else {} - ) + update = {"on_update": on_update, "always_update": True, "blocking": True} + + match attribute.datatype: + case Bool(): + record = builder.boolOut( + pv, ZNAM="False", ONAM="True", **update, **attribute_record_metadata + ) + case Int(): + record = builder.longOut( + pv, + LOPR=attribute.datatype.min_alarm, + HOPR=attribute.datatype.max_alarm, + EGU=attribute.datatype.units, + DRVL=attribute.datatype.min, + DRVH=attribute.datatype.max, + **update, + **attribute_record_metadata, + ) + case Float(): + record = builder.aOut( + pv, + LOPR=attribute.datatype.min_alarm, + HOPR=attribute.datatype.max_alarm, + EGU=attribute.datatype.units, + PREC=attribute.datatype.prec, + DRVL=attribute.datatype.min, + DRVH=attribute.datatype.max, + **update, + **attribute_record_metadata, + ) + case String(): + record = builder.longStringOut( + pv, + length=attribute.datatype.length or DEFAULT_STRING_WAVEFORM_LENGTH, + **update, + **attribute_record_metadata, + ) + case Enum(): + if len(attribute.datatype.members) > MBB_MAX_CHOICES: + datatype: Enum = attribute.datatype + + def _verify_in_datatype(_, value): + return value in datatype.names + + record = builder.longStringOut( + pv, + validate=_verify_in_datatype, + **update, + **attribute_record_metadata, + ) - record = builder_callable( - pv, **update, **datatype_record_metadata, **attribute_record_metadata - ) + else: + attribute_record_metadata.update(create_state_keys(attribute.datatype)) + record = builder.mbbOut( + pv, + **update, + **attribute_record_metadata, + ) + case Waveform(): + record = builder.WaveformOut( + pv, + length=attribute.datatype.shape[0], + **update, + **attribute_record_metadata, + ) + case _: + raise FastCSError( + f"EPICS unsupported datatype on {attribute}: {attribute.datatype}" + ) def datatype_updater(datatype: DataType): - for name, value in record_metadata_from_datatype(datatype, out_record).items(): + # Filter out keys that can't be set via set field + builder_only_keys = {"validate", "length"} + for name, value in record_metadata_from_datatype( + datatype, out_record=True + ).items(): + if name in builder_only_keys: + continue record.set_field(name, value) attribute.add_update_datatype_callback(datatype_updater) @@ -240,7 +373,7 @@ async def set_setpoint_without_process(value: DType_T): record.set(cast_to_epics_type(attribute.datatype, value), process=False) - record = _make_record(pv, attribute, on_update=on_update, out_record=True) + record = _make_out_record(pv, attribute, on_update=on_update) _add_attr_pvi_info(record, pv_prefix, attr_name, "w") diff --git a/src/fastcs/transports/epics/ca/util.py b/src/fastcs/transports/epics/ca/util.py index 2f19d250..bbfbaca2 100644 --- a/src/fastcs/transports/epics/ca/util.py +++ b/src/fastcs/transports/epics/ca/util.py @@ -2,12 +2,8 @@ from dataclasses import asdict from typing import Any -from softioc import builder - -from fastcs.attributes import Attribute, AttrR, AttrRW, AttrW from fastcs.datatypes import Bool, DType_T, Enum, Float, Int, String, Waveform from fastcs.datatypes.datatype import DataType -from fastcs.exceptions import FastCSError _MBB_FIELD_PREFIXES = ( "ZR", @@ -46,21 +42,6 @@ } -def record_metadata_from_attribute(attribute: Attribute[DType_T]) -> dict[str, Any]: - """Converts attributes on the `Attribute` to the - field name/value in the record metadata.""" - metadata: dict[str, Any] = {"DESC": attribute.description} - initial = None - match attribute: - case AttrR(): - initial = attribute.get() - case AttrW(): - initial = attribute.datatype.initial_value - if initial is not None: - metadata["initial_value"] = cast_to_epics_type(attribute.datatype, initial) - return metadata - - def record_metadata_from_datatype( datatype: DataType[Any], out_record: bool = False ) -> dict[str, str]: @@ -90,14 +71,7 @@ def record_metadata_from_datatype( arguments["length"] = datatype.shape[0] case Enum(): if len(datatype.members) <= MBB_MAX_CHOICES: - state_keys = dict( - zip( - MBB_STATE_FIELDS, - datatype.names, - strict=False, - ) - ) - arguments.update(state_keys) + arguments.update(create_state_keys(datatype)) elif out_record: # no validators for in type records def _verify_in_datatype(_, value): @@ -111,6 +85,17 @@ def _verify_in_datatype(_, value): return arguments +def create_state_keys(datatype: Enum): + """Creates a dictionary of state field keys to names""" + return dict( + zip( + MBB_STATE_FIELDS, + datatype.names, + strict=False, + ) + ) + + def cast_from_epics_type(datatype: DataType[DType_T], value: object) -> DType_T: """Casts from an EPICS datatype to a FastCS datatype.""" match datatype: @@ -154,29 +139,3 @@ def cast_to_epics_type(datatype: DataType[DType_T], value: DType_T) -> Any: return value case _: raise ValueError(f"Unsupported datatype {datatype}") - - -def builder_callable_from_attribute( - attribute: AttrR | AttrW | AttrRW, make_in_record: bool -): - """Returns a callable to make the softioc record from an attribute instance.""" - match attribute.datatype: - case Bool(): - return builder.boolIn if make_in_record else builder.boolOut - case Int(): - return builder.longIn if make_in_record else builder.longOut - case Float(): - return builder.aIn if make_in_record else builder.aOut - case String(): - return builder.longStringIn if make_in_record else builder.longStringOut - case Enum(): - if len(attribute.datatype.members) > MBB_MAX_CHOICES: - return builder.longStringIn if make_in_record else builder.longStringOut - else: - return builder.mbbIn if make_in_record else builder.mbbOut - case Waveform(): - return builder.WaveformIn if make_in_record else builder.WaveformOut - case _: - raise FastCSError( - f"EPICS unsupported datatype on {attribute}: {attribute.datatype}" - ) diff --git a/tests/transports/epics/ca/test_ca_util.py b/tests/transports/epics/ca/test_ca_util.py index 2993b1e6..8488c41d 100644 --- a/tests/transports/epics/ca/test_ca_util.py +++ b/tests/transports/epics/ca/test_ca_util.py @@ -1,12 +1,9 @@ import enum import pytest -from softioc import builder -from fastcs.attributes import AttrRW from fastcs.datatypes import Bool, Enum, Float, Int, String from fastcs.transports.epics.ca.util import ( - builder_callable_from_attribute, cast_from_epics_type, cast_to_epics_type, record_metadata_from_datatype, @@ -137,21 +134,6 @@ def test_cast_from_epics_validations(datatype, input): cast_from_epics_type(datatype, input) -@pytest.mark.parametrize( - "datatype,in_record,out_record", - [ - (Enum(ShortEnum), builder.mbbIn, builder.mbbOut), - # long enums use string even if all values are ints - (Enum(LongEnum), builder.longStringIn, builder.longStringOut), - (Enum(LongMixedEnum), builder.longStringIn, builder.longStringOut), - ], -) -def test_builder_callable_enum_types(datatype, in_record, out_record): - attr = AttrRW(datatype) - assert builder_callable_from_attribute(attr, False) == out_record - assert builder_callable_from_attribute(attr, True) == in_record - - def test_drive_metadata_from_datatype(): dtype = Float(units="mm", min=-10.0, max=10.0, min_alarm=-5, max_alarm=5, prec=3) out_arguments = record_metadata_from_datatype(dtype, True) diff --git a/tests/transports/epics/ca/test_initial_value.py b/tests/transports/epics/ca/test_initial_value.py index e340b4c4..727cb0d7 100644 --- a/tests/transports/epics/ca/test_initial_value.py +++ b/tests/transports/epics/ca/test_initial_value.py @@ -59,16 +59,18 @@ async def test_initial_values_set_in_ca(mocker): loop, ) - record_spy = mocker.spy(ca_ioc, "_make_record") + record_spy = mocker.spy(ca_ioc, "_make_in_record") + record_spy_out = mocker.spy(ca_ioc, "_make_out_record") task = asyncio.create_task(fastcs.serve(interactive=False)) try: async with asyncio.timeout(3): - while not record_spy.spy_return_list: + while not record_spy.spy_return_list or not record_spy_out.spy_return_list: await asyncio.sleep(0) initial_values = { - wrapper.name: wrapper.get() for wrapper in record_spy.spy_return_list + wrapper.name: wrapper.get() + for wrapper in record_spy.spy_return_list + record_spy_out.spy_return_list } for name, value in { "SOFTIOC_INITIAL_DEVICE:Bool": 1, diff --git a/tests/transports/epics/ca/test_softioc.py b/tests/transports/epics/ca/test_softioc.py index 27a53e25..38ed6861 100644 --- a/tests/transports/epics/ca/test_softioc.py +++ b/tests/transports/epics/ca/test_softioc.py @@ -27,10 +27,10 @@ _add_sub_controller_pvi_info, _create_and_link_read_pv, _create_and_link_write_pv, - _make_record, + _make_in_record, + _make_out_record, ) from fastcs.transports.epics.ca.util import ( - record_metadata_from_attribute, record_metadata_from_datatype, ) @@ -46,7 +46,7 @@ class OnOffStates(enum.IntEnum): @pytest.mark.asyncio async def test_create_and_link_read_pv(mocker: MockerFixture): - make_record = mocker.patch("fastcs.transports.epics.ca.ioc._make_record") + make_record = mocker.patch("fastcs.transports.epics.ca.ioc._make_in_record") add_attr_pvi_info = mocker.patch( "fastcs.transports.epics.ca.ioc._add_attr_pvi_info" ) @@ -71,18 +71,41 @@ async def test_create_and_link_read_pv(mocker: MockerFixture): @pytest.mark.parametrize( "attribute,record_type,kwargs", ( - (AttrR(String()), "longStringIn", {}), + (AttrR(String()), "longStringIn", {"DESC": None, "initial_value": ""}), ( AttrR(Enum(ColourEnum)), "mbbIn", - {"ZRST": "RED", "ONST": "GREEN", "TWST": "BLUE"}, + { + "ZRST": "RED", + "ONST": "GREEN", + "TWST": "BLUE", + "DESC": None, + "initial_value": 0, + }, ), ( - AttrR(Enum(enum.IntEnum("ONOFF_STATES", {"DISABLED": 0, "ENABLED": 1}))), + AttrR( + Enum( + enum.IntEnum( + "ONOFF_STATES", + {"DISABLED": 0, "ENABLED": 1}, + ) + ) + ), "mbbIn", - {"ZRST": "DISABLED", "ONST": "ENABLED"}, + {"ZRST": "DISABLED", "ONST": "ENABLED", "DESC": None, "initial_value": 0}, + ), + ( + AttrR(Waveform(np.int32, (10,))), + "WaveformIn", + { + "DESC": None, + # array( + # [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], dtype=np.int32 + # ), + }, + # {"DESC": None, "initial_value": None}, ), - (AttrR(Waveform(np.int32, (10,))), "WaveformIn", {}), ), ) def test_make_input_record( @@ -91,13 +114,14 @@ def test_make_input_record( kwargs: dict[str, Any], mocker: MockerFixture, ): - builder = mocker.patch("fastcs.transports.epics.ca.util.builder") + builder = mocker.patch("fastcs.transports.epics.ca.ioc.builder") pv = "PV" - _make_record(pv, attribute) + _make_in_record(pv, attribute) kwargs.update(record_metadata_from_datatype(attribute.datatype)) - kwargs.update(record_metadata_from_attribute(attribute)) + if record_type == "WaveformIn": + kwargs["initial_value"] = mocker.ANY getattr(builder, record_type).assert_called_once_with( pv, **kwargs, @@ -105,14 +129,16 @@ def test_make_input_record( def test_make_record_raises(mocker: MockerFixture): + mocker.patch("fastcs.transports.epics.ca.ioc.record_metadata_from_datatype") + mocker.patch("fastcs.transports.epics.ca.ioc.cast_to_epics_type") # Pass a mock as attribute to provoke the fallback case matching on datatype with pytest.raises(FastCSError): - _make_record("PV", mocker.MagicMock()) + _make_in_record("PV", mocker.MagicMock()) @pytest.mark.asyncio async def test_create_and_link_write_pv(mocker: MockerFixture): - make_record = mocker.patch("fastcs.transports.epics.ca.ioc._make_record") + make_record = mocker.patch("fastcs.transports.epics.ca.ioc._make_out_record") add_attr_pvi_info = mocker.patch( "fastcs.transports.epics.ca.ioc._add_attr_pvi_info" ) @@ -124,9 +150,7 @@ async def test_create_and_link_write_pv(mocker: MockerFixture): _create_and_link_write_pv("PREFIX", "PV", "attr", attribute) - make_record.assert_called_once_with( - "PREFIX:PV", attribute, on_update=mocker.ANY, out_record=True - ) + make_record.assert_called_once_with("PREFIX:PV", attribute, on_update=mocker.ANY) add_attr_pvi_info.assert_called_once_with(record, "PREFIX", "attr", "w") # Extract the write update callback generated and set in the function and call it @@ -169,7 +193,12 @@ class LongEnum(enum.Enum): ( AttrW(Enum(enum.IntEnum("ONOFF_STATES", {"DISABLED": 0, "ENABLED": 1}))), "mbbOut", - {"ZRST": "DISABLED", "ONST": "ENABLED"}, + { + "ZRST": "DISABLED", + "ONST": "ENABLED", + "DESC": None, + "initial_value": 0, + }, ), ), ) @@ -179,14 +208,13 @@ def test_make_output_record( kwargs: dict[str, Any], mocker: MockerFixture, ): - builder = mocker.patch("fastcs.transports.epics.ca.util.builder") + builder = mocker.patch("fastcs.transports.epics.ca.ioc.builder") update = mocker.MagicMock() pv = "PV" - _make_record(pv, attribute, on_update=update, out_record=True) + _make_out_record(pv, attribute, on_update=update) kwargs.update(record_metadata_from_datatype(attribute.datatype, out_record=True)) - kwargs.update(record_metadata_from_attribute(attribute)) kwargs.update({"always_update": True, "on_update": update, "blocking": True}) getattr(builder, record_type).assert_called_once_with( @@ -196,20 +224,30 @@ def test_make_output_record( def test_long_enum_validator(mocker: MockerFixture): - builder = mocker.patch("fastcs.transports.epics.ca.util.builder") + builder = mocker.patch("fastcs.transports.epics.ca.ioc.builder") update = mocker.MagicMock() attribute = AttrRW(Enum(LongEnum)) pv = "PV" - record = _make_record(pv, attribute, on_update=update, out_record=True) + record = _make_out_record(pv, attribute, on_update=update) validator = builder.longStringOut.call_args.kwargs["validate"] assert validator(record, "THIS") # value is one of the Enum names assert not validator(record, "an invalid string value") +def test_long_enum_in_creation(mocker: MockerFixture): + builder = mocker.patch("fastcs.transports.epics.ca.ioc.builder") + attribute = AttrR(Enum(LongEnum)) + pv = "PV" + _make_in_record(pv, attribute) + assert builder.longStringIn.call_args.kwargs["initial_value"] == "THIS" + + def test_get_output_record_raises(mocker: MockerFixture): + mocker.patch("fastcs.transports.epics.ca.ioc.record_metadata_from_datatype") + mocker.patch("fastcs.transports.epics.ca.ioc.cast_to_epics_type") # Pass a mock as attribute to provoke the fallback case matching on datatype with pytest.raises(FastCSError): - _make_record("PV", mocker.MagicMock(), on_update=mocker.MagicMock()) + _make_out_record("PV", mocker.MagicMock(), on_update=mocker.MagicMock()) class EpicsController(MyTestController): @@ -230,7 +268,6 @@ def epics_controller_api(class_mocker: MockerFixture): def test_ioc(mocker: MockerFixture, epics_controller_api: ControllerAPI): ioc_builder = mocker.patch("fastcs.transports.epics.ca.ioc.builder") - builder = mocker.patch("fastcs.transports.epics.ca.util.builder") add_pvi_info = mocker.patch("fastcs.transports.epics.ca.ioc._add_pvi_info") add_sub_controller_pvi_info = mocker.patch( "fastcs.transports.epics.ca.ioc._add_sub_controller_pvi_info" @@ -239,86 +276,87 @@ def test_ioc(mocker: MockerFixture, epics_controller_api: ControllerAPI): EpicsCAIOC(DEVICE, epics_controller_api) # Check records are created - builder.boolIn.assert_called_once_with( + ioc_builder.boolIn.assert_called_once_with( f"{DEVICE}:ReadBool", - **record_metadata_from_attribute(epics_controller_api.attributes["read_bool"]), + DESC=None, + initial_value=False, **record_metadata_from_datatype( epics_controller_api.attributes["read_bool"].datatype ), ) - builder.longIn.assert_any_call( + ioc_builder.longIn.assert_any_call( f"{DEVICE}:ReadInt", - **record_metadata_from_attribute(epics_controller_api.attributes["read_int"]), + DESC=None, + initial_value=0, **record_metadata_from_datatype( epics_controller_api.attributes["read_int"].datatype ), ) - builder.aIn.assert_called_once_with( + ioc_builder.aIn.assert_called_once_with( f"{DEVICE}:ReadWriteFloat_RBV", - **record_metadata_from_attribute( - epics_controller_api.attributes["read_write_float"] - ), + DESC=None, + initial_value=0.0, **record_metadata_from_datatype( epics_controller_api.attributes["read_write_float"].datatype ), ) - builder.aOut.assert_any_call( + ioc_builder.aOut.assert_any_call( f"{DEVICE}:ReadWriteFloat", + DESC=None, + initial_value=0.0, always_update=True, blocking=True, on_update=mocker.ANY, - **record_metadata_from_attribute( - epics_controller_api.attributes["read_write_float"] - ), **record_metadata_from_datatype( epics_controller_api.attributes["read_write_float"].datatype, out_record=True, ), ) - builder.longIn.assert_any_call( + ioc_builder.longIn.assert_any_call( f"{DEVICE}:ReadWriteInt_RBV", - **record_metadata_from_attribute( - epics_controller_api.attributes["read_write_int"] - ), + DESC=None, + initial_value=0, **record_metadata_from_datatype( epics_controller_api.attributes["read_write_int"].datatype ), ) - builder.longOut.assert_called_with( + ioc_builder.longOut.assert_called_with( f"{DEVICE}:ReadWriteInt", + DESC=None, + initial_value=0, always_update=True, blocking=True, on_update=mocker.ANY, - **record_metadata_from_attribute( - epics_controller_api.attributes["read_write_int"] - ), **record_metadata_from_datatype( epics_controller_api.attributes["read_write_int"].datatype, out_record=True ), ) - builder.mbbIn.assert_called_once_with( + ioc_builder.mbbIn.assert_called_once_with( f"{DEVICE}:Enum_RBV", - **record_metadata_from_attribute(epics_controller_api.attributes["enum"]), + DESC=None, + initial_value=0, **record_metadata_from_datatype( epics_controller_api.attributes["enum"].datatype ), ) - builder.mbbOut.assert_called_once_with( + ioc_builder.mbbOut.assert_called_once_with( f"{DEVICE}:Enum", + DESC=None, + initial_value=0, always_update=True, blocking=True, on_update=mocker.ANY, - **record_metadata_from_attribute(epics_controller_api.attributes["enum"]), **record_metadata_from_datatype( epics_controller_api.attributes["enum"].datatype, out_record=True ), ) - builder.boolOut.assert_called_once_with( + ioc_builder.boolOut.assert_called_once_with( f"{DEVICE}:WriteBool", always_update=True, blocking=True, on_update=mocker.ANY, - **record_metadata_from_attribute(epics_controller_api.attributes["write_bool"]), + DESC=None, + initial_value=False, **record_metadata_from_datatype( epics_controller_api.attributes["write_bool"].datatype, out_record=True ), @@ -450,7 +488,6 @@ class ControllerLongNames(Controller): def test_long_pv_names_discarded(mocker: MockerFixture): ioc_builder = mocker.patch("fastcs.transports.epics.ca.ioc.builder") - builder = mocker.patch("fastcs.transports.epics.ca.util.builder") long_name_controller_api = AssertableControllerAPI(ControllerLongNames(), mocker) long_attr_name = "attr_r_with_reallyreallyreallyreallyreallyreallyreally_long_name" long_rw_name = "attr_rw_with_a_reallyreally_long_name_that_is_too_long_for_RBV" @@ -461,36 +498,32 @@ def test_long_pv_names_discarded(mocker: MockerFixture): assert not long_name_controller_api.attributes[long_attr_name].enabled short_pv_name = "attr_rw_short_name".title().replace("_", "") - builder.longOut.assert_called_once_with( + ioc_builder.longOut.assert_called_once_with( f"{DEVICE}:{short_pv_name}", always_update=True, blocking=True, on_update=mocker.ANY, + DESC=None, + initial_value=0, **record_metadata_from_datatype( long_name_controller_api.attributes["attr_rw_short_name"].datatype, out_record=True, ), - **record_metadata_from_attribute( - long_name_controller_api.attributes["attr_rw_short_name"] - ), ) - builder.longIn.assert_called_once_with( + ioc_builder.longIn.assert_called_once_with( f"{DEVICE}:{short_pv_name}_RBV", + DESC=None, + initial_value=0, **record_metadata_from_datatype( long_name_controller_api.attributes[ "attr_rw_with_a_reallyreally_long_name_that_is_too_long_for_rbv" ].datatype ), - **record_metadata_from_attribute( - long_name_controller_api.attributes[ - "attr_rw_with_a_reallyreally_long_name_that_is_too_long_for_rbv" - ] - ), ) long_pv_name = long_attr_name.title().replace("_", "") with pytest.raises(AssertionError): - builder.longIn.assert_called_once_with(f"{DEVICE}:{long_pv_name}") + ioc_builder.longIn.assert_called_once_with(f"{DEVICE}:{long_pv_name}") long_rw_pv_name = long_rw_name.title().replace("_", "") # neither the readback nor setpoint PV gets made if the full pv name with _RBV @@ -502,14 +535,14 @@ def test_long_pv_names_discarded(mocker: MockerFixture): ) with pytest.raises(AssertionError): - builder.longOut.assert_called_once_with( + ioc_builder.longOut.assert_called_once_with( f"{DEVICE}:{long_rw_pv_name}", always_update=True, blocking=True, on_update=mocker.ANY, ) with pytest.raises(AssertionError): - builder.longIn.assert_called_once_with(f"{DEVICE}:{long_rw_pv_name}_RBV") + ioc_builder.longIn.assert_called_once_with(f"{DEVICE}:{long_rw_pv_name}_RBV") assert long_name_controller_api.command_methods["command_short_name"].enabled long_command_name = ( @@ -528,7 +561,7 @@ def test_long_pv_names_discarded(mocker: MockerFixture): ) with pytest.raises(AssertionError): long_command_pv_name = long_command_name.title().replace("_", "") - builder.aOut.assert_called_once_with( + ioc_builder.aOut.assert_called_once_with( f"{DEVICE}:{long_command_pv_name}", initial_value=0, always_update=True, @@ -557,17 +590,18 @@ def test_non_1d_waveforms_discarded(mocker: MockerFixture): def test_update_datatype(mocker: MockerFixture): - builder = mocker.patch("fastcs.transports.epics.ca.util.builder") + builder = mocker.patch("fastcs.transports.epics.ca.ioc.builder") pv_name = f"{DEVICE}:Attr" attr_r = AttrR(Int()) - record_r = _make_record(pv_name, attr_r) + record_r = _make_in_record(pv_name, attr_r) builder.longIn.assert_called_once_with( pv_name, - **record_metadata_from_attribute(attr_r), **record_metadata_from_datatype(attr_r.datatype), + DESC=None, + initial_value=0, ) record_r.set_field.assert_not_called() attr_r.update_datatype(Int(units="m", min_alarm=-3)) @@ -581,12 +615,18 @@ def test_update_datatype(mocker: MockerFixture): attr_r.update_datatype(String()) # type: ignore attr_w = AttrW(Int()) - record_w = _make_record(pv_name, attr_w, on_update=mocker.ANY, out_record=True) + record_w = _make_out_record(pv_name, attr_w, on_update=mocker.ANY) - builder.longIn.assert_called_once_with( + builder.longOut.assert_called_once_with( pv_name, - **record_metadata_from_attribute(attr_w), **record_metadata_from_datatype(attr_w.datatype), + DESC=None, + initial_value=0, + DRVL=None, + DRVH=None, + on_update=mocker.ANY, + always_update=True, + blocking=True, ) record_w.set_field.assert_not_called() attr_w.update_datatype(Int(units="m", min_alarm=-1, min=-3))