def test_create_update_handler_throws_if_protocol_not_specified(): producer = FakeProducer() channel_with_no_protocol = Channel( "name", EpicsProtocol.NONE, "output_topic", "f142" ) with pytest.raises(RuntimeError): create_update_handler(producer, None, None, channel_with_no_protocol, 20000) # type: ignore
def test_update_handler_publishes_int_update(pv_value, pv_caproto_type, pv_numpy_type): producer = FakeProducer() context = FakeContext() pv_source_name = "source_name" update_handler = CAUpdateHandler(producer, context, pv_source_name, "output_topic", "f142") # type: ignore metadata = (0, 0, TimeStamp(4, 0)) context.call_monitor_callback_with_fake_pv_update( ReadNotifyResponse( np.array([pv_value]).astype(pv_numpy_type), pv_caproto_type, 1, 1, 1, metadata=metadata, )) assert producer.published_payload is not None pv_update_output = deserialise_f142(producer.published_payload) assert pv_update_output.value == pv_value assert pv_update_output.source_name == pv_source_name update_handler.stop()
def test_update_handler_throws_if_schema_not_recognised(): producer = FakeProducer() context = FakeContext() non_existing_schema = "DOESNTEXIST" with pytest.raises(ValueError): PVAUpdateHandler(producer, context, "source_name", "output_topic", non_existing_schema) # type: ignore
def test_update_handlers_can_be_removed_by_schema(update_handlers, ): status_reporter = StubStatusReporter() producer = FakeProducer() schema_1 = "f142" schema_2 = "tdct" update_handlers[Channel("", EpicsProtocol.NONE, "", schema_1)] = StubUpdateHandler() # type: ignore update_handlers[Channel("", EpicsProtocol.NONE, "", schema_2)] = StubUpdateHandler() # type: ignore update_handlers[Channel("", EpicsProtocol.NONE, "", schema_1)] = StubUpdateHandler() # type: ignore config_update = ConfigUpdate( CommandType.REMOVE, (Channel(None, EpicsProtocol.NONE, None, schema_1), ), ) handle_configuration_change(config_update, 20000, None, update_handlers, producer, None, None, _logger, status_reporter) # type: ignore assert len(update_handlers) == 1 assert ( list(update_handlers.keys())[0].schema == schema_2 ), "Expected handlers for schema_1 to have been removed, leaving only one for schema_2"
def test_update_handlers_can_be_removed_by_topic(update_handlers, ): status_reporter = StubStatusReporter() producer = FakeProducer() topic_name_1 = "topic_1" topic_name_2 = "topic_2" update_handlers[Channel("", EpicsProtocol.NONE, topic_name_1, None)] = StubUpdateHandler() # type: ignore update_handlers[Channel("", EpicsProtocol.NONE, topic_name_2, None)] = StubUpdateHandler() # type: ignore update_handlers[Channel("", EpicsProtocol.NONE, topic_name_1, None)] = StubUpdateHandler() # type: ignore config_update = ConfigUpdate( CommandType.REMOVE, (Channel(None, EpicsProtocol.NONE, topic_name_1, None), ), ) handle_configuration_change(config_update, 20000, None, update_handlers, producer, None, None, _logger, status_reporter) # type: ignore assert len(update_handlers) == 1 assert ( list(update_handlers.keys())[0].output_topic == topic_name_2 ), "Expected handlers for topic_1 to have been removed, leaving only one for topic_2"
def test_update_handler_publishes_alarm_update(): producer = FakeProducer() context = FakeContext() pv_value = 42 pv_caproto_type = ChannelType.TIME_INT pv_numpy_type = np.int32 pv_source_name = "source_name" alarm_status = 6 # AlarmStatus.LOW alarm_severity = 1 # AlarmSeverity.MINOR update_handler = CAUpdateHandler(producer, context, pv_source_name, "output_topic", "f142") # type: ignore metadata = (alarm_status, alarm_severity, TimeStamp(4, 0)) context.call_monitor_callback_with_fake_pv_update( ReadNotifyResponse( np.array([pv_value]).astype(pv_numpy_type), pv_caproto_type, 1, 1, 1, metadata=metadata, )) assert producer.published_payload is not None pv_update_output = deserialise_f142(producer.published_payload) assert pv_update_output.value == pv_value assert pv_update_output.source_name == pv_source_name assert pv_update_output.alarm_status == AlarmStatus.LOW assert pv_update_output.alarm_severity == AlarmSeverity.MINOR update_handler.stop()
def test_update_handler_publishes_periodic_update(): producer = FakeProducer() context = FakeContext() pv_value = 42 pv_caproto_type = ChannelType.TIME_INT pv_numpy_type = np.int32 pv_source_name = "source_name" update_period_ms = 10 update_handler = CAUpdateHandler(producer, context, pv_source_name, "output_topic", "f142", update_period_ms) # type: ignore metadata = (0, 0, TimeStamp(4, 0)) context.call_monitor_callback_with_fake_pv_update( ReadNotifyResponse( np.array([pv_value]).astype(pv_numpy_type), pv_caproto_type, 1, 1, 1, metadata=metadata, )) assert producer.published_payload is not None pv_update_output = deserialise_f142(producer.published_payload) assert pv_update_output.value == pv_value assert pv_update_output.source_name == pv_source_name sleep(0.05) assert ( producer.messages_published > 1 ), "Expected more than the 1 message from triggered update due to periodic updates being active" update_handler.stop()
def test_wildcard_cannot_be_used_to_remove_channels_by_schema( update_handlers, ): # No wildcard matching on schemas because ? and * are allowed characters in schema identifiers status_reporter = StubStatusReporter() producer = FakeProducer() test_channel_3 = Channel("channel_3", EpicsProtocol.NONE, "topic_3", "f142") update_handlers[Channel("channel_1", EpicsProtocol.NONE, "topic_1", "f142")] = StubUpdateHandler() # type: ignore update_handlers[Channel("channel_2", EpicsProtocol.NONE, "topic_2", "f142")] = StubUpdateHandler() # type: ignore update_handlers[test_channel_3] = StubUpdateHandler() # type: ignore config_update = ConfigUpdate( CommandType.REMOVE, (Channel(None, EpicsProtocol.NONE, None, "f?42"), ), ) handle_configuration_change(config_update, 20000, None, update_handlers, producer, None, None, _logger, status_reporter) # type: ignore assert ( len(update_handlers) == 3 ), "Expected no channels to be removed as ? is not treated as a wildcard when matching schemas"
def test_update_handlers_can_be_removed_by_name_and_schema(update_handlers, ): status_reporter = StubStatusReporter() producer = FakeProducer() schema_1 = "f142" schema_2 = "tdct" channel_name_1 = "channel_1" channel_name_2 = "channel_2" test_channel_3 = Channel(channel_name_2, EpicsProtocol.NONE, None, schema_1) update_handlers[Channel(channel_name_1, EpicsProtocol.NONE, None, schema_1)] = StubUpdateHandler() # type: ignore update_handlers[Channel(channel_name_2, EpicsProtocol.NONE, None, schema_2)] = StubUpdateHandler() # type: ignore update_handlers[test_channel_3] = StubUpdateHandler() # type: ignore # Only the handler with the channel matching provided name AND schema should be removed config_update = ConfigUpdate( CommandType.REMOVE, (test_channel_3, ), ) handle_configuration_change(config_update, 20000, None, update_handlers, producer, None, None, _logger, status_reporter) # type: ignore assert len(update_handlers) == 2 assert test_channel_3 not in update_handlers.keys()
def test_can_add_multiple_channels_with_same_name_if_protocol_topic_or_schema_are_different( update_handlers, ): status_reporter = StubStatusReporter() producer = FakeProducer() channel_name = "test_channel" test_channel_1 = Channel(channel_name, EpicsProtocol.FAKE, "output_topic", "f142") test_channel_2 = Channel(channel_name, EpicsProtocol.FAKE, "output_topic_2", "f142") test_channel_3 = Channel(channel_name, EpicsProtocol.FAKE, "output_topic", "tdct") config_update = ConfigUpdate( CommandType.ADD, ( test_channel_1, test_channel_2, test_channel_3, ), ) handle_configuration_change(config_update, 20000, None, update_handlers, producer, None, None, _logger, status_reporter) # type: ignore assert len(update_handlers) == 3 assert test_channel_1 in update_handlers.keys() assert test_channel_2 in update_handlers.keys() assert test_channel_3 in update_handlers.keys()
def test_when_update_handlers_exist_their_channel_names_are_reported_in_status( ): test_channel_name_1 = "test_channel_name_1" test_channel_name_2 = "test_channel_name_2" # Normally the values in this dictionary are the update handler objects # but the StatusReporter only uses the keys update_handlers = { Channel(test_channel_name_1, EpicsProtocol.NONE, None, None): 1, Channel(test_channel_name_2, EpicsProtocol.NONE, None, None): 2, } fake_producer = FakeProducer() status_reporter = StatusReporter(update_handlers, fake_producer, "status_topic", "", "version", logger) # type: ignore status_reporter.report_status() if fake_producer.published_payload is not None: deserialised_payload = deserialise_x5f2( fake_producer.published_payload) produced_status_message = json.loads(deserialised_payload.status_json) # Using set comprehension as order is unimportant assert { stream["channel_name"] for stream in produced_status_message["streams"] } == { test_channel_name_1, test_channel_name_2, }, "Expected channel names for existing update handlers to be reported in the status message"
def test_identical_configurations_are_not_added(update_handlers, ): status_reporter = StubStatusReporter() producer = FakeProducer() channel_name = "test_channel" test_channel_1 = Channel(channel_name, EpicsProtocol.FAKE, "output_topic", "f142") test_channel_2 = Channel(channel_name, EpicsProtocol.FAKE, "output_topic", "f142") test_channel_3 = Channel(channel_name, EpicsProtocol.FAKE, "output_topic", "f142") config_update = ConfigUpdate( CommandType.ADD, ( test_channel_1, test_channel_2, test_channel_3, ), ) handle_configuration_change(config_update, 20000, None, update_handlers, producer, None, None, _logger, status_reporter) # type: ignore assert ( len(update_handlers) == 1 ), "Only expect one channel to be added as others requested were identical" assert test_channel_1 in update_handlers.keys()
def test_configuration_stored_when_channels_added(update_handlers, ): status_reporter = StubStatusReporter() producer = FakeProducer() channel_name = "test_channel" test_channel_1 = Channel(channel_name, EpicsProtocol.FAKE, "output_topic", "f142") test_channel_2 = Channel(channel_name, EpicsProtocol.FAKE, "output_topic", "f142") test_channel_3 = Channel(channel_name, EpicsProtocol.FAKE, "output_topic", "f142") config_update = ConfigUpdate( CommandType.ADD, ( test_channel_1, test_channel_2, test_channel_3, ), ) config_store = mock.create_autospec(ConfigurationStore) handle_configuration_change(config_update, 20000, None, update_handlers, producer, None, None, _logger, status_reporter, config_store) # type: ignore config_store.save_configuration.assert_called_once()
def test_update_handler_publishes_enum_update(): producer = FakeProducer() context = FakeContext() pv_index = 0 pv_value_str = "choice0" pv_timestamp_s = 1.1 # seconds from unix epoch pv_source_name = "source_name" pva_update_handler = PVAUpdateHandler(producer, context, pv_source_name, "output_topic", "f142") # type: ignore context.call_monitor_callback_with_fake_pv_update( NTEnum(valueAlarm=True).wrap( { "index": pv_index, "choices": [pv_value_str, "choice1", "choice2"] }, timestamp=pv_timestamp_s, )) assert producer.published_payload is not None pv_update_output = deserialise_f142(producer.published_payload) assert pv_update_output.value == pv_value_str assert pv_update_output.source_name == pv_source_name pva_update_handler.stop()
def test_update_handler_publishes_periodic_update(): producer = FakeProducer() context = FakeContext() pv_timestamp_s = 1.1 # seconds from unix epoch pv_source_name = "source_name" pv_value = -3 pv_type = "i" update_period_ms = 10 pva_update_handler = PVAUpdateHandler(producer, context, pv_source_name, "output_topic", "f142", update_period_ms) # type: ignore context.call_monitor_callback_with_fake_pv_update( NTScalar(pv_type, valueAlarm=True).wrap(pv_value, timestamp=pv_timestamp_s)) assert producer.published_payload is not None pv_update_output = deserialise_f142(producer.published_payload) assert pv_update_output.value == pv_value assert pv_update_output.source_name == pv_source_name sleep(0.05) assert ( producer.messages_published > 1 ), "Expected more than the 1 message from triggered update due to periodic updates being active" pva_update_handler.stop()
def test_pva_handler_created_when_pva_protocol_specified(): producer = FakeProducer() context = FakePVAContext() channel_with_pva_protocol = Channel( "name", EpicsProtocol.PVA, "output_topic", "f142" ) handler = create_update_handler(producer, None, context, channel_with_pva_protocol, 20000) # type: ignore assert isinstance(handler, PVAUpdateHandler)
def test_create_update_handler_throws_if_channel_has_no_schema(): producer = FakeProducer() channel_with_no_topic = Channel("name", EpicsProtocol.PVA, "output_topic", None) with pytest.raises(RuntimeError): create_update_handler(producer, None, None, channel_with_no_topic, 20000) # type: ignore channel_with_empty_topic = Channel("name", EpicsProtocol.PVA, "output_topic", "") with pytest.raises(RuntimeError): create_update_handler(producer, None, None, channel_with_empty_topic, 20000) # type: ignore
def test_no_change_to_empty_update_handlers_when_malformed_config_update_handled( update_handlers, ): status_reporter = StubStatusReporter() producer = FakeProducer() config_update = ConfigUpdate(CommandType.MALFORMED, None) handle_configuration_change(config_update, 20000, None, update_handlers, producer, None, None, _logger, status_reporter) # type: ignore assert not update_handlers
def test_when_multiple_pvs_dumped_config_contains_all_pv_details(): producer = FakeProducer() store = ConfigurationStore(producer, consumer=None, topic="store_topic") store.save_configuration(CHANNELS_TO_STORE) stored_message = parse_config_update( producer.published_payload) # type: ignore stored_channels = stored_message.channels assert_stored_channel_correct(stored_channels[0]) # type: ignore assert_stored_channel_correct(stored_channels[1]) # type: ignore
def test_when_no_pvs_stored_message_type_is_remove_all(): producer = FakeProducer() store = ConfigurationStore(producer, consumer=None, topic="store_topic") store.save_configuration({}) stored_message = parse_config_update( producer.published_payload) # type: ignore assert stored_message.channels is None assert (stored_message.command_type == config_change_to_command_type[ UpdateType.REMOVEALL])
def test_all_update_handlers_are_removed_when_removeall_config_update_is_handled( update_handlers, ): status_reporter = StubStatusReporter() producer = FakeProducer() config_update = ConfigUpdate(CommandType.REMOVE_ALL, None) update_handlers[Channel("test_channel_1", EpicsProtocol.NONE, None, None)] = StubUpdateHandler() # type: ignore update_handlers[Channel("test_channel_2", EpicsProtocol.NONE, None, None)] = StubUpdateHandler() # type: ignore handle_configuration_change(config_update, 20000, None, update_handlers, producer, None, None, _logger, status_reporter) # type: ignore assert not update_handlers
def test_no_change_to_update_handlers_when_malformed_config_update_handled( update_handlers, ): status_reporter = StubStatusReporter() producer = FakeProducer() config_update = ConfigUpdate(CommandType.MALFORMED, None) existing_channel_name = "test_channel" update_handlers[Channel(existing_channel_name, EpicsProtocol.NONE, None, None)] = StubUpdateHandler() # type: ignore handle_configuration_change(config_update, 20000, None, update_handlers, producer, None, None, _logger, status_reporter) # type: ignore assert len(update_handlers) == 1 assert existing_channel_name in _get_channel_names(update_handlers)
def test_update_handler_publishes_tdct_update(): producer = FakeProducer() pv_source_name = "source_name" fake_update_handler = FakeUpdateHandler(producer, pv_source_name, "output_topic", "tdct", 100) # type: ignore fake_update_handler._timer_callback() assert producer.published_payload is not None pv_update_output = deserialise_tdct(producer.published_payload) assert pv_update_output.name == pv_source_name fake_update_handler.stop()
def test_status_message_contains_service_id(): service_id = "test_service_id" update_handlers: Dict = {} fake_producer = FakeProducer() status_reporter = StatusReporter(update_handlers, fake_producer, "status_topic", service_id, "version", logger) # type: ignore status_reporter.report_status() if fake_producer.published_payload is not None: deserialised_payload = deserialise_x5f2( fake_producer.published_payload) assert deserialised_payload.service_id == service_id
def test_update_handler_does_not_include_alarm_details_if_unchanged_in_subsequent_updates( ): producer = FakeProducer() context = FakeContext() pv_timestamp_s = 1.1 # seconds from unix epoch pv_source_name = "source_name" pv_value = -3 pv_type = "i" alarm_status = 4 # Indicates RECORD alarm, we map the alarm message to a specific alarm status to forward alarm_severity = 1 # AlarmSeverity.MINOR alarm_message = "HIGH_ALARM" pva_update_handler = PVAUpdateHandler(producer, context, pv_source_name, "output_topic", "f142") # type: ignore context.call_monitor_callback_with_fake_pv_update( NTScalar(pv_type, valueAlarm=True).wrap( { "value": pv_value, "alarm": { "status": alarm_status, "severity": alarm_severity, "message": alarm_message, }, }, timestamp=pv_timestamp_s, )) # Second update, with unchanged alarm context.call_monitor_callback_with_fake_pv_update( NTScalar(pv_type, valueAlarm=True).wrap( { "value": pv_value, "alarm": { "status": alarm_status, "severity": alarm_severity, "message": alarm_message, }, }, timestamp=pv_timestamp_s, )) assert producer.messages_published == 2 pv_update_output = deserialise_f142(producer.published_payload) assert pv_update_output.alarm_status == AlarmStatus.NO_CHANGE assert pv_update_output.alarm_severity == AlarmSeverity.NO_CHANGE pva_update_handler.stop()
def test_when_no_update_handlers_exist_no_streams_are_present_in_reported_status( ): update_handlers: Dict = {} fake_producer = FakeProducer() status_reporter = StatusReporter(update_handlers, fake_producer, "status_topic", "", "version", logger) # type: ignore status_reporter.report_status() if fake_producer.published_payload is not None: deserialised_payload = deserialise_x5f2( fake_producer.published_payload) produced_status_message = json.loads(deserialised_payload.status_json) assert ( len(produced_status_message["streams"]) == 0 ), "Expected no streams in reported status message as there are no update handlers"
def test_configuration_not_stored_when_command_is_malformed(update_handlers, ): status_reporter = StubStatusReporter() producer = FakeProducer() test_channel_1 = Channel("test_channel", EpicsProtocol.FAKE, "output_topic", "f142") update_handlers[test_channel_1] = StubUpdateHandler() # type: ignore config_update = ConfigUpdate( CommandType.MALFORMED, None, ) config_store = mock.create_autospec(ConfigurationStore) handle_configuration_change(config_update, 20000, None, update_handlers, producer, None, None, _logger, status_reporter, config_store) # type: ignore config_store.save_configuration.assert_not_called()
def test_update_handler_publishes_enum_update(): producer = FakeProducer() context = FakeContext() pv_caproto_type = ChannelType.TIME_ENUM pv_source_name = "source_name" update_handler = CAUpdateHandler(producer, context, pv_source_name, "output_topic", "f142") # type: ignore # Nothing gets published when ENUM type update is received, the handler will resubscribe using STRING # type as the string is more useful to forwarder to the filewriter than the enum int metadata = (0, 0, TimeStamp(4, 0)) context.call_monitor_callback_with_fake_pv_update( ReadNotifyResponse( np.array([0]), pv_caproto_type, 1, 1, 1, metadata=metadata, )) # Second update, with STRING type enum_string_value = "ENUM_STRING" context.call_monitor_callback_with_fake_pv_update( ReadNotifyResponse( [enum_string_value.encode("utf8")], ChannelType.TIME_STRING, 1, 1, 1, metadata=metadata, )) assert ( producer.messages_published == 1 ), "Only expected a single message with string payload, not the original enum update" assert producer.published_payload is not None pv_update_output = deserialise_f142(producer.published_payload) assert pv_update_output.value == enum_string_value assert pv_update_output.source_name == pv_source_name update_handler.stop()
def test_update_handler_publishes_int_update(pv_value, pv_type): producer = FakeProducer() context = FakeContext() pv_timestamp_s = 1.1 # seconds from unix epoch pv_source_name = "source_name" pva_update_handler = PVAUpdateHandler(producer, context, pv_source_name, "output_topic", "f142") # type: ignore context.call_monitor_callback_with_fake_pv_update( NTScalar(pv_type, valueAlarm=True).wrap(pv_value, timestamp=pv_timestamp_s)) assert producer.published_payload is not None pv_update_output = deserialise_f142(producer.published_payload) assert pv_update_output.value == pv_value assert pv_update_output.source_name == pv_source_name pva_update_handler.stop()
def test_update_handler_does_not_include_alarm_details_if_unchanged_in_subsequent_updates( ): producer = FakeProducer() context = FakeContext() pv_value = 42 pv_caproto_type = ChannelType.TIME_INT pv_numpy_type = np.int32 pv_source_name = "source_name" alarm_status = 6 # AlarmStatus.LOW alarm_severity = 1 # AlarmSeverity.MINOR update_handler = CAUpdateHandler(producer, context, pv_source_name, "output_topic", "f142") # type: ignore metadata = (alarm_status, alarm_severity, TimeStamp(4, 0)) context.call_monitor_callback_with_fake_pv_update( ReadNotifyResponse( np.array([pv_value]).astype(pv_numpy_type), pv_caproto_type, 1, 1, 1, metadata=metadata, )) # Second update, with unchanged alarm context.call_monitor_callback_with_fake_pv_update( ReadNotifyResponse( np.array([pv_value]).astype(pv_numpy_type), pv_caproto_type, 1, 1, 1, metadata=metadata, )) assert producer.messages_published == 2 pv_update_output = deserialise_f142(producer.published_payload) assert pv_update_output.alarm_status == AlarmStatus.NO_CHANGE assert pv_update_output.alarm_severity == AlarmSeverity.NO_CHANGE update_handler.stop()