From a00f1d2a75b1b20d8f574509455d8f3231a23d32 Mon Sep 17 00:00:00 2001 From: Deichmann Date: Wed, 6 Sep 2023 15:35:48 +0200 Subject: [PATCH 01/16] added ReferenceTestV2 to examples --- examples/ReferenceTestV2/logging_default.jsn | 110 +++ .../mdib_test_sequence_2_v4(temp).xml | 283 ++++++++ .../ReferenceTestV2/reference_consumer_v2.py | 665 ++++++++++++++++++ .../ReferenceTestV2/reference_provider_v2.py | 308 ++++++++ 4 files changed, 1366 insertions(+) create mode 100644 examples/ReferenceTestV2/logging_default.jsn create mode 100644 examples/ReferenceTestV2/mdib_test_sequence_2_v4(temp).xml create mode 100644 examples/ReferenceTestV2/reference_consumer_v2.py create mode 100644 examples/ReferenceTestV2/reference_provider_v2.py diff --git a/examples/ReferenceTestV2/logging_default.jsn b/examples/ReferenceTestV2/logging_default.jsn new file mode 100644 index 00000000..3e4ee096 --- /dev/null +++ b/examples/ReferenceTestV2/logging_default.jsn @@ -0,0 +1,110 @@ +{ + "version": 1, + "formatters":{ + "default":{ + "class": "logging.Formatter", + "format": "{asctime} - {name} - {levelname} - {message}", + "style": "{" + } + }, + "handlers":{ + "file":{ + "class": "logging.FileHandler", + "filename": "sdc_ref_dev.log", + "mode":"w", + "formatter": "default" + }, + "console": { + "class": "logging.StreamHandler", + "formatter": "default" + } + }, + "loggers":{ + "sdc": { + "handlers": ["file", "console"], + "level":"INFO" + }, + "sdc.discover": { + "level": "INFO" + }, + "sdc.discover.monitor": { + "level": null + }, + "sdc.schema_resolver": { + "comment": "logs resolving of schema locations", + "level": null + }, + "sdc.device": { + "comment": "base logger for device", + "level": null + }, + "sdc.device.soap": { + "level": null + }, + "sdc.device.mdib": { + "level": null + }, + "sdc.device.httpsrv": { + "level": null + }, + "sdc.device.op_worker": { + "comment": "logs execution of called operations", + "level": null + }, + "sdc.device.op_reg": { + "comment": "logs operation creation ", + "level": null + }, + "sdc.device.op_mgr": { + "comment": "logs operation calls", + "level": null + }, + "sdc.device.op": { + "comment": "for all operations", + "level": null + }, + "sdc.device.ops": { + "level": null + }, + "sdc.device.subscrMgr": { + "level": null + }, + "sdc.device.player": { + "comment": "if set to DEBUG, it will log all notifications that are replayed", + "level": null + }, + "sdc.client": { + "comment": "base logger for client", + "level": "WARN" + }, + "sdc.client.soap": { + "level": null + }, + "sdc.client.subscr": { + "comment": "logs events per subscription, e.g. renew", + "level": null + }, + "sdc.client.subscrMgr": { + "comment": "logs events of Subscriptions Manager, mainly errors if something goes wrong.", + "level": null + }, + "sdc.client.notif_dispatch": { + "comment": "debug level logs every incoming notification", + "level": null + }, + "sdc.client.mdib": { + "level": null + }, + "sdc.client.mdib.rt": { + "comment": "logs for real time data buffer", + "level": null + }, + "sdc.client.wf": { + "comment": "on debug it logs every incoming waveform", + "level": null + }, + "sdc.client.op_mgr": { + "level": null + } + } +} diff --git a/examples/ReferenceTestV2/mdib_test_sequence_2_v4(temp).xml b/examples/ReferenceTestV2/mdib_test_sequence_2_v4(temp).xml new file mode 100644 index 00000000..60d50f88 --- /dev/null +++ b/examples/ReferenceTestV2/mdib_test_sequence_2_v4(temp).xml @@ -0,0 +1,283 @@ + + + + + + + + + + equipment-label + + + + + SDPi Test MDS + + + + + Alert Condition that is present when the source metric exceeds 80 + + numeric_metric_1.channel_1.vmd_0.mds_0 + + + + + + + + Adds a exactly one Patient Context to the operation target by using the attributes and elements from the SCO Operation's payload + + + + + Sets the value of the targeted Numeric Metric + + + + + Sets the value of the targeted Enum String Metric + + + + + Performs nothing. The operational state will be toggled periodically at least every 5 seconds in order to produce Operational State Reports. + + + + + + + + + + + None + + + NTPv4 + + + EBWW + + + + + + Magnitude ampere(s) hour + + + + + Magnitude volt(s) + + + + + + SDPi Test VMD that contains settings and measurements including waveforms + + + + Channel that contains settings + + + + Numeric setting, externally controllable + + + Dimensionless + + + + + + Enum setting, externally controllable + + + Dimensionless + + + ON + + Enum Value ON + + + + OFF + + Enum Value OFF + + + + STANDBY + + Enum Value STANDBY + + + + + + String setting + + + Dimensionless + + + + + + Channel that contains measurements + + + + Periodically determined intermittent numeric measurement metric + + + Dimensionless + + + + + + Waveform metric 1 + + + Dimensionless + + + + + Waveform metric 2 + + + Dimensionless + + + + + Waveform metric 3 + + + Dimensionless + + + + + + + SDPi Test VMD that contains settings to be externally controlled by bulk operations + + + + + Sets the @Value of 2 metric states at once + + + + + + Channel that contains settings to be externally controlled by bulk operations + + + + Numeric setting, externally controllable by bulk update + + + Dimensionless + + + + + Numeric setting, externally controllable by bulk update + + + Dimensionless + + + + + + + + SDPi Test MDS used for description modification reports. This MDS periodically inserts and deletes a VMD including Channels including Metrics. + + + + SDPi Test VMD that contains a metric and an alarm for which units and cause-remedy information is periodically updated (description updates) + + + + + An alert condition that periodically changes its cause-remedy information at least every 5 seconds + + numeric_metric_0.channel_0.vmd_0.mds_1 + + + Remedy Info + + Cause Info + + + + + + Channel that contains a metric which is periodically changing its unit of measure + + + + Flow Rate: Numeric measurement that periodically changes the unit of measure at least every 5 seconds + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/ReferenceTestV2/reference_consumer_v2.py b/examples/ReferenceTestV2/reference_consumer_v2.py new file mode 100644 index 00000000..aa1b000c --- /dev/null +++ b/examples/ReferenceTestV2/reference_consumer_v2.py @@ -0,0 +1,665 @@ +import os +import time +import traceback +import uuid +from collections import defaultdict +from concurrent import futures +from decimal import Decimal +from sdc11073 import commlog +from sdc11073 import observableproperties +from sdc11073.certloader import mk_ssl_contexts_from_folder +from sdc11073.definitions_sdc import SDC_v1_Definitions +from sdc11073.mdib.consumermdib import ConsumerMdib +from sdc11073.mdib.consumermdibxtra import ConsumerMdibMethods +from sdc11073.consumer import SdcConsumer +from sdc11073.wsdiscovery import WSDiscovery +from sdc11073.xml_types import pm_qnames, msg_types + +ConsumerMdibMethods.DETERMINATIONTIME_WARN_LIMIT = 2.0 + +adapter_ip = os.getenv('ref_ip') or '127.0.0.1' +ca_folder = os.getenv('ref_ca') +ssl_passwd = os.getenv('ref_ssl_passwd') or None +search_epr = os.getenv('ref_search_epr') or 'abc' # 'abc' # abc is fixed ending in reference_device uuid. + +numeric_metric_handle = "numeric_metric_0.channel_0.vmd_0.mds_0" +alert_condition_handle = "alert_condition_0.vmd_0.mds_1" +set_value_handle = "set_value_0.sco.mds_0" +set_string_handle = "set_string_0.sco.mds_0" +set_context_state_handle = "set_context_0.sco.mds_0" + +ENABLE_COMMLOG = False +if ENABLE_COMMLOG: + comm_logger = commlog.CommLogger(log_folder=r'c:\temp\sdc_refclient_commlog', + log_out=True, + log_in=True, + broadcast_ip_filter=None) + commlog.set_communication_logger(comm_logger) + + +class ValuesCollectorPlus(observableproperties.ValuesCollector): + """ add a finish call + """ + def finish(self): + with self._cond: + self._state = self.FINISHED + observableproperties.unbind(self._obj, **{self._prop_name: self._on_data}) + self._cond.notify_all() + +sleep_timer = 10 + + +def test_1b(wsd, my_service) -> str: + # send resolve and check response + wsd.clear_remote_services() + wsd._send_resolve(my_service.epr) + time.sleep(3) + if len(wsd._remote_services) == 0: + return ('### Test 1b ### failed, no response') + elif len(wsd._remote_services) > 1: + return ('### Test 1b ### failed, multiple response') + else: + service = wsd._remote_services.get(my_service.epr) + if service.epr != my_service.epr: + return ('### Test 1b ### failed, not the same epr') + else: + return ('### Test 1b ### passed') + + +def connect_client(my_service) -> SdcConsumer: + if ca_folder: + ssl_contexts = mk_ssl_contexts_from_folder(ca_folder, + cyphers_file=None, + private_key='user_private_key_encrypted.pem', + certificate='user_certificate_root_signed.pem', + ca_public_key='root_certificate.pem', + ssl_passwd=ssl_passwd, + ) + else: + ssl_contexts = None + client = SdcConsumer.from_wsd_service(my_service, + ssl_context_container=ssl_contexts, + validate=True) + client.start_all() + return client + + +def init_mdib(client) -> ConsumerMdib: + # The Reference Provider answers to GetMdib + mdib = ConsumerMdib(client) + mdib.init_mdib() + return mdib + + +def test_min_updates_per_handle(updates_dict, min_updates, node_type_filter = None) -> (bool, str): # True ok + results = [] + is_ok = True + if len(updates_dict) == 0: + is_ok = False + results.append('no updates') + else: + for k, v in updates_dict.items(): + if node_type_filter: + v = [n for n in v if n.NODETYPE == node_type_filter] + if len(v) < min_updates: + is_ok = False + results.append(f'Handle {k} only {len(v)} updates, expect >= {min_updates}') + return is_ok, '\n'.join(results) + + +def test_min_updates_for_type(updates_dict, min_updates, q_name) -> (bool, str): # True ok + flat_list = [] + for v in updates_dict.values(): + flat_list.extend(v) + matches = [x for x in flat_list if x.NODETYPE == q_name] + if len(matches) >= min_updates: + return True, '' + return False, f'expect >= {min_updates}, got {len(matches)} out of {len(flat_list)}' + + +def log_result(is_ok, result_list, step, info, extra_info=None): + xtra = f' ({extra_info}) ' if extra_info else '' + if is_ok: + result_list.append(f'{step} => passed {xtra}{info}') + else: + result_list.append(f'{step} => failed {xtra}{info}') + + +def run_ref_test(): + results = [] + print(f'using adapter address {adapter_ip}') + print('Test step 1: discover device which endpoint ends with "{}"'.format(search_epr)) + wsd = WSDiscovery(adapter_ip) + wsd.start() + + # 1. Device Discovery + # a) The Reference Provider sends Hello messages + # b) The Reference Provider answers to Probe and Resolve messages + + my_service = None + while my_service is None: + services = wsd.search_services(types=SDC_v1_Definitions.MedicalDeviceTypesFilter) + print('found {} services {}'.format(len(services), ', '.join([s.epr for s in services]))) + for s in services: + if s.epr.endswith(search_epr): + my_service = s + print('found service {}'.format(s.epr)) + break + print('Test step 1 successful: device discovered') + results.append('### Test 1 ### passed') + + print('Test step 1b: send resolve and check response') + result = test_1b(wsd, my_service) + results.append(result) + print(f'{result} : resolve and check response') + + # print('Test step 1c: connect to device...') + # try: + # client = test_1c(my_service) + # results.append('### Test 1c ### passed') + # except: + # print (traceback.format_exc()) + # results.append('### Test 1c ### failed') + # return results + + # 2. BICEPS Services Discovery and binding + # a) The Reference Provider answers to TransferGet + # b) The SDCri Reference Provider grants subscription runtime of at most 15 seconds in order to enforce Reference Consumers to send renew requests + + """2. BICEPS Services Discovery and binding + a) The Reference Provider answers to TransferGet + b) The Reference Consumer renews at least one subscription once during the test phase; + the Reference Provider grants subscriptions of at most 15 seconds + (this allows for the Reference Consumer to verify if auto-renew works)""" + step = '2a' + info = 'The Reference Provider answers to TransferGet' + print(step, info) + try: + client = connect_client(my_service) + log_result(client.host_description is not None, results, step, info) + except: + print(traceback.format_exc()) + results.append(f'{step} => failed') + return results + + step = '2b.1' + info = 'the Reference Provider grants subscriptions of at most 15 seconds' + now = time.time() + durations = [s.expires_at - now for s in client.subscription_mgr.subscriptions.values()] + print(f'subscription durations = {durations}') + log_result(max(durations) <= 15, results, step, info) + step = '2b.2' + info = 'the Reference Provider grants subscriptions of at most 15 seconds (renew)' + granted = list(client.subscription_mgr.subscriptions.items())[0][1].renew(30000) + print(f'renew granted = {granted}') + log_result(max(durations) <= 15, results, step, info) + + # 3. Request Response + # a) The Reference Provider answers to GetMdib + # b) The Reference Provider answers to GetContextStates messages + # b.1) The Reference Provider provides at least one location context state + step = '3a' + info = 'The Reference Provider answers to GetMdib' + print(step, info) + try: + mdib = init_mdib(client) + log_result(mdib is not None, results, step, info) + except: + print(traceback.format_exc()) + results.append(f'{step} => failed') + return results + + step = '3b' + info = 'The Reference Provider answers to GetContextStates messages' + context_service = client.context_service_client + if context_service is None: + results.append(f'{step} => failed {info}') + else: + try: + states = context_service.get_context_states().result.ContextState + results.append(f'{step} => passed {info}') + except: + print(traceback.format_exc()) + results.append(f'{step} => failed {info}') + return results + step = 'Test step 3b.1: The Reference Provider provides at least one location context state' + loc_states = [ s for s in states if s.NODETYPE == pm_qnames.LocationContextState] + log_result(len(loc_states) > 0, results, step, info) + + # 4 State Reports + # a)The Reference Provider produces at least 5 metric updates in 30 seconds + # The metric types shall comprise numeric and string metrics + # b) The Reference Provider produces at least 5 alert condition updates in 30 seconds + # c) The Reference Provider produces at least 5 alert signal updates in 30 seconds + # d) The Reference Provider provides alert system self checks in accordance to the periodicity defined in the MDIB (at least every 10 seconds) + # e) The Reference Provider provides 3 waveforms x 10 messages per second x 100 samples per message + # f) The Reference Provider provides changes for the following components: + # * Clock/Battery object (Component report) + # * The Reference Provider provides changes for the VMD/MDS (Component report) + # g) The Reference Provider provides changes for the following operational states: + # Enable/Disable operations (some different than the ones mentioned above) (Operational State Report)""" + + # setup data collectors for next test steps + numeric_metric_updates = defaultdict(list) + string_metric_updates = defaultdict(list) + alert_condition_updates = defaultdict(list) + alert_signal_updates = defaultdict(list) + # other_alert_updates = defaultdict(list) + alert_system_updates = defaultdict(list) + component_updates = defaultdict(list) + waveform_updates = defaultdict(list) + description_updates = [] + + def onMetricUpdates(metrics_by_handle): + # print('onMetricUpdates', metrics_by_handle) + for k, v in metrics_by_handle.items(): + print(f'State {v.NODETYPE.localname} {v.DescriptorHandle}') + if v.NODETYPE == pm_qnames.NumericMetricState: + numeric_metric_updates[k].append(v) + elif v.NODETYPE == pm_qnames.StringMetricState: + string_metric_updates[k].append(v) + + def on_alert_updates(alerts_by_handle): + for k, v in alerts_by_handle.items(): + print(f'State {v.NODETYPE.localname} {v.DescriptorHandle}') + if v.is_alert_condition: + alert_condition_updates[k].append(v) + elif v.is_alert_signal: + alert_signal_updates[k].append(v) + elif v.NODETYPE == pm_qnames.AlertSystemState: + alert_system_updates[k].append(v) + # else: + # other_alert_updates[k].append(v) + + def on_component_updates(components_by_handle): + # print('on_component_updates', alerts_by_handle) + for k, v in components_by_handle.items(): + print(f'State {v.NODETYPE.localname} {v.DescriptorHandle}') + component_updates[k].append(v) + + def on_waveform_updates(waveforms_by_handle): + # print('on_waveform_updates', alerts_by_handle) + for k, v in waveforms_by_handle.items(): + # print(f'State {v.NODETYPE.localname} {v.DescriptorHandle}') + waveform_updates[k].append(v) + + def on_description_modification(description_modification_report): + print('on_description_modification') + description_updates.append(description_modification_report) + + observableproperties.bind(mdib, metrics_by_handle=onMetricUpdates) + observableproperties.bind(mdib, alert_by_handle=on_alert_updates) + observableproperties.bind(mdib, component_by_handle=on_component_updates) + observableproperties.bind(mdib, waveform_by_handle=on_waveform_updates) + observableproperties.bind(mdib, description_modifications=on_description_modification) + + min_updates = sleep_timer // 5 - 1 + print('will wait for {} seconds now, expecting at least {} updates per Handle'.format(sleep_timer, min_updates)) + time.sleep(sleep_timer) + + step = '4a' + info ='count numeric metric state updates' + print(step, info) + is_ok, result = test_min_updates_per_handle(numeric_metric_updates, min_updates) + log_result(is_ok, results, step, info) + + step = '4b' + info = 'count string metric state updates' + print(step) + is_ok, result = test_min_updates_per_handle(string_metric_updates, min_updates,) + log_result(is_ok, results, step, info) + + step = '4c' + info = 'count alert condition updates' + print(step) + is_ok, result = test_min_updates_per_handle(alert_condition_updates, min_updates) + log_result(is_ok, results, step, info) + + step = '4d' + info =' count alert signal updates' + print(step, info) + is_ok, result = test_min_updates_per_handle(alert_signal_updates, min_updates) + log_result(is_ok, results, step, info) + + step ='4e' + info = 'count alert system self checks' + is_ok, result = test_min_updates_per_handle(alert_system_updates, min_updates) + log_result(is_ok, results, step, info) + + step = '4f' + info = 'count waveform updates' + print(step, info) + is_ok, result = test_min_updates_per_handle(waveform_updates, min_updates) + log_result(is_ok, results, step, info) + + pm = mdib.data_model.pm_names + pm_types = mdib.data_model.pm_types + + # The Reference Provider provides changes for the following reports as well: + # Clock/Battery object (Component report) + step = '4g' + info = 'count battery updates' + print(step, info) + is_ok, result = test_min_updates_for_type(component_updates, 1, pm.BatteryState) + log_result(is_ok, results, step, info) + + step = '4g' + info ='count VMD updates' + print(step, info) + is_ok, result = test_min_updates_for_type(component_updates, 1, pm.VmdState) + log_result(is_ok, results, step, info) + + step = '4g' + info = 'count MDS updates' + print(step, info) + is_ok, result = test_min_updates_for_type(component_updates, 1, pm.MdsState) + log_result(is_ok, results, step, info) + + step = '4h' + info = 'Enable/Disable operations' + results.append(f'{step} => failed, not implemented {info}') + + + """ + 5 Description Modifications: + a) The Reference Provider produces at least 1 update every 10 seconds comprising + * Update Alert condition concept description of type + * Update Alert condition cause-remedy information + * Update Unit of measure (metrics) + b) The Reference Provider produces at least 1 insertion followed by a deletion every 10 seconds comprising + * Insert a VMD including Channels including metrics + * Remove the VMD + """ + step = '5a' + info = 'Update Alert condition concept description of type' + print(step, info) + # verify only that there are Alert Condition Descriptors updated + found = False + for report in description_updates: + for report_part in report.ReportPart: + if report_part.ModificationType == msg_types.DescriptionModificationType.UPDATE: + for descriptor in report_part.Descriptor: + if descriptor.NODETYPE == pm_qnames.AlertConditionDescriptor: + found = True + log_result(found, results, step, info) + + step = '5a' + info = 'Update Unit of measure' + print(step, info) + # verify only that there are Alert Condition Descriptors updated + found = False + for report in description_updates: + for report_part in report.ReportPart: + if report_part.ModificationType == msg_types.DescriptionModificationType.UPDATE: + for descriptor in report_part.Descriptor: + if descriptor.NODETYPE == pm_qnames.NumericMetricDescriptor: + found = True + log_result(found, results, step, info) + + step = '5b' + info = 'Add / remove vmd' + print(step, info) + # verify only that there are Alert Condition Descriptors updated + add_found = False + rm_found = False + for report in description_updates: + for report_part in report.ReportPart: + if report_part.ModificationType == msg_types.DescriptionModificationType.CREATE: + for descriptor in report_part.Descriptor: + if descriptor.NODETYPE == pm_qnames.VmdDescriptor: + add_found = True + if report_part.ModificationType == msg_types.DescriptionModificationType.DELETE: + for descriptor in report_part.Descriptor: + if descriptor.NODETYPE == pm_qnames.VmdDescriptor: + rm_found = True + log_result(add_found, results, step, info, 'add') + log_result(rm_found, results, step, info, 'remove') + + """ + 6 Operation invocation + a) (removed) + b) SetContextState: + * Payload: 1 Patient Context + * Context state is added to the MDIB including context association and validation + * If there is an associated context already, that context shall be disassociated + * Handle and version information is generated by the provider + * In order to avoid infinite growth of patient contexts, older contexts are allowed to be removed from the MDIB (=ContextAssociation=No) + c) SetValue: Immediately answers with "finished" + * Finished has to be sent as a report in addition to the response => + d) SetString: Initiates a transaction that sends Wait, Start and Finished + e) SetMetricStates: + * Payload: 2 Metric States (settings; consider alert limits) + * Immediately sends finished + * Action: Alter values of metrics """ + + + step = '6b' + info = 'SetContextState' + print(step, info) + # patients = mdib.context_states.NODETYPE.get(pm.PatientContextState, []) + patient_context_descriptors = mdib.descriptions.NODETYPE.get(pm.PatientContextDescriptor, []) + generated_family_names = [] + if len(patient_context_descriptors) == 0: + log_result(False, results, step, info, extra_info='no PatientContextDescriptor') + else: + try: + for i, p in enumerate(patient_context_descriptors): + pat = client.context_service_client.mk_proposed_context_object(p.Handle) + pat.CoreData.Familyname = uuid.uuid4().hex # f'Fam{i}' + pat.ContextAssociation = pm_types.ContextAssociation.ASSOCIATED + generated_family_names.append(pat.CoreData.Familyname) + client.context_service_client.set_context_state(set_context_state_handle, [pat]) + time.sleep(1) # allow update notification to arrive + patients = mdib.context_states.NODETYPE.get(pm_qnames.PatientContextState, []) + if len(patients) == 0: + log_result(False, results, step, info, extra_info='no patients found') + else: + all_ok = True + for patient in patients: + if patient.CoreData.Familyname in generated_family_names: + if patient.ContextAssociation != pm_types.ContextAssociation.ASSOCIATED: + log_result(False, results, step, info, + extra_info=f'new patient {patient.CoreData.Familyname} is {patient.ContextAssociation}') + # print(f'{step} => failed {info}, new patient {patient.CoreData.Familyname} is {patient.ContextAssociation}') + all_ok = False + else: + if patient.ContextAssociation == pm_types.ContextAssociation.ASSOCIATED: + log_result(False, results, step, info, + extra_info=f'old patient {patient.CoreData.Familyname} is {patient.ContextAssociation}') + all_ok = False + log_result(all_ok, results, step, info) + except Exception as ex: + print(traceback.format_exc()) + log_result(False, results, step, info, ex) + + step = '6c' + info = 'SetValue: Immediately answers with "finished"' + print(step, info) + subscriptions = client.subscription_mgr.subscriptions.values() + operation_invoked_subscriptions = [subscr for subscr in subscriptions + if subscr.short_filter_string == 'OperationInvokedReport'] + if len(operation_invoked_subscriptions) == 0: + log_result(False, results, step, info, 'OperationInvokedReport not subscribed, cannot test') + elif len(operation_invoked_subscriptions) > 1: + log_result(False, results, step, info, f'found {len(operation_invoked_subscriptions)} OperationInvokedReport subscribed, cannot test') + else: + try: + coll = ValuesCollectorPlus(operation_invoked_subscriptions[0], 'notification_data',5) + operation = client.mdib.descriptions.NODETYPE.get_one(pm_qnames.SetValueOperationDescriptor) + client.set_service_client.set_numeric_value(operation.Handle, Decimal(42)) + time.sleep(2) + coll.finish() + coll_result = coll.result() + messages = [] + for entry in coll_result: + messages.append(msg_types.OperationInvokedReport.from_node(entry.p_msg.msg_node)) + if len(coll_result) == 0: + log_result(False, results, step, info, 'no notification') + elif len(coll_result) > 1: + log_result(False, results, step, info, f'got {len(coll_result)} notifications, expect only one') + else: + first_notification = coll_result[0] + log_result(True, results, step, info, f'got {len(coll_result)} notifications : {first_notification}') + except Exception as ex: + print(traceback.format_exc()) + log_result(False, results, step, info, ex) + + step = '6d' + info = 'SetString: Initiates a transaction that sends Wait, Start and Finished' + print(step, info) + operation = client.mdib.descriptions.NODETYPE.get_one(pm_qnames.SetStringOperationDescriptor) + try: + coll = ValuesCollectorPlus(operation_invoked_subscriptions[0], 'notification_data', 5) + client.set_service_client.set_string(operation.Handle, '42') + time.sleep(2) + coll.finish() + coll_result = coll.result() + if len(coll_result) == 0: + log_result(False, results, step, info, 'no notification') + elif len(coll_result) >= 3: + log_result(True, results, step, info, f'got {len(coll_result)} notifications') + + # log_result(False, results, step, info, 'not implemented ') + except Exception as ex: + print(traceback.format_exc()) + log_result(False, results, step, info, ex) + + step = '6e' + info = 'SetMetricStates Immediately sends finished' + print(step, info) + try: + operation = client.mdib.descriptions.NODETYPE.get_one(pm_qnames.SetMetricStateOperationDescriptor) + # client.set_service_client.set_metric_state(operation.Handle, '42') + log_result(False, results, step, info, 'not implemented ') + except Exception as ex: + print(traceback.format_exc()) + log_result(False, results, step, info, ex) + + step = '7' + info = 'Graceful shutdown (at least subscriptions are ended; optionally Bye is sent)' + try: + success = client._subscription_mgr.unsubscribe_all() + log_result(success, results, step, info) + except Exception as ex: + print(traceback.format_exc()) + log_result(False, results, step, info, ex) + time.sleep(2) + return results + + # print('Test step 9: call SetString operation') + # setstring_operations = mdib.descriptions.NODETYPE.get(pm.SetStringOperationDescriptor, []) + # setst_handle = 'string.ch0.vmd1_sco_0' + # if len(setstring_operations) == 0: + # print('Test step 9(SetString) failed, no SetString operation found') + # results.append('### Test 9 ### failed') + # else: + # for s in setstring_operations: + # if s.Handle != setst_handle: + # continue + # print('setString Op ={}'.format(s)) + # try: + # fut = client.set_service_client.set_string(s.Handle, 'hoppeldipop') + # try: + # res = fut.result(timeout=10) + # print(res) + # if res.InvocationInfo.InvocationState != msgtypes.InvocationState.FINISHED: + # print('set string operation {} did not finish with "Fin":{}'.format(s.Handle, res)) + # results.append('### Test 9(SetString) ### failed') + # else: + # print('set string operation {} ok:{}'.format(s.Handle, res)) + # results.append('### Test 9(SetString) ### passed') + # except futures.TimeoutError: + # print('timeout error') + # results.append('### Test 9(SetString) ### failed') + # except Exception as ex: + # print(f'Test 9(SetString): {ex}') + # results.append('### Test 9(SetString) ### failed') + # + # print('Test step 9: call SetValue operation') + # setvalue_operations = mdib.descriptions.NODETYPE.get(pm.SetValueOperationDescriptor, []) + # # print('setvalue_operations', setvalue_operations) + # setval_handle = 'numeric.ch0.vmd1_sco_0' + # if len(setvalue_operations) == 0: + # print('Test step 9 failed, no SetValue operation found') + # results.append('### Test 9(SetValue) ### failed') + # else: + # for s in setvalue_operations: + # if s.Handle != setval_handle: + # continue + # print('setNumericValue Op ={}'.format(s)) + # try: + # fut = client.set_service_client.set_numeric_value(s.Handle, 42) + # try: + # res = fut.result(timeout=10) + # print(res) + # if res.InvocationInfo.InvocationState != msgtypes.InvocationState.FINISHED: + # print('set value operation {} did not finish with "Fin":{}'.format(s.Handle, res)) + # else: + # print('set value operation {} ok:{}'.format(s.Handle, res)) + # results.append('### Test 9(SetValue) ### passed') + # except futures.TimeoutError: + # print('timeout error') + # results.append('### Test 9(SetValue) ### failed') + # except Exception as ex: + # print(f'Test 9(SetValue): {ex}') + # results.append('### Test 9(SetValue) ### failed') + # + # print('Test step 9: call Activate operation') + # activate_operations = mdib.descriptions.NODETYPE.get(pm.ActivateOperationDescriptor, []) + # activate_handle = 'actop.vmd1_sco_0' + # if len(setstring_operations) == 0: + # print('Test step 9 failed, no Activate operation found') + # results.append('### Test 9(Activate) ### failed') + # else: + # for s in activate_operations: + # if s.Handle != activate_handle: + # continue + # print('activate Op ={}'.format(s)) + # try: + # fut = client.set_service_client.activate(s.Handle, 'hoppeldipop') + # try: + # res = fut.result(timeout=10) + # print(res) + # if res.InvocationInfo.InvocationState != msgtypes.InvocationState.FINISHED: + # print('activate operation {} did not finish with "Fin":{}'.format(s.Handle, res)) + # results.append('### Test 9(Activate) ### failed') + # else: + # print('activate operation {} ok:{}'.format(s.Handle, res)) + # results.append('### Test 9(Activate) ### passed') + # except futures.TimeoutError: + # print('timeout error') + # results.append('### Test 9(Activate) ### failed') + # except Exception as ex: + # print(f'Test 9(Activate): {ex}') + # results.append('### Test 9(Activate) ### failed') + + # print('Test step 10: cancel all subscriptions') + # success = client._subscription_mgr.unsubscribe_all() + # if success: + # results.append('### Test 10(unsubscribe) ### passed') + # else: + # results.append('### Test 10(unsubscribe) ### failed') + # time.sleep(2) + # return results + + +if __name__ == '__main__': + xtra_log_config = os.getenv('ref_xtra_log_cnf') # or None + + import json + import logging.config + + here = os.path.dirname(__file__) + + with open(os.path.join(here, 'logging_default.jsn')) as f: + logging_setup = json.load(f) + logging.config.dictConfig(logging_setup) + if xtra_log_config is not None: + with open(xtra_log_config) as f: + logging_setup2 = json.load(f) + logging.config.dictConfig(logging_setup2) + + run_results = run_ref_test() + print('\n### Summary ###') + for r in run_results: + print(r) diff --git a/examples/ReferenceTestV2/reference_provider_v2.py b/examples/ReferenceTestV2/reference_provider_v2.py new file mode 100644 index 00000000..df4f99e7 --- /dev/null +++ b/examples/ReferenceTestV2/reference_provider_v2.py @@ -0,0 +1,308 @@ +from __future__ import annotations +import json +import logging.config +import os +import traceback +from decimal import Decimal +from time import sleep +from uuid import UUID +import datetime +from typing import TYPE_CHECKING + +from sdc11073.certloader import mk_ssl_contexts_from_folder +from sdc11073.location import SdcLocation +from sdc11073.loghelper import LoggerAdapter +from sdc11073.mdib import ProviderMdib, descriptorcontainers +from sdc11073.pysoap.soapclient_async import SoapClientAsync +from sdc11073.provider.components import SdcProviderComponents +from sdc11073.provider import SdcProvider +from sdc11073.provider.servicesfactory import DPWSHostedService +from sdc11073.provider.servicesfactory import HostedServices, mk_dpws_hosts +from sdc11073.provider.subscriptionmgr_async import SubscriptionsManagerReferenceParamAsync +from sdc11073.provider import waveforms +from sdc11073.wsdiscovery import WSDiscovery +from sdc11073.xml_types import pm_types, pm_qnames +from sdc11073.xml_types.dpws_types import ThisDeviceType, ThisModelType + +here = os.path.dirname(__file__) +default_mdib_path = os.path.join(here, 'mdib_test_sequence_2_v4(temp).xml') +mdib_path = os.getenv('ref_mdib') or default_mdib_path +xtra_log_config = os.getenv('ref_xtra_log_cnf') # or None + +My_UUID_str = '12345678-6f55-11ea-9697-123456789bcd' + +# these variables define how the device is published on the network: +adapter_ip = os.getenv('ref_ip') or '127.0.0.1' +ca_folder = os.getenv('ref_ca') +ref_fac = os.getenv('ref_fac') or 'r_fac' +ref_poc = os.getenv('ref_poc') or 'r_poc' +ref_bed = os.getenv('ref_bed') or 'r_bed' +ssl_passwd = os.getenv('ref_ssl_passwd') or None + +numeric_metric_handle = "numeric_metric_0.channel_0.vmd_0.mds_0" +string_metric_handle = "string_metric_0.channel_0.vmd_0.mds_0" +alert_condition_handle = "alert_condition_0.vmd_0.mds_1" +alert_signal_handle = "alert_signal_0.mds_0" +set_value_handle = "set_value_0.sco.mds_0" +set_string_handle = "set_string_0.sco.mds_0" +battery_handle = 'battery_0.mds_0' +vmd_handle = "vmd_0.mds_0" +mds_handle = "mds_0" +USE_REFERENCE_PARAMETERS = False + + +def mk_all_services_except_localization(sdc_provider, components, subscription_managers) -> HostedServices: + # register all services with their endpoint references acc. to structure in components + dpws_services, services_by_name = mk_dpws_hosts(sdc_provider, components, DPWSHostedService, subscription_managers) + hosted_services = HostedServices(dpws_services, + services_by_name['GetService'], + set_service=services_by_name.get('SetService'), + context_service=services_by_name.get('ContextService'), + description_event_service=services_by_name.get('DescriptionEventService'), + state_event_service=services_by_name.get('StateEventService'), + waveform_service=services_by_name.get('WaveformService'), + containment_tree_service=services_by_name.get('ContainmentTreeService'), + # localization_service=services_by_name.get('LocalizationService') + ) + return hosted_services + + +def provide_realtime_data(sdc_provider): + waveform_provider = sdc_provider.mdib.xtra.waveform_provider + if waveform_provider is None: + return + mdib_waveforms = sdc_provider.mdib.descriptions.NODETYPE.get(pm_qnames.RealTimeSampleArrayMetricDescriptor) + for waveform in mdib_waveforms: + wf_generator = waveforms.SawtoothGenerator(min_value=0, max_value=10, waveformperiod=1.1, sampleperiod=0.01) + waveform_provider.register_waveform_generator(waveform.Handle, wf_generator) + + +if __name__ == '__main__': + with open(os.path.join(here, 'logging_default.jsn')) as f: + logging_setup = json.load(f) + logging.config.dictConfig(logging_setup) + if xtra_log_config is not None: + with open(xtra_log_config) as f: + logging_setup2 = json.load(f) + logging.config.dictConfig(logging_setup2) + + logger = logging.getLogger('sdc') + logger = LoggerAdapter(logger) + logger.info('{}', 'start') + wsd = WSDiscovery(adapter_ip) + wsd.start() + my_mdib = ProviderMdib.from_mdib_file(mdib_path) + my_uuid = UUID(My_UUID_str) + print("UUID for this device is {}".format(my_uuid)) + loc = SdcLocation(ref_fac, ref_poc, ref_bed) + print("location for this device is {}".format(loc)) + dpwsModel = ThisModelType(manufacturer='sdc11073', + manufacturer_url='www.sdc11073.com', + model_name='TestDevice', + model_number='1.0', + model_url='www.sdc11073.com/model', + presentation_url='www.sdc11073.com/model/presentation') + + dpwsDevice = ThisDeviceType(friendly_name='TestDevice', + firmware_version='Version1', + serial_number='12345') + if ca_folder: + ssl_contexts = mk_ssl_contexts_from_folder(ca_folder, + private_key='user_private_key_encrypted.pem', + certificate='user_certificate_root_signed.pem', + ca_public_key='root_certificate.pem', + cyphers_file=None, + ssl_passwd=ssl_passwd) + else: + ssl_contexts = None + if USE_REFERENCE_PARAMETERS: + tmp = {'StateEvent': SubscriptionsManagerReferenceParamAsync} + specific_components = SdcProviderComponents(subscriptions_manager_class=tmp, + services_factory=mk_all_services_except_localization, + soap_client_class=SoapClientAsync) + else: + specific_components = None # SdcDeviceComponents(services_factory=mk_all_services_except_localization) + sdc_provider = SdcProvider(wsd, dpwsModel, dpwsDevice, my_mdib, my_uuid, + ssl_context_container=ssl_contexts, + specific_components=specific_components, + max_subscription_duration=15 + ) + sdc_provider.start_all() + + validators = [pm_types.InstanceIdentifier('Validator', extension_string='System')] + sdc_provider.set_location(loc, validators) + provide_realtime_data(sdc_provider) + pm = my_mdib.data_model.pm_names + pm_types = my_mdib.data_model.pm_types + patientDescriptorHandle = my_mdib.descriptions.NODETYPE.get(pm.PatientContextDescriptor)[0].Handle + with my_mdib.transaction_manager() as mgr: + patientContainer = mgr.mk_context_state(patientDescriptorHandle) + patientContainer.CoreData.Givenname = "Given" + patientContainer.CoreData.Middlename = ["Middle"] + patientContainer.CoreData.Familyname = "Familiy" + patientContainer.CoreData.Birthname = "Birthname" + patientContainer.CoreData.Title = "Title" + patientContainer.ContextAssociation = pm_types.ContextAssociation.ASSOCIATED + identifiers = [] + patientContainer.Identification = identifiers + + descs = list(sdc_provider.mdib.descriptions.objects) + descs.sort(key=lambda x: x.Handle) + numeric_metric = None + string_metric = None + alertCondition = None + alertSignal = None + battery_descriptor = None + activateOperation = None + stringOperation = None + valueOperation = None + for oneContainer in descs: + if oneContainer.Handle == numeric_metric_handle: + numeric_metric = oneContainer + if oneContainer.Handle == string_metric_handle: + string_metric = oneContainer + if oneContainer.Handle == alert_condition_handle: + alertCondition = oneContainer + if oneContainer.Handle == alert_signal_handle: + alertSignal = oneContainer + if oneContainer.Handle == battery_handle: + battery_descriptor = oneContainer + if oneContainer.Handle == set_value_handle: + valueOperation = oneContainer + if oneContainer.Handle == set_string_handle: + stringOperation = oneContainer + + with sdc_provider.mdib.transaction_manager() as mgr: + state = mgr.get_state(valueOperation.OperationTarget) + if not state.MetricValue: + state.mk_metric_value() + state = mgr.get_state(stringOperation.OperationTarget) + if not state.MetricValue: + state.mk_metric_value() + print("Running forever, CTRL-C to exit") + try: + num_current_value = 0 + str_current_value = 0 + while True: + if numeric_metric: + try: + with sdc_provider.mdib.transaction_manager() as mgr: + state = mgr.get_state(numeric_metric.Handle) + if not state.MetricValue: + state.mk_metric_value() + state.MetricValue.Value = Decimal(num_current_value) + num_current_value += 1 + with sdc_provider.mdib.transaction_manager() as mgr: + descriptor: descriptorcontainers.AbstractMetricDescriptorContainer = mgr.get_descriptor(numeric_metric.Handle) + descriptor.Unit.Code = 'code1' if descriptor.Unit.Code == 'code2' else 'code2' + except Exception as ex: + print(traceback.format_exc()) + else: + print("Numeric Metric not found in MDIB!") + if string_metric: + try: + with sdc_provider.mdib.transaction_manager() as mgr: + state = mgr.get_state(string_metric.Handle) + if not state.MetricValue: + state.mk_metric_value() + state.MetricValue.Value = f'string {str_current_value}' + str_current_value += 1 + except Exception as ex: + print(traceback.format_exc()) + else: + print("Numeric Metric not found in MDIB!") + + if alertCondition: + try: + with sdc_provider.mdib.transaction_manager() as mgr: + state = mgr.get_state(alertCondition.Handle) + state.Presence = not state.Presence + except Exception as ex: + print(traceback.format_exc()) + try: + with sdc_provider.mdib.transaction_manager() as mgr: + now = datetime.datetime.now() + text = f'last changed at {now.hour:02d}:{now.minute:02d}:{now.second:02d}' + descriptor: descriptorcontainers.AlertConditionDescriptorContainer = mgr.get_descriptor(alertCondition.Handle) + if len(descriptor.Type.ConceptDescription) == 0: + descriptor.Type.ConceptDescription.append(pm_types.LocalizedText(text)) + else: + descriptor.Type.ConceptDescription[0].text = text + if len(descriptor.CauseInfo) == 0: + descriptor.CauseInfo.append(pm_types.CauseInfo()) + if len(descriptor.CauseInfo[0].RemedyInfo.Description) == 0: + descriptor.CauseInfo[0].RemedyInfo.Description.append(pm_types.LocalizedText(text)) + else: + descriptor.CauseInfo[0].RemedyInfo.Description[0].text = text + except Exception as ex: + print(traceback.format_exc()) + + else: + print("Alert condition not found in MDIB") + + if alertSignal: + try: + with sdc_provider.mdib.transaction_manager() as mgr: + state = mgr.get_state(alertSignal.Handle) + if state.Slot is None: + state.Slot = 1 + else: + state.Slot += 1 + except Exception as ex: + print(traceback.format_exc()) + else: + print("Alert signal not found in MDIB") + + if battery_descriptor: + try: + with sdc_provider.mdib.transaction_manager() as mgr: + state = mgr.get_state(battery_descriptor.Handle) + if state.Voltage is None: + state.Voltage = pm_types.Measurement(value=Decimal('14.4'), unit=pm_types.CodedValue('xyz')) + else: + state.Voltage.MeasuredValue += Decimal('0.1') + print(f'battery voltage = {state.Voltage.MeasuredValue}') + except Exception as ex: + print(traceback.format_exc()) + else: + print("battery state not found in MDIB") + + try: + with sdc_provider.mdib.transaction_manager() as mgr: + state = mgr.get_state(vmd_handle) + state.OperatingHours = 2 if state.OperatingHours != 2 else 1 + print(f'operating hours = {state.OperatingHours}') + except Exception as ex: + print(traceback.format_exc()) + + try: + with sdc_provider.mdib.transaction_manager() as mgr: + state = mgr.get_state(mds_handle) + state.Lang = 'de' if state.Lang != 'de' else 'en' + print(f'mds lang = {state.Lang}') + except Exception as ex: + print(traceback.format_exc()) + + + # add or rm vmd + add_rm_metric_handle = 'add_rm_metric' + add_rm_channel_handle = 'add_rm_channel' + add_rm_vmd_handle = 'add_rm_vmd' + add_rm_mds_handle = 'mds_0' + vmd_descriptor = sdc_provider.mdib.descriptions.handle.get_one(add_rm_vmd_handle, allow_none=True) + if vmd_descriptor is None: + vmd = descriptorcontainers.VmdDescriptorContainer(add_rm_vmd_handle, add_rm_mds_handle) + channel = descriptorcontainers.ChannelDescriptorContainer(add_rm_channel_handle, add_rm_vmd_handle) + metric = descriptorcontainers.StringMetricDescriptorContainer(add_rm_metric_handle, add_rm_channel_handle) + metric.Unit = pm_types.CodedValue('123') + with sdc_provider.mdib.transaction_manager() as mgr: + mgr.add_descriptor(vmd) + mgr.add_descriptor(channel) + mgr.add_descriptor(metric) + else: + with sdc_provider.mdib.transaction_manager() as mgr: + mgr.remove_descriptor(add_rm_vmd_handle) + + sleep(5) + except KeyboardInterrupt: + print("Exiting...") From 663e3fbd33a3507840a6ec5f549e21e2a1205f1f Mon Sep 17 00:00:00 2001 From: Deichmann Date: Tue, 12 Sep 2023 17:14:20 +0200 Subject: [PATCH 02/16] improved referencetests v2 --- .../ReferenceTestV2/reference_consumer_v2.py | 48 ++++++++++++++++--- .../ReferenceTestV2/reference_provider_v2.py | 32 ++++++++++--- src/sdc11073/roles/metricprovider.py | 10 ++-- 3 files changed, 73 insertions(+), 17 deletions(-) diff --git a/examples/ReferenceTestV2/reference_consumer_v2.py b/examples/ReferenceTestV2/reference_consumer_v2.py index aa1b000c..b0ef0307 100644 --- a/examples/ReferenceTestV2/reference_consumer_v2.py +++ b/examples/ReferenceTestV2/reference_consumer_v2.py @@ -46,7 +46,7 @@ def finish(self): observableproperties.unbind(self._obj, **{self._prop_name: self._on_data}) self._cond.notify_all() -sleep_timer = 10 +sleep_timer = 30 def test_1b(wsd, my_service) -> str: @@ -330,7 +330,25 @@ def on_description_modification(description_modification_report): info = 'count waveform updates' print(step, info) is_ok, result = test_min_updates_per_handle(waveform_updates, min_updates) - log_result(is_ok, results, step, info) + log_result(is_ok, results, step, info+ ' notifications per second') + if len(waveform_updates) < 3: + log_result(False, results, step, info+' number of waveforms') + else: + log_result(True, results, step, info+' number of waveforms') + +# print(f'expect 3 waveforms, got {len(waveform_updates)}') + expected_samples = 1000 * sleep_timer*0.9 + for handle, reports in waveform_updates.items(): + notifications = [n for n in reports if n.MetricValue is not None] + samples = sum([len(n.MetricValue.Samples) for n in notifications]) + if samples < expected_samples: + log_result(False, results, step, info + f' waveform {handle} has {samples} samples, expecting {expected_samples}') + is_ok = False +# print(f'waveform {handle} has {samples} samples, expecting {expected_samples}') + else: + log_result(True, results, step, info + f' waveform {handle} has {samples} samples') + + pm = mdib.data_model.pm_names pm_types = mdib.data_model.pm_types @@ -525,12 +543,30 @@ def on_description_modification(description_modification_report): log_result(False, results, step, info, ex) step = '6e' - info = 'SetMetricStates Immediately sends finished' + info = 'SetMetricStates Immediately answers with finished' print(step, info) + operation = client.mdib.descriptions.NODETYPE.get_one(pm_qnames.SetMetricStateOperationDescriptor) try: - operation = client.mdib.descriptions.NODETYPE.get_one(pm_qnames.SetMetricStateOperationDescriptor) - # client.set_service_client.set_metric_state(operation.Handle, '42') - log_result(False, results, step, info, 'not implemented ') + coll = ValuesCollectorPlus(operation_invoked_subscriptions[0], 'notification_data', 5) + proposed_metric_state1 = client.mdib.xtra.mk_proposed_state("numeric_metric_0.channel_0.vmd_1.mds_0") + proposed_metric_state2 = client.mdib.xtra.mk_proposed_state("numeric_metric_0.channel_0.vmd_1.mds_0") + for st in (proposed_metric_state1, proposed_metric_state2): + if st.MetricValue is None: + st.mk_metric_value() + st.MetricValue.Value = Decimal(1) + else: + st.MetricValue.Value += Decimal(0.1) + client.set_service_client.set_metric_state(operation.Handle, [proposed_metric_state1, proposed_metric_state2]) + time.sleep(3) + coll.finish() + coll_result = coll.result() + if len(coll_result) == 0: + log_result(False, results, step, info, 'no notification') + elif len(coll_result) > 1: + log_result(False, results, step, info, f'got {len(coll_result)} notifications, expect only one') + else: + first_notification = coll_result[0] + log_result(True, results, step, info, f'got {len(coll_result)} notifications : {first_notification}') except Exception as ex: print(traceback.format_exc()) log_result(False, results, step, info, ex) diff --git a/examples/ReferenceTestV2/reference_provider_v2.py b/examples/ReferenceTestV2/reference_provider_v2.py index df4f99e7..c002d37d 100644 --- a/examples/ReferenceTestV2/reference_provider_v2.py +++ b/examples/ReferenceTestV2/reference_provider_v2.py @@ -14,7 +14,7 @@ from sdc11073.loghelper import LoggerAdapter from sdc11073.mdib import ProviderMdib, descriptorcontainers from sdc11073.pysoap.soapclient_async import SoapClientAsync -from sdc11073.provider.components import SdcProviderComponents +from sdc11073.provider import components from sdc11073.provider import SdcProvider from sdc11073.provider.servicesfactory import DPWSHostedService from sdc11073.provider.servicesfactory import HostedServices, mk_dpws_hosts @@ -73,7 +73,7 @@ def provide_realtime_data(sdc_provider): return mdib_waveforms = sdc_provider.mdib.descriptions.NODETYPE.get(pm_qnames.RealTimeSampleArrayMetricDescriptor) for waveform in mdib_waveforms: - wf_generator = waveforms.SawtoothGenerator(min_value=0, max_value=10, waveformperiod=1.1, sampleperiod=0.01) + wf_generator = waveforms.SawtoothGenerator(min_value=0, max_value=10, waveformperiod=1.1, sampleperiod=0.001) waveform_provider.register_waveform_generator(waveform.Handle, wf_generator) @@ -117,11 +117,27 @@ def provide_realtime_data(sdc_provider): ssl_contexts = None if USE_REFERENCE_PARAMETERS: tmp = {'StateEvent': SubscriptionsManagerReferenceParamAsync} - specific_components = SdcProviderComponents(subscriptions_manager_class=tmp, - services_factory=mk_all_services_except_localization, - soap_client_class=SoapClientAsync) + specific_components = components.SdcProviderComponents( + subscriptions_manager_class=tmp, + hosted_services={'Get': [components.GetService], + 'StateEvent': [components.StateEventService, + components.ContextService, + components.DescriptionEventService, + components.WaveformService], + 'Set': [components.SetService], + 'ContainmentTree': [components.ContainmentTreeService]}, + soap_client_class=SoapClientAsync) else: - specific_components = None # SdcDeviceComponents(services_factory=mk_all_services_except_localization) +# specific_components = SdcProviderComponents(services_factory=mk_all_services_except_localization, +# soap_client_class=SoapClientAsync) + specific_components = components.SdcProviderComponents( + hosted_services={'Get': [components.GetService], + 'StateEvent': [components.StateEventService, + components.ContextService, + components.DescriptionEventService, + components.WaveformService], + 'Set': [components.SetService], + 'ContainmentTree': [components.ContainmentTreeService]}) sdc_provider = SdcProvider(wsd, dpwsModel, dpwsDevice, my_mdib, my_uuid, ssl_context_container=ssl_contexts, specific_components=specific_components, @@ -143,6 +159,7 @@ def provide_realtime_data(sdc_provider): patientContainer.CoreData.Birthname = "Birthname" patientContainer.CoreData.Title = "Title" patientContainer.ContextAssociation = pm_types.ContextAssociation.ASSOCIATED + patientContainer.Validator.extend(validators) identifiers = [] patientContainer.Identification = identifiers @@ -299,6 +316,9 @@ def provide_realtime_data(sdc_provider): mgr.add_descriptor(vmd) mgr.add_descriptor(channel) mgr.add_descriptor(metric) + mgr.add_state(sdc_provider.mdib.data_model.mk_state_container(vmd)) + mgr.add_state(sdc_provider.mdib.data_model.mk_state_container(channel)) + mgr.add_state(sdc_provider.mdib.data_model.mk_state_container(metric)) else: with sdc_provider.mdib.transaction_manager() as mgr: mgr.remove_descriptor(add_rm_vmd_handle) diff --git a/src/sdc11073/roles/metricprovider.py b/src/sdc11073/roles/metricprovider.py index 542745cf..1133e1a9 100644 --- a/src/sdc11073/roles/metricprovider.py +++ b/src/sdc11073/roles/metricprovider.py @@ -28,11 +28,11 @@ def make_operation_instance(self, operation_descriptor_container, operation_cls_ operation_target_handle = operation_descriptor_container.OperationTarget op_target_descriptor_container = self._mdib.descriptions.handle.get_one(operation_target_handle) - if op_target_descriptor_container.NODETYPE not in (pm_names.StringMetricDescriptor, - pm_names.EnumStringMetricDescriptor, - pm_names.NumericMetricDescriptor, - pm_names.RealTimeSampleArrayMetricDescriptor): - return None # this is not metric provider role +# if op_target_descriptor_container.NODETYPE not in (pm_names.StringMetricDescriptor, +# pm_names.EnumStringMetricDescriptor, +# pm_names.NumericMetricDescriptor, +# pm_names.RealTimeSampleArrayMetricDescriptor): +# return None # this is not metric provider role if operation_descriptor_container.NODETYPE == pm_names.SetValueOperationDescriptor: if op_target_descriptor_container.NODETYPE == pm_names.NumericMetricDescriptor: From 56eb90cf7733657e0347a5df801fbfc587d69d14 Mon Sep 17 00:00:00 2001 From: Deichmann Date: Thu, 23 Nov 2023 11:46:49 +0100 Subject: [PATCH 03/16] changes in plug-a-thon #15 2023-11-23 --- examples/ReferenceTestV2/discoproxyclient.py | 357 ++++++++++++++++++ .../ReferenceTestV2/reference_consumer_v2.py | 127 ++++--- .../ReferenceTestV2/reference_provider_v2.py | 13 + 3 files changed, 441 insertions(+), 56 deletions(-) create mode 100644 examples/ReferenceTestV2/discoproxyclient.py diff --git a/examples/ReferenceTestV2/discoproxyclient.py b/examples/ReferenceTestV2/discoproxyclient.py new file mode 100644 index 00000000..5e6b2a75 --- /dev/null +++ b/examples/ReferenceTestV2/discoproxyclient.py @@ -0,0 +1,357 @@ +from __future__ import annotations + +import os +import random +import time +from typing import TYPE_CHECKING, Iterable +from uuid import UUID + +from sdc11073.certloader import mk_ssl_contexts_from_folder +from sdc11073.consumer.request_handler_deferred import EmptyResponse +from sdc11073.definitions_sdc import SdcV1Definitions +from sdc11073.dispatch import MessageConverterMiddleware +from sdc11073.httpserver.httpserverimpl import HttpServerThreadBase +from sdc11073.location import SdcLocation +from sdc11073.loghelper import get_logger_adapter, basic_logging_setup +from sdc11073.mdib import ProviderMdib +from sdc11073.namespaces import EventingActions +from sdc11073.namespaces import default_ns_helper as nsh +from sdc11073.provider import SdcProvider +from sdc11073.pysoap.msgfactory import CreatedMessage +from sdc11073.pysoap.msgfactory import MessageFactory +from sdc11073.pysoap.msgreader import MessageReader +from sdc11073.pysoap.soapclient import Fault +from sdc11073.pysoap.soapclient import SoapClient +from sdc11073.wsdiscovery.wsdimpl import Service +from sdc11073.xml_types import wsd_types, pm_types, eventing_types +from sdc11073.xml_types.addressing_types import HeaderInformationBlock +from sdc11073.xml_types.dpws_types import ThisDeviceType, ThisModelType + +if TYPE_CHECKING: + from lxml.etree import QName + from sdc11073.certloader import SSLContextContainer + from sdc11073.xml_types.basetypes import MessageType + from sdc11073.dispatch.request import RequestData + +message_factory = MessageFactory(SdcV1Definitions, None, logger=get_logger_adapter('sdc.disco.msg')) +message_reader = MessageReader(SdcV1Definitions, None, logger=get_logger_adapter('sdc.disco.msg')) + +ADDRESS_ALL = "urn:docs-oasis-open-org:ws-dd:ns:discovery:2009:01" # format acc to RFC 2141 + + +def _mk_wsd_soap_message(header_info: HeaderInformationBlock, + payload: MessageType) -> CreatedMessage: + # use discovery specific namespaces + return message_factory.mk_soap_message(header_info, payload, + ns_list=[nsh.S12, nsh.WSA, nsh.WSD], use_defaults=False) + + +class DiscoProxyClient: + def __init__(self, + disco_proxy_address: str, + my_address: str, + ssl_context_container: SSLContextContainer | None = None): + self._proxy_address = disco_proxy_address + self._my_address = my_address + self._ssl_context_container = ssl_context_container + self._logger = get_logger_adapter('sdc.disco') + self._local_services: dict[str, Service] = {} + self._remote_services: dict[str, Service] = {} + ssl_context = None if ssl_context_container is None else ssl_context_container.client_context + self._soap_client = SoapClient(disco_proxy_address, + socket_timeout=5, + logger=get_logger_adapter('sdc.disco.client'), + ssl_context=ssl_context, + sdc_definitions=SdcV1Definitions, + msg_reader=message_reader + ) + self._http_server = HttpServerThreadBase( + my_address, + ssl_context_container.server_context if ssl_context_container else None, + logger=get_logger_adapter('sdc.disco.httpsrv'), + supported_encodings=['gzip'], + ) + + self._msg_converter = MessageConverterMiddleware( + message_reader, message_factory, self._logger, self) + self._my_server_port = None + self.subscribe_response = None + + def start(self, subscribe=True): + # first start http server, the services need to know the ip port number + self._http_server.start() + + event_is_set = self._http_server.started_evt.wait(timeout=15.0) + if not event_is_set: + self._logger.error('Cannot start device, start event of http server not set.') + raise RuntimeError('Cannot start device, start event of http server not set.') + self._my_server_port = self._http_server.my_port + self._http_server.dispatcher.register_instance('', self._msg_converter) + + if subscribe: + self.send_subscribe() + + def stop(self, unsubscribe=False): + # it seems that unsubscribe is not supported + if unsubscribe: + self.send_unsubscribe() + self._http_server.stop() + + def get_active_addresses(self) -> list[str]: + """Get active addresses.""" + # TODO: do not return list + return [self._my_address] + + def search_services(self, + types: Iterable[QName] | None = None, + scopes: wsd_types.ScopesType | None = None) -> list[Service]: + """Send a Probe message. + + Update known services with found services. Return list of services found in probe response.""" + payload = wsd_types.ProbeType() + payload.Types = types + if scopes is not None: + payload.Scopes = scopes + + inf = HeaderInformationBlock(action=payload.action, addr_to=ADDRESS_ALL) + created_message = _mk_wsd_soap_message(inf, payload) + received_message = self._soap_client.post_message_to('', created_message) + probe_response = wsd_types.ProbeMatchesType.from_node(received_message.p_msg.msg_node) + result = [] + for probe_match in probe_response.ProbeMatch: + service = Service(types=probe_match.Types, + scopes=probe_match.Scopes, + x_addrs=probe_match.XAddrs, + epr=probe_match.EndpointReference.Address, + instance_id='', + metadata_version=probe_match.MetadataVersion) + self._remote_services[service.epr] = service + result.append(service) + return result + + def send_resolve(self, epr) -> wsd_types.ResolveMatchesType: + payload = wsd_types.ResolveType() + payload.EndpointReference.Address = epr + inf = HeaderInformationBlock(action=payload.action, addr_to=ADDRESS_ALL) + created_message = _mk_wsd_soap_message(inf, payload) + received_message = self._soap_client.post_message_to('', created_message) + resolve_response = wsd_types.ResolveMatchesType.from_node(received_message.p_msg.msg_node) + return resolve_response + + def clear_remote_services(self): + """Clear remotely discovered services.""" + self._remote_services.clear() + + def publish_service(self, epr: str, + types: list[QName], + scopes: wsd_types.ScopesType, + x_addrs: list[str]): + metadata_version = 1 + instance_id = str(random.randint(1, 0xFFFFFFFF)) # noqa: S311 + service = Service(types, scopes, x_addrs, epr, instance_id, metadata_version=metadata_version) + self._logger.info('publishing %r', service) + self._local_services[epr] = service + + service.increment_message_number() + app_sequence = wsd_types.AppSequenceType() + app_sequence.InstanceId = int(service.instance_id) + app_sequence.MessageNumber = service.message_number + + payload = wsd_types.HelloType() + payload.Types = service.types + payload.Scopes = service.scopes + payload.XAddrs = service.x_addrs + payload.EndpointReference.Address = service.epr + + inf = HeaderInformationBlock(action=payload.action, addr_to=ADDRESS_ALL) + + created_message = _mk_wsd_soap_message(inf, payload) + created_message.p_msg.add_header_element(app_sequence.as_etree_node(nsh.WSD.tag('AppSequence'), + ns_map=nsh.partial_map(nsh.WSD))) + created_message = _mk_wsd_soap_message(inf, payload) + self._soap_client.post_message_to('', created_message) + + def send_subscribe(self): + subscribe_request = eventing_types.Subscribe() + subscribe_request.Delivery.NotifyTo.Address = f'https://{self._my_address}:{self._my_server_port}' + subscribe_request.Expires = 3600 + subscribe_request.set_filter('', dialect='http://discoproxy') + inf = HeaderInformationBlock(action=subscribe_request.action, addr_to=ADDRESS_ALL) + created_message = _mk_wsd_soap_message(inf, subscribe_request) + received_message = self._soap_client.post_message_to('', created_message) + response_action = received_message.action + if response_action == EventingActions.SubscribeResponse: + self.subscribe_response = eventing_types.SubscribeResponse.from_node(received_message.p_msg.msg_node) + elif response_action == Fault.NODETYPE: + fault = Fault.from_node(received_message.p_msg.msg_node) + self._logger.error( # noqa: PLE1205 + 'subscribe: Fault response : {}', fault) + return received_message + + def send_unsubscribe(self): + """Send an unsubscribe request to the provider and handle the response.""" + if not self.subscribe_response: + return + subscribe_response, self.subscribe_response = self.subscribe_response, None + request = eventing_types.Unsubscribe() + dev_reference_param = subscribe_response.SubscriptionManager.ReferenceParameters + subscription_manager_address = subscribe_response.SubscriptionManager.Address + inf = HeaderInformationBlock(action=request.action, + addr_to=subscription_manager_address, + reference_parameters=dev_reference_param) + message = message_factory.mk_soap_message(inf, payload=request) + received_message_data = self._soap_client.post_message_to('', message, msg='unsubscribe') + response_action = received_message_data.action + # check response: response does not contain explicit status. If action== UnsubscribeResponse all is fine. + if response_action == EventingActions.UnsubscribeResponse: + self._logger.info( # noqa: PLE1205 + 'unsubscribe: end of subscription {} was confirmed.', self.notification_url) + elif response_action == Fault.NODETYPE: + fault = Fault.from_node(received_message_data.p_msg.msg_node) + self._logger.error( # noqa: PLE1205 + 'unsubscribe: Fault response : {}', fault) + + else: + self._logger.error( # noqa: PLE1205 + 'unsubscribe: unexpected response action: {}', received_message_data.p_msg.raw_data) + raise ValueError(f'unsubscribe: unexpected response action: {received_message_data.p_msg.raw_data}') + + def clear_service(self, epr: str): + service = self._local_services[epr] + self._send_bye(service) + del self._local_services[epr] + + def _send_bye(self, service: Service): + self._logger.debug('sending bye on %s', service) + + bye = wsd_types.ByeType() + bye.EndpointReference.Address = service.epr + + inf = HeaderInformationBlock(action=bye.action, addr_to=ADDRESS_ALL) + + app_sequence = wsd_types.AppSequenceType() + app_sequence.InstanceId = int(service.instance_id) + app_sequence.MessageNumber = service.message_number + + created_message = _mk_wsd_soap_message(inf, bye) + created_message.p_msg.add_header_element(app_sequence.as_etree_node(nsh.WSD.tag('AppSequence'), + ns_map=nsh.partial_map(nsh.WSD))) + created_message = _mk_wsd_soap_message(inf, bye) + received_message = self._soap_client.post_message_to('', created_message) + # hello_response = wsd_types.HelloType.from_node(received_message.p_msg.msg_node) + # print(hello_response) + + def on_post(self, request_data: RequestData) -> CreatedMessage: + print('on_post') + if request_data.message_data.action == wsd_types.HelloType.action: + hello = wsd_types.HelloType.from_node(request_data.message_data.p_msg.msg_node) + service = Service(types=hello.Types, + scopes=hello.Scopes, + x_addrs=hello.XAddrs, + epr=hello.EndpointReference.Address, + instance_id='', # Todo: needed in any way? + metadata_version=hello.MetadataVersion) + self._remote_services[service.epr] = service + self._logger.info('hello epr = %s, xaddrs =%r', service.epr, service.x_addrs) + + elif request_data.message_data.action == wsd_types.ByeType.action: + bye = wsd_types.ByeType.from_node(request_data.message_data.p_msg.msg_node) + epr = bye.EndpointReference.Address + self._logger.info('bye epr = %s, xaddrs =%r', epr, bye.XAddrs) + if epr in self._remote_services: + del self._remote_services[epr] + self._logger.info('removed %s from known remote services') + else: + self._logger.info('unknown remote service %s', epr) + return EmptyResponse() + + def on_get(self, request_data: RequestData) -> CreatedMessage: + print('on_get') + return EmptyResponse() + + +def mk_provider(wsd: DiscoProxyClient, mdib_path: str, uuid_str: str): + my_mdib = ProviderMdib.from_mdib_file(mdib_path) + print("UUID for this device is {}".format(uuid_str)) + dpwsModel = ThisModelType(manufacturer='sdc11073', + manufacturer_url='www.sdc11073.com', + model_name='TestDevice', + model_number='1.0', + model_url='www.sdc11073.com/model', + presentation_url='www.sdc11073.com/model/presentation') + + dpwsDevice = ThisDeviceType(friendly_name='TestDevice', + firmware_version='Version1', + serial_number='12345') + specific_components = None + sdc_provider = SdcProvider(wsd, dpwsModel, dpwsDevice, my_mdib, UUID(uuid_str), + ssl_context_container=ssl_contexts, + specific_components=specific_components, + max_subscription_duration=15 + ) + return sdc_provider + + +def log_services(log, the_services): + log.info('found %d services:', len(the_services)) + for the_service in the_services: + log.info('found service: %r', the_service) + + +if __name__ == '__main__': + basic_logging_setup() + logger = get_logger_adapter('sdc.disco.main') + ca_folder = r'C:\tmp\ORNET_REF_Certificates' + ssl_passwd = 'dummypass' + disco_ip = '192.168.30.5:33479' + my_ip = '192.168.30.106' + My_UUID_str = '12345678-6f55-11ea-9697-123456789bcd' + here = os.path.dirname(__file__) + default_mdib_path = os.path.join(here, 'mdib_test_sequence_2_v4(temp).xml') + mdib_path = os.getenv('ref_mdib') or default_mdib_path + ref_fac = os.getenv('ref_fac') or 'r_fac' + ref_poc = os.getenv('ref_poc') or 'r_poc' + ref_bed = os.getenv('ref_bed') or 'r_bed' + loc = SdcLocation(ref_fac, ref_poc, ref_bed) + + ssl_contexts = mk_ssl_contexts_from_folder(ca_folder, + cyphers_file=None, + private_key='user_private_key_encrypted.pem', + certificate='user_certificate_root_signed.pem', + ca_public_key='root_certificate.pem', + ssl_passwd=ssl_passwd, + ) + + proxy = DiscoProxyClient(disco_ip, my_ip, ssl_contexts) + # proxy = DiscoProxyClient(disco_ip, my_ip) + proxy.start() + try: + services = proxy.search_services() + log_services(logger, services) + + # now publish a device + loc = SdcLocation(ref_fac, ref_poc, ref_bed) + logger.info("location for this device is {}", loc) + logger.info('start provider...') + + sdc_provider = mk_provider(proxy, mdib_path, My_UUID_str) + sdc_provider.start_all() + + validators = [pm_types.InstanceIdentifier('Validator', extension_string='System')] + sdc_provider.set_location(loc, validators) + + services = proxy.search_services() + log_services(logger, services) + for service in services: + result = proxy.send_resolve(service.epr) + logger.info('resolvematches: %r', result.ResolveMatch) + + time.sleep(5) + logger.info('stop provider...') + sdc_provider.stop_all() + + services = proxy.search_services() + log_services(logger, services) + + finally: + proxy.stop() diff --git a/examples/ReferenceTestV2/reference_consumer_v2.py b/examples/ReferenceTestV2/reference_consumer_v2.py index b311be66..06c54f19 100644 --- a/examples/ReferenceTestV2/reference_consumer_v2.py +++ b/examples/ReferenceTestV2/reference_consumer_v2.py @@ -36,15 +36,6 @@ commlog.set_communication_logger(comm_logger) -class ValuesCollectorPlus(observableproperties.ValuesCollector): - """ add a finish call - """ - def finish(self): - with self._cond: - self._state = self.FINISHED - observableproperties.unbind(self._obj, **{self._prop_name: self._on_data}) - self._cond.notify_all() - sleep_timer = 30 @@ -72,7 +63,7 @@ def connect_client(my_service) -> SdcConsumer: private_key='user_private_key_encrypted.pem', certificate='user_certificate_root_signed.pem', ca_public_key='root_certificate.pem', - ssl_passwd=ssl_passwd, + ssl_passwd=ssl_passwd ) else: ssl_contexts = None @@ -476,7 +467,7 @@ def on_description_modification(description_modification_report): print(step, info) subscriptions = client.subscription_mgr.subscriptions.values() operation_invoked_subscriptions = [subscr for subscr in subscriptions - if subscr.short_filter_string == 'OperationInvokedReport'] + if 'OperationInvokedReport' in subscr.short_filter_string] if len(operation_invoked_subscriptions) == 0: log_result(False, results, step, info, 'OperationInvokedReport not subscribed, cannot test') elif len(operation_invoked_subscriptions) > 1: @@ -484,22 +475,25 @@ def on_description_modification(description_modification_report): f'found {len(operation_invoked_subscriptions)} OperationInvokedReport subscribed, cannot test') else: try: - coll = ValuesCollectorPlus(operation_invoked_subscriptions[0], 'notification_data',5) - operation = client.mdib.descriptions.NODETYPE.get_one(pm_qnames.SetValueOperationDescriptor) - client.set_service_client.set_numeric_value(operation.Handle, Decimal(42)) - time.sleep(2) - coll.finish() - coll_result = coll.result() - messages = [] - for entry in coll_result: - messages.append(msg_types.OperationInvokedReport.from_node(entry.p_msg.msg_node)) - if len(coll_result) == 0: - log_result(False, results, step, info, 'no notification') - elif len(coll_result) > 1: - log_result(False, results, step, info, f'got {len(coll_result)} notifications, expect only one') + operations = client.mdib.descriptions.NODETYPE.get(pm_qnames.SetValueOperationDescriptor, []) + my_ops = [op for op in operations if op.Type.Code == "67108888"] + if len(my_ops) != 1: + log_result(False, results, step, info, f'found {len(my_ops)} operations with code "67108888"') else: - first_notification = coll_result[0] - log_result(True, results, step, info, f'got {len(coll_result)} notifications : {first_notification}') + operation = my_ops[0] + future_object = client.set_service_client.set_numeric_value(operation.Handle, Decimal(42)) + operation_result = future_object.result() + if len(operation_result.report_parts) == 0: + log_result(False, results, step, info, 'no notification') + elif len(operation_result.report_parts) > 1: + log_result(False, results, step, info, f'got {len(operation_result.report_parts)} notifications, expect only one') + else: + log_result(True, results, step, info, f'got {len(operation_result.report_parts)} notifications') + if operation_result.InvocationInfo.InvocationState != msg_types.InvocationState.FINISHED: + log_result(False, results, step, info, + f'got result {operation_result.InvocationInfo.InvocationState} ' + f'{operation_result.InvocationInfo.InvocationError} ' + f'{operation_result.InvocationInfo.InvocationErrorMessage}') except Exception as ex: print(traceback.format_exc()) log_result(False, results, step, info, ex) @@ -507,17 +501,32 @@ def on_description_modification(description_modification_report): step = '6d' info = 'SetString: Initiates a transaction that sends Wait, Start and Finished' print(step, info) - operation = client.mdib.descriptions.NODETYPE.get_one(pm_qnames.SetStringOperationDescriptor) try: - coll = ValuesCollectorPlus(operation_invoked_subscriptions[0], 'notification_data', 5) - client.set_service_client.set_string(operation.Handle, '42') - time.sleep(2) - coll.finish() - coll_result = coll.result() - if len(coll_result) == 0: - log_result(False, results, step, info, 'no notification') - elif len(coll_result) >= 3: - log_result(True, results, step, info, f'got {len(coll_result)} notifications') + operations = client.mdib.descriptions.NODETYPE.get(pm_qnames.SetStringOperationDescriptor, []) + my_ops = [op for op in operations if op.Type.Code == "67108889"] + if len(my_ops) != 1: + log_result(False, results, step, info, f'found {len(my_ops)} operations with code "67108889"') + else: + operation = my_ops[0] + future_object = client.set_service_client.set_string(operation.Handle, 'STANDBY') + operation_result = future_object.result() + if len(operation_result.report_parts) == 0: + log_result(False, results, step, info, 'no notification') + elif len(operation_result.report_parts) >= 3: + # check order of operation invoked reports (simple expectation, there could be multiple WAIT in theory) + expectation = [msg_types.InvocationState.WAIT, + msg_types.InvocationState.START, + msg_types.InvocationState.FINISHED] + inv_states = [p.InvocationInfo.InvocationState for p in operation_result.report_parts] + if inv_states != expectation: + log_result(False, results, step, info, f'wrong order {inv_states}') + else: + log_result(True, results, step, info, f'got {len(operation_result.report_parts)} notifications') + if operation_result.InvocationInfo.InvocationState != msg_types.InvocationState.FINISHED: + log_result(False, results, step, info, + f'got result {operation_result.InvocationInfo.InvocationState} ' + f'{operation_result.InvocationInfo.InvocationError} ' + f'{operation_result.InvocationInfo.InvocationErrorMessage}') except Exception as ex: print(traceback.format_exc()) @@ -526,28 +535,34 @@ def on_description_modification(description_modification_report): step = '6e' info = 'SetMetricStates Immediately answers with finished' print(step, info) - operation = client.mdib.descriptions.NODETYPE.get_one(pm_qnames.SetMetricStateOperationDescriptor) try: - coll = ValuesCollectorPlus(operation_invoked_subscriptions[0], 'notification_data', 5) - proposed_metric_state1 = client.mdib.xtra.mk_proposed_state("numeric_metric_0.channel_0.vmd_1.mds_0") - proposed_metric_state2 = client.mdib.xtra.mk_proposed_state("numeric_metric_0.channel_0.vmd_1.mds_0") - for st in (proposed_metric_state1, proposed_metric_state2): - if st.MetricValue is None: - st.mk_metric_value() - st.MetricValue.Value = Decimal(1) - else: - st.MetricValue.Value += Decimal(0.1) - client.set_service_client.set_metric_state(operation.Handle, [proposed_metric_state1, proposed_metric_state2]) - time.sleep(3) - coll.finish() - coll_result = coll.result() - if len(coll_result) == 0: - log_result(False, results, step, info, 'no notification') - elif len(coll_result) > 1: - log_result(False, results, step, info, f'got {len(coll_result)} notifications, expect only one') + operations = client.mdib.descriptions.NODETYPE.get(pm_qnames.SetMetricStateOperationDescriptor, []) + my_ops = [op for op in operations if op.Type.Code == "67108890"] + if len(my_ops) != 1: + log_result(False, results, step, info, f'found {len(my_ops)} operations with code "67108890"') else: - first_notification = coll_result[0] - log_result(True, results, step, info, f'got {len(coll_result)} notifications : {first_notification}') + operation = my_ops[0] + proposed_metric_state1 = client.mdib.xtra.mk_proposed_state("numeric_metric_0.channel_0.vmd_1.mds_0") + proposed_metric_state2 = client.mdib.xtra.mk_proposed_state("numeric_metric_1.channel_0.vmd_1.mds_0") + for st in (proposed_metric_state1, proposed_metric_state2): + if st.MetricValue is None: + st.mk_metric_value() + st.MetricValue.Value = Decimal(1) + else: + st.MetricValue.Value += Decimal(0.1) + future_object = client.set_service_client.set_metric_state(operation.Handle, [proposed_metric_state1, proposed_metric_state2]) + operation_result = future_object.result() + if len(operation_result.report_parts) == 0: + log_result(False, results, step, info, 'no notification') + elif len(operation_result.report_parts) > 1: + log_result(False, results, step, info, f'got {len(operation_result.report_parts)} notifications, expect only one') + else: + log_result(True, results, step, info, f'got {len(operation_result.report_parts)} notifications') + if operation_result.InvocationInfo.InvocationState != msg_types.InvocationState.FINISHED: + log_result(False, results, step, info, + f'got result {operation_result.InvocationInfo.InvocationState} ' + f'{operation_result.InvocationInfo.InvocationError} ' + f'{operation_result.InvocationInfo.InvocationErrorMessage}') except Exception as ex: print(traceback.format_exc()) log_result(False, results, step, info, ex) diff --git a/examples/ReferenceTestV2/reference_provider_v2.py b/examples/ReferenceTestV2/reference_provider_v2.py index 49ed9385..98dc7100 100644 --- a/examples/ReferenceTestV2/reference_provider_v2.py +++ b/examples/ReferenceTestV2/reference_provider_v2.py @@ -142,6 +142,11 @@ def provide_realtime_data(sdc_provider): ) sdc_provider.start_all() + # disable delayed processing for 2 operations + + sdc_provider.get_operation_by_handle('set_value_0.sco.mds_0').delayed_processing = False + sdc_provider.get_operation_by_handle('set_metric_0.sco.vmd_1.mds_0').delayed_processing = False + validators = [pm_types.InstanceIdentifier('Validator', extension_string='System')] sdc_provider.set_location(loc, validators) provide_realtime_data(sdc_provider) @@ -319,6 +324,14 @@ def provide_realtime_data(sdc_provider): with sdc_provider.mdib.transaction_manager() as mgr: mgr.remove_descriptor(add_rm_vmd_handle) + # enable disable operation + with sdc_provider.mdib.transaction_manager() as mgr: + op_state = mgr.get_state('activate_0.sco.mds_0') + op_state.OperatingMode = pm_types.OperatingMode.ENABLED \ + if op_state.OperatingMode == pm_types.OperatingMode.ENABLED \ + else pm_types.OperatingMode.DISABLED + print(f'operation activate_0.sco.mds_0 {op_state.OperatingMode}') + sleep(5) except KeyboardInterrupt: print("Exiting...") From b6131c4ca5a1d3453f6aef489b5c1658a52990c7 Mon Sep 17 00:00:00 2001 From: Deichmann Date: Wed, 7 Feb 2024 15:06:11 +0100 Subject: [PATCH 04/16] merged master --- .../ReferenceTestV2/reference_provider_v2.py | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/examples/ReferenceTestV2/reference_provider_v2.py b/examples/ReferenceTestV2/reference_provider_v2.py index 98dc7100..fef19088 100644 --- a/examples/ReferenceTestV2/reference_provider_v2.py +++ b/examples/ReferenceTestV2/reference_provider_v2.py @@ -153,7 +153,7 @@ def provide_realtime_data(sdc_provider): pm = my_mdib.data_model.pm_names pm_types = my_mdib.data_model.pm_types patientDescriptorHandle = my_mdib.descriptions.NODETYPE.get(pm.PatientContextDescriptor)[0].Handle - with my_mdib.transaction_manager() as mgr: + with my_mdib.context_state_transaction() as mgr: patientContainer = mgr.mk_context_state(patientDescriptorHandle) patientContainer.CoreData.Givenname = "Given" patientContainer.CoreData.Middlename = ["Middle"] @@ -191,7 +191,7 @@ def provide_realtime_data(sdc_provider): if oneContainer.Handle == set_string_handle: stringOperation = oneContainer - with sdc_provider.mdib.transaction_manager() as mgr: + with sdc_provider.mdib.metric_state_transaction() as mgr: state = mgr.get_state(valueOperation.OperationTarget) if not state.MetricValue: state.mk_metric_value() @@ -205,13 +205,13 @@ def provide_realtime_data(sdc_provider): while True: if numeric_metric: try: - with sdc_provider.mdib.transaction_manager() as mgr: + with sdc_provider.mdib.metric_state_transaction() as mgr: state = mgr.get_state(numeric_metric.Handle) if not state.MetricValue: state.mk_metric_value() state.MetricValue.Value = Decimal(num_current_value) num_current_value += 1 - with sdc_provider.mdib.transaction_manager() as mgr: + with sdc_provider.mdib.descriptor_transaction() as mgr: descriptor: descriptorcontainers.AbstractMetricDescriptorContainer = mgr.get_descriptor(numeric_metric.Handle) descriptor.Unit.Code = 'code1' if descriptor.Unit.Code == 'code2' else 'code2' except Exception as ex: @@ -220,7 +220,7 @@ def provide_realtime_data(sdc_provider): print("Numeric Metric not found in MDIB!") if string_metric: try: - with sdc_provider.mdib.transaction_manager() as mgr: + with sdc_provider.mdib.metric_state_transaction() as mgr: state = mgr.get_state(string_metric.Handle) if not state.MetricValue: state.mk_metric_value() @@ -233,13 +233,13 @@ def provide_realtime_data(sdc_provider): if alertCondition: try: - with sdc_provider.mdib.transaction_manager() as mgr: + with sdc_provider.mdib.alert_state_transaction() as mgr: state = mgr.get_state(alertCondition.Handle) state.Presence = not state.Presence except Exception as ex: print(traceback.format_exc()) try: - with sdc_provider.mdib.transaction_manager() as mgr: + with sdc_provider.mdib.descriptor_transaction() as mgr: now = datetime.datetime.now() text = f'last changed at {now.hour:02d}:{now.minute:02d}:{now.second:02d}' descriptor: descriptorcontainers.AlertConditionDescriptorContainer = mgr.get_descriptor(alertCondition.Handle) @@ -261,7 +261,7 @@ def provide_realtime_data(sdc_provider): if alertSignal: try: - with sdc_provider.mdib.transaction_manager() as mgr: + with sdc_provider.mdib.alert_state_transaction() as mgr: state = mgr.get_state(alertSignal.Handle) if state.Slot is None: state.Slot = 1 @@ -274,7 +274,7 @@ def provide_realtime_data(sdc_provider): if battery_descriptor: try: - with sdc_provider.mdib.transaction_manager() as mgr: + with sdc_provider.mdib.component_state_transaction() as mgr: state = mgr.get_state(battery_descriptor.Handle) if state.Voltage is None: state.Voltage = pm_types.Measurement(value=Decimal('14.4'), unit=pm_types.CodedValue('xyz')) @@ -287,7 +287,7 @@ def provide_realtime_data(sdc_provider): print("battery state not found in MDIB") try: - with sdc_provider.mdib.transaction_manager() as mgr: + with sdc_provider.mdib.component_state_transaction() as mgr: state = mgr.get_state(vmd_handle) state.OperatingHours = 2 if state.OperatingHours != 2 else 1 print(f'operating hours = {state.OperatingHours}') @@ -295,7 +295,7 @@ def provide_realtime_data(sdc_provider): print(traceback.format_exc()) try: - with sdc_provider.mdib.transaction_manager() as mgr: + with sdc_provider.mdib.component_state_transaction() as mgr: state = mgr.get_state(mds_handle) state.Lang = 'de' if state.Lang != 'de' else 'en' print(f'mds lang = {state.Lang}') @@ -313,7 +313,7 @@ def provide_realtime_data(sdc_provider): channel = descriptorcontainers.ChannelDescriptorContainer(add_rm_channel_handle, add_rm_vmd_handle) metric = descriptorcontainers.StringMetricDescriptorContainer(add_rm_metric_handle, add_rm_channel_handle) metric.Unit = pm_types.CodedValue('123') - with sdc_provider.mdib.transaction_manager() as mgr: + with sdc_provider.mdib.descriptor_transaction() as mgr: mgr.add_descriptor(vmd) mgr.add_descriptor(channel) mgr.add_descriptor(metric) @@ -321,11 +321,11 @@ def provide_realtime_data(sdc_provider): mgr.add_state(sdc_provider.mdib.data_model.mk_state_container(channel)) mgr.add_state(sdc_provider.mdib.data_model.mk_state_container(metric)) else: - with sdc_provider.mdib.transaction_manager() as mgr: + with sdc_provider.mdib.descriptor_transaction() as mgr: mgr.remove_descriptor(add_rm_vmd_handle) # enable disable operation - with sdc_provider.mdib.transaction_manager() as mgr: + with sdc_provider.mdib.operational_state_transaction() as mgr: op_state = mgr.get_state('activate_0.sco.mds_0') op_state.OperatingMode = pm_types.OperatingMode.ENABLED \ if op_state.OperatingMode == pm_types.OperatingMode.ENABLED \ From 881735e6a3932b64c896f1dc300a9e36f538261b Mon Sep 17 00:00:00 2001 From: Bernd Deichmann <68051229+deichmab-draeger@users.noreply.github.com> Date: Thu, 15 Aug 2024 18:10:24 +0200 Subject: [PATCH 05/16] Update reference tests v2 (#387) ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) ## Checklist: - [ ] I have updated the [changelog](../CHANGELOG.md) accordingly. - [ ] I have added tests to cover my changes. --------- Co-authored-by: Leon Co-authored-by: a-kleinf <87371692+a-kleinf@users.noreply.github.com> --- .github/workflows/build.yml | 6 +- CHANGELOG.md | 48 ++ codecov.yml | 4 +- examples/ReferenceTest/reference_consumer.py | 123 ++-- examples/ReferenceTest/reference_provider.py | 8 +- examples/ReferenceTest/test_reference.py | 64 +- examples/ReferenceTestV2/discoproxyclient.py | 164 ++--- .../ReferenceTestV2/reference_consumer_v2.py | 599 +++++++++++------- .../ReferenceTestV2/reference_provider_v2.py | 220 +++++-- pyproject.toml | 28 +- src/sdc11073/commlog.py | 5 +- src/sdc11073/consumer/components.py | 5 +- src/sdc11073/consumer/consumerimpl.py | 10 +- .../consumer/serviceclients/setservice.py | 8 +- src/sdc11073/mdib/consumermdib.py | 5 - src/sdc11073/mdib/containerbase.py | 18 +- src/sdc11073/mdib/descriptorcontainers.py | 1 + src/sdc11073/mdib/mdibbase.py | 11 +- src/sdc11073/mdib/providermdib.py | 32 +- src/sdc11073/mdib/providermdibxtra.py | 77 ++- src/sdc11073/mdib/transactions.py | 91 ++- src/sdc11073/mdib/transactionsprotocol.py | 7 + src/sdc11073/multikey.py | 194 ++++-- src/sdc11073/provider/components.py | 5 +- .../provider/porttypes/setserviceimpl.py | 2 +- src/sdc11073/provider/providerimpl.py | 35 +- src/sdc11073/pysoap/msgfactory.py | 2 +- src/sdc11073/roles/contextprovider.py | 74 ++- src/sdc11073/wsdiscovery/networkingthread.py | 277 +++----- src/sdc11073/wsdiscovery/wsdimpl.py | 33 +- src/sdc11073/xml_types/pm_types.py | 5 +- src/sdc11073/xml_types/xml_structure.py | 24 +- tests/foo_schema.xsd | 16 + tests/mdib_two_mds.xml | 2 + tests/mockstuff.py | 1 + tests/test_additional_schema.py | 116 ++++ tests/test_alertsignaldelegate.py | 3 +- tests/test_client_device.py | 25 +- tests/test_comm_logger.py | 6 +- tests/test_device.py | 59 ++ tests/test_discovery.py | 29 +- tests/test_operations.py | 25 +- tests/test_pmtypes.py | 26 +- tests/test_statecontainers.py | 22 + tests/test_transaction.py | 188 ++++-- tutorial/provider/provider.py | 31 +- 46 files changed, 1779 insertions(+), 955 deletions(-) create mode 100644 tests/foo_schema.xsd create mode 100644 tests/test_additional_schema.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d1fbdd24..f9f57ac1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,7 +23,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: "3.11" + python-version: "3.12" - name: Setup hatch run: python -m pip install hatch @@ -52,7 +52,7 @@ jobs: strategy: matrix: - python-version: [ "3.9", "3.10", "3.11" ] + python-version: [ "3.9", "3.10", "3.11", "3.12"] os: [ ubuntu-latest, windows-latest ] distribution: [ "${{ needs.build.outputs.WHL }}", "${{ needs.build.outputs.TARGZ }}" ] @@ -113,7 +113,7 @@ jobs: retention-days: 5 - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: fail_ci_if_error: true token: "8c8b98c0-4c00-4f83-a598-81f3857e63e9" # https://github.com/codecov/codecov-action/issues/837#issuecomment-1530053511 diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f54cf17..14e25141 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,58 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- support for python version 3.12 +- new method ContextStateTransaction.disaccociate_all +- additional schemata for validation can be declared in SdcProviderComponents and SdcConsumerComponents + +### Fixed + +- possible exception in ConsumerMdib._get_context_states [#350](https://github.com/Draegerwerk/sdc11073/issues/350) +- reference tests and example provider +- node member of DescriptorContainer not updated on description modification report [#357](https://github.com/Draegerwerk/sdc11073/issues/357) +- accessing a multikey may lead to IndexError [#359](https://github.com/Draegerwerk/sdc11073/issues/359) +- wrong data type for ClinicalInfo.RelatedMeasurement[#362](https://github.com/Draegerwerk/sdc11073/issues/362) +- fixed a bug where sending and receiving socket are used after they have been closed already [#328](https://github.com/Draegerwerk/sdc11073/issues/328) +- reduced the amount of udp sockets to 2 [#328](https://github.com/Draegerwerk/sdc11073/issues/328) +- fixed `address already in use` bug [#328](https://github.com/Draegerwerk/sdc11073/issues/328) +- fixed SetServiceClient.set_numeric_value not accepting float, int or string. +- fixed a bug where a message would be thrown away, if the udp binding of the discovery would be used by provider and consumer, as they would share the same ip and port [#367](https://github.com/Draegerwerk/sdc11073/issues/367) +- Exception in wsdiscovery if no Scopes in message [#356](https://github.com/Draegerwerk/sdc11073/issues/356) +- fixed error message not set in OperationInvokedReport[#375](https://github.com/Draegerwerk/sdc11073/issues/375). +- incorrect BindingMdibVersion and UnbindingMdibVersion [#168](https://github.com/Draegerwerk/sdc11073/issues/168) +- ensure_location_context_descriptor and ensure_patient_context_descriptor also work for multiple system contexts in mdib. +- provider mdib observables are updated [#365](https://github.com/Draegerwerk/sdc11073/issues/365) + +### Changed + +- ContainerBase.diff uses math.isclose for comparison, test added +- removed the `MULTICAST_OUT` logger from commlog [#328](https://github.com/Draegerwerk/sdc11073/issues/328) +- SetContextState operation sets ContextAssociation according to value in proposed context state. + Before the proposed state was always associated. + Check added in SetContextState that max. one proposed state per descriptor is associated. + +## [2.0.1] - 2024-02-21 + +### Fixed + +- inconsistent InstanceId attributes in GetMdibResponse + +## [2.0.0] - 2024-02-14 + +### Fixed + +- `RuntimeError: dictionary changed size during iteration` for `remote_services` in the discovery [#335](https://github.com/Draegerwerk/sdc11073/issues/335) +- DescriptorTransaction sometimes causes wrong DescriptorVersion in states [#340](https://github.com/Draegerwerk/sdc11073/issues/340) + +## [2.0.0rc2] - 2024-02-08 + ### Fixed - fixed a bug where sending and receiving socket are used after they have been closed already [#328](https://github.com/Draegerwerk/sdc11073/issues/328) - fixed a bug where `getsockname()` is called before the socket is binded +- fixed cyclic import [#333](https://github.com/Draegerwerk/sdc11073/issues/333) ## [2.0.0rc1] - 2024-01-31 diff --git a/codecov.yml b/codecov.yml index 4bb10d3a..2ef36aed 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,7 +1,7 @@ codecov: notify: - # do not notify until at least 12 builds have been uploaded from the CI pipeline - after_n_builds: 12 # https://docs.codecov.com/docs/codecovyml-reference#codecovnotify + # do not notify until at least 16 builds (python versions * os * extension = 4 * 2 * 2 = 16) have been uploaded from the CI pipeline + after_n_builds: 16 # https://docs.codecov.com/docs/codecovyml-reference#codecovnotify coverage: status: project: diff --git a/examples/ReferenceTest/reference_consumer.py b/examples/ReferenceTest/reference_consumer.py index 621df308..12d6fe1c 100644 --- a/examples/ReferenceTest/reference_consumer.py +++ b/examples/ReferenceTest/reference_consumer.py @@ -1,6 +1,10 @@ +import dataclasses +import enum import os +import sys import time import traceback +import typing from collections import defaultdict from concurrent import futures from decimal import Decimal @@ -21,17 +25,40 @@ ca_folder = os.getenv('ref_ca') # noqa: SIM112 ssl_passwd = os.getenv('ref_ssl_passwd') or None # noqa: SIM112 search_epr = os.getenv('ref_search_epr') or 'abc' # noqa: SIM112 +# ref_discovery_runs indicates the maximum executions of wsdiscovery search services, "0" -> run until service is found +discovery_runs = int(os.getenv('ref_discovery_runs', 0)) # noqa: SIM112 ENABLE_COMMLOG = True -def run_ref_test(): - results = [] +class TestResult(enum.Enum): + """ + Represents the overall test result. + """ + PASSED = 'PASSED' + FAILED = 'FAILED' + +@dataclasses.dataclass +class TestCollector: + overall_test_result: TestResult = TestResult.PASSED + test_messages: typing.List = dataclasses.field(default_factory=list) + + def add_result(self, test_step_message: str, test_step_result: TestResult): + if not isinstance(test_step_result, TestResult): + raise ValueError("Unexpected parameter") + if self.overall_test_result is not TestResult.FAILED: + self.overall_test_result = test_step_result + self.test_messages.append(test_step_message) + + +def run_ref_test() -> TestCollector: + test_collector = TestCollector() print(f'using adapter address {adapter_ip}') print('Test step 1: discover device which endpoint ends with "{}"'.format(search_epr)) wsd = WSDiscovery(adapter_ip) wsd.start() my_service = None + discovery_counter = 0 while my_service is None: services = wsd.search_services(types=SdcV1Definitions.MedicalDeviceTypesFilter) print('found {} services {}'.format(len(services), ', '.join([s.epr for s in services]))) @@ -40,8 +67,13 @@ def run_ref_test(): my_service = s print('found service {}'.format(s.epr)) break - print('Test step 1 successful: device discovered') - results.append('### Test 1 ### passed') + discovery_counter += 1 + if discovery_runs and discovery_counter >= discovery_runs: + print('### Test 1 ### failed - No suitable service was discovered') + test_collector.add_result('### Test 1 ### failed', TestResult.FAILED) + return test_collector + print('Test step 1 passed: device discovered') + test_collector.add_result('### Test 1 ### passed', TestResult.PASSED) print('Test step 2: connect to device...') try: @@ -59,45 +91,46 @@ def run_ref_test(): ssl_context_container=ssl_context_container, validate=True) client.start_all() - print('Test step 2 successful: connected to device') - results.append('### Test 2 ### passed') + print('Test step 2 passed: connected to device') + test_collector.add_result('### Test 2 ### passed', TestResult.PASSED) except: print(traceback.format_exc()) - results.append('### Test 2 ### failed') - return results + test_collector.add_result('### Test 2 ### failed', TestResult.FAILED) + return test_collector print('Test step 3&4: get mdib and subscribe...') try: mdib = ConsumerMdib(client) mdib.init_mdib() - print('Test step 3&4 successful') - results.append('### Test 3 ### passed') - results.append('### Test 4 ### passed') + print('Test step 3&4 passed') + test_collector.add_result('### Test 3 ### passed', TestResult.PASSED) + test_collector.add_result('### Test 4 ### passed', TestResult.PASSED) except: print(traceback.format_exc()) - results.append('### Test 3 ### failed') - results.append('### Test 4 ### failed') - return results + test_collector.add_result('### Test 3 ### failed', TestResult.FAILED) + test_collector.add_result('### Test 4 ### failed', TestResult.FAILED) + return test_collector pm = mdib.data_model.pm_names print('Test step 5: check that at least one patient context exists') patients = mdib.context_states.NODETYPE.get(pm.PatientContextState, []) if len(patients) > 0: - print('found {} patients, Test step 5 successful'.format(len(patients))) - results.append('### Test 5 ### passed') + print('found {} patients, Test step 5 passed'.format(len(patients))) + test_collector.add_result('### Test 5 ### passed', TestResult.PASSED) else: print('found no patients, Test step 5 failed') - results.append('### Test 5 ### failed') + test_collector.add_result('### Test 5 ### failed', TestResult.FAILED) + print('Test step 6: check that at least one location context exists') locations = mdib.context_states.NODETYPE.get(pm.LocationContextState, []) if len(locations) > 0: - print('found {} locations, Test step 6 successful'.format(len(locations))) - results.append('### Test 6 ### passed') + print('found {} locations, Test step 6 passed'.format(len(locations))) + test_collector.add_result('### Test 6 ### passed', TestResult.PASSED) else: print('found no locations, Test step 6 failed') - results.append('### Test 6 ### failed') + test_collector.add_result('### Test 6 ### failed', TestResult.FAILED) print('Test step 7&8: count metric state updates and alert state updates') metric_updates = defaultdict(list) @@ -123,32 +156,32 @@ def onAlertUpdates(alertsbyhandle): print(metric_updates) print(alert_updates) if len(metric_updates) == 0: - results.append('### Test 7 ### failed') + test_collector.add_result('### Test 7 ### failed', TestResult.FAILED) else: for k, v in metric_updates.items(): if len(v) < min_updates: print('found only {} updates for {}, test step 7 failed'.format(len(v), k)) - results.append(f'### Test 7 Handle {k} ### failed') + test_collector.add_result(f'### Test 7 Handle {k} ### failed', TestResult.FAILED) else: print('found {} updates for {}, test step 7 ok'.format(len(v), k)) - results.append(f'### Test 7 Handle {k} ### passed') + test_collector.add_result(f'### Test 7 Handle {k} ### passed', TestResult.PASSED) if len(alert_updates) == 0: - results.append('### Test 8 ### failed') + test_collector.add_result('### Test 8 ### failed', TestResult.FAILED) else: for k, v in alert_updates.items(): if len(v) < min_updates: print('found only {} updates for {}, test step 8 failed'.format(len(v), k)) - results.append(f'### Test 8 Handle {k} ### failed') + test_collector.add_result(f'### Test 8 Handle {k} ### failed', TestResult.FAILED) else: print('found {} updates for {}, test step 8 ok'.format(len(v), k)) - results.append(f'### Test 8 Handle {k} ### passed') + test_collector.add_result(f'### Test 8 Handle {k} ### passed', TestResult.PASSED) print('Test step 9: call SetString operation') setstring_operations = mdib.descriptions.NODETYPE.get(pm.SetStringOperationDescriptor, []) setst_handle = 'string.ch0.vmd1_sco_0' if len(setstring_operations) == 0: print('Test step 9(SetString) failed, no SetString operation found') - results.append('### Test 9 ### failed') + test_collector.add_result('### Test 9 ### failed', TestResult.FAILED) else: for s in setstring_operations: if s.Handle != setst_handle: @@ -161,16 +194,16 @@ def onAlertUpdates(alertsbyhandle): print(res) if res.InvocationInfo.InvocationState != InvocationState.FINISHED: print('set string operation {} did not finish with "Fin":{}'.format(s.Handle, res)) - results.append('### Test 9(SetString) ### failed') + test_collector.add_result('### Test 9(SetString) ### failed', TestResult.FAILED) else: print('set string operation {} ok:{}'.format(s.Handle, res)) - results.append('### Test 9(SetString) ### passed') + test_collector.add_result('### Test 9(SetString) ### passed', TestResult.PASSED) except futures.TimeoutError: print('timeout error') - results.append('### Test 9(SetString) ### failed') + test_collector.add_result('### Test 9(SetString) ### failed', TestResult.FAILED) except Exception as ex: print(f'Test 9(SetString): {ex}') - results.append('### Test 9(SetString) ### failed') + test_collector.add_result('### Test 9(SetString) ### failed', TestResult.FAILED) print('Test step 9: call SetValue operation') setvalue_operations = mdib.descriptions.NODETYPE.get(pm.SetValueOperationDescriptor, []) @@ -178,7 +211,7 @@ def onAlertUpdates(alertsbyhandle): setval_handle = 'numeric.ch0.vmd1_sco_0' if len(setvalue_operations) == 0: print('Test step 9 failed, no SetValue operation found') - results.append('### Test 9(SetValue) ### failed') + test_collector.add_result('### Test 9(SetValue) ### failed', TestResult.FAILED) else: for s in setvalue_operations: if s.Handle != setval_handle: @@ -191,22 +224,23 @@ def onAlertUpdates(alertsbyhandle): print(res) if res.InvocationInfo.InvocationState != InvocationState.FINISHED: print('set value operation {} did not finish with "Fin":{}'.format(s.Handle, res)) + test_collector.add_result('### Test 9(SetValue) ### failed', TestResult.FAILED) else: print('set value operation {} ok:{}'.format(s.Handle, res)) - results.append('### Test 9(SetValue) ### passed') + test_collector.add_result('### Test 9(SetValue) ### passed', TestResult.PASSED) except futures.TimeoutError: print('timeout error') - results.append('### Test 9(SetValue) ### failed') + test_collector.add_result('### Test 9(SetValue) ### failed', TestResult.FAILED) except Exception as ex: print(f'Test 9(SetValue): {ex}') - results.append('### Test 9(SetValue) ### failed') + test_collector.add_result('### Test 9(SetValue) ### failed', TestResult.FAILED) print('Test step 9: call Activate operation') activate_operations = mdib.descriptions.NODETYPE.get(pm.ActivateOperationDescriptor, []) activate_handle = 'actop.vmd1_sco_0' if len(setstring_operations) == 0: print('Test step 9 failed, no Activate operation found') - results.append('### Test 9(Activate) ### failed') + test_collector.add_result('### Test 9(Activate) ### failed', TestResult.FAILED) else: for s in activate_operations: if s.Handle != activate_handle: @@ -219,25 +253,25 @@ def onAlertUpdates(alertsbyhandle): print(res) if res.InvocationInfo.InvocationState != InvocationState.FINISHED: print('activate operation {} did not finish with "Fin":{}'.format(s.Handle, res)) - results.append('### Test 9(Activate) ### failed') + test_collector.add_result('### Test 9(Activate) ### failed', TestResult.FAILED) else: print('activate operation {} ok:{}'.format(s.Handle, res)) - results.append('### Test 9(Activate) ### passed') + test_collector.add_result('### Test 9(Activate) ### passed', TestResult.PASSED) except futures.TimeoutError: print('timeout error') - results.append('### Test 9(Activate) ### failed') + test_collector.add_result('### Test 9(Activate) ### failed', TestResult.FAILED) except Exception as ex: print(f'Test 9(Activate): {ex}') - results.append('### Test 9(Activate) ### failed') + test_collector.add_result('### Test 9(Activate) ### failed', TestResult.FAILED) print('Test step 10: cancel all subscriptions') success = client._subscription_mgr.unsubscribe_all() if success: - results.append('### Test 10(unsubscribe) ### passed') + test_collector.add_result('### Test 10(unsubscribe) ### passed', TestResult.PASSED) else: - results.append('### Test 10(unsubscribe) ### failed') + test_collector.add_result('### Test 10(unsubscribe) ### failed', TestResult.FAILED) time.sleep(2) - return results + return test_collector if __name__ == '__main__': @@ -264,5 +298,6 @@ def onAlertUpdates(alertsbyhandle): logging.getLogger(name).setLevel(logging.DEBUG) comm_logger.start() run_results = run_ref_test() - for r in run_results: + for r in run_results.test_messages: print(r) + sys.exit(0 if run_results.overall_test_result is TestResult.PASSED else 1) diff --git a/examples/ReferenceTest/reference_provider.py b/examples/ReferenceTest/reference_provider.py index 4bbb21d6..77d515fe 100644 --- a/examples/ReferenceTest/reference_provider.py +++ b/examples/ReferenceTest/reference_provider.py @@ -102,7 +102,7 @@ def set_reference_data(prov: provider.SdcProvider, loc: location.SdcLocation = N loc = loc or get_location() prov.set_location(loc, [pm_types.InstanceIdentifier('Validator', extension_string='System')]) patient_handle = prov.mdib.descriptions.NODETYPE.get_one(pm.PatientContextDescriptor).Handle - with prov.mdib.transaction_manager() as mgr: + with prov.mdib.context_state_transaction() as mgr: patient_state = mgr.mk_context_state(patient_handle) patient_state.CoreData.Givenname = "Given" patient_state.CoreData.Middlename = ["Middle"] @@ -160,7 +160,7 @@ def run_provider(): value_operation = prov.mdib.descriptions.handle.get_one('numeric.ch0.vmd1_sco_0') string_operation = prov.mdib.descriptions.handle.get_one('enumstring.ch0.vmd1_sco_0') - with prov.mdib.transaction_manager() as mgr: + with prov.mdib.metric_state_transaction() as mgr: state = mgr.get_state(value_operation.OperationTarget) if not state.MetricValue: state.mk_metric_value() @@ -174,7 +174,7 @@ def run_provider(): current_value = 0 while True: try: - with prov.mdib.transaction_manager() as mgr: + with prov.mdib.metric_state_transaction() as mgr: state = mgr.get_state(metric.Handle) if not state.MetricValue: state.mk_metric_value() @@ -185,7 +185,7 @@ def run_provider(): except Exception: # noqa: BLE001 logger.error(traceback.format_exc()) try: - with prov.mdib.transaction_manager() as mgr: + with prov.mdib.alert_state_transaction() as mgr: state = mgr.get_state(alert_condition.Handle) state.Presence = not state.Presence logger.info(f'Set @Presence={state.Presence} of the alert condition with the handle ' diff --git a/examples/ReferenceTest/test_reference.py b/examples/ReferenceTest/test_reference.py index 18efafc0..ffee879f 100644 --- a/examples/ReferenceTest/test_reference.py +++ b/examples/ReferenceTest/test_reference.py @@ -1,3 +1,4 @@ +import os import threading import time import traceback @@ -11,8 +12,10 @@ from sdc11073.consumer import SdcConsumer from sdc11073.definitions_sdc import SdcV1Definitions from sdc11073.mdib import ConsumerMdib -from sdc11073.wsdiscovery import WSDiscovery, ScopesType -from sdc11073.xml_types import msg_types, pm_qnames as pm +from sdc11073.wsdiscovery import ScopesType +from sdc11073.wsdiscovery import WSDiscovery +from sdc11073.xml_types import msg_types +from sdc11073.xml_types import pm_qnames as pm class DeviceActivity(threading.Thread): @@ -41,7 +44,7 @@ def run(self): valueOperation = oneContainer if oneContainer.Handle == "enumstring.ch0.vmd1_sco_0": stringOperation = oneContainer - with self.device.mdib.transaction_manager() as mgr: + with self.device.mdib.metric_state_transaction() as mgr: state = mgr.get_state(valueOperation.OperationTarget) if not state.MetricValue: state.mk_metric_value() @@ -53,7 +56,7 @@ def run(self): currentValue = Decimal(0) while True: if metric: - with self.device.mdib.transaction_manager() as mgr: + with self.device.mdib.metric_state_transaction() as mgr: state = mgr.get_state(metric.Handle) if not state.MetricValue: state.mk_metric_value() @@ -63,7 +66,7 @@ def run(self): else: print("Metric not found in MDIB!") if alertCondition: - with self.device.mdib.transaction_manager() as mgr: + with self.device.mdib.alert_state_transaction() as mgr: state = mgr.get_state(alertCondition.Handle) state.Presence = not state.Presence print('set alertstate presence to {}'.format(state.Presence)) @@ -85,37 +88,41 @@ class Test_Reference(unittest.TestCase): def setUp(self) -> None: - # tests fill these lists with what they create, teardown cleans up after them. - self.my_devices = [] + # tests fill this list with what they create, teardown cleans up after them. self.my_clients = [] # define how the provider is published on the network and how the client tries to detect the device self.loc = reference_provider.get_location() self.ip = reference_provider.get_network_adapter().ip - self.provider_discovery = WSDiscovery(self.ip) - self.provider_discovery.start() - self.provider = reference_provider.create_reference_provider(ws_discovery=self.provider_discovery) - reference_provider.set_reference_data(self.provider, loc=self.loc) - self.device_activity = DeviceActivity(self.provider) - self.device_activity.start() def tearDown(self) -> None: - self.provider_discovery.stop() for cl in self.my_clients: print('stopping {}'.format(cl)) cl.stop_all() - self.device_activity.running = False - self.device_activity.join() def test_with_created_device(self): # This test creates its own device and runs the tests against it # A WsDiscovery instance is needed to publish devices on the network. # In this case we want to publish them only on localhost 127.0.0.1. - self._runtest_client_connects() - @unittest.skip + self.provider_discovery = WSDiscovery(self.ip) + self.provider_discovery.start() + self.provider = reference_provider.create_reference_provider(ws_discovery=self.provider_discovery) + reference_provider.set_reference_data(self.provider, loc=self.loc) + self.device_activity = DeviceActivity(self.provider) + self.device_activity.start() + try: + self._runtest_client_connects() + finally: + self.provider_discovery.stop() + self.device_activity.running = False + self.device_activity.join() + + @unittest.skipUnless(os.getenv('EXTERNAL_DEVICE_RUNNING') == "true", + reason='Environment variable EXTERNAL_DEVICE_RUNNING is not "true", ' + 'indicating that no external SDC Provider was started to test against.') def test_client_connects(self): - # This test need an externally started device to run the tests against it. - # + # This test needs an externally started SDC Provider to run the tests against. + print('Start unittest "test_client_connects" against externally started SDC Provider.') self._runtest_client_connects() def _runtest_client_connects(self): @@ -136,8 +143,11 @@ def _runtest_client_connects(self): my_service = services[0] print('Test step 1 successful: device discovered') + ssl_context_container = reference_provider.get_ssl_context() + print(f'Used ssl context: {ssl_context_container}') + print('Test step 2: connect to device...') - client = SdcConsumer.from_wsd_service(my_service, ssl_context_container=None) + client = SdcConsumer.from_wsd_service(my_service, ssl_context_container=ssl_context_container) self.my_clients.append(client) client.start_all() self.assertTrue(client.is_connected) @@ -173,7 +183,8 @@ def _runtest_client_connects(self): self.assertEqual(len(errors), 0, msg='expected no Errors, got:{}'.format(', '.join(errors))) self.assertEqual(len(passed), 4, msg='expected 4 Passed, got :{}'.format(', '.join(passed))) - def _test_state_updates(self, mdib): + @staticmethod + def _test_state_updates(mdib): passed = [] errors = [] print('Test step 7&8: count metric state updates and alert state updates') @@ -230,7 +241,8 @@ def onAlertUpdates(alertsbyhandle): passed.append('### Test 8 ### passed') return passed, errors - def _test_setstring_operation(self, mdib, client): + @staticmethod + def _test_setstring_operation(mdib, client): passed = [] errors = [] print('Test step 9: call SetString operation') @@ -260,12 +272,12 @@ def _test_setstring_operation(self, mdib, client): errors.append('### Test 9 ### failed') return passed, errors - def _test_setvalue_operation(self, mdib, client): + @staticmethod + def _test_setvalue_operation(mdib, client): passed = [] errors = [] print('Test step 10: call SetValue operation') - setvalue_operations = mdib.descriptions.NODETYPE.get(pm.SetValueOperationDescriptor, - []) + setvalue_operations = mdib.descriptions.NODETYPE.get(pm.SetValueOperationDescriptor, []) setval_handle = 'numeric.ch0.vmd1_sco_0' if len(setvalue_operations) == 0: print('Test step 10 failed, no SetValue operation found') diff --git a/examples/ReferenceTestV2/discoproxyclient.py b/examples/ReferenceTestV2/discoproxyclient.py index 5e6b2a75..1414c8b3 100644 --- a/examples/ReferenceTestV2/discoproxyclient.py +++ b/examples/ReferenceTestV2/discoproxyclient.py @@ -1,3 +1,10 @@ +"""Implementation of discovery proxy client. + +A discovery proxy prototype has been implemented based on +https://confluence.hl7.org/display/GP/Topic%3A+Discovery+Proxy+Actors + +This client connects to that proxy. +""" from __future__ import annotations import os @@ -270,88 +277,97 @@ def on_get(self, request_data: RequestData) -> CreatedMessage: return EmptyResponse() -def mk_provider(wsd: DiscoProxyClient, mdib_path: str, uuid_str: str): - my_mdib = ProviderMdib.from_mdib_file(mdib_path) - print("UUID for this device is {}".format(uuid_str)) - dpwsModel = ThisModelType(manufacturer='sdc11073', - manufacturer_url='www.sdc11073.com', - model_name='TestDevice', - model_number='1.0', - model_url='www.sdc11073.com/model', - presentation_url='www.sdc11073.com/model/presentation') - - dpwsDevice = ThisDeviceType(friendly_name='TestDevice', - firmware_version='Version1', - serial_number='12345') - specific_components = None - sdc_provider = SdcProvider(wsd, dpwsModel, dpwsDevice, my_mdib, UUID(uuid_str), - ssl_context_container=ssl_contexts, - specific_components=specific_components, - max_subscription_duration=15 - ) - return sdc_provider -def log_services(log, the_services): - log.info('found %d services:', len(the_services)) - for the_service in the_services: - log.info('found service: %r', the_service) - if __name__ == '__main__': - basic_logging_setup() - logger = get_logger_adapter('sdc.disco.main') - ca_folder = r'C:\tmp\ORNET_REF_Certificates' - ssl_passwd = 'dummypass' - disco_ip = '192.168.30.5:33479' - my_ip = '192.168.30.106' - My_UUID_str = '12345678-6f55-11ea-9697-123456789bcd' - here = os.path.dirname(__file__) - default_mdib_path = os.path.join(here, 'mdib_test_sequence_2_v4(temp).xml') - mdib_path = os.getenv('ref_mdib') or default_mdib_path - ref_fac = os.getenv('ref_fac') or 'r_fac' - ref_poc = os.getenv('ref_poc') or 'r_poc' - ref_bed = os.getenv('ref_bed') or 'r_bed' - loc = SdcLocation(ref_fac, ref_poc, ref_bed) - - ssl_contexts = mk_ssl_contexts_from_folder(ca_folder, - cyphers_file=None, - private_key='user_private_key_encrypted.pem', - certificate='user_certificate_root_signed.pem', - ca_public_key='root_certificate.pem', - ssl_passwd=ssl_passwd, - ) - - proxy = DiscoProxyClient(disco_ip, my_ip, ssl_contexts) - # proxy = DiscoProxyClient(disco_ip, my_ip) - proxy.start() - try: - services = proxy.search_services() - log_services(logger, services) - - # now publish a device + + def mk_provider(wsd: DiscoProxyClient, mdib_path: str, uuid_str: str, ssl_contexts: SSLContextContainer): + my_mdib = ProviderMdib.from_mdib_file(mdib_path) + print("UUID for this device is {}".format(uuid_str)) + dpwsModel = ThisModelType(manufacturer='sdc11073', + manufacturer_url='www.sdc11073.com', + model_name='TestDevice', + model_number='1.0', + model_url='www.sdc11073.com/model', + presentation_url='www.sdc11073.com/model/presentation') + + dpwsDevice = ThisDeviceType(friendly_name='TestDevice', + firmware_version='Version1', + serial_number='12345') + specific_components = None + sdc_provider = SdcProvider(wsd, dpwsModel, dpwsDevice, my_mdib, UUID(uuid_str), + ssl_context_container=ssl_contexts, + specific_components=specific_components, + max_subscription_duration=15 + ) + return sdc_provider + + + def log_services(log, the_services): + """Print the found services.""" + log.info('found %d services:', len(the_services)) + for the_service in the_services: + log.info('found service: %r', the_service) + + + def main(): + # example code how to use the DiscoProxyClient. + # It assumes a discovery proxy is reachable on disco_ip address. + basic_logging_setup() + logger = get_logger_adapter('sdc.disco.main') + ca_folder = r'C:\tmp\ORNET_REF_Certificates' + ssl_passwd = 'dummypass' + disco_ip = '192.168.30.5:33479' + my_ip = '192.168.30.106' + My_UUID_str = '12345678-6f55-11ea-9697-123456789bcd' + here = os.path.dirname(__file__) + default_mdib_path = os.path.join(here, 'mdib_test_sequence_2_v4(temp).xml') + mdib_path = os.getenv('ref_mdib') or default_mdib_path + ref_fac = os.getenv('ref_fac') or 'r_fac' + ref_poc = os.getenv('ref_poc') or 'r_poc' + ref_bed = os.getenv('ref_bed') or 'r_bed' loc = SdcLocation(ref_fac, ref_poc, ref_bed) - logger.info("location for this device is {}", loc) - logger.info('start provider...') - sdc_provider = mk_provider(proxy, mdib_path, My_UUID_str) - sdc_provider.start_all() + ssl_contexts = mk_ssl_contexts_from_folder(ca_folder, + cyphers_file=None, + private_key='user_private_key_encrypted.pem', + certificate='user_certificate_root_signed.pem', + ca_public_key='root_certificate.pem', + ssl_passwd=ssl_passwd, + ) + + proxy = DiscoProxyClient(disco_ip, my_ip, ssl_contexts) + proxy.start() + try: + services = proxy.search_services() + log_services(logger, services) + + # now publish a device + loc = SdcLocation(ref_fac, ref_poc, ref_bed) + logger.info("location for this device is {}", loc) + logger.info('start provider...') + + sdc_provider = mk_provider(proxy, mdib_path, My_UUID_str, ssl_contexts) + sdc_provider.start_all() + + validators = [pm_types.InstanceIdentifier('Validator', extension_string='System')] + sdc_provider.set_location(loc, validators) - validators = [pm_types.InstanceIdentifier('Validator', extension_string='System')] - sdc_provider.set_location(loc, validators) + services = proxy.search_services() + log_services(logger, services) + for service in services: + result = proxy.send_resolve(service.epr) + logger.info('resolvematches: %r', result.ResolveMatch) - services = proxy.search_services() - log_services(logger, services) - for service in services: - result = proxy.send_resolve(service.epr) - logger.info('resolvematches: %r', result.ResolveMatch) + time.sleep(5) + logger.info('stop provider...') + sdc_provider.stop_all() - time.sleep(5) - logger.info('stop provider...') - sdc_provider.stop_all() + services = proxy.search_services() + log_services(logger, services) - services = proxy.search_services() - log_services(logger, services) + finally: + proxy.stop() - finally: - proxy.stop() + main() \ No newline at end of file diff --git a/examples/ReferenceTestV2/reference_consumer_v2.py b/examples/ReferenceTestV2/reference_consumer_v2.py index 06c54f19..12e91498 100644 --- a/examples/ReferenceTestV2/reference_consumer_v2.py +++ b/examples/ReferenceTestV2/reference_consumer_v2.py @@ -1,19 +1,39 @@ +"""Implementation of reference consumer. + +The reference consumer gets its parameters from environment variables: +- adapter_ip specifies which ip address shall be used +- ca_folder specifies where the communication certificates are located. +- ssl_passwd specifies an optional password for the certificates +- search_epr specifies the last characters of the endpoint reference of the device that the consumer shall connect to. + It is not necessary to provide the full epr, just enough to be unique in the current network. + +If a value is not provided as environment variable, the default value (see code below) will be used. +""" +from __future__ import annotations + import os import time import traceback import uuid from collections import defaultdict +from dataclasses import dataclass from decimal import Decimal -from sdc11073 import commlog +from typing import TYPE_CHECKING + from sdc11073 import observableproperties from sdc11073.certloader import mk_ssl_contexts_from_folder +from sdc11073.consumer import SdcConsumer from sdc11073.definitions_sdc import SdcV1Definitions from sdc11073.mdib.consumermdib import ConsumerMdib from sdc11073.mdib.consumermdibxtra import ConsumerMdibMethods -from sdc11073.consumer import SdcConsumer from sdc11073.wsdiscovery import WSDiscovery from sdc11073.xml_types import pm_qnames, msg_types +if TYPE_CHECKING: + from lxml.etree import QName + from sdc11073.wsdiscovery.service import Service + from sdc11073.pysoap.msgreader import ReceivedMessage + ConsumerMdibMethods.DETERMINATIONTIME_WARN_LIMIT = 2.0 adapter_ip = os.getenv('ref_ip') or '127.0.0.1' @@ -27,61 +47,139 @@ set_string_handle = "set_string_0.sco.mds_0" set_context_state_handle = "set_context_0.sco.mds_0" -ENABLE_COMMLOG = False -if ENABLE_COMMLOG: - comm_logger = commlog.CommLogger(log_folder=r'c:\temp\sdc_refclient_commlog', - log_out=True, - log_in=True, - broadcast_ip_filter=None) - commlog.set_communication_logger(comm_logger) +@dataclass +class ResultEntry: + verdict: bool | None + step: str + info: str + xtra: str + + def __str__(self): + verdict_str = {None: 'no result', True: 'passed', False: 'failed'} + return f'{self.step:6s}:{verdict_str[self.verdict]:10s} {self.info}{self.xtra}' + + +class ResultsCollector: + def __init__(self): + self._results: list[ResultEntry] = [] + + def log_result(self, is_ok: bool | None, step: str, info: str, extra_info: str | None = None): + xtra = f' ({extra_info}) ' if extra_info else '' + self._results.append(ResultEntry(is_ok, step, info, xtra)) + + def print_summary(self): + print('\n### Summary ###') + for r in self._results: + print(r) + + @property + def failed_count(self): + return len([r for r in self._results if r.verdict is False]) -sleep_timer = 30 +class ConsumerMdibMethodsReferenceTest(ConsumerMdibMethods): + def __init__(self, consumer_mdib, logger): + super().__init__(consumer_mdib, logger) + self.alert_condition_type_concept_updates: list[float] = [] # for test 5a.1 + self._last_alert_condition_type_concept_updates = time.monotonic() # timestamp -def test_1b(wsd, my_service) -> str: - # send resolve and check response + self.alert_condition_cause_remedy_updates: list[float] = [] # for test 5a.2 + self._last_alert_condition_cause_remedy_updates = time.monotonic() # timestamp + + self.unit_of_measure_updates: list[float] = [] # for test 5a.3 + self._last_unit_of_measure_updates = time.monotonic() # timestamp + + def _on_episodic_metric_report(self, received_message_data: ReceivedMessage): + # test 4.1 : count numeric metric updates + # The Reference Provider produces at least 5 numeric metric updates in 30 seconds + super()._on_episodic_metric_report(received_message_data) + + def _on_description_modification_report(self, received_message_data: ReceivedMessage): + """For Test 5a.1 check if the concept description of updated alert condition Type changed. + For Test 5a.2 check if alert condition cause-remedy information changed. + """ + cls = self._mdib.data_model.msg_types.DescriptionModificationReport + report = cls.from_node(received_message_data.p_msg.msg_node) + now = time.monotonic() + dmt = self._mdib.sdc_definitions.data_model.msg_types.DescriptionModificationType + for report_part in report.ReportPart: + modification_type = report_part.ModificationType + if modification_type == dmt.UPDATE: + for descriptor_container in report_part.Descriptor: + if descriptor_container.is_alert_condition_descriptor: + old_descriptor = self._mdib.descriptions.handle.get_one(descriptor_container.Handle) + # test 5a.1 + if descriptor_container.Type.ConceptDescription != old_descriptor.Type.ConceptDescription: + print(f'concept description {descriptor_container.Type.ConceptDescription} <=> ' + f'{old_descriptor.Type.ConceptDescription}') + self.alert_condition_type_concept_updates.append( + now - self._last_alert_condition_type_concept_updates) + self._last_alert_condition_type_concept_updates = now + # test 5a.2 + # (CauseInfo is a list) + detected_5a2 = False + if len(descriptor_container.CauseInfo) != len(old_descriptor.CauseInfo): + print(f'RemedyInfo no. of CauseInfo {len(descriptor_container.CauseInfo)} <=> ' + f'{len(old_descriptor.CauseInfo)}') + detected_5a2 = True + else: + for i, cause_info in enumerate(descriptor_container.CauseInfo): + old_cause_info = old_descriptor.CauseInfo[i] + if cause_info.RemedyInfo != old_cause_info.RemedyInfo: + print(f'RemedyInfo {cause_info.RemedyInfo} <=> ' + f'{old_cause_info.RemedyInfo}') + detected_5a2 = True + if detected_5a2: + self.alert_condition_cause_remedy_updates.append( + now - self._last_alert_condition_cause_remedy_updates) + self._last_alert_condition_cause_remedy_updates = now + elif descriptor_container.is_metric_descriptor: + # test 5a.3 + old_descriptor = self._mdib.descriptions.handle.get_one(descriptor_container.Handle) + if old_descriptor.Unit != descriptor_container.Unit: + self.unit_of_measure_updates.append(now - self._last_unit_of_measure_updates) + self._last_unit_of_measure_updates = now + + super()._on_description_modification_report(received_message_data) + + +def test_1b_resolve(wsd, my_service) -> (bool, str): + """Send resolve and check response.""" wsd.clear_remote_services() wsd._send_resolve(my_service.epr) time.sleep(3) if len(wsd._remote_services) == 0: - return ('### Test 1b ### failed, no response') + return False, 'no response' elif len(wsd._remote_services) > 1: - return ('### Test 1b ### failed, multiple response') + return False, 'multiple response' else: service = wsd._remote_services.get(my_service.epr) if service.epr != my_service.epr: - return ('### Test 1b ### failed, not the same epr') + return False, 'not the same epr' else: - return ('### Test 1b ### passed') + return True, 'resolve answered' -def connect_client(my_service) -> SdcConsumer: +def connect_client(my_service: Service) -> SdcConsumer: if ca_folder: ssl_contexts = mk_ssl_contexts_from_folder(ca_folder, - cyphers_file=None, - private_key='user_private_key_encrypted.pem', - certificate='user_certificate_root_signed.pem', - ca_public_key='root_certificate.pem', - ssl_passwd=ssl_passwd - ) + cyphers_file=None, + private_key='user_private_key_encrypted.pem', + certificate='user_certificate_root_signed.pem', + ca_public_key='root_certificate.pem', + ssl_passwd=ssl_passwd + ) else: ssl_contexts = None client = SdcConsumer.from_wsd_service(my_service, - ssl_context_container=ssl_contexts, - validate=True) + ssl_context_container=ssl_contexts, + validate=True) client.start_all() return client -def init_mdib(client) -> ConsumerMdib: - # The Reference Provider answers to GetMdib - mdib = ConsumerMdib(client) - mdib.init_mdib() - return mdib - - -def test_min_updates_per_handle(updates_dict, min_updates, node_type_filter = None) -> (bool, str): # True ok +def test_min_updates_per_handle(updates_dict, min_updates, node_type_filter=None) -> (bool, str): # True ok results = [] is_ok = True if len(updates_dict) == 0: @@ -97,26 +195,26 @@ def test_min_updates_per_handle(updates_dict, min_updates, node_type_filter = No return is_ok, '\n'.join(results) -def test_min_updates_for_type(updates_dict, min_updates, q_name) -> (bool, str): # True ok +def test_min_updates_for_type(updates_dict: dict, min_updates: int, q_name_list: list[QName]) -> (bool, str): # True ok flat_list = [] for v in updates_dict.values(): flat_list.extend(v) - matches = [x for x in flat_list if x.NODETYPE == q_name] + matches = [x for x in flat_list if x.NODETYPE in q_name_list] if len(matches) >= min_updates: return True, '' return False, f'expect >= {min_updates}, got {len(matches)} out of {len(flat_list)}' -def log_result(is_ok, result_list, step, info, extra_info=None): - xtra = f' ({extra_info}) ' if extra_info else '' - if is_ok: - result_list.append(f'{step} => passed {xtra}{info}') - else: - result_list.append(f'{step} => failed {xtra}{info}') +# def log_result(is_ok, result_list, step, info, extra_info=None): +# xtra = f' ({extra_info}) ' if extra_info else '' +# if is_ok: +# result_list.append(f'{step} => passed {xtra}{info}') +# else: +# result_list.append(f'{step} => failed {xtra}{info}') -def run_ref_test(): - results = [] +def run_ref_test(results_collector: ResultsCollector): + # results = [] print(f'using adapter address {adapter_ip}') print('Test step 1: discover device which endpoint ends with "{}"'.format(search_epr)) wsd = WSDiscovery(adapter_ip) @@ -126,6 +224,13 @@ def run_ref_test(): # a) The Reference Provider sends Hello messages # b) The Reference Provider answers to Probe and Resolve messages + # Remark: 1a) is not testable because provider can't be forced to send a hello while this test is running. + step = '1a' + info = 'The Reference Provider sends Hello messages' + results_collector.log_result(None, step, info, extra_info='not testable') + + step = '1b.1' + info = 'The Reference Provider answers to Probe messages' my_service = None while my_service is None: services = wsd.search_services(types=SdcV1Definitions.MedicalDeviceTypesFilter) @@ -136,12 +241,13 @@ def run_ref_test(): print('found service {}'.format(s.epr)) break print('Test step 1 successful: device discovered') - results.append('### Test 1 ### passed') + results_collector.log_result(True, step, info) + step = '1b.2' + info = 'The Reference Provider answers to Resolve messages' print('Test step 1b: send resolve and check response') - result = test_1b(wsd, my_service) - results.append(result) - print(f'{result} : resolve and check response') + is_ok, txt = test_1b_resolve(wsd, my_service) + results_collector.log_result(is_ok, step, info, extra_info=txt) # 2. BICEPS Services Discovery and binding # a) The Reference Provider answers to TransferGet @@ -157,23 +263,24 @@ def run_ref_test(): print(step, info) try: client = connect_client(my_service) - log_result(client.host_description is not None, results, step, info) + results_collector.log_result(client.host_description is not None, step, info) except: print(traceback.format_exc()) - results.append(f'{step} => failed') - return results + results_collector.log_result(False, step, info) + return # results step = '2b.1' info = 'the Reference Provider grants subscriptions of at most 15 seconds' now = time.time() durations = [s.expires_at - now for s in client.subscription_mgr.subscriptions.values()] print(f'subscription durations = {durations}') - log_result(max(durations) <= 15, results, step, info) + results_collector.log_result(max(durations) <= 15, step, info) step = '2b.2' info = 'the Reference Provider grants subscriptions of at most 15 seconds (renew)' - granted = list(client.subscription_mgr.subscriptions.items())[0][1].renew(30000) + subscription = list(client.subscription_mgr.subscriptions.values())[0] + granted = subscription.renew(30000) print(f'renew granted = {granted}') - log_result(max(durations) <= 15, results, step, info) + results_collector.log_result(max(durations) <= 15, step, info) # 3. Request Response # a) The Reference Provider answers to GetMdib @@ -183,56 +290,61 @@ def run_ref_test(): info = 'The Reference Provider answers to GetMdib' print(step, info) try: - mdib = init_mdib(client) - log_result(mdib is not None, results, step, info) + mdib = ConsumerMdib(client, extras_cls=ConsumerMdibMethodsReferenceTest) + mdib.init_mdib() # throws an exception if provider did not answer to GetMdib + results_collector.log_result(True, step, info) except: print(traceback.format_exc()) - results.append(f'{step} => failed') - return results + results_collector.log_result(False, step, info) + # results.append(f'{step} => failed') + return # results step = '3b' info = 'The Reference Provider answers to GetContextStates messages' context_service = client.context_service_client if context_service is None: - results.append(f'{step} => failed {info}') + results_collector.log_result(False, step, info, extra_info='no context service') else: try: states = context_service.get_context_states().result.ContextState - results.append(f'{step} => passed {info}') + results_collector.log_result(True, step, info) except: print(traceback.format_exc()) - results.append(f'{step} => failed {info}') - return results - step = 'Test step 3b.1: The Reference Provider provides at least one location context state' - loc_states = [ s for s in states if s.NODETYPE == pm_qnames.LocationContextState] - log_result(len(loc_states) > 0, results, step, info) + results_collector.log_result(False, step, info, extra_info='exception') + step = '3b.1' + info = 'The Reference Provider provides at least one location context state' + loc_states = [s for s in states if s.NODETYPE == pm_qnames.LocationContextState] + results_collector.log_result(len(loc_states) > 0, step, info) # 4 State Reports - # a)The Reference Provider produces at least 5 metric updates in 30 seconds - # The metric types shall comprise numeric and string metrics - # b) The Reference Provider produces at least 5 alert condition updates in 30 seconds - # c) The Reference Provider produces at least 5 alert signal updates in 30 seconds - # d) The Reference Provider provides alert system self checks in accordance to the periodicity defined in the MDIB (at least every 10 seconds) - # e) The Reference Provider provides 3 waveforms x 10 messages per second x 100 samples per message - # f) The Reference Provider provides changes for the following components: - # * Clock/Battery object (Component report) - # * The Reference Provider provides changes for the VMD/MDS (Component report) + # a) The Reference Provider produces at least 5 numeric metric updates in 30 seconds + # b) The Reference Provider produces at least 5 string metric updates (StringMetric or EnumStringMetric) in 30 seconds + # c) The Reference Provider produces at least 5 alert condition updates (AlertCondition or LimitAlertCondition) in 30 seconds + # d) The Reference Provider produces at least 5 alert signal updates in 30 seconds + # e) The Reference Provider provides alert system self checks in accordance to the periodicity defined in the MDIB (at least every 10 seconds) + # f) The Reference Provider provides 3 waveforms (RealTimeSampleArrayMetric) x 10 messages per second x 100 samples per message + # g) The Reference Provider provides changes for the following components: + # * At least 5 Clock or Battery object updates in 30 seconds (Component report) + # * At least 5 MDS or VMD updates in 30 seconds (Component report) # g) The Reference Provider provides changes for the following operational states: - # Enable/Disable operations (some different than the ones mentioned above) (Operational State Report)""" + # At least 5 Operation updates in 30 seconds; enable/disable operations; some different than the ones mentioned above (Operational State Report)""" # setup data collectors for next test steps numeric_metric_updates = defaultdict(list) string_metric_updates = defaultdict(list) alert_condition_updates = defaultdict(list) alert_signal_updates = defaultdict(list) - # other_alert_updates = defaultdict(list) alert_system_updates = defaultdict(list) component_updates = defaultdict(list) waveform_updates = defaultdict(list) + operational_state_updates = defaultdict(list) description_updates = [] - def onMetricUpdates(metrics_by_handle): - # print('onMetricUpdates', metrics_by_handle) + def on_metric_updates(metrics_by_handle): + """Callback for all metric state updates. + + Writes to numeric_metric_updates or string_metric_updates, depending on type of state. + """ for k, v in metrics_by_handle.items(): print(f'State {v.NODETYPE.localname} {v.DescriptorHandle}') if v.NODETYPE == pm_qnames.NumericMetricState: @@ -241,6 +353,10 @@ def onMetricUpdates(metrics_by_handle): string_metric_updates[k].append(v) def on_alert_updates(alerts_by_handle): + """Callback for all alert state updates. + + Writes to alert_condition_updates, alert_signal_updates or alert_system_updates, depending on type of state. + """ for k, v in alerts_by_handle.items(): print(f'State {v.NODETYPE.localname} {v.DescriptorHandle}') if v.is_alert_condition: @@ -249,144 +365,185 @@ def on_alert_updates(alerts_by_handle): alert_signal_updates[k].append(v) elif v.NODETYPE == pm_qnames.AlertSystemState: alert_system_updates[k].append(v) - # else: - # other_alert_updates[k].append(v) def on_component_updates(components_by_handle): - # print('on_component_updates', alerts_by_handle) + """Callback for all component state updates. + + Writes to component_updates . + """ for k, v in components_by_handle.items(): print(f'State {v.NODETYPE.localname} {v.DescriptorHandle}') component_updates[k].append(v) def on_waveform_updates(waveforms_by_handle): + """Callback for all waveform state updates. + + Writes to waveform_updates . + """ for k, v in waveforms_by_handle.items(): waveform_updates[k].append(v) def on_description_modification(description_modification_report): + """Callback for all description modification updates. + + Writes to description_updates . + """ print('on_description_modification') description_updates.append(description_modification_report) - observableproperties.bind(mdib, metrics_by_handle=onMetricUpdates) + def on_operational_state_updates(operational_states_by_handle): + """Callback for all operational state updates. + + Writes to operational_state_updates . + """ + for k, v in operational_states_by_handle.items(): + print(f'State {v.NODETYPE.localname} {v.DescriptorHandle}') + operational_state_updates[k].append(v) + + observableproperties.bind(mdib, metrics_by_handle=on_metric_updates) observableproperties.bind(mdib, alert_by_handle=on_alert_updates) observableproperties.bind(mdib, component_by_handle=on_component_updates) observableproperties.bind(mdib, waveform_by_handle=on_waveform_updates) observableproperties.bind(mdib, description_modifications=on_description_modification) + observableproperties.bind(mdib, operation_by_handle=on_operational_state_updates) - min_updates = sleep_timer // 5 - 1 - print('will wait for {} seconds now, expecting at least {} updates per Handle'.format(sleep_timer, min_updates)) + # now collect reports + sleep_timer = 30 + min_updates = 5 + print(f'will wait for {sleep_timer} seconds now, expecting at least {min_updates} updates per Handle') time.sleep(sleep_timer) + # now check report count step = '4a' - info ='count numeric metric state updates' + info = 'count numeric metric state updates' print(step, info) is_ok, result = test_min_updates_per_handle(numeric_metric_updates, min_updates) - log_result(is_ok, results, step, info) + results_collector.log_result(is_ok, step, info) step = '4b' info = 'count string metric state updates' print(step) - is_ok, result = test_min_updates_per_handle(string_metric_updates, min_updates,) - log_result(is_ok, results, step, info) + is_ok, result = test_min_updates_per_handle(string_metric_updates, min_updates) + results_collector.log_result(is_ok, step, info) step = '4c' info = 'count alert condition updates' print(step) is_ok, result = test_min_updates_per_handle(alert_condition_updates, min_updates) - log_result(is_ok, results, step, info) + results_collector.log_result(is_ok, step, info) step = '4d' - info =' count alert signal updates' + info = ' count alert signal updates' print(step, info) is_ok, result = test_min_updates_per_handle(alert_signal_updates, min_updates) - log_result(is_ok, results, step, info) + results_collector.log_result(is_ok, step, info) - step ='4e' + step = '4e' info = 'count alert system self checks' is_ok, result = test_min_updates_per_handle(alert_system_updates, min_updates) - log_result(is_ok, results, step, info) + results_collector.log_result(is_ok, step, info) step = '4f' info = 'count waveform updates' + # 3 waveforms (RealTimeSampleArrayMetric) x 10 messages per second x 100 samples per message print(step, info) is_ok, result = test_min_updates_per_handle(waveform_updates, min_updates) - log_result(is_ok, results, step, info+ ' notifications per second') - if len(waveform_updates) < 3: - log_result(False, results, step, info+' number of waveforms') - else: - log_result(True, results, step, info+' number of waveforms') + results_collector.log_result(is_ok, step, info + ' notifications per second') + results_collector.log_result(len(waveform_updates) >= 3, step, info + ' number of waveforms') - expected_samples = 1000 * sleep_timer*0.9 + expected_samples = 1000 * sleep_timer * 0.9 for handle, reports in waveform_updates.items(): notifications = [n for n in reports if n.MetricValue is not None] samples = sum([len(n.MetricValue.Samples) for n in notifications]) if samples < expected_samples: - log_result(False, results, step, info + f' waveform {handle} has {samples} samples, expecting {expected_samples}') - is_ok = False + results_collector.log_result(False, step, + info + f' waveform {handle} has {samples} samples, expecting {expected_samples}') else: - log_result(True, results, step, info + f' waveform {handle} has {samples} samples') + results_collector.log_result(True, step, info + f' waveform {handle} has {samples} samples') pm = mdib.data_model.pm_names pm_types = mdib.data_model.pm_types - # The Reference Provider provides changes for the following reports as well: - # Clock/Battery object (Component report) - step = '4g' - info = 'count battery updates' - print(step, info) - is_ok, result = test_min_updates_for_type(component_updates, 1, pm.BatteryState) - log_result(is_ok, results, step, info) - - step = '4g' - info ='count VMD updates' + step = '4g.1' + info = 'count battery or clock updates' print(step, info) - is_ok, result = test_min_updates_for_type(component_updates, 1, pm.VmdState) - log_result(is_ok, results, step, info) + is_ok, result = test_min_updates_for_type(component_updates, + min_updates, + [pm.BatteryState, pm.ClockState]) + results_collector.log_result(is_ok, step, info) - step = '4g' - info = 'count MDS updates' + step = '4g.2' + info = 'count VMD or MDS updates' print(step, info) - is_ok, result = test_min_updates_for_type(component_updates, 1, pm.MdsState) - log_result(is_ok, results, step, info) + is_ok, result = test_min_updates_for_type(component_updates, + min_updates, + [pm.VmdState, pm.MdsState]) + results_collector.log_result(is_ok, step, info) step = '4h' info = 'Enable/Disable operations' - results.append(f'{step} => failed, not implemented {info}') - - """ - 5 Description Modifications: - a) The Reference Provider produces at least 1 update every 10 seconds comprising - * Update Alert condition concept description of type - * Update Alert condition cause-remedy information - * Update Unit of measure (metrics) - b) The Reference Provider produces at least 1 insertion followed by a deletion every 10 seconds comprising - * Insert a VMD including Channels including metrics - * Remove the VMD - """ - step = '5a' - info = 'Update Alert condition concept description of type' + print(step, info) + is_ok, result = test_min_updates_for_type(operational_state_updates, + min_updates, + [pm.SetValueOperationState, + pm.SetStringOperationState, + pm.ActivateOperationState, + pm.SetContextStateOperationState, + pm.SetMetricStateOperationState, + pm.SetComponentStateOperationState, + pm.SetAlertStateOperationState]) + results_collector.log_result(is_ok, step, info) + + # 5 Description Modifications: + # a) The Reference Provider produces at least 1 update every 10 seconds comprising + # * Update Alert condition concept description of Type + # * Update Alert condition cause-remedy information + # * Update Unit of measure (metrics) + # b) The Reference Provider produces at least 1 insertion followed by a deletion every 10 seconds comprising + # * Insert a VMD including Channels including metrics (inserted VMDs/Channels/Metrics are required to have + # a new handle assigned on each insertion such that containment tree entries are not recycled). + # (Tests for the handling of re-insertion of previously inserted objects should be tested additionally) + # * Remove the VMD + step = '5a.1' + info = 'Update Alert condition concept description of Type' print(step, info) # verify only that there are Alert Condition Descriptors updated - found = False - for report in description_updates: - for report_part in report.ReportPart: - if report_part.ModificationType == msg_types.DescriptionModificationType.UPDATE: - for descriptor in report_part.Descriptor: - if descriptor.NODETYPE == pm_qnames.AlertConditionDescriptor: - found = True - log_result(found, results, step, info) + updates = mdib.xtra.alert_condition_type_concept_updates + if not updates: + results_collector.log_result(False, step, info, 'no updates') + else: + max_diff = max(updates) + if max_diff > 10: + results_collector.log_result(False, step, info, f'max dt={max_diff}') + else: + results_collector.log_result(True, step, info, f'{len(updates) - 1} updates, max diff= {max_diff:.1f}') - step = '5a' + step = '5a.2' + info = 'Update Alert condition cause-remedy information' + print(step, info) + # verify only that there are remedy infos updated + updates = mdib.xtra.alert_condition_cause_remedy_updates + if not updates: + results_collector.log_result(False, step, info, 'no updates') + else: + max_diff = max(updates) + if max_diff > 10: + results_collector.log_result(False, step, info, f'{updates} => max dt={max_diff}') + else: + results_collector.log_result(True, step, info, f'{len(updates) - 1} updates, max diff= {max_diff:.1f}') + + step = '5a.3' info = 'Update Unit of measure' print(step, info) - # verify only that there are Alert Condition Descriptors updated - found = False - for report in description_updates: - for report_part in report.ReportPart: - if report_part.ModificationType == msg_types.DescriptionModificationType.UPDATE: - for descriptor in report_part.Descriptor: - if descriptor.NODETYPE == pm_qnames.NumericMetricDescriptor: - found = True - log_result(found, results, step, info) + updates = mdib.xtra.unit_of_measure_updates + if not updates: + results_collector.log_result(False, step, info, 'no updates') + else: + max_diff = max(updates) + if max_diff > 10: + results_collector.log_result(False, step, info, f'max dt={max_diff}') + else: + results_collector.log_result(True, step, info, f'{len(updates) - 1} updates, max diff= {max_diff:.1f}') step = '5b' info = 'Add / remove vmd' @@ -404,25 +561,25 @@ def on_description_modification(description_modification_report): for descriptor in report_part.Descriptor: if descriptor.NODETYPE == pm_qnames.VmdDescriptor: rm_found = True - log_result(add_found, results, step, info, 'add') - log_result(rm_found, results, step, info, 'remove') - - """ - 6 Operation invocation - a) (removed) - b) SetContextState: - * Payload: 1 Patient Context - * Context state is added to the MDIB including context association and validation - * If there is an associated context already, that context shall be disassociated - * Handle and version information is generated by the provider - * In order to avoid infinite growth of patient contexts, older contexts are allowed to be removed from the MDIB (=ContextAssociation=No) - c) SetValue: Immediately answers with "finished" - * Finished has to be sent as a report in addition to the response => - d) SetString: Initiates a transaction that sends Wait, Start and Finished - e) SetMetricStates: - * Payload: 2 Metric States (settings; consider alert limits) - * Immediately sends finished - * Action: Alter values of metrics """ + results_collector.log_result(add_found, step, info, 'add') + results_collector.log_result(rm_found, step, info, 'remove') + + # 6 Operation invocation + # a) (removed) + # b) SetContextState: + # * Payload: 1 Patient Context + # * Context state is added to the MDIB including context association and validation + # * If there is an associated context already, that context shall be disassociated + # * Handle and version information is generated by the provider + # * In order to avoid infinite growth of patient contexts, older contexts are allowed to be removed from the MDIB + # (=ContextAssociation=No) + # c) SetValue: Immediately answers with "finished" + # * Finished has to be sent as a report in addition to the response => + # d) SetString: Initiates a transaction that sends Wait, Start and Finished + # e) SetMetricStates: + # * Payload: 2 Metric States (settings; consider alert limits) + # * Immediately sends finished + # * Action: Alter values of metrics step = '6b' info = 'SetContextState' @@ -431,7 +588,7 @@ def on_description_modification(description_modification_report): patient_context_descriptors = mdib.descriptions.NODETYPE.get(pm.PatientContextDescriptor, []) generated_family_names = [] if len(patient_context_descriptors) == 0: - log_result(False, results, step, info, extra_info='no PatientContextDescriptor') + results_collector.log_result(False, step, info, extra_info='no PatientContextDescriptor') else: try: for i, p in enumerate(patient_context_descriptors): @@ -443,24 +600,24 @@ def on_description_modification(description_modification_report): time.sleep(1) # allow update notification to arrive patients = mdib.context_states.NODETYPE.get(pm_qnames.PatientContextState, []) if len(patients) == 0: - log_result(False, results, step, info, extra_info='no patients found') + results_collector.log_result(False, step, info, extra_info='no patients found') else: all_ok = True for patient in patients: if patient.CoreData.Familyname in generated_family_names: if patient.ContextAssociation != pm_types.ContextAssociation.ASSOCIATED: - log_result(False, results, step, info, - extra_info=f'new patient {patient.CoreData.Familyname} is {patient.ContextAssociation}') + results_collector.log_result(False, step, info, + extra_info=f'new patient {patient.CoreData.Familyname} is {patient.ContextAssociation}') all_ok = False else: if patient.ContextAssociation == pm_types.ContextAssociation.ASSOCIATED: - log_result(False, results, step, info, - extra_info=f'old patient {patient.CoreData.Familyname} is {patient.ContextAssociation}') + results_collector.log_result(False, step, info, + extra_info=f'old patient {patient.CoreData.Familyname} is {patient.ContextAssociation}') all_ok = False - log_result(all_ok, results, step, info) + results_collector.log_result(all_ok, step, info) except Exception as ex: print(traceback.format_exc()) - log_result(False, results, step, info, ex) + results_collector.log_result(False, step, info, ex) step = '6c' info = 'SetValue: Immediately answers with "finished"' @@ -469,34 +626,36 @@ def on_description_modification(description_modification_report): operation_invoked_subscriptions = [subscr for subscr in subscriptions if 'OperationInvokedReport' in subscr.short_filter_string] if len(operation_invoked_subscriptions) == 0: - log_result(False, results, step, info, 'OperationInvokedReport not subscribed, cannot test') + results_collector.log_result(False, step, info, 'OperationInvokedReport not subscribed, cannot test') elif len(operation_invoked_subscriptions) > 1: - log_result(False, results, step, info, - f'found {len(operation_invoked_subscriptions)} OperationInvokedReport subscribed, cannot test') + results_collector.log_result(False, step, info, + f'found {len(operation_invoked_subscriptions)} OperationInvokedReport subscribed, cannot test') else: try: operations = client.mdib.descriptions.NODETYPE.get(pm_qnames.SetValueOperationDescriptor, []) my_ops = [op for op in operations if op.Type.Code == "67108888"] if len(my_ops) != 1: - log_result(False, results, step, info, f'found {len(my_ops)} operations with code "67108888"') + results_collector.log_result(False, step, info, f'found {len(my_ops)} operations with code "67108888"') else: operation = my_ops[0] future_object = client.set_service_client.set_numeric_value(operation.Handle, Decimal(42)) operation_result = future_object.result() if len(operation_result.report_parts) == 0: - log_result(False, results, step, info, 'no notification') + results_collector.log_result(False, step, info, 'no notification') elif len(operation_result.report_parts) > 1: - log_result(False, results, step, info, f'got {len(operation_result.report_parts)} notifications, expect only one') + results_collector.log_result(False, step, info, + f'got {len(operation_result.report_parts)} notifications, expect only one') else: - log_result(True, results, step, info, f'got {len(operation_result.report_parts)} notifications') + results_collector.log_result(True, step, info, + f'got {len(operation_result.report_parts)} notifications') if operation_result.InvocationInfo.InvocationState != msg_types.InvocationState.FINISHED: - log_result(False, results, step, info, - f'got result {operation_result.InvocationInfo.InvocationState} ' - f'{operation_result.InvocationInfo.InvocationError} ' - f'{operation_result.InvocationInfo.InvocationErrorMessage}') + results_collector.log_result(False, step, info, + f'got result {operation_result.InvocationInfo.InvocationState} ' + f'{operation_result.InvocationInfo.InvocationError} ' + f'{operation_result.InvocationInfo.InvocationErrorMessage}') except Exception as ex: print(traceback.format_exc()) - log_result(False, results, step, info, ex) + results_collector.log_result(False, step, info, ex) step = '6d' info = 'SetString: Initiates a transaction that sends Wait, Start and Finished' @@ -505,32 +664,34 @@ def on_description_modification(description_modification_report): operations = client.mdib.descriptions.NODETYPE.get(pm_qnames.SetStringOperationDescriptor, []) my_ops = [op for op in operations if op.Type.Code == "67108889"] if len(my_ops) != 1: - log_result(False, results, step, info, f'found {len(my_ops)} operations with code "67108889"') + results_collector.log_result(False, step, info, f'found {len(my_ops)} operations with code "67108889"') else: operation = my_ops[0] future_object = client.set_service_client.set_string(operation.Handle, 'STANDBY') operation_result = future_object.result() - if len(operation_result.report_parts) == 0: - log_result(False, results, step, info, 'no notification') + if len(operation_result.report_parts) < 3: + results_collector.log_result(False, step, info, + f'only {len(operation_result.report_parts)} notification(s)') elif len(operation_result.report_parts) >= 3: # check order of operation invoked reports (simple expectation, there could be multiple WAIT in theory) expectation = [msg_types.InvocationState.WAIT, - msg_types.InvocationState.START, - msg_types.InvocationState.FINISHED] + msg_types.InvocationState.START, + msg_types.InvocationState.FINISHED] inv_states = [p.InvocationInfo.InvocationState for p in operation_result.report_parts] if inv_states != expectation: - log_result(False, results, step, info, f'wrong order {inv_states}') + results_collector.log_result(False, step, info, f'wrong order {inv_states}') else: - log_result(True, results, step, info, f'got {len(operation_result.report_parts)} notifications') + results_collector.log_result(True, step, info, + f'got {len(operation_result.report_parts)} notifications') if operation_result.InvocationInfo.InvocationState != msg_types.InvocationState.FINISHED: - log_result(False, results, step, info, - f'got result {operation_result.InvocationInfo.InvocationState} ' - f'{operation_result.InvocationInfo.InvocationError} ' - f'{operation_result.InvocationInfo.InvocationErrorMessage}') + results_collector.log_result(False, step, info, + f'got result {operation_result.InvocationInfo.InvocationState} ' + f'{operation_result.InvocationInfo.InvocationError} ' + f'{operation_result.InvocationInfo.InvocationErrorMessage}') except Exception as ex: print(traceback.format_exc()) - log_result(False, results, step, info, ex) + results_collector.log_result(False, step, info, ex) step = '6e' info = 'SetMetricStates Immediately answers with finished' @@ -539,7 +700,7 @@ def on_description_modification(description_modification_report): operations = client.mdib.descriptions.NODETYPE.get(pm_qnames.SetMetricStateOperationDescriptor, []) my_ops = [op for op in operations if op.Type.Code == "67108890"] if len(my_ops) != 1: - log_result(False, results, step, info, f'found {len(my_ops)} operations with code "67108890"') + results_collector.log_result(False, step, info, f'found {len(my_ops)} operations with code "67108890"') else: operation = my_ops[0] proposed_metric_state1 = client.mdib.xtra.mk_proposed_state("numeric_metric_0.channel_0.vmd_1.mds_0") @@ -550,31 +711,34 @@ def on_description_modification(description_modification_report): st.MetricValue.Value = Decimal(1) else: st.MetricValue.Value += Decimal(0.1) - future_object = client.set_service_client.set_metric_state(operation.Handle, [proposed_metric_state1, proposed_metric_state2]) + future_object = client.set_service_client.set_metric_state(operation.Handle, + [proposed_metric_state1, proposed_metric_state2]) operation_result = future_object.result() if len(operation_result.report_parts) == 0: - log_result(False, results, step, info, 'no notification') + results_collector.log_result(False, step, info, 'no notification') elif len(operation_result.report_parts) > 1: - log_result(False, results, step, info, f'got {len(operation_result.report_parts)} notifications, expect only one') + results_collector.log_result(False, step, info, + f'got {len(operation_result.report_parts)} notifications, expect only one') else: - log_result(True, results, step, info, f'got {len(operation_result.report_parts)} notifications') + results_collector.log_result(True, step, info, + f'got {len(operation_result.report_parts)} notifications') if operation_result.InvocationInfo.InvocationState != msg_types.InvocationState.FINISHED: - log_result(False, results, step, info, - f'got result {operation_result.InvocationInfo.InvocationState} ' - f'{operation_result.InvocationInfo.InvocationError} ' - f'{operation_result.InvocationInfo.InvocationErrorMessage}') + results_collector.log_result(False, step, info, + f'got result {operation_result.InvocationInfo.InvocationState} ' + f'{operation_result.InvocationInfo.InvocationError} ' + f'{operation_result.InvocationInfo.InvocationErrorMessage}') except Exception as ex: print(traceback.format_exc()) - log_result(False, results, step, info, ex) + results_collector.log_result(False, step, info, ex) step = '7' info = 'Graceful shutdown (at least subscriptions are ended; optionally Bye is sent)' try: success = client._subscription_mgr.unsubscribe_all() - log_result(success, results, step, info) + results_collector.log_result(success, step, info) except Exception as ex: print(traceback.format_exc()) - log_result(False, results, step, info, ex) + results_collector.log_result(False, step, info, ex) time.sleep(2) return results @@ -595,7 +759,10 @@ def on_description_modification(description_modification_report): logging_setup2 = json.load(f) logging.config.dictConfig(logging_setup2) - run_results = run_ref_test() - print('\n### Summary ###') - for r in run_results: - print(r) + results = ResultsCollector() + + run_ref_test(results) + results.print_summary() + if results.failed_count: + exit(1) + exit(0) diff --git a/examples/ReferenceTestV2/reference_provider_v2.py b/examples/ReferenceTestV2/reference_provider_v2.py index fef19088..f9ce8214 100644 --- a/examples/ReferenceTestV2/reference_provider_v2.py +++ b/examples/ReferenceTestV2/reference_provider_v2.py @@ -1,4 +1,17 @@ +"""Implementation of reference provider. + +The reference provider gets its parameters from environment variables: +- adapter_ip specifies which ip address shall be used +- ca_folder specifies where the communication certificates are located. +- ref_fac, ref_poc and ref_bed specify the location values facility, point of care and bed. +- ssl_passwd specifies an optional password for the certificates. + +If a value is not provided as environment variable, the default value (see code below) will be used. +""" + from __future__ import annotations + +import datetime import json import logging.config import os @@ -6,18 +19,17 @@ from decimal import Decimal from time import sleep from uuid import UUID -import datetime from sdc11073.certloader import mk_ssl_contexts_from_folder from sdc11073.location import SdcLocation from sdc11073.loghelper import LoggerAdapter from sdc11073.mdib import ProviderMdib, descriptorcontainers -from sdc11073.pysoap.soapclient_async import SoapClientAsync -from sdc11073.provider import components from sdc11073.provider import SdcProvider +from sdc11073.provider import components from sdc11073.provider.servicesfactory import DPWSHostedService from sdc11073.provider.servicesfactory import HostedServices, mk_dpws_hosts from sdc11073.provider.subscriptionmgr_async import SubscriptionsManagerReferenceParamAsync +from sdc11073.pysoap.soapclient_async import SoapClientAsync from sdc11073.roles.waveformprovider import waveforms from sdc11073.wsdiscovery import WSDiscovery from sdc11073.xml_types import pm_types, pm_qnames @@ -49,6 +61,57 @@ mds_handle = "mds_0" USE_REFERENCE_PARAMETERS = False +# some switches to enable/disable some of the provider data updates +# enabling allows to verify that the reference consumer detects missing updates + +# 4 State Reports +# a) The Reference Provider produces at least 5 numeric metric updates in 30 seconds +# b) The Reference Provider produces at least 5 string metric updates (StringMetric or EnumStringMetric) in 30 seconds +# c) The Reference Provider produces at least 5 alert condition updates (AlertCondition or LimitAlertCondition) in 30 seconds +# d) The Reference Provider produces at least 5 alert signal updates in 30 seconds +# e) The Reference Provider provides alert system self checks in accordance to the periodicity defined in the MDIB (at least every 10 seconds) +# f) The Reference Provider provides 3 waveforms (RealTimeSampleArrayMetric) x 10 messages per second x 100 samples per message +# g) The Reference Provider provides changes for the following components: +# * At least 5 Clock or Battery object updates in 30 seconds (Component report) +# * At least 5 MDS or VMD updates in 30 seconds (Component report) +# g) The Reference Provider provides changes for the following operational states: +# At least 5 Operation updates in 30 seconds; enable/disable operations; some different than the ones mentioned above (Operational State Report)""" +enable_4a = True +enable_4b = True +enable_4c = True +enable_4d = True +# switching 4e not implemented +enable_4f = True + +# 5 Description Modifications: +# a) The Reference Provider produces at least 1 update every 10 seconds comprising +# * Update Alert condition concept description of Type +# * Update Alert condition cause-remedy information +# * Update Unit of measure (metrics) +enable_5a1 = True +enable_5a2 = True +enable_5a3 = True + +# 6 Operation invocation +# a) (removed) +# b) SetContextState: +# * Payload: 1 Patient Context +# * Context state is added to the MDIB including context association and validation +# * If there is an associated context already, that context shall be disassociated +# * Handle and version information is generated by the provider +# * In order to avoid infinite growth of patient contexts, older contexts are allowed to be removed from the MDIB +# (=ContextAssociation=No) +# c) SetValue: Immediately answers with "finished" +# * Finished has to be sent as a report in addition to the response => +# d) SetString: Initiates a transaction that sends Wait, Start and Finished +# e) SetMetricStates: +# * Payload: 2 Metric States (settings; consider alert limits) +# * Immediately sends finished +# * Action: Alter values of metrics +enable_6c = True +enable_6d = True +enable_6e = True + def mk_all_services_except_localization(sdc_provider, components, subscription_managers) -> HostedServices: # register all services with their endpoint references acc. to structure in components @@ -107,11 +170,11 @@ def provide_realtime_data(sdc_provider): serial_number='12345') if ca_folder: ssl_contexts = mk_ssl_contexts_from_folder(ca_folder, - private_key='user_private_key_encrypted.pem', - certificate='user_certificate_root_signed.pem', - ca_public_key='root_certificate.pem', - cyphers_file=None, - ssl_passwd=ssl_passwd) + private_key='user_private_key_encrypted.pem', + certificate='user_certificate_root_signed.pem', + ca_public_key='root_certificate.pem', + cyphers_file=None, + ssl_passwd=ssl_passwd) else: ssl_contexts = None if USE_REFERENCE_PARAMETERS: @@ -136,20 +199,24 @@ def provide_realtime_data(sdc_provider): 'Set': [components.SetService], 'ContainmentTree': [components.ContainmentTreeService]}) sdc_provider = SdcProvider(wsd, dpwsModel, dpwsDevice, my_mdib, my_uuid, - ssl_context_container=ssl_contexts, - specific_components=specific_components, - max_subscription_duration=15 - ) + ssl_context_container=ssl_contexts, + specific_components=specific_components, + max_subscription_duration=15 + ) sdc_provider.start_all() # disable delayed processing for 2 operations - - sdc_provider.get_operation_by_handle('set_value_0.sco.mds_0').delayed_processing = False - sdc_provider.get_operation_by_handle('set_metric_0.sco.vmd_1.mds_0').delayed_processing = False + if enable_6c: + sdc_provider.get_operation_by_handle('set_value_0.sco.mds_0').delayed_processing = False + if not enable_6d: + sdc_provider.get_operation_by_handle('set_string_0.sco.mds_0').delayed_processing = False + if enable_6e: + sdc_provider.get_operation_by_handle('set_metric_0.sco.vmd_1.mds_0').delayed_processing = False validators = [pm_types.InstanceIdentifier('Validator', extension_string='System')] sdc_provider.set_location(loc, validators) - provide_realtime_data(sdc_provider) + if enable_4f: + provide_realtime_data(sdc_provider) pm = my_mdib.data_model.pm_names pm_types = my_mdib.data_model.pm_types patientDescriptorHandle = my_mdib.descriptions.NODETYPE.get(pm.PatientContextDescriptor)[0].Handle @@ -165,8 +232,8 @@ def provide_realtime_data(sdc_provider): identifiers = [] patientContainer.Identification = identifiers - descs = list(sdc_provider.mdib.descriptions.objects) - descs.sort(key=lambda x: x.Handle) + all_descriptors = list(sdc_provider.mdib.descriptions.objects) + all_descriptors.sort(key=lambda x: x.Handle) numeric_metric = None string_metric = None alertCondition = None @@ -175,21 +242,23 @@ def provide_realtime_data(sdc_provider): activateOperation = None stringOperation = None valueOperation = None - for oneContainer in descs: - if oneContainer.Handle == numeric_metric_handle: - numeric_metric = oneContainer - if oneContainer.Handle == string_metric_handle: - string_metric = oneContainer - if oneContainer.Handle == alert_condition_handle: - alertCondition = oneContainer - if oneContainer.Handle == alert_signal_handle: - alertSignal = oneContainer - if oneContainer.Handle == battery_handle: - battery_descriptor = oneContainer - if oneContainer.Handle == set_value_handle: - valueOperation = oneContainer - if oneContainer.Handle == set_string_handle: - stringOperation = oneContainer + + # search for descriptors of specific types + for one_descriptor in all_descriptors: + if one_descriptor.Handle == numeric_metric_handle: + numeric_metric = one_descriptor + if one_descriptor.Handle == string_metric_handle: + string_metric = one_descriptor + if one_descriptor.Handle == alert_condition_handle: + alertCondition = one_descriptor + if one_descriptor.Handle == alert_signal_handle: + alertSignal = one_descriptor + if one_descriptor.Handle == battery_handle: + battery_descriptor = one_descriptor + if one_descriptor.Handle == set_value_handle: + valueOperation = one_descriptor + if one_descriptor.Handle == set_string_handle: + stringOperation = one_descriptor with sdc_provider.mdib.metric_state_transaction() as mgr: state = mgr.get_state(valueOperation.OperationTarget) @@ -200,32 +269,37 @@ def provide_realtime_data(sdc_provider): state.mk_metric_value() print("Running forever, CTRL-C to exit") try: - num_current_value = 0 str_current_value = 0 while True: if numeric_metric: try: - with sdc_provider.mdib.metric_state_transaction() as mgr: - state = mgr.get_state(numeric_metric.Handle) - if not state.MetricValue: - state.mk_metric_value() - state.MetricValue.Value = Decimal(num_current_value) - num_current_value += 1 - with sdc_provider.mdib.descriptor_transaction() as mgr: - descriptor: descriptorcontainers.AbstractMetricDescriptorContainer = mgr.get_descriptor(numeric_metric.Handle) - descriptor.Unit.Code = 'code1' if descriptor.Unit.Code == 'code2' else 'code2' + if enable_4a: + with sdc_provider.mdib.metric_state_transaction() as mgr: + state = mgr.get_state(numeric_metric.Handle) + if not state.MetricValue: + state.mk_metric_value() + if state.MetricValue.Value is None: + state.MetricValue.Value = Decimal('0') + else: + state.MetricValue.Value += Decimal(1) + if enable_5a3: + with sdc_provider.mdib.descriptor_transaction() as mgr: + descriptor: descriptorcontainers.AbstractMetricDescriptorContainer = mgr.get_descriptor( + numeric_metric.Handle) + descriptor.Unit.Code = 'code1' if descriptor.Unit.Code == 'code2' else 'code2' except Exception as ex: print(traceback.format_exc()) else: print("Numeric Metric not found in MDIB!") if string_metric: try: - with sdc_provider.mdib.metric_state_transaction() as mgr: - state = mgr.get_state(string_metric.Handle) - if not state.MetricValue: - state.mk_metric_value() - state.MetricValue.Value = f'string {str_current_value}' - str_current_value += 1 + if enable_4b: + with sdc_provider.mdib.metric_state_transaction() as mgr: + state = mgr.get_state(string_metric.Handle) + if not state.MetricValue: + state.mk_metric_value() + state.MetricValue.Value = f'my string {str_current_value}' + str_current_value += 1 except Exception as ex: print(traceback.format_exc()) else: @@ -233,26 +307,30 @@ def provide_realtime_data(sdc_provider): if alertCondition: try: - with sdc_provider.mdib.alert_state_transaction() as mgr: - state = mgr.get_state(alertCondition.Handle) - state.Presence = not state.Presence + if enable_4c: + with sdc_provider.mdib.alert_state_transaction() as mgr: + state = mgr.get_state(alertCondition.Handle) + state.Presence = not state.Presence except Exception as ex: print(traceback.format_exc()) try: with sdc_provider.mdib.descriptor_transaction() as mgr: now = datetime.datetime.now() text = f'last changed at {now.hour:02d}:{now.minute:02d}:{now.second:02d}' - descriptor: descriptorcontainers.AlertConditionDescriptorContainer = mgr.get_descriptor(alertCondition.Handle) - if len(descriptor.Type.ConceptDescription) == 0: - descriptor.Type.ConceptDescription.append(pm_types.LocalizedText(text)) - else: - descriptor.Type.ConceptDescription[0].text = text - if len(descriptor.CauseInfo) == 0: - descriptor.CauseInfo.append(pm_types.CauseInfo()) - if len(descriptor.CauseInfo[0].RemedyInfo.Description) == 0: - descriptor.CauseInfo[0].RemedyInfo.Description.append(pm_types.LocalizedText(text)) - else: - descriptor.CauseInfo[0].RemedyInfo.Description[0].text = text + descriptor: descriptorcontainers.AlertConditionDescriptorContainer = mgr.get_descriptor( + alertCondition.Handle) + if enable_5a1: + if len(descriptor.Type.ConceptDescription) == 0: + descriptor.Type.ConceptDescription.append(pm_types.LocalizedText(text)) + else: + descriptor.Type.ConceptDescription[0].text = text + if enable_5a2: + if len(descriptor.CauseInfo) == 0: + descriptor.CauseInfo.append(pm_types.CauseInfo()) + if len(descriptor.CauseInfo[0].RemedyInfo.Description) == 0: + descriptor.CauseInfo[0].RemedyInfo.Description.append(pm_types.LocalizedText(text)) + else: + descriptor.CauseInfo[0].RemedyInfo.Description[0].text = text except Exception as ex: print(traceback.format_exc()) @@ -261,12 +339,13 @@ def provide_realtime_data(sdc_provider): if alertSignal: try: - with sdc_provider.mdib.alert_state_transaction() as mgr: - state = mgr.get_state(alertSignal.Handle) - if state.Slot is None: - state.Slot = 1 - else: - state.Slot += 1 + if enable_4d: + with sdc_provider.mdib.alert_state_transaction() as mgr: + state = mgr.get_state(alertSignal.Handle) + if state.Slot is None: + state.Slot = 1 + else: + state.Slot += 1 except Exception as ex: print(traceback.format_exc()) else: @@ -311,7 +390,8 @@ def provide_realtime_data(sdc_provider): if vmd_descriptor is None: vmd = descriptorcontainers.VmdDescriptorContainer(add_rm_vmd_handle, add_rm_mds_handle) channel = descriptorcontainers.ChannelDescriptorContainer(add_rm_channel_handle, add_rm_vmd_handle) - metric = descriptorcontainers.StringMetricDescriptorContainer(add_rm_metric_handle, add_rm_channel_handle) + metric = descriptorcontainers.StringMetricDescriptorContainer(add_rm_metric_handle, + add_rm_channel_handle) metric.Unit = pm_types.CodedValue('123') with sdc_provider.mdib.descriptor_transaction() as mgr: mgr.add_descriptor(vmd) diff --git a/pyproject.toml b/pyproject.toml index 6eed469f..2ea9df14 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ authors = [ ] description = "Pure python implementation of IEEE11073 SDC protocol" readme = "README.rst" -requires-python = ">=3.9, <3.12" +requires-python = ">=3.9, <3.13" classifiers = [ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', @@ -20,6 +20,7 @@ classifiers = [ 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', 'Operating System :: Microsoft :: Windows', 'Operating System :: POSIX :: Linux' ] @@ -54,7 +55,7 @@ mypy = [ "types-lxml", ] dev = [ - "ruff", + "ruff>=0.2.0", "sdc11073[mypy]", "sdc11073[test]", ] @@ -97,6 +98,14 @@ log_file_format = "%(asctime)s %(levelname)s (%(threadName)-10s) %(filename)s:%( log_file_date_format = "%Y-%m-%d %H:%M:%S:%f" [tool.ruff] +line-length = 120 # https://beta.ruff.rs/docs/settings/#line-length +target-version = "py39" # https://beta.ruff.rs/docs/settings/#target-version +# Allow imports relative to the "src" and "test" directories. +src = ["src", "test"] # https://beta.ruff.rs/docs/settings/#src +# In addition to the standard set of exclusions, omit all tutorials and examples +extend-exclude = ["examples", "tools", "tutorial"] # https://beta.ruff.rs/docs/settings/#extend-exclude + +[tool.ruff.lint] extend-select = [# https://beta.ruff.rs/docs/settings/#extend-select "A", # https://beta.ruff.rs/docs/rules/#flake8-builtins-a "ANN", # https://beta.ruff.rs/docs/rules/#flake8-annotations-ann @@ -150,26 +159,19 @@ extend-ignore = [# https://beta.ruff.rs/docs/settings/#extend-ignore "S311", # https://beta.ruff.rs/docs/rules/suspicious-non-cryptographic-random-usage/ "SIM102", # collapsible-if "T201", # https://beta.ruff.rs/docs/rules/print/ - ] -line-length = 120 # https://beta.ruff.rs/docs/settings/#line-length -target-version = "py39" # https://beta.ruff.rs/docs/settings/#target-version -# Allow imports relative to the "src" and "test" directories. -src = ["src", "test"] # https://beta.ruff.rs/docs/settings/#src -# In addition to the standard set of exclusions, omit all tutorials and examples -extend-exclude = ["examples", "tools", "tutorial"] # https://beta.ruff.rs/docs/settings/#extend-exclude -[tool.ruff.flake8-annotations] +[tool.ruff.lint.flake8-annotations] allow-star-arg-any = true # https://beta.ruff.rs/docs/settings/#allow-star-arg-any suppress-none-returning = true # https://beta.ruff.rs/docs/settings/#suppress-none-returning -[tool.ruff.flake8-comprehensions] +[tool.ruff.lint.flake8-comprehensions] allow-dict-calls-with-keyword-arguments = true # https://beta.ruff.rs/docs/settings/#allow-dict-calls-with-keyword-arguments -[tool.ruff.pycodestyle] +[tool.ruff.lint.pycodestyle] max-doc-length = 120 # https://beta.ruff.rs/docs/settings/#max-doc-length -[tool.ruff.per-file-ignores] +[tool.ruff.lint.per-file-ignores] "__init__.py" = ["D104"] [tool.mypy] diff --git a/src/sdc11073/commlog.py b/src/sdc11073/commlog.py index 08d49a58..af4bf16e 100644 --- a/src/sdc11073/commlog.py +++ b/src/sdc11073/commlog.py @@ -8,7 +8,6 @@ import time from typing import Any, Callable -MULTICAST_OUT = 'sdc_comm.multicast.out' DISCOVERY_IN = 'sdc_comm.discovery.in' DISCOVERY_OUT = 'sdc_comm.discovery.out' SOAP_REQUEST_IN = 'sdc_comm.soap.request.in' @@ -18,7 +17,7 @@ SOAP_SUBSCRIPTION_IN = 'sdc_comm.soap.subscription.in' WSDL = 'sdc_comm.wsdl' -LOGGER_NAMES = (MULTICAST_OUT, DISCOVERY_IN, DISCOVERY_OUT, SOAP_REQUEST_IN, SOAP_REQUEST_OUT, +LOGGER_NAMES = (DISCOVERY_IN, DISCOVERY_OUT, SOAP_REQUEST_IN, SOAP_REQUEST_OUT, SOAP_RESPONSE_IN, SOAP_RESPONSE_OUT, SOAP_SUBSCRIPTION_IN, WSDL) @@ -91,8 +90,6 @@ def __init__(self, log_folder: str | pathlib.Path, log_out: bool = False, log_in }) if log_out: self.handlers.update({ - MULTICAST_OUT: self._GenericHandler( - functools.partial(self._write_log, self.T_UDP_MULTICAST, self.D_OUT)), DISCOVERY_OUT: self._GenericHandler(functools.partial(self._write_log, self.T_UDP, self.D_OUT)), SOAP_REQUEST_OUT: self._GenericHandler(functools.partial(self._write_log, self.T_HTTP_REQ, self.D_OUT)), SOAP_RESPONSE_OUT: self._GenericHandler( diff --git a/src/sdc11073/consumer/components.py b/src/sdc11073/consumer/components.py index 0593d74a..fffd4ce7 100644 --- a/src/sdc11073/consumer/components.py +++ b/src/sdc11073/consumer/components.py @@ -1,6 +1,6 @@ from __future__ import annotations -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import TYPE_CHECKING from sdc11073.pysoap.msgfactory import MessageFactory @@ -22,6 +22,7 @@ if TYPE_CHECKING: from sdc11073.pysoap.soapclient import SoapClientProtocol from sdc11073.dispatch.dispatchkey import RequestDispatcherProtocol + from sdc11073.namespaces import PrefixNamespace @dataclass() @@ -35,6 +36,7 @@ class SdcConsumerComponents: subscription_manager_class: type[ConsumerSubscriptionManagerProtocol] = None operations_manager_class: type[OperationsManagerProtocol] | None = None service_handlers: list = None + additional_schema_specs: list[PrefixNamespace] = field(default_factory=list) def merge(self, other: SdcConsumerComponents): """Add data from other to self.""" @@ -53,6 +55,7 @@ def _merge(attr_name: str): for handler in other.service_handlers: if handler not in self.service_handlers: self.service_handlers.append(handler) + self.additional_schema_specs = list(set(self.additional_schema_specs).union(set(other.additional_schema_specs))) default_sdc_consumer_components = SdcConsumerComponents( diff --git a/src/sdc11073/consumer/consumerimpl.py b/src/sdc11073/consumer/consumerimpl.py index b8103a97..c53fd406 100644 --- a/src/sdc11073/consumer/consumerimpl.py +++ b/src/sdc11073/consumer/consumerimpl.py @@ -271,19 +271,19 @@ def __init__(self, device_location: str, # noqa: PLR0913 self.peer_certificate = None self.binary_peer_certificate = None self.all_subscribed = False - # look for schemas added by services - additional_schema_specs = [] + # look for schemas added by services and components spec + additional_schema_specs = set(self._components.additional_schema_specs) for handler_cls in self._components.service_handlers: - additional_schema_specs.extend(handler_cls.additional_namespaces) + additional_schema_specs.update(handler_cls.additional_namespaces) msg_reader_cls = self._components.msg_reader_class self.msg_reader = msg_reader_cls(self.sdc_definitions, - additional_schema_specs, + list(additional_schema_specs), self._logger, validate=validate) msg_factory_cls = self._components.msg_factory_class self.msg_factory = msg_factory_cls(self.sdc_definitions, - additional_schema_specs, + list(additional_schema_specs), self._logger, validate=validate) diff --git a/src/sdc11073/consumer/serviceclients/setservice.py b/src/sdc11073/consumer/serviceclients/setservice.py index 60cc835c..44aab828 100644 --- a/src/sdc11073/consumer/serviceclients/setservice.py +++ b/src/sdc11073/consumer/serviceclients/setservice.py @@ -1,5 +1,6 @@ from __future__ import annotations +from decimal import Decimal from typing import TYPE_CHECKING from sdc11073.dispatch import DispatchKey @@ -12,7 +13,6 @@ if TYPE_CHECKING: from concurrent.futures import Future - from decimal import Decimal from sdc11073.consumer.manipulator import RequestManipulatorProtocol from sdc11073.mdib.statecontainers import AbstractStateProtocol @@ -40,7 +40,11 @@ def set_numeric_value(self, operation_handle: str, operation_handle, requested_numeric_value) request = data_model.msg_types.SetValue() request.OperationHandleRef = operation_handle - request.RequestedNumericValue = requested_numeric_value + if isinstance(requested_numeric_value, float): + # convert to string first in order to limit possible excessive number of digits + request.RequestedNumericValue = Decimal(str(requested_numeric_value)) + else: + request.RequestedNumericValue = Decimal(requested_numeric_value) inf = HeaderInformationBlock(action=request.action, addr_to=self.endpoint_reference.Address) message = self._msg_factory.mk_soap_message(inf, payload=request) return self._call_operation(message, request_manipulator=request_manipulator) diff --git a/src/sdc11073/mdib/consumermdib.py b/src/sdc11073/mdib/consumermdib.py index 8bfa0f3f..518bf9db 100644 --- a/src/sdc11073/mdib/consumermdib.py +++ b/src/sdc11073/mdib/consumermdib.py @@ -159,7 +159,6 @@ def __init__(self, self.rt_buffers = {} # key is a handle, value is a ConsumerRtBuffer self._max_realtime_samples = max_realtime_samples self._last_wf_age_log = time.time() - self._context_mdib_version = None # a buffer for notifications that are received before initial get_mdib is done self._buffered_notifications = [] self._buffered_notifications_lock = Lock() @@ -273,10 +272,6 @@ def _get_context_states(self, handles: list[str] | None = None): response = context_service.get_context_states(handles) context_state_containers = response.result.ContextState - self._context_mdib_version = response.mdib_version - self._logger.debug('_get_context_states: setting _context_mdib_version to {}', # noqa: PLE1205 - self._context_mdib_version) - self._logger.debug('got {} context states', len(context_state_containers)) # noqa: PLE1205 with self.context_states.lock: for state_container in context_state_containers: diff --git a/src/sdc11073/mdib/containerbase.py b/src/sdc11073/mdib/containerbase.py index 60a33e80..fcbe17be 100644 --- a/src/sdc11073/mdib/containerbase.py +++ b/src/sdc11073/mdib/containerbase.py @@ -3,7 +3,7 @@ import copy import inspect from typing import Any - +import math from lxml.etree import Element, SubElement, QName from sdc11073 import observableproperties as properties @@ -114,14 +114,17 @@ def sorted_container_properties(self) -> list: ret.append((name, obj)) return ret - def diff(self, other: ContainerBase, ignore_property_names: list[str] | None = None) -> None | list[str]: + def diff(self, other: ContainerBase, + ignore_property_names: list[str] | None = None, + max_float_diff = 1e-15) -> None | list[str]: """Compare all properties (except to be ignored ones). :param other: the object to compare with :param ignore_property_names: list of properties that shall be excluded from diff calculation + :param max_float_diff: parameter for math.isclose() if float values are incorporated. + 1e-15 corresponds to 15 digits max. accuracy (see sys.float_info.dig) :return: textual representation of differences or None if equal """ - max_float_diff = 1e-6 # if difference is less or equal, two floats are considered equal ret = [] ignore_list = ignore_property_names or [] my_properties = self.sorted_container_properties() @@ -135,13 +138,8 @@ def diff(self, other: ContainerBase, ignore_property_names: list[str] | None = N ret.append(f'{name}={my_value}, other does not have this attribute') else: if isinstance(my_value, float) or isinstance(other_value, float): - # cast both to float, if one is a Decimal Exception might be thrown - try: - if abs((float(my_value) - float(other_value)) / float(my_value)) > max_float_diff: - ret.append(f'{name}={my_value}, other={other_value}') - except ZeroDivisionError: - if abs(float(my_value) - float(other_value)) > max_float_diff: - ret.append(f'{name}={my_value}, other={other_value}') + if not math.isclose(my_value, other_value, rel_tol=max_float_diff, abs_tol=max_float_diff): + ret.append(f'{name}={my_value}, other={other_value}') elif my_value != other_value: ret.append(f'{name}={my_value}, other={other_value}') # check also if other has a different list of properties diff --git a/src/sdc11073/mdib/descriptorcontainers.py b/src/sdc11073/mdib/descriptorcontainers.py index 7be0a519..4858fecc 100644 --- a/src/sdc11073/mdib/descriptorcontainers.py +++ b/src/sdc11073/mdib/descriptorcontainers.py @@ -178,6 +178,7 @@ def update_from_other_container(self, other: AbstractDescriptorContainer, f'Update from a container with different handle is not possible! ' f'Have "{self.Handle}", got "{other.Handle}"') self._update_from_other(other, skipped_properties) + self.node = other.node def get_actual_value(self, attr_name: str) -> Any: """Ignores default value and implied value, e.g. returns None if value is not present in xml.""" diff --git a/src/sdc11073/mdib/mdibbase.py b/src/sdc11073/mdib/mdibbase.py index 7e6bf0fe..1d2af76e 100644 --- a/src/sdc11073/mdib/mdibbase.py +++ b/src/sdc11073/mdib/mdibbase.py @@ -141,15 +141,6 @@ def remove_objects_no_lock(self, objects: list[AbstractDescriptorContainer]): """Remove objects from table without locking.""" apply_map(self.remove_object_no_lock, objects) - def replace_object_no_lock(self, new_obj: AbstractDescriptorContainer): - """Remove existing descriptor_container and add new one, but do not touch child list of parent. - - This keeps order. - """ - orig_obj = self.handle.get_one(new_obj.Handle) - self.remove_object_no_lock(orig_obj) - self.add_object_no_lock(new_obj) - class StatesLookup(_MultikeyWithVersionLookup): """StatesLookup is the table-like storage for states. @@ -400,6 +391,8 @@ def _reconstruct_mdib(self, add_context_states: bool) -> xml_utils.LxmlElement: mdib_node = etree_.Element(msg.Mdib, nsmap=doc_nsmap) mdib_node.set('MdibVersion', str(self.mdib_version)) mdib_node.set('SequenceId', self.sequence_id) + if self.instance_id is not None: + mdib_node.set('InstanceId', str(self.instance_id)) md_description_node = self._reconstruct_md_description() mdib_node.append(md_description_node) diff --git a/src/sdc11073/mdib/providermdib.py b/src/sdc11073/mdib/providermdib.py index b72cadef..2ebf30fa 100644 --- a/src/sdc11073/mdib/providermdib.py +++ b/src/sdc11073/mdib/providermdib.py @@ -25,6 +25,7 @@ ContextStateTransactionManagerProtocol, DescriptorTransactionManagerProtocol, StateTransactionManagerProtocol, + TransactionResultProtocol ) TransactionFactory = Callable[[mdibbase.MdibBase, TransactionType, LoggerAdapter], @@ -38,7 +39,7 @@ class ProviderMdib(mdibbase.MdibBase): Transactions keep track of changes and initiate sending of update notifications to clients. """ - transaction = ObservableProperty(fire_only_on_changed_value=False) + transaction: TransactionResultProtocol | None = ObservableProperty(fire_only_on_changed_value=False) rt_updates = ObservableProperty(fire_only_on_changed_value=False) # different observable for performance def __init__(self, @@ -96,9 +97,32 @@ def _transaction_manager(self, if self.current_transaction.error: self._logger.info('transaction_manager: transaction without updates!') else: - processor = self.current_transaction.process_transaction(set_determination_time) - self.transaction = processor # update observable - self.current_transaction.mdib_version = self.mdib_version + # update observables + transaction_result = self.current_transaction.process_transaction(set_determination_time) + self.transaction = transaction_result + + if transaction_result.alert_updates: + self.alert_by_handle = {st.DescriptorHandle: st for st in transaction_result.alert_updates} + if transaction_result.comp_updates: + self.component_by_handle = {st.DescriptorHandle: st for st in transaction_result.comp_updates} + if transaction_result.ctxt_updates: + self.context_by_handle = {st.Handle: st for st in transaction_result.ctxt_updates} + if transaction_result.descr_created: + self.new_descriptors_by_handle = {descr.Handle: descr for descr + in transaction_result.descr_created} + if transaction_result.descr_deleted: + self.deleted_descriptors_by_handle = {descr.Handle: descr for descr + in transaction_result.descr_deleted} + if transaction_result.descr_updated: + self.updated_descriptors_by_handle = {descr.Handle: descr for descr + in transaction_result.descr_updated} + if transaction_result.metric_updates: + self.metrics_by_handle = {st.DescriptorHandle: st for st in transaction_result.metric_updates} + if transaction_result.op_updates: + self.operation_by_handle = {st.DescriptorHandle: st for st in transaction_result.op_updates} + if transaction_result.rt_updates: + self.waveform_by_handle = {st.DescriptorHandle: st for st in transaction_result.rt_updates} + if callable(self.post_commit_handler): self.post_commit_handler(self, self.current_transaction) diff --git a/src/sdc11073/mdib/providermdibxtra.py b/src/sdc11073/mdib/providermdibxtra.py index 968b7ff8..298c8b23 100644 --- a/src/sdc11073/mdib/providermdibxtra.py +++ b/src/sdc11073/mdib/providermdibxtra.py @@ -27,61 +27,74 @@ class ProviderMdibMethods: def __init__(self, provider_mdib: ProviderMdib): self._mdib = provider_mdib self.descriptor_factory = DescriptorFactory(provider_mdib) - self.default_instance_identifiers = (provider_mdib.data_model.pm_types.InstanceIdentifier( + self.default_validators = (provider_mdib.data_model.pm_types.InstanceIdentifier( root='rootWithNoMeaning', extension_string='System'),) def ensure_location_context_descriptor(self): """Create a LocationContextDescriptor if there is none in mdib.""" mdib = self._mdib pm = mdib.data_model.pm_names - location_context_container = mdib.descriptions.NODETYPE.get_one(pm.LocationContextDescriptor, allow_none=True) - if location_context_container is None: - system_context_container = mdib.descriptions.NODETYPE.get_one(pm.SystemContextDescriptor) - descr_cls = mdib.data_model.get_descriptor_container_class(pm.LocationContextDescriptor) - descr_container = descr_cls(handle=uuid.uuid4().hex, parent_handle=system_context_container.Handle) - descr_container.SafetyClassification = mdib.data_model.pm_types.SafetyClassification.INF - mdib.descriptions.add_object(descr_container) + system_context_descriptors = mdib.descriptions.NODETYPE.get(pm.SystemContextDescriptor, []) + location_context_descriptors = mdib.descriptions.NODETYPE.get(pm.LocationContextDescriptor, []) + + for system_context_descriptor in system_context_descriptors: + child_location_descriptors = [d for d in location_context_descriptors + if d.parent_handle == system_context_descriptor.Handle + and d.NODETYPE == pm.LocationContextDescriptor] + if not child_location_descriptors: + descr_cls = mdib.data_model.get_descriptor_container_class(pm.LocationContextDescriptor) + descr_container = descr_cls(handle=uuid.uuid4().hex, parent_handle=system_context_descriptor.Handle) + descr_container.SafetyClassification = mdib.data_model.pm_types.SafetyClassification.INF + mdib.descriptions.add_object(descr_container) def ensure_patient_context_descriptor(self): - """Create PatientContextDescriptor if there is none in mdib.""" + """Create a PatientContextDescriptor if there is none in mdib.""" mdib = self._mdib pm = mdib.data_model.pm_names - patient_context_container = mdib.descriptions.NODETYPE.get_one(pm.PatientContextDescriptor, allow_none=True) - if patient_context_container is None: - system_context_container = mdib.descriptions.NODETYPE.get_one(pm.SystemContextDescriptor) - descr_cls = mdib.data_model.get_descriptor_container_class(pm.PatientContextDescriptor) - descr_container = descr_cls(handle=uuid.uuid4().hex, parent_handle=system_context_container.Handle) - descr_container.SafetyClassification = mdib.data_model.pm_types.SafetyClassification.INF - mdib.descriptions.add_object(descr_container) + system_context_descriptors = mdib.descriptions.NODETYPE.get(pm.SystemContextDescriptor, []) + patient_context_descriptors = mdib.descriptions.NODETYPE.get(pm.PatientContextDescriptor, []) + + for system_context_descriptor in system_context_descriptors: + child_location_descriptors = [d for d in patient_context_descriptors + if d.parent_handle == system_context_descriptor.Handle + and d.NODETYPE == pm.PatientContextDescriptor] + if not child_location_descriptors: + descr_cls = mdib.data_model.get_descriptor_container_class(pm.PatientContextDescriptor) + descr_container = descr_cls(handle=uuid.uuid4().hex, parent_handle=system_context_descriptor.Handle) + descr_container.SafetyClassification = mdib.data_model.pm_types.SafetyClassification.INF + mdib.descriptions.add_object(descr_container) def set_location(self, sdc_location: SdcLocation, validators: list[InstanceIdentifier] | None = None, - set_associated: bool = True): - """Create a location context state. + location_context_descriptor_handle: str | None = None): + """Create a location context state. The new state will be the associated state. This method updates only the mdib data! Use the SdcProvider.set_location method if you want to publish the address on the network. :param sdc_location: a sdc11073.location.SdcLocation instance - :param validators: a list of pysdc.pmtypes.InstanceIdentifier objects or None - :param set_associated: if True, BindingTime, BindingMdibVersion and ContextAssociation are set + :param validators: a list of InstanceIdentifier objects or None + If None, self.default_validators is used. + :param location_context_descriptor_handle: Only needed if the mdib contains more than one + LocationContextDescriptor. Then this defines the descriptor for which a new LocationContextState + shall be created. """ mdib = self._mdib pm = mdib.data_model.pm_names - with mdib.context_state_transaction() as mgr: - all_location_contexts = mdib.context_states.NODETYPE.get(pm.LocationContextState, []) - # set all to currently associated Locations to Disassociated - associated_locations = [loc for loc in all_location_contexts if - loc.ContextAssociation == mdib.data_model.pm_types.ContextAssociation.ASSOCIATED] - for location in associated_locations: - location_context = mgr.get_context_state(location.Handle) - location_context.ContextAssociation = mdib.data_model.pm_types.ContextAssociation.DISASSOCIATED - # UnbindingMdibVersion is the first version in which it is no longer bound ( == this version) - location_context.UnbindingMdibVersion = mdib.mdib_version + + if location_context_descriptor_handle is None: + # assume there is only one descriptor in mdib, user has not provided a handle. descriptor_container = mdib.descriptions.NODETYPE.get_one(pm.LocationContextDescriptor) + else: + descriptor_container = mdib.descriptions.handle.get_one(location_context_descriptor_handle) + + with mdib.context_state_transaction() as mgr: + mgr.disassociate_all(descriptor_container.Handle) - new_location = mgr.mk_context_state(descriptor_container.Handle, set_associated=set_associated) + new_location = mgr.mk_context_state(descriptor_container.Handle, set_associated=True) new_location.update_from_sdc_location(sdc_location) - if validators is not None: + if validators is None: + new_location.Validator = self.default_validators + else: new_location.Validator = validators def mk_state_containers_for_all_descriptors(self): diff --git a/src/sdc11073/mdib/transactions.py b/src/sdc11073/mdib/transactions.py index 9cf05a09..b14ecdc5 100644 --- a/src/sdc11073/mdib/transactions.py +++ b/src/sdc11073/mdib/transactions.py @@ -19,7 +19,7 @@ from .descriptorcontainers import AbstractDescriptorProtocol from .providermdib import ProviderMdib - from .statecontainers import AbstractStateContainer, AbstractStateProtocol + from .statecontainers import AbstractStateProtocol class _TransactionBase: @@ -27,6 +27,8 @@ def __init__(self, device_mdib_container: ProviderMdib, logger: LoggerAdapter): self._mdib = device_mdib_container + # provide the new mdib version that the commit of this transaction will create + self.new_mdib_version = device_mdib_container.mdib_version + 1 self._logger = logger self.descriptor_updates: dict[str, TransactionItem] = {} self.metric_state_updates: dict[str, TransactionItem] = {} @@ -185,9 +187,12 @@ def add_state(self, state_container: AbstractStateProtocol, adjust_state_version # set reference to descriptor state_container.descriptor_container = self.descriptor_updates[state_container.DescriptorHandle].new - + state_container.DescriptorVersion = state_container.descriptor_container.DescriptorVersion if adjust_state_version: - self._mdib.states.set_version(state_container) + if state_container.is_context_state: + self._mdib.context_states.set_version(state_container) + else: + self._mdib.states.set_version(state_container) updates_dict[key] = TransactionItem(None, state_container) def process_transaction(self, set_determination_time: bool) -> TransactionResultProtocol: # noqa: ARG002 @@ -198,27 +203,27 @@ def process_transaction(self, set_determination_time: bool) -> TransactionResult """ proc = TransactionResult() if self.descriptor_updates: - self._mdib.mdib_version += 1 + self._mdib.mdib_version = self.new_mdib_version # need to know all to be deleted and to be created descriptors to_be_deleted_handles = [tr_item.old.Handle for tr_item in self.descriptor_updates.values() if tr_item.new is None and tr_item.old is not None] to_be_created_handles = [tr_item.new.Handle for tr_item in self.descriptor_updates.values() if tr_item.old is None and tr_item.new is not None] + # Remark 1: # handling only updated states here: If a descriptor is created, it can be assumed that the # application also creates the state in a transaction. # The state will then be transported via that notification report. # Maybe this needs to be reworked, but at the time of this writing it seems fine. + # + # Remark 2: + # DescriptionModificationReport also contains the states that are related to the descriptors. + # => if there is one, update its DescriptorVersion and add it to list of states that shall be sent + # (Assuming that context descriptors (patient, location) are never changed, + # additional check for states in self.context_states is not needed. + # If this assumption is wrong, that functionality must be added!) + for tr_item in self.descriptor_updates.values(): orig_descriptor, new_descriptor = tr_item.old, tr_item.new - if new_descriptor is not None: - # DescriptionModificationReport also contains the states that are related to the descriptors. - # => if there is one, update its DescriptorVersion and add it to list of states that shall be sent - # (Assuming that context descriptors (patient, location) are never changed, - # additional check for states in self.context_states is not needed. - # If this assumption is wrong, that functionality must be added!) - self._update_corresponding_state(new_descriptor) - else: # descriptor delete - self._remove_corresponding_state(orig_descriptor) if orig_descriptor is None: # this is a create operation self._logger.debug( # noqa: PLE1205 @@ -231,6 +236,7 @@ def process_transaction(self, set_determination_time: bool) -> TransactionResult and new_descriptor.parent_handle not in to_be_created_handles: # only update parent if it is not also created in this transaction self._increment_parent_descriptor_version(proc, new_descriptor) + self._update_corresponding_state(new_descriptor) elif new_descriptor is None: # this is a delete operation self._logger.debug( # noqa: PLE1205 @@ -250,8 +256,9 @@ def process_transaction(self, set_determination_time: bool) -> TransactionResult self._logger.debug( # noqa: PLE1205 'transaction_manager: update descriptor Handle={}, DescriptorVersion={}', new_descriptor.Handle, new_descriptor.DescriptorVersion) - self._mdib.descriptions.replace_object_no_lock(new_descriptor) - + orig_descriptor.update_from_other_container(new_descriptor) + self._update_corresponding_state(orig_descriptor) + self._mdib.descriptions.update_object_no_lock(orig_descriptor) for updates_dict, dest_list in ((self.alert_state_updates, proc.alert_updates), (self.metric_state_updates, proc.metric_updates), (self.context_state_updates, proc.ctxt_updates), @@ -301,14 +308,6 @@ def _update_corresponding_state(self, descriptor_container: AbstractDescriptorPr new_state.increment_state_version() updates_dict[descriptor_container.Handle] = TransactionItem(old_state, new_state) - def _remove_corresponding_state(self, descriptor_container: AbstractDescriptorProtocol): - if descriptor_container.is_context_descriptor: - for state in self._mdib.context_states.descriptor_handle.get(descriptor_container.Handle, [])[:]: - self._mdib.context_states.remove_object_no_lock(state) - else: - state = self._mdib.states.descriptor_handle.get_one(descriptor_container.Handle, allow_none=True) - self._mdib.states.remove_object_no_lock(state) - def _increment_parent_descriptor_version(self, proc: TransactionResult, descriptor_container: AbstractDescriptorProtocol): parent_descriptor_container = self._mdib.descriptions.handle.get_one( @@ -405,7 +404,7 @@ def process_transaction(self, set_determination_time: bool) -> TransactionResult new_state.DeterminationTime = time.time() proc = TransactionResult() if self._state_updates: - self._mdib.mdib_version += 1 + self._mdib.mdib_version = self.new_mdib_version updates = self._handle_state_updates(self._state_updates) proc.alert_updates.extend(updates) return proc @@ -432,7 +431,7 @@ def process_transaction(self, set_determination_time: bool) -> TransactionResult tr_item.new.MetricValue.DeterminationTime = time.time() proc = TransactionResult() if self._state_updates: - self._mdib.mdib_version += 1 + self._mdib.mdib_version = self.new_mdib_version updates = self._handle_state_updates(self._state_updates) proc.metric_updates.extend(updates) return proc @@ -455,7 +454,7 @@ def process_transaction(self, set_determination_time: bool) -> TransactionResult """Process transaction and create a TransactionResult.""" proc = TransactionResult() if self._state_updates: - self._mdib.mdib_version += 1 + self._mdib.mdib_version = self.new_mdib_version updates = self._handle_state_updates(self._state_updates) proc.comp_updates.extend(updates) return proc @@ -478,7 +477,7 @@ def process_transaction(self, set_determination_time: bool) -> TransactionResult """Process transaction and create a TransactionResult.""" proc = TransactionResult() if self._state_updates: - self._mdib.mdib_version += 1 + self._mdib.mdib_version = self.new_mdib_version updates = self._handle_state_updates(self._state_updates) proc.rt_updates.extend(updates) return proc @@ -501,7 +500,7 @@ def process_transaction(self, set_determination_time: bool) -> TransactionResult """Process transaction and create a TransactionResult.""" proc = TransactionResult() if self._state_updates: - self._mdib.mdib_version += 1 + self._mdib.mdib_version = self.new_mdib_version updates = self._handle_state_updates(self._state_updates) proc.op_updates.extend(updates) return proc @@ -557,8 +556,8 @@ def mk_context_state(self, descriptor_handle: str, new_state_container = self._mdib.data_model.mk_state_container(descriptor_container) new_state_container.Handle = context_state_handle or uuid.uuid4().hex if set_associated: - # bind to mdib version AFTER this transaction - new_state_container.BindingMdibVersion = self._mdib.mdib_version + 1 + # bind to new mdib version of this transaction + new_state_container.BindingMdibVersion = self.new_mdib_version new_state_container.BindingStartTime = time.time() new_state_container.ContextAssociation = \ self._mdib.data_model.pm_types.ContextAssociation.ASSOCIATED @@ -577,16 +576,46 @@ def add_state(self, state_container: AbstractMultiStateProtocol, adjust_state_ve if state_container.descriptor_container is None: descr = self._mdib.descriptions.handle.get_one(state_container.DescriptorHandle) state_container.descriptor_container = descr + state_container.DescriptorVersion = state_container.descriptor_container.DescriptorVersion if adjust_state_version: self._mdib.context_states.set_version(state_container) self._state_updates[state_container.Handle] = TransactionItem(None, state_container) + def disassociate_all(self, + context_descriptor_handle: str, + ignored_handle: str | None = None) -> list[str]: + """Disassociate all associated states in mdib for context_descriptor_handle. + + The updated states are added to the transaction. + The method returns a list of states that were disassociated. + :param context_descriptor_handle: the handle of the context descriptor + :param ignored_handle: the context state with this Handle shall not be touched. + """ + pm_types = self._mdib.data_model.pm_types + disassociated_state_handles = [] + old_state_containers = self._mdib.context_states.descriptor_handle.get(context_descriptor_handle, []) + for old_state in old_state_containers: + if old_state.Handle == ignored_handle or old_state.Handle in self._state_updates: + # If state is already part of this transaction leave it also untouched, accept what the user wanted. + continue + if old_state.ContextAssociation != pm_types.ContextAssociation.DISASSOCIATED \ + or old_state.UnbindingMdibVersion is None: + self._logger.info('disassociate %s, handle=%s', old_state.NODETYPE.localname, + old_state.Handle) + transaction_state = self.get_context_state(old_state.Handle) + transaction_state.ContextAssociation = pm_types.ContextAssociation.DISASSOCIATED + if transaction_state.UnbindingMdibVersion is None: + transaction_state.UnbindingMdibVersion = self.new_mdib_version + transaction_state.BindingEndTime = time.time() + disassociated_state_handles.append(transaction_state.Handle) + return disassociated_state_handles + def process_transaction(self, set_determination_time: bool) -> TransactionResultProtocol: # noqa: ARG002 """Process transaction and create a TransactionResult.""" proc = TransactionResult() if self._state_updates: - self._mdib.mdib_version += 1 + self._mdib.mdib_version = self.new_mdib_version updates = self._handle_state_updates(self._state_updates) proc.ctxt_updates.extend(updates) return proc diff --git a/src/sdc11073/mdib/transactionsprotocol.py b/src/sdc11073/mdib/transactionsprotocol.py index 367e4e79..55a6e558 100644 --- a/src/sdc11073/mdib/transactionsprotocol.py +++ b/src/sdc11073/mdib/transactionsprotocol.py @@ -70,6 +70,8 @@ class TransactionItem: class AbstractTransactionManagerProtocol(Protocol): """Interface of a TransactionManager.""" + new_mdib_version: int + def process_transaction(self, set_determination_time: bool) -> TransactionResultProtocol: """Process the transaction.""" @@ -161,6 +163,11 @@ def mk_context_state(self, descriptor_handle: str, set_associated: bool = False) -> AbstractMultiStateProtocol: """Create a new ContextStateContainer.""" + def disassociate_all(self, + context_descriptor_handle: str, + ignored_handle: str | None = None) -> list[str]: + """Disassociate all associated states in mdib for context_descriptor_handle.""" + AnyTransactionManagerProtocol = Union[ContextStateTransactionManagerProtocol, StateTransactionManagerProtocol, diff --git a/src/sdc11073/multikey.py b/src/sdc11073/multikey.py index 44257e71..b755ee1c 100644 --- a/src/sdc11073/multikey.py +++ b/src/sdc11073/multikey.py @@ -1,3 +1,12 @@ +from __future__ import annotations + +from collections import defaultdict, namedtuple +from threading import RLock +from typing import TYPE_CHECKING, Any, Callable + +if TYPE_CHECKING: + from collections.abc import Iterable + """ This module implements an in-memory table with indices for faster access to objects. Example: You have a class @@ -25,18 +34,18 @@ def getAge(self): all_42_agers = person_lookup.by_age.get(42) """ -from collections import defaultdict, namedtuple -from threading import RLock -import warnings class IndexDefinition(dict): - """ An index allows to group objects by values. + """An index allows to group objects by values. + This is a dictionary that has lists ob objects as value. - Each list contains objects that have the same key member""" + Each list contains objects that have the same key member. + """ + + def __init__(self, get_key_func:Callable[[Any], Any], index_none_values: bool=True): + """Construct an index. - def __init__(self, get_key_func, index_none_values=True): - """ :param get_key_func: a callable that returns a key value from a given object :param index_none_values: if True, a None key is handled like every other value. if False,a None key is not added to index. @@ -44,27 +53,44 @@ def __init__(self, get_key_func, index_none_values=True): super().__init__() self._get_key_func = get_key_func self._index_none_values = index_none_values + self._lock: RLock | None = None + + def get_one(self, key: Any, allow_none: bool=False) -> Any | None: + """Return exactly one object instead of a list (like get method does). - def get_one(self, key, allow_none=False): - """ - returns one object instead of a list (like get method does) It raises a ValueError if there are multiple values available for the key. It raises a KeyError if allow_none is False and the key is not present. :param key: :param allow_none: :return: """ - if allow_none: - result = self.get(key) - if result is None: - return result - else: - result = self[key] - if len(result) > 1: - raise ValueError(f'get_one: key "{key}" has {len(result)} objects') - return result[0] - - def mk_keys(self, obj): + with self._lock: + if allow_none: + result = self.get(key) + if result is None: + return result + else: + result = self[key] + if len(result) > 1: + raise ValueError(f'get_one: key "{key}" has {len(result)} objects') + return result[0] + + def get(self, *args, **kwargs) -> list[Any] | None: + """Overwritten get method that uses lock.""" + with self._lock: + return super().get(*args, **kwargs) + + def __getitem__(self, key) -> list[Any] | None: + """Overwritten __getitem__ method that uses lock.""" + with self._lock: + return super().__getitem__(key) + + def set_lock(self, lock: RLock): + """Set the lock to be used.""" + self._lock = lock + + def mk_keys(self, obj: Any) -> list[Any] | None: + """Determine key for obj and add it to list in self[key].""" key = self._get_key_func(obj) if not self._index_none_values and key is None: return None @@ -74,7 +100,8 @@ def mk_keys(self, obj): self[key] = [obj] return [key] - def rm_key(self, key, obj): + def rm_key(self, key: Any, obj: Any): + """Remove obj from list self[key].""" try: obj_list = self[key] obj_list.remove(obj) @@ -85,9 +112,13 @@ def rm_key(self, key, obj): class UIndexDefinition(IndexDefinition): - """ A unique Index, there can only be one object with that key""" + """A unique Index, there can only be one object with that key.""" + + def mk_keys(self, obj: Any) -> list[Any] | None: + """Determine key for obj and add it to list in self[key]. - def mk_keys(self, obj): + If key is already known, raise a KeyError. + """ keys = self._get_key_func(obj) if not self._index_none_values and keys is None: return None @@ -102,9 +133,10 @@ def mk_keys(self, obj): class IndexDefinition1n(IndexDefinition): - """ For member values that are a list of keys (1:n relationship)""" + """Index for member values that are a list of keys (1:n relationship).""" - def mk_keys(self, obj): + def mk_keys(self, obj: Any) -> list[Any] | None: + """Determine key for obj and add it to list in self[key].""" keys = self._get_key_func(obj) if not self._index_none_values and keys is None: return None @@ -117,11 +149,22 @@ def mk_keys(self, obj): class ObjectSelector: - def __init__(self, selected_objects): + """Implements a mechanism to filter objects.""" + + def __init__(self, selected_objects: Iterable[Any]): self.objects = selected_objects - def find(self, **kwargs): - """ OR combination of args. Values are compared for equality (==), not identity (is).""" + def find(self, **kwargs) -> ObjectSelector: + """Return an ObjectSelector with a subset of data. + + The filter in kwargs implements an OR combination of args. + Values are compared for equality (==), not identity (is). + + Example: + ------- + - table contains persons with first_name and family_name. + - find(first_name='Mike') returns all persons with first_name == 'Mike'. + """ result = [] for obj in self.objects: for name, value in kwargs.items(): @@ -144,6 +187,11 @@ def find(self, **kwargs): # when we remove an object we need it to delete all indices referencing it class MultiKeyLookup: + """A combination of a list of objects and dictionaries. + + This mimics a database table with multiple indices. + The dictionaries can be used to directly access values by a key. + """ def __init__(self): self._objects = set() # contains the objects @@ -153,52 +201,69 @@ def __init__(self): self._lock = RLock() @property - def objects(self): + def objects(self) -> set[Any]: + """Return a set of all objects in table.""" return self._objects @property - def lock(self): + def lock(self) -> RLock: + """Return the lock of the table.""" return self._lock - # def getIndexDict(self, index_name): - # return self._idx_defs[index_name] # .indices - - def __getattr__(self, name): - return self._idx_defs[name] # .indices + def __getattr__(self, name: str): + return self._idx_defs[name] - def add_index(self, index_name, index_definition): + def add_index(self, index_name: str, index_definition: IndexDefinition): + """Add index to table.""" self._idx_defs[index_name] = index_definition + index_definition.set_lock(self._lock) # add existing objects to new lookup for obj in self._objects: keys = index_definition.mk_keys(obj) for k in keys: self._object_ids[id(obj)].append(_ObjRef(index_definition, k)) - def add_object(self, obj): + def add_object(self, obj: Any): + """Add object to table. + + Indices are updated accordingly. + """ if obj in self._objects: return with self._lock: self._objects.add(obj) self._mk_indices(obj) - def add_object_no_lock(self, obj): + def add_object_no_lock(self, obj: Any): + """Add object to table without using the lock. + + Indices are updated accordingly. + """ if obj in self._objects: return self._objects.add(obj) self._mk_indices(obj) - def add_objects(self, objects): + def add_objects(self, objects: list[Any]): + """Add objects to table. + + Indices are updated accordingly. + """ with self._lock: self.add_objects_no_lock(objects) - def add_objects_no_lock(self, objects): + def add_objects_no_lock(self, objects: list[Any]): + """Add objects to table without using the lock. + + Indices are updated accordingly. + """ for obj in objects: if obj in self._objects: continue self._objects.add(obj) self._mk_indices(obj) - def _mk_indices(self, obj): + def _mk_indices(self, obj: Any): all_keys = [] # for this object for index_definition in self._idx_defs.values(): try: @@ -209,13 +274,17 @@ def _mk_indices(self, obj): pass self._object_ids[id(obj)].extend(all_keys) - def _rm_indices(self, obj): + def _rm_indices(self, obj: Any): obj_refs = self._object_ids.get(id(obj), []) for obj_ref in obj_refs: obj_ref.index_dict.rm_key(obj_ref.key, obj) del self._object_ids[id(obj)] - def remove_object(self, obj): + def remove_object(self, obj: Any): + """Remove object from table. + + Indices are updated accordingly. + """ obj_refs = self._object_ids.get(id(obj)) if obj_refs is None: return @@ -223,18 +292,30 @@ def remove_object(self, obj): self._rm_indices(obj) self._objects.remove(obj) - def remove_object_no_lock(self, obj): + def remove_object_no_lock(self, obj: Any): + """Remove object from table without using lock. + + Indices are updated accordingly. + """ obj_refs = self._object_ids.get(id(obj)) if obj_refs is None: return self._rm_indices(obj) self._objects.remove(obj) - def remove_objects(self, objects): + def remove_objects(self, objects: list[Any]): + """Remove objects from table. + + Indices are updated accordingly. + """ with self._lock: self.remove_objects_no_lock(objects) - def remove_objects_no_lock(self, objects): + def remove_objects_no_lock(self, objects: list[Any]): + """Remove objects from table without using lock. + + Indices are updated accordingly. + """ for obj in objects: obj_refs = self._object_ids.get(id(obj)) if obj_refs is None: @@ -242,24 +323,28 @@ def remove_objects_no_lock(self, objects): self._rm_indices(obj) self._objects.remove(obj) - def update_object(self, obj): + def update_object(self, obj: Any): + """Update indices according to current values in obj.""" if obj not in self._objects: raise ValueError(f'object {obj} not known') with self._lock: self._rm_indices(obj) self._mk_indices(obj) - def update_object_no_lock(self, obj): + def update_object_no_lock(self, obj: Any): + """Update indices according to current values in obj without using lock.""" if obj not in self._objects: raise ValueError(f'object {obj} not known') self._rm_indices(obj) self._mk_indices(obj) - def update_objects(self, objs): + def update_objects(self, objs: list[Any]): + """Update indices according to current values in objs.""" with self._lock: self.update_objects_no_lock(objs) - def update_objects_no_lock(self, objs): + def update_objects_no_lock(self, objs: list[Any]): + """Update indices according to current values in objs without using lock.""" for obj in objs: if obj not in self._objects: raise ValueError(f'object {obj} not known') @@ -268,17 +353,20 @@ def update_objects_no_lock(self, objs): self._mk_indices(obj) def clear(self): + """Remove all objects from table.""" with self._lock: for index_definition in self._idx_defs.values(): index_definition.clear() self._object_ids.clear() self._objects.clear() - def find(self, **kwargs): + def find(self, **kwargs) -> ObjectSelector: + """Return an ObjectSelector with a subset of all objects that match the filter criteria in kwargs.""" sel = ObjectSelector(self._objects) with self._lock: return sel.find(**kwargs) - def find_no_lock(self, **kwargs): + def find_no_lock(self, **kwargs) -> ObjectSelector: + """Like find, but without using lock.""" sel = ObjectSelector(self._objects) return sel.find(**kwargs) diff --git a/src/sdc11073/provider/components.py b/src/sdc11073/provider/components.py index b03bc59e..77e7d987 100644 --- a/src/sdc11073/provider/components.py +++ b/src/sdc11073/provider/components.py @@ -1,7 +1,7 @@ from __future__ import annotations import copy -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any, Callable from sdc11073.pysoap.msgfactory import MessageFactory @@ -34,6 +34,7 @@ from sdc11073.mdib.providermdib import ProviderMdib from sdc11073.provider.servicesfactory import HostedServices from sdc11073.xml_types.wsd_types import ScopesType + from sdc11073.namespaces import PrefixNamespace from .sco import AbstractScoOperationsRegistry from .subscriptionmgr_base import SubscriptionManagerProtocol @@ -60,6 +61,7 @@ class SdcProviderComponents: waveform_provider_class: type | None = None scopes_factory: Callable[[ProviderMdib], ScopesType] = None hosted_services: dict = None + additional_schema_specs: list[PrefixNamespace] = field(default_factory=list) def merge(self, other: SdcProviderComponents): """Add data from other to self.""" @@ -82,6 +84,7 @@ def _merge(attr_name: str): if other.subscriptions_manager_class is not None: for key, value in other.subscriptions_manager_class.items(): self.subscriptions_manager_class[key] = value + self.additional_schema_specs = list(set(self.additional_schema_specs).union(set(other.additional_schema_specs))) default_sdc_provider_components_sync = SdcProviderComponents( diff --git a/src/sdc11073/provider/porttypes/setserviceimpl.py b/src/sdc11073/provider/porttypes/setserviceimpl.py index 21338b18..dd71b1f3 100644 --- a/src/sdc11073/provider/porttypes/setserviceimpl.py +++ b/src/sdc11073/provider/porttypes/setserviceimpl.py @@ -175,7 +175,7 @@ def notify_operation(self, if error is not None: report_part.InvocationInfo.InvocationError = error if error_message is not None: - report_part.InvocationErrorMessage = data_model.pm_types.LocalizedText(error_message) + report_part.InvocationInfo.InvocationErrorMessage.append(data_model.pm_types.LocalizedText(error_message)) # implemented is only SDC R0077 for value of invocationSource: # Extension = "AnonymousSdcParticipant". # a known participant (R0078) is currently not supported diff --git a/src/sdc11073/provider/providerimpl.py b/src/sdc11073/provider/providerimpl.py index 9c5775a0..bd6bb7cd 100644 --- a/src/sdc11073/provider/providerimpl.py +++ b/src/sdc11073/provider/providerimpl.py @@ -27,7 +27,6 @@ from sdc11073.xml_types.wsd_types import ProbeMatchesType, ProbeMatchType from sdc11073.roles.protocols import ProductProtocol, WaveformProviderProtocol # import here for code cov. :( -from .components import default_sdc_provider_components from .periodicreports import PeriodicReportsHandler, PeriodicReportsNullHandler if TYPE_CHECKING: @@ -40,6 +39,7 @@ from sdc11073.pysoap.msgfactory import CreatedMessage from sdc11073.pysoap.soapenvelope import ReceivedSoapMessage from sdc11073.xml_types.msg_types import AbstractSet + from sdc11073.xml_types.pm_types import InstanceIdentifier from sdc11073.xml_types.wsd_types import ScopesType from .components import SdcProviderComponents @@ -126,6 +126,7 @@ def __init__(self, ws_discovery: WsDiscoveryProtocol, self._socket_timeout = socket_timeout or int(max_subscription_duration * 1.2) self._log_prefix = log_prefix if default_components is None: + from .components import default_sdc_provider_components # lazy import avoids cyclic import default_components = default_sdc_provider_components self._components = copy.deepcopy(default_components) if specific_components is not None: @@ -147,20 +148,20 @@ def __init__(self, ws_discovery: WsDiscoveryProtocol, self.collect_rt_samples_period = 0.1 # in seconds self.contextstates_in_getmdib = self.DEFAULT_CONTEXTSTATES_IN_GETMDIB # can be overridden per instance - # look for schemas added by services - additional_schema_specs = [] + # look for schemas added by services and components spec + additional_schema_specs = set(self._components.additional_schema_specs) for hosted_service in self._components.hosted_services.values(): for port_type_impl in hosted_service: - additional_schema_specs.extend(port_type_impl.additional_namespaces) + additional_schema_specs.update(port_type_impl.additional_namespaces) logger = loghelper.get_logger_adapter('sdc.device.msgreader', log_prefix) self.msg_reader = self._components.msg_reader_class(self._mdib.sdc_definitions, - additional_schema_specs, + list(additional_schema_specs), logger, validate=validate) logger = loghelper.get_logger_adapter('sdc.device.msgfactory', log_prefix) self.msg_factory = self._components.msg_factory_class(self._mdib.sdc_definitions, - additional_schema_specs, + list(additional_schema_specs), logger=logger, validate=validate) @@ -315,18 +316,26 @@ def _on_probe_request(self, request: RequestData) -> CreatedMessage: def set_location(self, location: SdcLocation, - validators: list | None = None, - publish_now: bool = True): - """:param location: an SdcLocation instance - :param validators: a list of pmtypes.InstanceIdentifier objects or None; in that case the defaultInstanceIdentifiers member is used + validators: list[InstanceIdentifier] | None = None, + publish_now: bool = True, + location_context_descriptor_handle: str | None = None): + """Set a new associated location. + + :param location: an SdcLocation instance + :param validators: a list of InstanceIdentifier objects or None; + If it is None, the defaultInstanceIdentifiers member is used :param publish_now: if True, the device is published via its wsdiscovery reference. + :param location_context_descriptor_handle: Only needed if the mdib contains more than one + LocationContextDescriptor. Then this defines the descriptor for which a new LocationContextState + shall be created. + """ if location == self._location: return self._location = location - if validators is None: - validators = self._mdib.xtra.default_instance_identifiers - self._mdib.xtra.set_location(location, validators) + self._mdib.xtra.set_location(location, + validators, + location_context_descriptor_handle = location_context_descriptor_handle) if publish_now: self.publish() diff --git a/src/sdc11073/pysoap/msgfactory.py b/src/sdc11073/pysoap/msgfactory.py index 84acdc6a..ba07ed44 100644 --- a/src/sdc11073/pysoap/msgfactory.py +++ b/src/sdc11073/pysoap/msgfactory.py @@ -18,7 +18,7 @@ class CreatedMessage: - def __init__(self, message, msg_factory): + def __init__(self, message: Soap12Envelope, msg_factory): self.p_msg = message self.msg_factory = msg_factory diff --git a/src/sdc11073/roles/contextprovider.py b/src/sdc11073/roles/contextprovider.py index 12bebc77..3135283f 100644 --- a/src/sdc11073/roles/contextprovider.py +++ b/src/sdc11073/roles/contextprovider.py @@ -2,10 +2,10 @@ import time import uuid +from collections import defaultdict from typing import TYPE_CHECKING from sdc11073.provider.operations import ExecuteResult - from . import providerbase if TYPE_CHECKING: @@ -13,8 +13,7 @@ from sdc11073.mdib.descriptorcontainers import AbstractOperationDescriptorProtocol from sdc11073.mdib.providermdib import ProviderMdib - from sdc11073.provider.operations import OperationDefinitionBase - from sdc11073.provider.operations import ExecuteParameters + from sdc11073.provider.operations import ExecuteParameters, OperationDefinitionBase from .providerbase import OperationClassGetter @@ -48,9 +47,24 @@ def make_operation_instance(self, return None def _set_context_state(self, params: ExecuteParameters) -> ExecuteResult: - """Execute the operation itself (ExecuteHandler).""" - proposed_context_states = params.operation_request.argument + """Execute the operation itself (ExecuteHandler). + + If the proposed context is a new context and ContextAssociation == pm_types.ContextAssociation.ASSOCIATED, + the before associates state(s) will be set to DISASSOCIATED and UnbindingMdibVersion/BindingEndTime + are set. + """ pm_types = self._mdib.data_model.pm_types + proposed_context_states = params.operation_request.argument + + # check if there is more than one associated state for a context descriptor in proposed_context_states + proposed_by_handle = defaultdict(list) + for st in proposed_context_states: + if st.ContextAssociation == pm_types.ContextAssociation.ASSOCIATED: + proposed_by_handle[st.DescriptorHandle].append(st) + for handle, states in proposed_by_handle.items(): + if len(states) > 1: + raise ValueError(f'more than one associated context for descriptor handle {handle}') + operation_target_handles = [] with self._mdib.context_state_transaction() as mgr: for proposed_st in proposed_context_states: @@ -65,40 +79,38 @@ def _set_context_state(self, params: ExecuteParameters) -> ExecuteResult: # this is a new context state # create a new unique handle proposed_st.Handle = uuid.uuid4().hex - operation_target_handles.append(proposed_st.Handle) - proposed_st.BindingMdibVersion = self._mdib.mdib_version - proposed_st.BindingStartTime = time.time() - proposed_st.ContextAssociation = pm_types.ContextAssociation.ASSOCIATED + if proposed_st.ContextAssociation == pm_types.ContextAssociation.ASSOCIATED: + proposed_st.BindingMdibVersion = mgr.new_mdib_version + proposed_st.BindingStartTime = time.time() self._logger.info('new %s, DescriptorHandle=%s Handle=%s', proposed_st.NODETYPE.localname, proposed_st.DescriptorHandle, proposed_st.Handle) mgr.add_state(proposed_st) - # find all associated context states, disassociate them, set unbinding info, and add them to updates - old_state_containers = self._mdib.context_states.descriptor_handle.get( - proposed_st.DescriptorHandle, []) - for old_state in old_state_containers: - if old_state.ContextAssociation != pm_types.ContextAssociation.DISASSOCIATED \ - or old_state.UnbindingMdibVersion is None: - self._logger.info('disassociate %s, handle=%s', old_state.NODETYPE.localname, - old_state.Handle) - new_state = mgr.get_context_state(old_state.Handle) - new_state.ContextAssociation = pm_types.ContextAssociation.DISASSOCIATED - if new_state.UnbindingMdibVersion is None: - new_state.UnbindingMdibVersion = self._mdib.mdib_version - new_state.BindingEndTime = time.time() - operation_target_handles.append(new_state.Handle) + if proposed_st.ContextAssociation == pm_types.ContextAssociation.ASSOCIATED: + operation_target_handles.extend(mgr.disassociate_all(proposed_st.DescriptorHandle)) else: # this is an update to an existing patient # use "regular" way to update via transaction manager self._logger.info('update %s, handle=%s', proposed_st.NODETYPE.localname, proposed_st.Handle) - tmp = mgr.get_context_state(proposed_st.Handle) - tmp.update_from_other_container(proposed_st, skipped_properties=['ContextAssociation', - 'BindingMdibVersion', - 'UnbindingMdibVersion', - 'BindingStartTime', - 'BindingEndTime', - 'StateVersion']) - operation_target_handles.append(proposed_st.Handle) + old_state = mgr.get_context_state(proposed_st.Handle) + # handle changed ContextAssociation + if (old_state.ContextAssociation == pm_types.ContextAssociation.ASSOCIATED + and proposed_st.ContextAssociation != pm_types.ContextAssociation.ASSOCIATED): + proposed_st.UnbindingMdibVersion = mgr.new_mdib_version + proposed_st.BindingEndTime = time.time() + elif (old_state.ContextAssociation != pm_types.ContextAssociation.ASSOCIATED + and proposed_st.ContextAssociation == pm_types.ContextAssociation.ASSOCIATED): + proposed_st.BindingMdibVersion = mgr.new_mdib_version + proposed_st.BindingStartTime = time.time() + operation_target_handles.extend(mgr.disassociate_all(proposed_st.DescriptorHandle, + ignored_handle=old_state.Handle)) + + old_state.update_from_other_container(proposed_st, skipped_properties=['BindingMdibVersion', + 'UnbindingMdibVersion', + 'BindingStartTime', + 'BindingEndTime', + 'StateVersion']) + operation_target_handles.append(proposed_st.Handle) if len(operation_target_handles) == 1: return ExecuteResult(operation_target_handles[0], self._mdib.data_model.msg_types.InvocationState.FINISHED) diff --git a/src/sdc11073/wsdiscovery/networkingthread.py b/src/sdc11073/wsdiscovery/networkingthread.py index 3c718530..67f21b83 100644 --- a/src/sdc11073/wsdiscovery/networkingthread.py +++ b/src/sdc11073/wsdiscovery/networkingthread.py @@ -1,6 +1,11 @@ +"""wsdiscovery networking input output.""" + from __future__ import annotations +import collections +import dataclasses import logging +import platform import queue import random import selectors @@ -9,35 +14,24 @@ import threading import time import traceback -from abc import ABC -from abc import abstractmethod -from collections import deque -from dataclasses import dataclass -from dataclasses import field -from enum import IntEnum -from typing import Any from typing import TYPE_CHECKING from lxml.etree import XMLSyntaxError + from sdc11073 import commlog from sdc11073.exceptions import ValidationError - -from .common import MULTICAST_IPV4_ADDRESS -from .common import MULTICAST_OUT_TTL -from .common import message_reader +from sdc11073.wsdiscovery.common import MULTICAST_IPV4_ADDRESS, MULTICAST_OUT_TTL, message_reader if TYPE_CHECKING: from logging import Logger from sdc11073.pysoap.msgfactory import CreatedMessage - - from .wsdimpl import WSDiscovery + from sdc11073.wsdiscovery.wsdimpl import WSDiscovery BUFFER_SIZE = 0xffff -DP_MAX_TIMEOUT = 5000 # 5 seconds -@dataclass +@dataclasses.dataclass(frozen=True) class _UdpRepeatParams: """Udp messages are send multiple times with random gaps. These parameters define the limits of the randomness.""" @@ -46,51 +40,37 @@ class _UdpRepeatParams: min_delay_ms: int # minimum delay for first repetition in ms max_delay_ms: int # maximal delay for first repetition in ms upper_delay_ms: int # max. delay between repetitions in ms - # (gap is doubled for each further repetition, but not more than this value) + # (gap is doubled for each further repetition, but not more than this value) -unicast_repeat_params = _UdpRepeatParams(500, 2, 50, 250, 500) -multicast_repeat_params = _UdpRepeatParams(500, 4, 50, 250, 500) +UNICAST_REPEAT_PARAMS = _UdpRepeatParams(500, 2, 50, 250, 500) +MULTICAST_REPEAT_PARAMS = _UdpRepeatParams(500, 4, 50, 250, 500) # these time constants control the send-loop SEND_LOOP_IDLE_SLEEP = 0.1 SEND_LOOP_BUSY_SLEEP = 0.01 -class _MessageType(IntEnum): - MULTICAST = 1 - UNICAST = 2 - - -@dataclass(frozen=True) +@dataclasses.dataclass(frozen=True) class OutgoingMessage: """OutgoingMessage instances contain a soap envelope, destination address and multicast / unicast information.""" created_message: CreatedMessage addr: str port: int - msg_type: _MessageType def __repr__(self): - return f"{self.__class__.__name__}(addr={self.addr}, port={self.port}, " \ - f"msg_type={self.msg_type}, created_message={self.created_message.serialize()})" + return (f"{self.__class__.__name__}(addr={self.addr}, port={self.port}, " + f"created_message={self.created_message.serialize()})") -@dataclass(frozen=True) -class _SocketsCollection: - multi_in: socket.socket - multi_out_uni_in: socket.socket - uni_out_socket: socket.socket - uni_in: socket.socket | None - - -class _NetworkingThreadBase(ABC): +class NetworkingThread: """Has one thread for sending and one for receiving.""" - @dataclass(order=True) + @dataclasses.dataclass(order=True) class _EnqueuedMessage: send_time: float - msg: Any = field(compare=False) + msg: OutgoingMessage = dataclasses.field(compare=False) repeat: int def __init__(self, @@ -109,60 +89,50 @@ def __init__(self, self._quit_send_event = threading.Event() self._send_queue = queue.PriorityQueue(10000) self._read_queue = queue.Queue(10000) - self._known_message_ids = deque(maxlen=50) - self._select_in = [] - self._full_selector = selectors.DefaultSelector() - self.sockets_collection = self._mk_sockets(my_ip_address) - - @abstractmethod - def _mk_sockets(self, addr: str): - ... + self._known_message_ids = collections.deque(maxlen=200) + self._inbound_selector = selectors.DefaultSelector() + self._outbound_selector = selectors.DefaultSelector() + self.multi_in = self._create_multicast_in_socket(my_ip_address, multicast_port) + self.multi_out_uni_in_out = self._create_multi_out_uni_in_out_socket(my_ip_address) - def _register(self, sock: socket.SocketType): - self._select_in.append(sock) - self._full_selector.register(sock, selectors.EVENT_READ) + def _register_inbound_socket(self, sock: socket.SocketType): + self._inbound_selector.register(sock, selectors.EVENT_READ) + self._logger.info('registered inbound socket on %s:%d', *sock.getsockname()) - def _unregister(self, sock: socket.SocketType): - self._select_in.remove(sock) - self._full_selector.unregister(sock) + def _register_outbound_socket(self, sock: socket.SocketType): + self._outbound_selector.register(sock, selectors.EVENT_WRITE) + self._logger.info('registered outbound socket on %s:%d', *sock.getsockname()) - @staticmethod - def _make_mreq(addr: str) -> bytes: - return struct.pack("4s4s", socket.inet_aton(MULTICAST_IPV4_ADDRESS), socket.inet_aton(addr)) - - @staticmethod - def _create_multicast_out_socket(addr: str) -> socket.SocketType: + def _create_multi_out_uni_in_out_socket(self, addr: str) -> socket.SocketType: sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, MULTICAST_OUT_TTL) - if addr is None: - sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF, socket.INADDR_ANY) - else: - _addr = socket.inet_aton(addr) - sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF, _addr) + # set port explicitly when creating (it would otherwise be set after sending first message via this socket) sock.bind((addr, 0)) + self._register_outbound_socket(sock) + self._register_inbound_socket(sock) + return sock + + def _create_multicast_in_socket(self, addr: str, port: int) -> socket.SocketType: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + if platform.system() != 'Windows': + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) + sock.bind((MULTICAST_IPV4_ADDRESS, port)) + else: + sock.bind((addr, port)) + sock.setblocking(False) + _addr = struct.pack("4s4s", socket.inet_aton(MULTICAST_IPV4_ADDRESS), socket.inet_aton(addr)) + sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, _addr) + self._register_inbound_socket(sock) return sock - def add_unicast_message(self, - created_message: CreatedMessage, - addr: str, - port: int): - msg = OutgoingMessage(created_message, addr, port, _MessageType.UNICAST) - self._logger.debug('add_unicast_message: adding message Id %s', - created_message.p_msg.header_info_block.MessageID) - self._repeated_enqueue_msg(msg, unicast_repeat_params) - - def add_multicast_message(self, - created_message: CreatedMessage, - addr: str, - port: int): - msg = OutgoingMessage(created_message, addr, port, _MessageType.MULTICAST) - self._logger.debug('add_multicast_message: adding message Id %s', - created_message.p_msg.header_info_block.MessageID) - self._repeated_enqueue_msg(msg, multicast_repeat_params) - - def _repeated_enqueue_msg(self, - msg: OutgoingMessage, - delay_params: _UdpRepeatParams): + def add_outbound_message(self, msg: CreatedMessage, addr: str, port: int, repeat_params: _UdpRepeatParams): + """Add a message to the sending queue.""" + self._logger.debug('adding outbound message with Id "%s" to sending queue', + msg.p_msg.header_info_block.MessageID) + self._known_message_ids.appendleft(msg.p_msg.header_info_block.MessageID) + self._repeated_enqueue_msg(OutgoingMessage(msg, addr, port), repeat_params) + + def _repeated_enqueue_msg(self, msg: OutgoingMessage, delay_params: _UdpRepeatParams): if self._quit_send_event.is_set(): self._logger.warning('_repeated_enqueue_msg: sending thread not running - message will be dropped - %s', msg) @@ -184,7 +154,8 @@ def _run_send(self): continue if self._send_queue.queue[0].send_time <= time.time(): enqueued_msg = self._send_queue.get() - self._send_msg(enqueued_msg) + for key, _ in self._outbound_selector.select(timeout=0.1): + self._send_msg(enqueued_msg, key.fileobj) else: time.sleep(SEND_LOOP_BUSY_SLEEP) # this creates a 10ms raster for sending, but that is good enough @@ -193,24 +164,12 @@ def _run_recv(self): while not self._quit_recv_event.is_set(): try: self._recv_messages() - except: # noqa: E722 - # use bare except here, this is a catch-all that keeps thread running. - if not self._quit_recv_event.is_set(): # only log error if it does not happen during stop - self._logger.error('_run_recv:%s', traceback.format_exc()) - - def is_from_my_socket(self, addr: str) -> bool: - if addr[0] == self._my_ip_address: - try: - sock_name = self.sockets_collection.multi_out_uni_in.getsockname() - if addr[1] == sock_name[1]: # compare ports - return True - except OSError as ex: # port is not opened? - self._logger.warning(str(ex)) - return False + except: # noqa: E722. use bare except here, this is a catch-all that keeps thread running. + self._logger.exception('exception during receiving') def _recv_messages(self): """For performance reasons this thread only writes to a queue, no parsing etc.""" - for key, _ in self._full_selector.select(timeout=0.1): + for key, _ in self._inbound_selector.select(timeout=0.1): sock: socket.SocketType = key.fileobj try: data, addr = sock.recvfrom(BUFFER_SIZE) @@ -218,8 +177,6 @@ def _recv_messages(self): self._logger.warning('socket read error %s', exc) time.sleep(0.01) continue - if self.is_from_my_socket(addr): - continue self._logger.debug('received data on my socket %s', sock.getsockname()) self._add_to_recv_queue(addr, data) @@ -248,44 +205,37 @@ def _run_q_read(self): else: mid = received_message.p_msg.header_info_block.MessageID if mid in self._known_message_ids: - self._logger.debug('incoming message already known :%s (from %r, Id %s).', + self._logger.debug('incoming message already known: %s (from %r, Id %s).', received_message.action, addr, mid) continue self._known_message_ids.appendleft(mid) self._wsd.handle_received_message(received_message, addr) - except Exception: + except Exception: # noqa: BLE001 self._logger.error('_run_q_read: %s', traceback.format_exc()) - def _send_msg(self, q_msg: _EnqueuedMessage): + def _send_msg(self, q_msg: _EnqueuedMessage, s: socket.socket): msg = q_msg.msg data = msg.created_message.serialize() - if msg.msg_type == _MessageType.UNICAST: - self._logger.debug('send unicast %d bytes (%d) action=%s: to=%s:%r id=%s', - len(data), - q_msg.repeat, - msg.created_message.p_msg.header_info_block.Action, - msg.addr, msg.port, - msg.created_message.p_msg.header_info_block.MessageID) - logging.getLogger(commlog.DISCOVERY_OUT).debug(data, extra={'ip_address': msg.addr}) - self.sockets_collection.uni_out_socket.sendto(data, (msg.addr, msg.port)) + self._logger.debug('send message %d bytes (%d) action=%s: to=%s:%r id=%s', + len(data), + q_msg.repeat, + msg.created_message.p_msg.header_info_block.Action, + msg.addr, msg.port, + msg.created_message.p_msg.header_info_block.MessageID) + try: + s.sendto(data, (msg.addr, msg.port)) + except: # noqa: E722. use bare except here, this is a catch-all that keeps thread running. + self._logger.exception('exception during sending') else: - logging.getLogger(commlog.MULTICAST_OUT).debug(data) - self._logger.debug('send multicast %d bytes, msg (%d) action=%s: to=%s:%r id=%s', - len(data), - q_msg.repeat, - msg.created_message.p_msg.header_info_block.Action, - msg.addr, msg.port, - msg.created_message.p_msg.header_info_block.MessageID) - self.sockets_collection.multi_out_uni_in.sendto(data, (msg.addr, msg.port)) + # log this if there was no exception during send + logging.getLogger(commlog.DISCOVERY_OUT).debug(data, extra={'ip_address': msg.addr}) def start(self): + """Start working for the sending and receiving queue.""" self._logger.debug('%s: starting ', self.__class__.__name__) - self._recv_thread = threading.Thread(target=self._run_recv, name='wsd.recvThread') - self._qread_thread = threading.Thread(target=self._run_q_read, name='wsd.qreadThread') - self._send_thread = threading.Thread(target=self._run_send, name='wsd.sendThread') - self._recv_thread.daemon = True - self._qread_thread.daemon = True - self._send_thread.daemon = True + self._recv_thread = threading.Thread(target=self._run_recv, name='wsd.recvThread', daemon=True) + self._qread_thread = threading.Thread(target=self._run_q_read, name='wsd.qreadThread', daemon=True) + self._send_thread = threading.Thread(target=self._run_send, name='wsd.sendThread', daemon=True) self._recv_thread.start() self._qread_thread.start() self._send_thread.start() @@ -300,6 +250,7 @@ def schedule_stop(self): self._quit_send_event.set() def join(self): + """Join threads and close sockets.""" self._logger.debug('%s: join... ', self.__class__.__name__) self._recv_thread.join() self._send_thread.join() @@ -307,68 +258,8 @@ def join(self): self._recv_thread = None self._send_thread = None self._qread_thread = None - for sock in self._select_in: - sock.close() - self.sockets_collection.uni_out_socket.close() - self._full_selector.close() + self.multi_in.close() + self.multi_out_uni_in_out.close() + self._inbound_selector.close() + self._outbound_selector.close() self._logger.debug('%s: ... join done', self.__class__.__name__) - - -class NetworkingThreadWindows(_NetworkingThreadBase): - """Implementation for Windows. Socket creation is OS specific.""" - - def _create_multicast_in_socket(self, addr: str, port: int) -> socket.SocketType: - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - sock.bind((addr, port)) - sock.setblocking(False) - sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, self._make_mreq(addr)) - self._logger.info('UDP socket listens on %s:%d', addr, port) - return sock - - def _mk_sockets(self, addr: str) -> _SocketsCollection: - multicast_in_sock = self._create_multicast_in_socket(addr, self.multicast_port) - multicast_out_sock = self._create_multicast_out_socket(addr) - uni_out_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - - self._register(multicast_out_sock) - self._register(multicast_in_sock) - return _SocketsCollection(multicast_in_sock, - multicast_out_sock, - uni_out_socket, - None) - - -class NetworkingThreadPosix(_NetworkingThreadBase): - """Implementation for Windows. Socket creation is OS specific.""" - - def _create_multicast_in_socket(self, addr: str, port: int) -> socket.SocketType: - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - sock.bind((MULTICAST_IPV4_ADDRESS, port)) - sock.setblocking(False) - sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, self._make_mreq(addr)) - self._logger.info('UDP socket listens on %s:%d', addr, port) - return sock - - def _create_unicast_in_socket(self, addr: str, port: int) -> socket.SocketType: - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - sock.bind((addr, port)) - sock.setblocking(False) - return sock - - def _mk_sockets(self, addr: str) -> _SocketsCollection: - multicast_in_sock = self._create_multicast_in_socket(addr, self.multicast_port) - - # The unicast_in_sock is needed for handling of unicast messages on multicast port - unicast_in_sock = self._create_unicast_in_socket(addr, self.multicast_port) - multicast_out_sock = self._create_multicast_out_socket(addr) - uni_out_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - self._register(multicast_out_sock) - self._register(unicast_in_sock) - self._register(multicast_in_sock) - return _SocketsCollection(multicast_in_sock, - multicast_out_sock, - uni_out_socket, - unicast_in_sock) diff --git a/src/sdc11073/wsdiscovery/wsdimpl.py b/src/sdc11073/wsdiscovery/wsdimpl.py index f146d1ea..9af60d25 100644 --- a/src/sdc11073/wsdiscovery/wsdimpl.py +++ b/src/sdc11073/wsdiscovery/wsdimpl.py @@ -3,7 +3,6 @@ from __future__ import annotations import logging -import platform import random import time from enum import Enum @@ -18,7 +17,7 @@ from sdc11073.xml_types.addressing_types import HeaderInformationBlock from .common import MULTICAST_IPV4_ADDRESS, MULTICAST_PORT, message_factory -from .networkingthread import NetworkingThreadPosix, NetworkingThreadWindows +from sdc11073.wsdiscovery import networkingthread from .service import Service if TYPE_CHECKING: @@ -209,7 +208,7 @@ def search_services(self, elif now < end: time.sleep(end - now) now = time.monotonic() - return filter_services(self._remote_services.values(), types, scopes) + return filter_services(list(self._remote_services.values()), types, scopes) def search_sdc_services(self, scopes: wsd_types.ScopesType | None = None, @@ -255,7 +254,7 @@ def search_multiple_types(self, # prevent possible duplicates by adding them to a dictionary by epr result = {} for _type in types_list: - tmp = filter_services(self._remote_services.values(), _type, scopes) + tmp = filter_services(list(self._remote_services.values()), _type, scopes) for srv in tmp: result[srv.epr] = srv return list(result.values()) @@ -511,7 +510,8 @@ def _send_resolve_match(self, service: Service, relates_to: str, addr: str): created_message = _mk_wsd_soap_message(inf, payload) created_message.p_msg.add_header_element(app_sequence.as_etree_node(nsh.WSD.tag('AppSequence'), ns_map=nsh.partial_map(nsh.WSD))) - self._networking_thread.add_unicast_message(created_message, addr[0], addr[1]) + self._networking_thread.add_outbound_message(created_message, addr[0], addr[1], + networkingthread.UNICAST_REPEAT_PARAMS) def _send_probe_match(self, services: list[Service], relates_to: str, addr: str): self._logger.info('sending probe match to %s for %d services', addr, len(services)) @@ -544,7 +544,8 @@ def _send_probe_match(self, services: list[Service], relates_to: str, addr: str) created_message = _mk_wsd_soap_message(inf, payload) created_message.p_msg.add_header_element(app_sequence.as_etree_node(nsh.WSD.tag('AppSequence'), ns_map=nsh.partial_map(nsh.WSD))) - self._networking_thread.add_unicast_message(created_message, addr[0], addr[1]) + self._networking_thread.add_outbound_message(created_message, addr[0], addr[1], + networkingthread.UNICAST_REPEAT_PARAMS) def _send_probe(self, types: Iterable[QName] | None = None, scopes: wsd_types.ScopesType | None = None): types = list(types) if types is not None else None # enforce iteration @@ -556,7 +557,8 @@ def _send_probe(self, types: Iterable[QName] | None = None, scopes: wsd_types.Sc inf = HeaderInformationBlock(action=payload.action, addr_to=ADDRESS_ALL) created_message = _mk_wsd_soap_message(inf, payload) - self._networking_thread.add_multicast_message(created_message, MULTICAST_IPV4_ADDRESS, self.multicast_port) + self._networking_thread.add_outbound_message(created_message, MULTICAST_IPV4_ADDRESS, self.multicast_port, + networkingthread.MULTICAST_REPEAT_PARAMS) def _send_resolve(self, epr: str): self._logger.debug('sending resolve on %s', epr) @@ -565,7 +567,8 @@ def _send_resolve(self, epr: str): inf = HeaderInformationBlock(action=payload.action, addr_to=ADDRESS_ALL) created_message = _mk_wsd_soap_message(inf, payload) - self._networking_thread.add_multicast_message(created_message, MULTICAST_IPV4_ADDRESS, self.multicast_port) + self._networking_thread.add_outbound_message(created_message, MULTICAST_IPV4_ADDRESS, self.multicast_port, + networkingthread.MULTICAST_REPEAT_PARAMS) def _send_hello(self, service: Service): self._logger.info('sending hello on %s', service) @@ -585,7 +588,8 @@ def _send_hello(self, service: Service): created_message = _mk_wsd_soap_message(inf, payload) created_message.p_msg.add_header_element(app_sequence.as_etree_node(nsh.WSD.tag('AppSequence'), ns_map=nsh.partial_map(nsh.WSD))) - self._networking_thread.add_multicast_message(created_message, MULTICAST_IPV4_ADDRESS, self.multicast_port) + self._networking_thread.add_outbound_message(created_message, MULTICAST_IPV4_ADDRESS, self.multicast_port, + networkingthread.MULTICAST_REPEAT_PARAMS) def _send_bye(self, service: Service): self._logger.debug('sending bye on %s', service) @@ -602,17 +606,14 @@ def _send_bye(self, service: Service): created_message = _mk_wsd_soap_message(inf, bye) created_message.p_msg.add_header_element(app_sequence.as_etree_node(nsh.WSD.tag('AppSequence'), ns_map=nsh.partial_map(nsh.WSD))) - self._networking_thread.add_multicast_message(created_message, MULTICAST_IPV4_ADDRESS, self.multicast_port) + self._networking_thread.add_outbound_message(created_message, MULTICAST_IPV4_ADDRESS, self.multicast_port, + networkingthread.MULTICAST_REPEAT_PARAMS) def _start_threads(self): if self._networking_thread is not None: return - if platform.system() != 'Windows': - self._networking_thread = NetworkingThreadPosix(str(self._adapter.ip), self, self._logger, - self.multicast_port) - else: - self._networking_thread = NetworkingThreadWindows(str(self._adapter.ip), self, self._logger, - self.multicast_port) + self._networking_thread = networkingthread.NetworkingThread(str(self._adapter.ip), self, self._logger, + self.multicast_port) self._networking_thread.start() def _stop_threads(self): diff --git a/src/sdc11073/xml_types/pm_types.py b/src/sdc11073/xml_types/pm_types.py index 999f0dea..2332dd69 100644 --- a/src/sdc11073/xml_types/pm_types.py +++ b/src/sdc11073/xml_types/pm_types.py @@ -1031,7 +1031,8 @@ class ClinicalInfo(PropertyBasedPMType): Criticality: CriticalityType = cp.NodeEnumTextProperty(pm.Criticality, Criticality, is_optional=True, implied_py_value=Criticality.Low) Description: list[LocalizedText] = cp.SubElementListProperty(pm.Description, value_class=LocalizedText) - RelatedMeasurement: list[Measurement] = cp.SubElementListProperty(pm.RelatedMeasurement, value_class=Measurement) + RelatedMeasurement: list[RelatedMeasurement] = cp.SubElementListProperty(pm.RelatedMeasurement, + value_class=RelatedMeasurement) _props = ('Type', 'Code', 'Criticality', 'Description', 'RelatedMeasurement') def __init__(self, # noqa: PLR0913 @@ -1039,7 +1040,7 @@ def __init__(self, # noqa: PLR0913 code: CodedValue | None = None, criticality: CriticalityType | None = None, descriptions: list[LocalizedText] | None = None, - related_measurements: list[Measurement] | None = None): + related_measurements: list[RelatedMeasurement] | None = None): super().__init__() self.Type = type_ self.Code = code diff --git a/src/sdc11073/xml_types/xml_structure.py b/src/sdc11073/xml_types/xml_structure.py index 959035f5..05109396 100644 --- a/src/sdc11073/xml_types/xml_structure.py +++ b/src/sdc11073/xml_types/xml_structure.py @@ -1071,6 +1071,18 @@ def __set__(self, instance, py_value): def init_instance_data(self, instance: Any): setattr(instance, self._local_var_name, []) + def update_from_node(self, instance: Any, node: xml_utils.LxmlElement): + """Update instance data with data from node. + + This method is used internally and should not be called by application. + :param instance:the instance that has the property as member + :param node:the etree node that provides the value + :return: + """ + value: list | None = self.get_py_value_from_node(instance, node) + if value is not None: + setattr(instance, self._local_var_name, value) + class SubElementListProperty(_ElementListProperty): """SubElementListProperty is a list of values that have an "as_etree_node" method. @@ -1323,15 +1335,19 @@ def __init__(self, sub_element_name: etree_.QName | None, super().__init__(sub_element_name, ListConverter(ClassCheckConverter(value_class)), is_optional=is_optional) - def get_py_value_from_node(self, instance: Any, node: xml_utils.LxmlElement) -> Any: # noqa: ARG002 - """Read value from node.""" + def get_py_value_from_node(self, instance: Any, node: xml_utils.LxmlElement) -> list[Any] | None: # noqa: ARG002 + """Read value from node. + + If the expected node does not exist, return _default_py_value (usually None). + If the node exists, but the text is None, return an empty list. + """ try: sub_node = self._get_element_by_child_name(node, self._sub_element_name, create_missing_nodes=False) if sub_node.text is not None: return sub_node.text.split() + return [] except ElementNotFoundError: - pass - return self._default_py_value + return self._default_py_value def update_xml_value(self, instance: Any, node: xml_utils.LxmlElement): """Write value to node.""" diff --git a/tests/foo_schema.xsd b/tests/foo_schema.xsd new file mode 100644 index 00000000..5965ac6f --- /dev/null +++ b/tests/foo_schema.xsd @@ -0,0 +1,16 @@ + + + + + a string with a minimum length of 3 characters. + + + + + + + + + + + diff --git a/tests/mdib_two_mds.xml b/tests/mdib_two_mds.xml index faa67691..a2844552 100644 --- a/tests/mdib_two_mds.xml +++ b/tests/mdib_two_mds.xml @@ -290,6 +290,8 @@ MdibVersion="5795" SequenceId="urn:uuid:4ed313b2-f925-418a-8476-6f3b4d06ee3e" In SDPi Test MDS used for description modification reports. This MDS periodically inserts and deletes a VMD including Channels including Metrics. + + SDPi Test VMD that contains a metric and an alarm for which units and cause-remedy information is periodically updated (description updates) diff --git a/tests/mockstuff.py b/tests/mockstuff.py index 9b0ab36b..e1d4ce1b 100644 --- a/tests/mockstuff.py +++ b/tests/mockstuff.py @@ -127,6 +127,7 @@ def __init__(self, wsdiscovery: WsDiscoveryProtocol, serial_number='12345') device_mdib_container = ProviderMdib.from_string(mdib_xml_data, log_prefix=log_prefix) + device_mdib_container.instance_id = 1 # set the optional value # set Metadata mdsDescriptors = device_mdib_container.descriptions.NODETYPE.get(pm.MdsDescriptor) for mdsDescriptor in mdsDescriptors: diff --git a/tests/test_additional_schema.py b/tests/test_additional_schema.py new file mode 100644 index 00000000..e085937c --- /dev/null +++ b/tests/test_additional_schema.py @@ -0,0 +1,116 @@ +import pathlib +import unittest +import uuid + +from lxml import etree + +from sdc11073.consumer.components import SdcConsumerComponents +from sdc11073.consumer.consumerimpl import SdcConsumer +from sdc11073.definitions_sdc import SdcV1Definitions +from sdc11073.exceptions import ValidationError +from sdc11073.namespaces import PrefixNamespace +from sdc11073.provider.components import SdcProviderComponents +from tests.mockstuff import SomeDevice + +here = pathlib.Path(__file__).parent + +# declaration of the foo schema +prefix_namespace_foo = PrefixNamespace('foo', + "http://test/foo", + "http://test/foo/foo_schema.xsd", + here.joinpath('foo_schema.xsd')) + +# a GetMdibResponse with a foo:Foo element in extension and a variable Bar attribute +mdib_data = """ + + +http://standards.ieee.org/downloads/11073/11073-20701-2018/GetService/GetMdibResponse +urn:uuid:aab2485a-e114-4c3c-850c-6200c6ecb49b +urn:uuid:96a2eaf3-e14c-49a8-9d03-bb1c217a46b9 + + + + + + + + + + + SDPi Test MDS + + + + + + + + + + + + + + + + + + +""" + + +class TestAdditionalSchema(unittest.TestCase): + + def setUp(self): + specific_components_consumer = SdcConsumerComponents(additional_schema_specs=[prefix_namespace_foo]) + specific_components_provider = SdcProviderComponents(additional_schema_specs=[prefix_namespace_foo]) + + # instantiate a provider and a consumer. + # It is not needed to start them, because msg_reader and msg_factory are created in constructor, + # and these are all that's needed in this test. + self.provider = SomeDevice.from_mdib_file(wsdiscovery=None, + epr=uuid.uuid4(), + specific_components=specific_components_provider, + mdib_xml_path='mdib_tns.xml') + self.consumer = SdcConsumer('http://127.0.0.1:10000', # exact value does not matter + sdc_definitions=SdcV1Definitions, + specific_components=specific_components_consumer, + ssl_context_container=None, + validate=True) + + def test_foo_consumer(self): + # Verify that foo schema is known in msg_reader and msg_factory of consumer + node = etree.Element(etree.QName("http://test/foo", 'Foo')) + node.attrib['Bar'] = 'abcd' # a valid value + self.consumer.msg_reader._validate_node(node) + self.consumer.msg_factory._validate_node(node) + + node.attrib['Bar'] = 'ab' # value too short + self.assertRaises(ValidationError, self.consumer.msg_reader._validate_node, node) + self.assertRaises(ValidationError, self.consumer.msg_factory._validate_node, node) + + def test_foo_provider(self): + # Verify that foo schema is known in msg_reader and msg_factory of provider + node = etree.Element(etree.QName("http://test/foo", 'Foo')) + node.attrib['Bar'] = 'abcd' # a valid value + self.provider.msg_reader._validate_node(node) + self.provider.msg_factory._validate_node(node) + node.attrib['Bar'] = 'ab' # value too short + self.assertRaises(ValidationError, self.provider.msg_reader._validate_node, node) + self.assertRaises(ValidationError, self.provider.msg_factory._validate_node, node) + + def test_consumer(self): + # Verify that Foo element in extension is validated. + # It is sufficient to test only one of the validators, because they are all the same + # in msg_factory and msg_reader of consumer and provider. + self.consumer.msg_reader.read_received_message(mdib_data.format('abcs').encode('utf-8')) # correct attribute + self.assertRaises(ValidationError, self.consumer.msg_reader.read_received_message, + mdib_data.format('ab').encode('utf-8')) diff --git a/tests/test_alertsignaldelegate.py b/tests/test_alertsignaldelegate.py index 340faafa..bc4cf15f 100644 --- a/tests/test_alertsignaldelegate.py +++ b/tests/test_alertsignaldelegate.py @@ -43,7 +43,8 @@ def setUp(self): self.sdc_device.start_all() self._loc_validators = [pm_types.InstanceIdentifier('Validator', extension_string='System')] self.sdc_device.mdib.xtra.ensure_location_context_descriptor() - self.sdc_device.set_location(utils.random_location(), self._loc_validators) + self.sdc_device.set_location(utils.random_location(), self._loc_validators, + location_context_descriptor_handle='LC.mds0') time.sleep(0.5) # allow full init of devices diff --git a/tests/test_client_device.py b/tests/test_client_device.py index b5bbeec0..48bfc965 100644 --- a/tests/test_client_device.py +++ b/tests/test_client_device.py @@ -27,6 +27,7 @@ from sdc11073.loghelper import basic_logging_setup, get_logger_adapter from sdc11073.mdib import ConsumerMdib from sdc11073.pysoap.msgfactory import CreatedMessage +from sdc11073.pysoap.msgreader import MdibVersionGroupReader from sdc11073.pysoap.soapclient import HTTPReturnCodeError from sdc11073.pysoap.soapclient_async import SoapClientAsync from sdc11073.pysoap.soapenvelope import Soap12Envelope, faultcodeEnum @@ -527,6 +528,20 @@ def tearDown(self): def test_basic_connect(self): runtest_basic_connect(self, self.sdc_client) runtest_directed_probe(self, self.sdc_client, self.sdc_device) + cl_get_service = self.sdc_client.client('Get') + get_request_result = cl_get_service.get_mdib() + # verify that mdib version groups are identical in elements GetMdibResponse and Mdib + mdib_version_group1 = MdibVersionGroupReader.from_node(get_request_result.p_msg.msg_node) + mdib_version_group2 = MdibVersionGroupReader.from_node(get_request_result.p_msg.msg_node[0]) + self.assertEqual(mdib_version_group1, mdib_version_group2) + + def test_init_mdib_context_states(self): + # verify that consumer requests context states if GetMdibResponse contains no context states + self.sdc_device.contextstates_in_getmdib = False # provider does not add context states to GetMdibResponse + cl_mdib = ConsumerMdib(self.sdc_client) + cl_mdib.init_mdib() + self.assertEqual(len(self.sdc_device.mdib.context_states.objects), + len(cl_mdib.context_states.objects)) def test_renew_get_status(self): for s in self.sdc_client._subscription_mgr.subscriptions.values(): @@ -719,7 +734,6 @@ def test_get_md_description_parameters(self): def test_instance_id(self): """ verify that the client receives correct EpisodicMetricReports and PeriodicMetricReports""" - self.assertIsNone(self.sdc_device.mdib.instance_id) cl_mdib = ConsumerMdib(self.sdc_client) cl_mdib.init_mdib() self.assertEqual(self.sdc_device.mdib.sequence_id, cl_mdib.sequence_id) @@ -781,6 +795,11 @@ def test_alert_reports(self): alert_condition_state = self.sdc_device.mdib.states.NODETYPE[pm.AlertConditionState][0] descriptor_handle = alert_condition_state.DescriptorHandle + # there are possible rounding problems in timestamps. + # calculate a max_float_diff for max. 1 millisecond difference. + now = time.time() + max_float_diff_1ms = (now + 0.001) / now - 1 + for _activation_state, _actual_priority, _presence in product(list(pm_types.AlertActivation), list(pm_types.AlertConditionPriority), (True, @@ -796,7 +815,7 @@ def test_alert_reports(self): coll.result(timeout=NOTIFICATION_TIMEOUT) client_state_container = client_mdib.states.descriptor_handle.get_one( descriptor_handle) # this shall be updated by notification - self.assertEqual(client_state_container.diff(st), None) + self.assertEqual(client_state_container.diff(st, max_float_diff=max_float_diff_1ms), None) # pick an AlertSignal for testing alert_condition_state = self.sdc_device.mdib.states.NODETYPE[pm.AlertSignalState][0] @@ -817,7 +836,7 @@ def test_alert_reports(self): coll.result(timeout=NOTIFICATION_TIMEOUT) client_state_container = client_mdib.states.descriptor_handle.get_one( descriptor_handle) # this shall be updated by notification - self.assertEqual(client_state_container.diff(st), None) + self.assertEqual(client_state_container.diff(st, max_float_diff=max_float_diff_1ms), None) # verify that client also got a PeriodicAlertReport message_data = coll2.result(timeout=NOTIFICATION_TIMEOUT) diff --git a/tests/test_comm_logger.py b/tests/test_comm_logger.py index e0f0c117..5101ce0a 100644 --- a/tests/test_comm_logger.py +++ b/tests/test_comm_logger.py @@ -71,8 +71,7 @@ def test_directory_direction_in(self): self.assertEqual(1, len([file for file in os.listdir(directory) if ip_address in file and http_method in file])) - for name in (commlog.MULTICAST_OUT, - commlog.DISCOVERY_OUT, + for name in (commlog.DISCOVERY_OUT, commlog.SOAP_REQUEST_OUT, commlog.SOAP_RESPONSE_OUT): logging.getLogger(name).debug(str(uuid.uuid4())) @@ -82,8 +81,7 @@ def test_directory_direction_out(self): """Test the comm directory logger out direction.""" with tempfile.TemporaryDirectory() as directory, commlog.DirectoryLogger(log_folder=directory, log_out=True): self.assertEqual(0, len(os.listdir(directory))) - for i, name in enumerate((commlog.MULTICAST_OUT, - commlog.DISCOVERY_OUT, + for i, name in enumerate((commlog.DISCOVERY_OUT, commlog.SOAP_REQUEST_OUT, commlog.SOAP_RESPONSE_OUT), start=1): diff --git a/tests/test_device.py b/tests/test_device.py index 160286b9..3d0f17a4 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -5,6 +5,7 @@ import uuid from sdc11073 import wsdiscovery +from sdc11073.xml_types import pm_qnames from sdc11073.xml_types import pm_types from sdc11073.xml_types import wsd_types @@ -46,6 +47,64 @@ def test_restart(self): sdc_device2.stop_all() + +class Test_Device_2_mds(unittest.TestCase): + + def setUp(self): + logging.getLogger('sdc').info('############### start setUp {} ##############'.format(self._testMethodName)) + self.wsd = wsdiscovery.WSDiscovery('127.0.0.1') + self.wsd.start() + self.sdc_device = SomeDevice.from_mdib_file(self.wsd, None, 'mdib_two_mds.xml') + self.sdc_device.start_all() + self._locValidators = [pm_types.InstanceIdentifier('Validator', extension_string='System')] + + time.sleep(0.1) # allow full init of device + + print('############### setUp done {} ##############'.format(self._testMethodName)) + logging.getLogger('sdc').info('############### setUp done {} ##############'.format(self._testMethodName)) + + def tearDown(self): + print('############### tearDown {}... ##############'.format(self._testMethodName)) + logging.getLogger('sdc').info('############### tearDown {} ... ##############'.format(self._testMethodName)) + self.sdc_device.stop_all() + self.wsd.stop() + + def test_set_location(self): + """Call of set_location without giving a descriptor handle shall raise a ValueError.""" + # first make sure there is only one LocationContextDescriptor + location_context_descriptors = self.sdc_device.mdib.descriptions.NODETYPE.get( + pm_qnames.LocationContextDescriptor) + self.assertEqual(len(location_context_descriptors), 1) + + context_descriptor_handle = location_context_descriptors[0].Handle + states_count = len(self.sdc_device.mdib.context_states.descriptor_handle.get(context_descriptor_handle, [])) + + self.sdc_device.mdib.xtra.ensure_location_context_descriptor() # this adds descriptor to 2nd mib + # verify that there are now two LocationContextDescriptors + location_context_descriptors = self.sdc_device.mdib.descriptions.NODETYPE.get( + pm_qnames.LocationContextDescriptor) + self.assertEqual(len(location_context_descriptors), 2) + + self.assertRaises(ValueError, self.sdc_device.set_location, utils.random_location(), self._locValidators) + + # with descriptor handle it shall work + self.sdc_device.set_location(utils.random_location(), self._locValidators, + location_context_descriptor_handle=context_descriptor_handle) + states2 = self.sdc_device.mdib.context_states.descriptor_handle.get(context_descriptor_handle) + self.assertEqual(len(states2), states_count + 1) + + def test_ensure_patient_context_descriptor(self): + """Verify that ensure_patient_context_descriptor creates the missing PatientContextDescriptor in 2nd mds.""" + patient_context_descriptors = self.sdc_device.mdib.descriptions.NODETYPE.get( + pm_qnames.PatientContextDescriptor) + self.assertEqual(len(patient_context_descriptors), 1) + self.sdc_device.mdib.xtra.ensure_patient_context_descriptor() + patient_context_descriptors = self.sdc_device.mdib.descriptions.NODETYPE.get( + pm_qnames.PatientContextDescriptor) + self.assertEqual(len(patient_context_descriptors), 2) + + + class Test_Hello_And_Bye(unittest.TestCase): def test_send_hello_and_bye_at_start_and_stop(self): """ diff --git a/tests/test_discovery.py b/tests/test_discovery.py index dd6e6660..c5ee138f 100644 --- a/tests/test_discovery.py +++ b/tests/test_discovery.py @@ -1,4 +1,5 @@ import logging +import selectors import socket import sys import threading @@ -347,7 +348,7 @@ def test_ScopeMatch(self): def test_publishManyServices_lateStartedClient(self): test_log.info('starting service...') self.wsd_service.start() - device_count = 20 + device_count = 1 eprs = [uuid.uuid4().hex for _ in range(device_count)] for i, epr in enumerate(eprs): self.wsd_service.publish_service(epr, @@ -355,7 +356,7 @@ def test_publishManyServices_lateStartedClient(self): scopes=utils.random_scope(), x_addrs=[f"localhost:{8080 + i}/{uuid.uuid4()}"]) - time.sleep(3.02) + time.sleep(10) test_log.info('starting client...') self.wsd_client.start() services = self.wsd_client.search_services(timeout=self.SEARCH_TIMEOUT) @@ -420,3 +421,27 @@ def send_and_assert_running(data): finally: unicast_sock.close() self.log_watcher_service.setPaused(False) + + def test_provider_and_consumer_share_same_udp_binding(self): + """Verify that a provider and a consumer can exchange messages even if they share the same ip and port.""" + self.wsd_service.start() + self.wsd_client.start() + self.wsd_service._networking_thread._outbound_selector.unregister(self.wsd_service._networking_thread.multi_out_uni_in_out) + self.wsd_service._networking_thread._outbound_selector.register(self.wsd_client._networking_thread.multi_out_uni_in_out, selectors.EVENT_WRITE) + time.sleep(0.1) + + ttype1 = [utils.random_qname()] + scopes1 = utils.random_scope() + + addresses = [f"http://localhost:8080/{uuid.uuid4()}", 'http://{ip}/' + str(uuid.uuid4())] + epr = uuid.uuid4().hex + self.wsd_service.publish_service(epr, types=ttype1, scopes=scopes1, x_addrs=addresses) + time.sleep(1) + + services = self.wsd_client.search_services(timeout=self.SEARCH_TIMEOUT) + try: + self.assertTrue(any(s for s in services if s.epr == epr)) + self.assertTrue(epr not in self.wsd_service._remote_services) + finally: + # ensure that ports are swapped back to avoid crashes in teardown + self.wsd_service._networking_thread._outbound_selector.unregister(self.wsd_client._networking_thread.multi_out_uni_in_out) diff --git a/tests/test_operations.py b/tests/test_operations.py index 7d1194ba..594cb600 100644 --- a/tests/test_operations.py +++ b/tests/test_operations.py @@ -101,6 +101,7 @@ def test_set_patient_context_operation(self): # insert a new patient with wrong handle, this shall fail proposed_context = context.mk_proposed_context_object(patient_descriptor_container.Handle) + proposed_context.ContextAssociation = pm_types.ContextAssociation.ASSOCIATED proposed_context.Handle = 'some_nonexisting_handle' proposed_context.CoreData.Givenname = 'Karl' proposed_context.CoreData.Middlename = ['M.'] @@ -119,6 +120,19 @@ def test_set_patient_context_operation(self): state = result.InvocationInfo.InvocationState self.assertEqual(state, msg_types.InvocationState.FAILED) self.assertIsNone(result.OperationTarget) + + # insert two new patients for same descriptor, both associated. This shall fail + proposed_context1 = context.mk_proposed_context_object(patient_descriptor_container.Handle) + proposed_context1.Handle = patient_descriptor_container.Handle + proposed_context1.ContextAssociation = pm_types.ContextAssociation.ASSOCIATED + proposed_context2 = context.mk_proposed_context_object(patient_descriptor_container.Handle) + proposed_context2.Handle = patient_descriptor_container.Handle + proposed_context2.ContextAssociation = pm_types.ContextAssociation.ASSOCIATED + future = context.set_context_state(operation_handle, [proposed_context1, proposed_context2]) + result = future.result(timeout=SET_TIMEOUT) + state = result.InvocationInfo.InvocationState + self.assertEqual(state, msg_types.InvocationState.FAILED) + self.log_watcher.setPaused(False) # insert a new patient with correct handle, this shall succeed @@ -146,6 +160,8 @@ def test_set_patient_context_operation(self): self.assertNotEqual(patient_context_state_container.Handle, patient_descriptor_container.Handle) # device replaced it with its own handle self.assertEqual(patient_context_state_container.ContextAssociation, pm_types.ContextAssociation.ASSOCIATED) + self.assertIsNotNone(patient_context_state_container.BindingMdibVersion) + self.assertIsNotNone(patient_context_state_container.BindingStartTime) # test update of the patient proposed_context = context.mk_proposed_context_object(patient_descriptor_container.Handle, @@ -163,6 +179,7 @@ def test_set_patient_context_operation(self): # set new patient, check binding mdib versions and context association proposed_context = context.mk_proposed_context_object(patient_descriptor_container.Handle) + proposed_context.ContextAssociation = pm_types.ContextAssociation.ASSOCIATED proposed_context.CoreData.Givenname = 'Heidi' proposed_context.CoreData.Middlename = ['M.'] proposed_context.CoreData.Familyname = 'Klammer' @@ -259,11 +276,11 @@ def test_location_context(self): for j, loc in enumerate(dev_locations[:-1]): self.assertEqual(loc.ContextAssociation, pm_types.ContextAssociation.DISASSOCIATED) - self.assertEqual(loc.UnbindingMdibVersion, dev_locations[j + 1].BindingMdibVersion - 1) + self.assertEqual(loc.UnbindingMdibVersion, dev_locations[j + 1].BindingMdibVersion) for j, loc in enumerate(cl_locations[:-1]): self.assertEqual(loc.ContextAssociation, pm_types.ContextAssociation.DISASSOCIATED) - self.assertEqual(loc.UnbindingMdibVersion, cl_locations[j + 1].BindingMdibVersion - 1) + self.assertEqual(loc.UnbindingMdibVersion, cl_locations[j + 1].BindingMdibVersion) def test_audio_pause(self): """Tests AudioPauseProvider @@ -642,7 +659,7 @@ def test_set_metric_value(self): my_operation_descriptor = self.sdc_device.mdib.descriptions.coding.get_one(coding, allow_none=True) operation_handle = my_operation_descriptor.Handle - for value in (Decimal(1), Decimal(42)): + for value in (Decimal(1), Decimal(42), 1.1, 10, "12"): self._logger.info('metric value = %s', value) future = set_service.set_numeric_value(operation_handle=operation_handle, requested_numeric_value=value) @@ -654,4 +671,4 @@ def test_set_metric_value(self): # verify that the corresponding state has been updated state = client_mdib.states.descriptor_handle.get_one(my_operation_descriptor.OperationTarget) - self.assertEqual(state.MetricValue.Value, value) + self.assertEqual(state.MetricValue.Value, Decimal(str(value))) diff --git a/tests/test_pmtypes.py b/tests/test_pmtypes.py index 10b87c36..8649070f 100644 --- a/tests/test_pmtypes.py +++ b/tests/test_pmtypes.py @@ -1,8 +1,8 @@ import unittest from unittest import mock -from lxml.etree import QName, fromstring, tostring +from lxml.etree import QName, Element, fromstring, tostring -from sdc11073.xml_types import pm_types, xml_structure +from sdc11073.xml_types import pm_types, xml_structure, basetypes class TestPmTypes(unittest.TestCase): @@ -384,3 +384,25 @@ def test_operators_of_extension_local_value(self): self.assertNotEqual(inst1, inst2) self.assertFalse(inst1 == inst2) self.assertTrue(inst1 != inst2) + + + def test_element_with_text_list(self): + """Verify that a ElementWithTextList.text is a list even if text of node is None or element does not exist.""" + node = Element('foo') + node.text = 'abc def ghi' + obj = basetypes.ElementWithTextList.from_node(node) + self.assertEqual(obj.text, ['abc', 'def', 'ghi']) + + node.text = None + obj = basetypes.ElementWithTextList.from_node(node) + self.assertEqual(obj.text, []) + + # test the case that the element that is supposed to contain the text does not exist. + obj = basetypes.ElementWithTextList() + mocked = unittest.mock.MagicMock(side_effect=xml_structure.ElementNotFoundError) + with unittest.mock.patch.object(xml_structure.NodeTextListProperty, + '_get_element_by_child_name', + new=mocked): + obj.update_from_node(node) + self.assertEqual(obj.text, []) + mocked.assert_called_once() diff --git a/tests/test_statecontainers.py b/tests/test_statecontainers.py index f1f4ef81..5713dc9a 100644 --- a/tests/test_statecontainers.py +++ b/tests/test_statecontainers.py @@ -72,6 +72,28 @@ def test_AbstractOperationStateContainer(self): state2.update_from_other_container(state) self.assertEqual(state2.OperatingMode, pm_types.OperatingMode.NA) + def test_diff_float(self): + """Verify correct results of ContainerBase.diff() method for float values.""" + sc1 = sc.ClockStateContainer(descriptor_container=self.descr) + sc2 = sc.ClockStateContainer(descriptor_container=self.descr) + sc1.LastSet = 0.0 + sc2.LastSet = 0.0 + # if sc1 is zero, a diff < 1e-6 is considered equal enough + self.assertIsNone(sc1.diff(sc2)) + sc2.LastSet = 1e-7 + self.assertIsNone(sc1.diff(sc2, max_float_diff = 1e-6)) + sc2.LastSet = 1e-5 + self.assertEqual(1, len(sc1.diff(sc2, max_float_diff = 1e-6))) + + # if sc1 is not zero, the value of abs((sc1-sc2)/sc1) < 1e-6 is considered equal enough + sc1.LastSet = 10000.0 + sc2.LastSet = 10000.0 + self.assertIsNone(sc1.diff(sc2)) + sc2.LastSet = 10000.001 + self.assertIsNone(sc1.diff(sc2, max_float_diff = 1e-6)) + sc2.LastSet = 10000.1 + self.assertEqual(1, len(sc1.diff(sc2, max_float_diff = 1e-6))) + def test_AbstractMetricStateContainer(self): descr = dc.NumericMetricDescriptorContainer(handle='123', parent_handle='456') state = sc.NumericMetricStateContainer(descriptor_container=descr) diff --git a/tests/test_transaction.py b/tests/test_transaction.py index b91220b7..1e0d01a0 100644 --- a/tests/test_transaction.py +++ b/tests/test_transaction.py @@ -1,4 +1,4 @@ -import os +import copy import pathlib import unittest @@ -24,23 +24,26 @@ def test_alert_state_update(self): - mdib_version is incremented - StateVersion is incremented in mdib state - updated state is referenced in transaction_result + - observable alert_by_handle is updated - ApiUsageError is thrown if state of wrong kind is added, """ mdib_version = self._mdib.mdib_version - alert_conditions = self._mdib.descriptions.NODETYPE.get(pm_qnames.AlertConditionDescriptor) + alert_condition_handle = self._mdib.descriptions.NODETYPE.get(pm_qnames.AlertConditionDescriptor)[0].Handle metrics = self._mdib.descriptions.NODETYPE.get(pm_qnames.NumericMetricDescriptor) - old_state = self._mdib.states.descriptor_handle.get_one(alert_conditions[0].Handle).mk_copy() + old_state = self._mdib.states.descriptor_handle.get_one(alert_condition_handle).mk_copy() state_version = old_state.StateVersion with self._mdib.alert_state_transaction() as mgr: - state = mgr.get_state(alert_conditions[0].Handle) + state = mgr.get_state(alert_condition_handle) state.Presence = True self.assertEqual(mdib_version + 1, self._mdib.mdib_version) - updated_state = self._mdib.states.descriptor_handle.get_one(alert_conditions[0].Handle) + updated_state = self._mdib.states.descriptor_handle.get_one(alert_condition_handle) self.assertEqual(state_version + 1, updated_state.StateVersion) transaction_result = self._mdib.transaction self.assertEqual(len(transaction_result.alert_updates), 1) # this causes an EpisodicAlertReport self.assertEqual(state_version + 1, transaction_result.alert_updates[0].StateVersion) + self.assertTrue(alert_condition_handle in self._mdib.alert_by_handle) + with self._mdib.alert_state_transaction() as mgr: self.assertRaises(ApiUsageError, mgr.get_state, metrics[0].Handle) self.assertEqual(mdib_version + 1, self._mdib.mdib_version) @@ -51,27 +54,30 @@ def test_metric_state_update(self): - mdib_version is incremented - StateVersion is incremented in mdib state - updated state is referenced in transaction_result + - observable metric_by_handle is updated - ApiUsageError is thrown if state of wrong kind is added, """ mdib_version = self._mdib.mdib_version - alert_conditions = self._mdib.descriptions.NODETYPE.get(pm_qnames.AlertConditionDescriptor) - metrics = self._mdib.descriptions.NODETYPE.get(pm_qnames.NumericMetricDescriptor) - old_state = self._mdib.states.descriptor_handle.get_one(metrics[0].Handle).mk_copy() + alert_condition_handle = self._mdib.descriptions.NODETYPE.get(pm_qnames.AlertConditionDescriptor)[0].Handle + metric_handle = self._mdib.descriptions.NODETYPE.get(pm_qnames.NumericMetricDescriptor)[0].Handle + old_state = self._mdib.states.descriptor_handle.get_one(metric_handle).mk_copy() state_version = old_state.StateVersion with self._mdib.metric_state_transaction() as mgr: - state = mgr.get_state(metrics[0].Handle) + state = mgr.get_state(metric_handle) state.LifeTimePeriod = 2 self.assertEqual(mdib_version + 1, self._mdib.mdib_version) - updated_state = self._mdib.states.descriptor_handle.get_one(metrics[0].Handle) + updated_state = self._mdib.states.descriptor_handle.get_one(metric_handle) self.assertEqual(state_version + 1, updated_state.StateVersion) transaction_result = self._mdib.transaction self.assertEqual(len(transaction_result.metric_updates), 1) self.assertEqual(state_version + 1, transaction_result.metric_updates[0].StateVersion) + self.assertTrue(metric_handle in self._mdib.metrics_by_handle) + with self._mdib.metric_state_transaction() as mgr: - self.assertRaises(ApiUsageError, mgr.get_state, alert_conditions[0].Handle) + self.assertRaises(ApiUsageError, mgr.get_state, alert_condition_handle) self.assertEqual(mdib_version + 1, self._mdib.mdib_version) def test_operational_state_update(self): @@ -80,27 +86,31 @@ def test_operational_state_update(self): - mdib_version is incremented - StateVersion is incremented in mdib state - updated state is referenced in transaction_result + - observable operation_by_handle is updated - ApiUsageError is thrown if state of wrong kind is added, """ mdib_version = self._mdib.mdib_version - op_descriptors = self._mdib.descriptions.NODETYPE.get(pm_qnames.SetAlertStateOperationDescriptor) - metrics = self._mdib.descriptions.NODETYPE.get(pm_qnames.NumericMetricDescriptor) - old_state = self._mdib.states.descriptor_handle.get_one(op_descriptors[0].Handle).mk_copy() + op_descriptor_handle = self._mdib.descriptions.NODETYPE.get( + pm_qnames.SetAlertStateOperationDescriptor)[0].Handle + metric_handle = self._mdib.descriptions.NODETYPE.get(pm_qnames.NumericMetricDescriptor)[0].Handle + old_state = self._mdib.states.descriptor_handle.get_one(op_descriptor_handle).mk_copy() state_version = old_state.StateVersion with self._mdib.operational_state_transaction() as mgr: - state = mgr.get_state(op_descriptors[0].Handle) + state = mgr.get_state(op_descriptor_handle) state.OperationMode = pm_types.OperatingMode.DISABLED self.assertEqual(mdib_version + 1, self._mdib.mdib_version) - updated_state = self._mdib.states.descriptor_handle.get_one(op_descriptors[0].Handle) + updated_state = self._mdib.states.descriptor_handle.get_one(op_descriptor_handle) self.assertEqual(state_version + 1, updated_state.StateVersion) transaction_result = self._mdib.transaction self.assertEqual(len(transaction_result.op_updates), 1) self.assertEqual(state_version + 1, transaction_result.op_updates[0].StateVersion) + self.assertTrue(op_descriptor_handle in self._mdib.operation_by_handle) + with self._mdib.operational_state_transaction() as mgr: - self.assertRaises(ApiUsageError, mgr.get_state, metrics[0].Handle) + self.assertRaises(ApiUsageError, mgr.get_state, metric_handle) self.assertEqual(mdib_version + 1, self._mdib.mdib_version) def text_context_state_transaction(self): @@ -110,73 +120,143 @@ def text_context_state_transaction(self): - mdib_version is incremented - StateVersion is incremented in mdib state - updated state is referenced in transaction_result + - observable context_by_handle is updated - ApiUsageError is thrown if state of wrong kind is added """ mdib_version = self._mdib.mdib_version - location_descr = self._mdib.descriptions.NODETYPE.get(pm_qnames.LocationContextDescriptor) + location_descr_handle = self._mdib.descriptions.NODETYPE.get(pm_qnames.LocationContextDescriptor)[0].Handle with self._mdib.context_state_transaction() as mgr: - state = mgr.mk_context_state(location_descr[0].Handle) + state = mgr.mk_context_state(location_descr_handle) state.Givenname = 'foo' state.Familyname = 'bar' self.assertIsNotNone(state.Handle) self.assertEqual(mdib_version + 1, self._mdib.mdib_version) - transaction_processor = self._mdib.transaction - self.assertEqual(len(transaction_processor.ctxt_updates), 1) - self.assertEqual(transaction_processor.ctxt_updates[0].StateVersion, 0) - self.assertEqual(transaction_processor.ctxt_updates[0].Givenname, 'foo') - self.assertEqual(transaction_processor.ctxt_updates[0].Familyname, 'bar') + transaction_result = self._mdib.transaction + self.assertEqual(len(transaction_result.ctxt_updates), 1) + self.assertEqual(transaction_result.ctxt_updates[0].StateVersion, 0) + self.assertEqual(transaction_result.ctxt_updates[0].Givenname, 'foo') + self.assertEqual(transaction_result.ctxt_updates[0].Familyname, 'bar') - handle = transaction_processor.ctxt_updates[0].Handle + handle = transaction_result.ctxt_updates[0].Handle with self._mdib.context_state_transaction() as mgr: state = mgr.get_context_state(handle) self.assertEqual(mdib_version + 2, self._mdib.mdib_version) - transaction_processor = self._mdib.transaction - self.assertEqual(len(transaction_processor.ctxt_updates), 1) + transaction_result = self._mdib.transaction + self.assertEqual(len(transaction_result.ctxt_updates), 1) + + self.assertTrue(location_descr_handle in self._mdib.context_by_handle) metrics_handle = self._mdib.descriptions.NODETYPE.get(pm_qnames.NumericMetricDescriptor)[0].Handle with self._mdib.context_state_transaction() as mgr: self.assertRaises(ApiUsageError, mgr.get_context_state, metrics_handle) def test_description_modification(self): + """Verify that descriptor_transaction works as expected. + + - mdib_version is incremented + - observable updated_descriptors_by_handle is updated + - corresponding states for descriptor modifications are also updated + - ApiUsageError is thrown if data of wrong kind is requested + """ mdib_version = self._mdib.mdib_version - alert_conditions = self._mdib.descriptions.NODETYPE.get(pm_qnames.AlertConditionDescriptor) - metrics = self._mdib.descriptions.NODETYPE.get(pm_qnames.NumericMetricDescriptor) - operational_descr = self._mdib.descriptions.NODETYPE.get(pm_qnames.SetAlertStateOperationDescriptor) - component_descr = self._mdib.descriptions.NODETYPE.get(pm_qnames.ChannelDescriptor) - rt_descr = self._mdib.descriptions.NODETYPE.get(pm_qnames.RealTimeSampleArrayMetricDescriptor) - context_descr = self._mdib.descriptions.NODETYPE.get(pm_qnames.PatientContextDescriptor) + alert_condition_handle = self._mdib.descriptions.NODETYPE.get(pm_qnames.AlertConditionDescriptor)[0].Handle + metric_handle = self._mdib.descriptions.NODETYPE.get(pm_qnames.NumericMetricDescriptor)[0].Handle + operational_descr_handle = self._mdib.descriptions.NODETYPE.get( + pm_qnames.SetAlertStateOperationDescriptor)[0].Handle + component_descr_handle = self._mdib.descriptions.NODETYPE.get(pm_qnames.ChannelDescriptor)[0].Handle + rt_descr_handle = self._mdib.descriptions.NODETYPE.get(pm_qnames.RealTimeSampleArrayMetricDescriptor)[0].Handle + context_descr_handle = self._mdib.descriptions.NODETYPE.get(pm_qnames.PatientContextDescriptor)[0].Handle with self._mdib.descriptor_transaction() as mgr: # verify that updating descriptors of different kinds and accessing corresponding states works - mgr.get_descriptor(alert_conditions[0].Handle) - mgr.get_state(alert_conditions[0].Handle) - mgr.get_descriptor(metrics[0].Handle) - mgr.get_state(metrics[0].Handle) - mgr.get_descriptor(operational_descr[0].Handle) - mgr.get_state(operational_descr[0].Handle) - mgr.get_descriptor(component_descr[0].Handle) - mgr.get_state(component_descr[0].Handle) - mgr.get_descriptor(rt_descr[0].Handle) - mgr.get_state(rt_descr[0].Handle) + mgr.get_descriptor(alert_condition_handle) + mgr.get_state(alert_condition_handle) + mgr.get_descriptor(metric_handle) + mgr.get_state(metric_handle) + mgr.get_descriptor(operational_descr_handle) + mgr.get_state(operational_descr_handle) + mgr.get_descriptor(component_descr_handle) + mgr.get_state(component_descr_handle) + mgr.get_descriptor(rt_descr_handle) + mgr.get_state(rt_descr_handle) self.assertEqual(mdib_version + 1, self._mdib.mdib_version) - transaction_processor = self._mdib.transaction - self.assertEqual(len(transaction_processor.metric_updates), 1) - self.assertEqual(len(transaction_processor.alert_updates), 1) - self.assertEqual(len(transaction_processor.op_updates), 1) - self.assertEqual(len(transaction_processor.comp_updates), 1) - self.assertEqual(len(transaction_processor.rt_updates), 1) - self.assertEqual(len(transaction_processor.descr_updated), 5) + transaction_result = self._mdib.transaction + self.assertEqual(len(transaction_result.metric_updates), 1) + self.assertEqual(len(transaction_result.alert_updates), 1) + self.assertEqual(len(transaction_result.op_updates), 1) + self.assertEqual(len(transaction_result.comp_updates), 1) + self.assertEqual(len(transaction_result.rt_updates), 1) + self.assertEqual(len(transaction_result.descr_updated), 5) + + self.assertTrue(alert_condition_handle in self._mdib.updated_descriptors_by_handle) + self.assertTrue(alert_condition_handle in self._mdib.alert_by_handle) + self.assertTrue(metric_handle in self._mdib.updated_descriptors_by_handle) + self.assertTrue(metric_handle in self._mdib.metrics_by_handle) + self.assertTrue(operational_descr_handle in self._mdib.updated_descriptors_by_handle) + self.assertTrue(operational_descr_handle in self._mdib.operation_by_handle) + self.assertTrue(component_descr_handle in self._mdib.updated_descriptors_by_handle) + self.assertTrue(component_descr_handle in self._mdib.component_by_handle) + self.assertTrue(rt_descr_handle in self._mdib.updated_descriptors_by_handle) + # verify that accessing a state for that the descriptor is not part of transaction is not allowed with self._mdib.descriptor_transaction() as mgr: - mgr.get_descriptor(alert_conditions[0].Handle) - self.assertRaises(ApiUsageError, mgr.get_state, metrics[0].Handle) + mgr.get_descriptor(alert_condition_handle) + self.assertRaises(ApiUsageError, mgr.get_state, metric_handle) self.assertEqual(mdib_version + 2, self._mdib.mdib_version) # verify that get_state for a context state raises an ApiUsageError with self._mdib.descriptor_transaction() as mgr: - mgr.get_descriptor(context_descr[0].Handle) - self.assertRaises(ApiUsageError, mgr.get_state, context_descr[0].Handle) + mgr.get_descriptor(context_descr_handle) + self.assertRaises(ApiUsageError, mgr.get_state, context_descr_handle) + + def test_remove_add(self): + """Verify that removing descriptors / states and adding them later again results in correct versions.""" + descriptors = {descr.Handle: descr.mk_copy() for descr in self._mdib.descriptions.objects} + states = {state.DescriptorHandle: state.mk_copy() for state in self._mdib.states.objects} + context_states = {state.Handle: state.mk_copy() for state in self._mdib.context_states.objects} + + # remove all root descriptors + root_descr = self._mdib.descriptions.parent_handle.get(None) + with self._mdib.descriptor_transaction() as mgr: + for descr in root_descr: + mgr.remove_descriptor(descr.Handle) + + self.assertEqual(0, len(self._mdib.descriptions.objects)) + self.assertEqual(0, len(self._mdib.states.objects)) + self.assertEqual(0, len(self._mdib.context_states.objects)) + + with self._mdib.descriptor_transaction() as mgr: + for descr in descriptors.values(): + mgr.add_descriptor(descr.mk_copy()) + for state in states.values(): + mgr.add_state(state.mk_copy()) + for state in context_states.values(): + mgr.add_state(state.mk_copy()) + + current_descriptors = {descr.Handle: descr.mk_copy() for descr in self._mdib.descriptions.objects} + + # verify that all descriptors have incremented version + for descr in self._mdib.descriptions.objects: + self.assertEqual(descr.DescriptorVersion, current_descriptors[descr.Handle].DescriptorVersion) + + # verify that all states have incremented version + for state in self._mdib.states.objects: + self.assertEqual(state.DescriptorVersion, current_descriptors[state.DescriptorHandle].DescriptorVersion) + self.assertEqual(state.StateVersion, states[state.DescriptorHandle].StateVersion + 1) + + # verify that all context states have incremented version + for state in self._mdib.context_states.objects: + self.assertEqual(state.DescriptorVersion, current_descriptors[state.DescriptorHandle].DescriptorVersion) + self.assertEqual(state.StateVersion, context_states[state.Handle].StateVersion + 1) + + # verify transaction content is als correct + transaction_result = self._mdib.transaction + for descr in transaction_result.descr_created: + self.assertEqual(descr.DescriptorVersion, current_descriptors[descr.Handle].DescriptorVersion) + + for state in transaction_result.all_states(): + self.assertEqual(state.DescriptorVersion, current_descriptors[state.DescriptorHandle].DescriptorVersion) diff --git a/tutorial/provider/provider.py b/tutorial/provider/provider.py index 9e41e815..7e0a4340 100644 --- a/tutorial/provider/provider.py +++ b/tutorial/provider/provider.py @@ -1,11 +1,12 @@ from __future__ import annotations +import logging import time import uuid -import logging from decimal import Decimal from sdc11073.location import SdcLocation +from sdc11073.loghelper import basic_logging_setup from sdc11073.mdib import ProviderMdib from sdc11073.provider import SdcProvider from sdc11073.provider.components import SdcProviderComponents @@ -13,8 +14,8 @@ from sdc11073.wsdiscovery import WSDiscoverySingleAdapter from sdc11073.xml_types import pm_qnames as pm from sdc11073.xml_types import pm_types -from sdc11073.xml_types.dpws_types import ThisDeviceType, ThisModelType -from sdc11073.loghelper import basic_logging_setup +from sdc11073.xml_types.dpws_types import ThisDeviceType +from sdc11073.xml_types.dpws_types import ThisModelType # example SDC provider (device) that sends out metrics every now and then @@ -32,7 +33,7 @@ def set_local_ensemble_context(mdib: ProviderMdib, ensemble_extension_string: st print("No ensemble contexts in mdib") return all_ensemble_context_states = mdib.context_states.descriptor_handle.get(descriptor_container.Handle, []) - with mdib.transaction_manager() as mgr: + with mdib.context_state_transaction() as mgr: # set all to currently associated Locations to Disassociated associated_ensemble_context_states = [l for l in all_ensemble_context_states if l.ContextAssociation == pm_types.ContextAssociation.ASSOCIATED] @@ -61,14 +62,14 @@ def set_local_ensemble_context(mdib: ProviderMdib, ensemble_extension_string: st my_location = SdcLocation(fac='HOSP', poc='CU2', bed='BedSim') # set model information for discovery dpws_model = ThisModelType(manufacturer='Draeger', - manufacturer_url='www.draeger.com', - model_name='TestDevice', - model_number='1.0', - model_url='www.draeger.com/model', - presentation_url='www.draeger.com/model/presentation') + manufacturer_url='www.draeger.com', + model_name='TestDevice', + model_number='1.0', + model_url='www.draeger.com/model', + presentation_url='www.draeger.com/model/presentation') dpws_device = ThisDeviceType(friendly_name='TestDevice', - firmware_version='Version1', - serial_number='12345') + firmware_version='Version1', + serial_number='12345') # create a device (provider) class that will do all the SDC magic # set role provider that supports Ensemble Contexts. specific_components = SdcProviderComponents(role_provider_class=ExtendedProduct) @@ -89,10 +90,10 @@ def set_local_ensemble_context(mdib: ProviderMdib, ensemble_extension_string: st # get all metrics from the mdib (as described in the file) all_metric_descrs = [c for c in my_mdib.descriptions.objects if c.NODETYPE == pm.NumericMetricDescriptor] # now change all the metrics in one transaction - with my_mdib.transaction_manager() as mgr: + with my_mdib.metric_state_transaction() as transaction_mgr: for metric_descr in all_metric_descrs: # get the metric state of this specific metric - st = mgr.get_state(metric_descr.Handle) + st = transaction_mgr.get_state(metric_descr.Handle) # create a value in case it is not there yet st.mk_metric_value() # set the value and some other fields to a fixed value @@ -105,8 +106,8 @@ def set_local_ensemble_context(mdib: ProviderMdib, ensemble_extension_string: st metric_value = 0 while True: metric_value += 1 - with my_mdib.transaction_manager() as mgr: + with my_mdib.metric_state_transaction() as transaction_mgr: for metricDescr in all_metric_descrs: - st = mgr.get_state(metricDescr.Handle) + st = transaction_mgr.get_state(metricDescr.Handle) st.MetricValue.Value = Decimal(metric_value) time.sleep(5) From 5d1fba7e26baf6c427b795ef5cc05c2d5278dc45 Mon Sep 17 00:00:00 2001 From: "Budnick, Leon" Date: Thu, 14 Nov 2024 14:16:35 +0100 Subject: [PATCH 06/16] fix ruff findings --- examples/ReferenceTestV2/discoproxyclient.py | 328 +++++---- .../ReferenceTestV2/reference_consumer_v2.py | 635 ++++++++++-------- .../ReferenceTestV2/reference_provider_v2.py | 348 ++++++---- 3 files changed, 731 insertions(+), 580 deletions(-) diff --git a/examples/ReferenceTestV2/discoproxyclient.py b/examples/ReferenceTestV2/discoproxyclient.py index 1414c8b3..b093358a 100644 --- a/examples/ReferenceTestV2/discoproxyclient.py +++ b/examples/ReferenceTestV2/discoproxyclient.py @@ -5,12 +5,14 @@ This client connects to that proxy. """ + from __future__ import annotations import os +import pathlib import random import time -from typing import TYPE_CHECKING, Iterable +from typing import TYPE_CHECKING from uuid import UUID from sdc11073.certloader import mk_ssl_contexts_from_folder @@ -19,86 +21,96 @@ from sdc11073.dispatch import MessageConverterMiddleware from sdc11073.httpserver.httpserverimpl import HttpServerThreadBase from sdc11073.location import SdcLocation -from sdc11073.loghelper import get_logger_adapter, basic_logging_setup +from sdc11073.loghelper import LoggerAdapter, basic_logging_setup, get_logger_adapter from sdc11073.mdib import ProviderMdib from sdc11073.namespaces import EventingActions from sdc11073.namespaces import default_ns_helper as nsh from sdc11073.provider import SdcProvider -from sdc11073.pysoap.msgfactory import CreatedMessage -from sdc11073.pysoap.msgfactory import MessageFactory -from sdc11073.pysoap.msgreader import MessageReader -from sdc11073.pysoap.soapclient import Fault -from sdc11073.pysoap.soapclient import SoapClient +from sdc11073.pysoap.msgfactory import CreatedMessage, MessageFactory +from sdc11073.pysoap.msgreader import MessageReader, ReceivedMessage +from sdc11073.pysoap.soapclient import Fault, SoapClient from sdc11073.wsdiscovery.wsdimpl import Service -from sdc11073.xml_types import wsd_types, pm_types, eventing_types +from sdc11073.xml_types import eventing_types, pm_types, wsd_types from sdc11073.xml_types.addressing_types import HeaderInformationBlock from sdc11073.xml_types.dpws_types import ThisDeviceType, ThisModelType if TYPE_CHECKING: + from collections.abc import Iterable + from lxml.etree import QName + from sdc11073.certloader import SSLContextContainer - from sdc11073.xml_types.basetypes import MessageType from sdc11073.dispatch.request import RequestData + from sdc11073.xml_types.basetypes import MessageType -message_factory = MessageFactory(SdcV1Definitions, None, logger=get_logger_adapter('sdc.disco.msg')) -message_reader = MessageReader(SdcV1Definitions, None, logger=get_logger_adapter('sdc.disco.msg')) +message_factory = MessageFactory(SdcV1Definitions, None, logger=get_logger_adapter("sdc.disco.msg")) +message_reader = MessageReader(SdcV1Definitions, None, logger=get_logger_adapter("sdc.disco.msg")) ADDRESS_ALL = "urn:docs-oasis-open-org:ws-dd:ns:discovery:2009:01" # format acc to RFC 2141 -def _mk_wsd_soap_message(header_info: HeaderInformationBlock, - payload: MessageType) -> CreatedMessage: +def _mk_wsd_soap_message(header_info: HeaderInformationBlock, payload: MessageType) -> CreatedMessage: # use discovery specific namespaces - return message_factory.mk_soap_message(header_info, payload, - ns_list=[nsh.S12, nsh.WSA, nsh.WSD], use_defaults=False) + return message_factory.mk_soap_message( + header_info, + payload, + ns_list=[nsh.S12, nsh.WSA, nsh.WSD], + use_defaults=False, + ) class DiscoProxyClient: - def __init__(self, - disco_proxy_address: str, - my_address: str, - ssl_context_container: SSLContextContainer | None = None): + """Discovery proxy consumer.""" + + def __init__( + self, + disco_proxy_address: str, + my_address: str, + ssl_context_container: SSLContextContainer | None = None, + ): self._proxy_address = disco_proxy_address self._my_address = my_address self._ssl_context_container = ssl_context_container - self._logger = get_logger_adapter('sdc.disco') + self._logger = get_logger_adapter("sdc.disco") self._local_services: dict[str, Service] = {} self._remote_services: dict[str, Service] = {} ssl_context = None if ssl_context_container is None else ssl_context_container.client_context - self._soap_client = SoapClient(disco_proxy_address, - socket_timeout=5, - logger=get_logger_adapter('sdc.disco.client'), - ssl_context=ssl_context, - sdc_definitions=SdcV1Definitions, - msg_reader=message_reader - ) + self._soap_client = SoapClient( + disco_proxy_address, + socket_timeout=5, + logger=get_logger_adapter("sdc.disco.client"), + ssl_context=ssl_context, + sdc_definitions=SdcV1Definitions, + msg_reader=message_reader, + ) self._http_server = HttpServerThreadBase( my_address, ssl_context_container.server_context if ssl_context_container else None, - logger=get_logger_adapter('sdc.disco.httpsrv'), - supported_encodings=['gzip'], + logger=get_logger_adapter("sdc.disco.httpsrv"), + supported_encodings=["gzip"], ) - self._msg_converter = MessageConverterMiddleware( - message_reader, message_factory, self._logger, self) + self._msg_converter = MessageConverterMiddleware(message_reader, message_factory, self._logger, self) self._my_server_port = None self.subscribe_response = None - def start(self, subscribe=True): + def start(self, subscribe: bool = True): + """Subscribe.""" # first start http server, the services need to know the ip port number self._http_server.start() event_is_set = self._http_server.started_evt.wait(timeout=15.0) if not event_is_set: - self._logger.error('Cannot start device, start event of http server not set.') - raise RuntimeError('Cannot start device, start event of http server not set.') + self._logger.error("Cannot start device, start event of http server not set.") + raise RuntimeError("Cannot start device, start event of http server not set.") self._my_server_port = self._http_server.my_port - self._http_server.dispatcher.register_instance('', self._msg_converter) + self._http_server.dispatcher.register_instance("", self._msg_converter) if subscribe: self.send_subscribe() - def stop(self, unsubscribe=False): + def stop(self, unsubscribe: bool = False): + """Unsubscribe.""" # it seems that unsubscribe is not supported if unsubscribe: self.send_unsubscribe() @@ -109,12 +121,15 @@ def get_active_addresses(self) -> list[str]: # TODO: do not return list return [self._my_address] - def search_services(self, - types: Iterable[QName] | None = None, - scopes: wsd_types.ScopesType | None = None) -> list[Service]: + def search_services( + self, + types: Iterable[QName] | None = None, + scopes: wsd_types.ScopesType | None = None, + ) -> list[Service]: """Send a Probe message. - Update known services with found services. Return list of services found in probe response.""" + Update known services with found services. Return list of services found in probe response. + """ payload = wsd_types.ProbeType() payload.Types = types if scopes is not None: @@ -122,41 +137,41 @@ def search_services(self, inf = HeaderInformationBlock(action=payload.action, addr_to=ADDRESS_ALL) created_message = _mk_wsd_soap_message(inf, payload) - received_message = self._soap_client.post_message_to('', created_message) + received_message = self._soap_client.post_message_to("", created_message) probe_response = wsd_types.ProbeMatchesType.from_node(received_message.p_msg.msg_node) result = [] for probe_match in probe_response.ProbeMatch: - service = Service(types=probe_match.Types, - scopes=probe_match.Scopes, - x_addrs=probe_match.XAddrs, - epr=probe_match.EndpointReference.Address, - instance_id='', - metadata_version=probe_match.MetadataVersion) + service = Service( + types=probe_match.Types, + scopes=probe_match.Scopes, + x_addrs=probe_match.XAddrs, + epr=probe_match.EndpointReference.Address, + instance_id="", + metadata_version=probe_match.MetadataVersion, + ) self._remote_services[service.epr] = service result.append(service) return result - def send_resolve(self, epr) -> wsd_types.ResolveMatchesType: + def send_resolve(self, epr: str) -> wsd_types.ResolveMatchesType: + """Send resolve.""" payload = wsd_types.ResolveType() payload.EndpointReference.Address = epr inf = HeaderInformationBlock(action=payload.action, addr_to=ADDRESS_ALL) created_message = _mk_wsd_soap_message(inf, payload) - received_message = self._soap_client.post_message_to('', created_message) - resolve_response = wsd_types.ResolveMatchesType.from_node(received_message.p_msg.msg_node) - return resolve_response + received_message = self._soap_client.post_message_to("", created_message) + return wsd_types.ResolveMatchesType.from_node(received_message.p_msg.msg_node) def clear_remote_services(self): """Clear remotely discovered services.""" self._remote_services.clear() - def publish_service(self, epr: str, - types: list[QName], - scopes: wsd_types.ScopesType, - x_addrs: list[str]): + def publish_service(self, epr: str, types: list[QName], scopes: wsd_types.ScopesType, x_addrs: list[str]): + """Publish services.""" metadata_version = 1 - instance_id = str(random.randint(1, 0xFFFFFFFF)) # noqa: S311 + instance_id = str(random.randint(1, 0xFFFFFFFF)) service = Service(types, scopes, x_addrs, epr, instance_id, metadata_version=metadata_version) - self._logger.info('publishing %r', service) + self._logger.info("publishing %r", service) self._local_services[epr] = service service.increment_message_number() @@ -173,26 +188,30 @@ def publish_service(self, epr: str, inf = HeaderInformationBlock(action=payload.action, addr_to=ADDRESS_ALL) created_message = _mk_wsd_soap_message(inf, payload) - created_message.p_msg.add_header_element(app_sequence.as_etree_node(nsh.WSD.tag('AppSequence'), - ns_map=nsh.partial_map(nsh.WSD))) + created_message.p_msg.add_header_element( + app_sequence.as_etree_node(nsh.WSD.tag("AppSequence"), ns_map=nsh.partial_map(nsh.WSD)), + ) created_message = _mk_wsd_soap_message(inf, payload) - self._soap_client.post_message_to('', created_message) + self._soap_client.post_message_to("", created_message) - def send_subscribe(self): + def send_subscribe(self) -> ReceivedMessage: + """Send subscribe message.""" subscribe_request = eventing_types.Subscribe() - subscribe_request.Delivery.NotifyTo.Address = f'https://{self._my_address}:{self._my_server_port}' + subscribe_request.Delivery.NotifyTo.Address = f"https://{self._my_address}:{self._my_server_port}" subscribe_request.Expires = 3600 - subscribe_request.set_filter('', dialect='http://discoproxy') + subscribe_request.set_filter("", dialect="http://discoproxy") inf = HeaderInformationBlock(action=subscribe_request.action, addr_to=ADDRESS_ALL) created_message = _mk_wsd_soap_message(inf, subscribe_request) - received_message = self._soap_client.post_message_to('', created_message) + received_message = self._soap_client.post_message_to("", created_message) response_action = received_message.action if response_action == EventingActions.SubscribeResponse: self.subscribe_response = eventing_types.SubscribeResponse.from_node(received_message.p_msg.msg_node) elif response_action == Fault.NODETYPE: fault = Fault.from_node(received_message.p_msg.msg_node) self._logger.error( # noqa: PLE1205 - 'subscribe: Fault response : {}', fault) + "subscribe: Fault response : {}", + fault, + ) return received_message def send_unsubscribe(self): @@ -203,33 +222,42 @@ def send_unsubscribe(self): request = eventing_types.Unsubscribe() dev_reference_param = subscribe_response.SubscriptionManager.ReferenceParameters subscription_manager_address = subscribe_response.SubscriptionManager.Address - inf = HeaderInformationBlock(action=request.action, - addr_to=subscription_manager_address, - reference_parameters=dev_reference_param) + inf = HeaderInformationBlock( + action=request.action, + addr_to=subscription_manager_address, + reference_parameters=dev_reference_param, + ) message = message_factory.mk_soap_message(inf, payload=request) - received_message_data = self._soap_client.post_message_to('', message, msg='unsubscribe') + received_message_data = self._soap_client.post_message_to("", message, msg="unsubscribe") response_action = received_message_data.action # check response: response does not contain explicit status. If action== UnsubscribeResponse all is fine. if response_action == EventingActions.UnsubscribeResponse: self._logger.info( # noqa: PLE1205 - 'unsubscribe: end of subscription {} was confirmed.', self.notification_url) + "unsubscribe: end of subscription {} was confirmed.", + self.notification_url, + ) elif response_action == Fault.NODETYPE: fault = Fault.from_node(received_message_data.p_msg.msg_node) self._logger.error( # noqa: PLE1205 - 'unsubscribe: Fault response : {}', fault) + "unsubscribe: Fault response : {}", + fault, + ) else: self._logger.error( # noqa: PLE1205 - 'unsubscribe: unexpected response action: {}', received_message_data.p_msg.raw_data) - raise ValueError(f'unsubscribe: unexpected response action: {received_message_data.p_msg.raw_data}') + "unsubscribe: unexpected response action: {}", + received_message_data.p_msg.raw_data, + ) + raise ValueError(f"unsubscribe: unexpected response action: {received_message_data.p_msg.raw_data}") def clear_service(self, epr: str): + """Clear services.""" service = self._local_services[epr] self._send_bye(service) del self._local_services[epr] def _send_bye(self, service: Service): - self._logger.debug('sending bye on %s', service) + self._logger.debug("sending bye on %s", service) bye = wsd_types.ByeType() bye.EndpointReference.Address = service.epr @@ -241,101 +269,110 @@ def _send_bye(self, service: Service): app_sequence.MessageNumber = service.message_number created_message = _mk_wsd_soap_message(inf, bye) - created_message.p_msg.add_header_element(app_sequence.as_etree_node(nsh.WSD.tag('AppSequence'), - ns_map=nsh.partial_map(nsh.WSD))) + created_message.p_msg.add_header_element( + app_sequence.as_etree_node(nsh.WSD.tag("AppSequence"), ns_map=nsh.partial_map(nsh.WSD)), + ) created_message = _mk_wsd_soap_message(inf, bye) - received_message = self._soap_client.post_message_to('', created_message) - # hello_response = wsd_types.HelloType.from_node(received_message.p_msg.msg_node) - # print(hello_response) + received_message = self._soap_client.post_message_to("", created_message) # noqa: F841 + # hello_response = wsd_types.HelloType.from_node(received_message.p_msg.msg_node) # noqa: ERA001 + # print(hello_response)# noqa: ERA001 def on_post(self, request_data: RequestData) -> CreatedMessage: - print('on_post') + """On post.""" + print("on_post") if request_data.message_data.action == wsd_types.HelloType.action: hello = wsd_types.HelloType.from_node(request_data.message_data.p_msg.msg_node) - service = Service(types=hello.Types, - scopes=hello.Scopes, - x_addrs=hello.XAddrs, - epr=hello.EndpointReference.Address, - instance_id='', # Todo: needed in any way? - metadata_version=hello.MetadataVersion) + service = Service( + types=hello.Types, + scopes=hello.Scopes, + x_addrs=hello.XAddrs, + epr=hello.EndpointReference.Address, + instance_id="", # Todo: needed in any way? + metadata_version=hello.MetadataVersion, + ) self._remote_services[service.epr] = service - self._logger.info('hello epr = %s, xaddrs =%r', service.epr, service.x_addrs) + self._logger.info("hello epr = %s, xaddrs =%r", service.epr, service.x_addrs) elif request_data.message_data.action == wsd_types.ByeType.action: bye = wsd_types.ByeType.from_node(request_data.message_data.p_msg.msg_node) epr = bye.EndpointReference.Address - self._logger.info('bye epr = %s, xaddrs =%r', epr, bye.XAddrs) + self._logger.info("bye epr = %s, xaddrs =%r", epr, bye.XAddrs) if epr in self._remote_services: del self._remote_services[epr] - self._logger.info('removed %s from known remote services') + self._logger.info("removed %s from known remote services") else: - self._logger.info('unknown remote service %s', epr) + self._logger.info("unknown remote service %s", epr) return EmptyResponse() - def on_get(self, request_data: RequestData) -> CreatedMessage: - print('on_get') + def on_get(self, _: RequestData) -> CreatedMessage: + """On get.""" + print("on_get") return EmptyResponse() +if __name__ == "__main__": - - -if __name__ == '__main__': - - def mk_provider(wsd: DiscoProxyClient, mdib_path: str, uuid_str: str, ssl_contexts: SSLContextContainer): + def mk_provider( + wsd: DiscoProxyClient, mdib_path: str, uuid_str: str, ssl_contexts: SSLContextContainer + ) -> SdcProvider: + """Create sdc provider.""" my_mdib = ProviderMdib.from_mdib_file(mdib_path) - print("UUID for this device is {}".format(uuid_str)) - dpwsModel = ThisModelType(manufacturer='sdc11073', - manufacturer_url='www.sdc11073.com', - model_name='TestDevice', - model_number='1.0', - model_url='www.sdc11073.com/model', - presentation_url='www.sdc11073.com/model/presentation') - - dpwsDevice = ThisDeviceType(friendly_name='TestDevice', - firmware_version='Version1', - serial_number='12345') - specific_components = None - sdc_provider = SdcProvider(wsd, dpwsModel, dpwsDevice, my_mdib, UUID(uuid_str), - ssl_context_container=ssl_contexts, - specific_components=specific_components, - max_subscription_duration=15 - ) - return sdc_provider + print(f"UUID for this device is {uuid_str}") + dpws_model = ThisModelType( + manufacturer="sdc11073", + manufacturer_url="www.sdc11073.com", + model_name="TestDevice", + model_number="1.0", + model_url="www.sdc11073.com/model", + presentation_url="www.sdc11073.com/model/presentation", + ) + dpws_device = ThisDeviceType(friendly_name="TestDevice", firmware_version="Version1", serial_number="12345") + specific_components = None + return SdcProvider( + wsd, + dpws_model, + dpws_device, + my_mdib, + UUID(uuid_str), + ssl_context_container=ssl_contexts, + specific_components=specific_components, + max_subscription_duration=15, + ) - def log_services(log, the_services): + def log_services(log: LoggerAdapter, the_services: list[Service]): """Print the found services.""" - log.info('found %d services:', len(the_services)) + log.info("found %d services:", len(the_services)) for the_service in the_services: - log.info('found service: %r', the_service) - + log.info("found service: %r", the_service) def main(): + """Execute disco proxy.""" # example code how to use the DiscoProxyClient. # It assumes a discovery proxy is reachable on disco_ip address. basic_logging_setup() - logger = get_logger_adapter('sdc.disco.main') - ca_folder = r'C:\tmp\ORNET_REF_Certificates' - ssl_passwd = 'dummypass' - disco_ip = '192.168.30.5:33479' - my_ip = '192.168.30.106' - My_UUID_str = '12345678-6f55-11ea-9697-123456789bcd' - here = os.path.dirname(__file__) - default_mdib_path = os.path.join(here, 'mdib_test_sequence_2_v4(temp).xml') - mdib_path = os.getenv('ref_mdib') or default_mdib_path - ref_fac = os.getenv('ref_fac') or 'r_fac' - ref_poc = os.getenv('ref_poc') or 'r_poc' - ref_bed = os.getenv('ref_bed') or 'r_bed' + logger = get_logger_adapter("sdc.disco.main") + ca_folder = r"C:\tmp\ORNET_REF_Certificates" + ssl_passwd = "dummypass" # noqa: S105 + disco_ip = "192.168.30.5:33479" + my_ip = "192.168.30.106" + my_uuid_str = "12345678-6f55-11ea-9697-123456789bcd" + mdib_path = os.getenv("ref_mdib") or str( + pathlib.Path(__file__).parent.joinpath("mdib_test_sequence_2_v4(temp).xml") + ) # noqa:SIM112 + ref_fac = os.getenv("ref_fac") or "r_fac" # noqa:SIM112 + ref_poc = os.getenv("ref_poc") or "r_poc" # noqa:SIM112 + ref_bed = os.getenv("ref_bed") or "r_bed" # noqa:SIM112 loc = SdcLocation(ref_fac, ref_poc, ref_bed) - ssl_contexts = mk_ssl_contexts_from_folder(ca_folder, - cyphers_file=None, - private_key='user_private_key_encrypted.pem', - certificate='user_certificate_root_signed.pem', - ca_public_key='root_certificate.pem', - ssl_passwd=ssl_passwd, - ) + ssl_contexts = mk_ssl_contexts_from_folder( + ca_folder, + cyphers_file=None, + private_key="user_private_key_encrypted.pem", + certificate="user_certificate_root_signed.pem", + ca_public_key="root_certificate.pem", + ssl_passwd=ssl_passwd, + ) proxy = DiscoProxyClient(disco_ip, my_ip, ssl_contexts) proxy.start() @@ -344,24 +381,23 @@ def main(): log_services(logger, services) # now publish a device - loc = SdcLocation(ref_fac, ref_poc, ref_bed) - logger.info("location for this device is {}", loc) - logger.info('start provider...') + logger.info("location for this device is %s", loc) + logger.info("start provider...") - sdc_provider = mk_provider(proxy, mdib_path, My_UUID_str, ssl_contexts) + sdc_provider = mk_provider(proxy, mdib_path, my_uuid_str, ssl_contexts) sdc_provider.start_all() - validators = [pm_types.InstanceIdentifier('Validator', extension_string='System')] + validators = [pm_types.InstanceIdentifier("Validator", extension_string="System")] sdc_provider.set_location(loc, validators) services = proxy.search_services() log_services(logger, services) for service in services: result = proxy.send_resolve(service.epr) - logger.info('resolvematches: %r', result.ResolveMatch) + logger.info("resolvematches: %r", result.ResolveMatch) time.sleep(5) - logger.info('stop provider...') + logger.info("stop provider...") sdc_provider.stop_all() services = proxy.search_services() @@ -370,4 +406,4 @@ def main(): finally: proxy.stop() - main() \ No newline at end of file + main() diff --git a/examples/ReferenceTestV2/reference_consumer_v2.py b/examples/ReferenceTestV2/reference_consumer_v2.py index 12e91498..fb679d3d 100644 --- a/examples/ReferenceTestV2/reference_consumer_v2.py +++ b/examples/ReferenceTestV2/reference_consumer_v2.py @@ -1,17 +1,13 @@ -"""Implementation of reference consumer. - -The reference consumer gets its parameters from environment variables: -- adapter_ip specifies which ip address shall be used -- ca_folder specifies where the communication certificates are located. -- ssl_passwd specifies an optional password for the certificates -- search_epr specifies the last characters of the endpoint reference of the device that the consumer shall connect to. - It is not necessary to provide the full epr, just enough to be unique in the current network. +"""Implementation of reference consumer v2. If a value is not provided as environment variable, the default value (see code below) will be used. """ + from __future__ import annotations import os +import pathlib +import sys import time import traceback import uuid @@ -20,26 +16,26 @@ from decimal import Decimal from typing import TYPE_CHECKING -from sdc11073 import observableproperties +import sdc11073 +from sdc11073 import network, observableproperties from sdc11073.certloader import mk_ssl_contexts_from_folder from sdc11073.consumer import SdcConsumer from sdc11073.definitions_sdc import SdcV1Definitions from sdc11073.mdib.consumermdib import ConsumerMdib from sdc11073.mdib.consumermdibxtra import ConsumerMdibMethods from sdc11073.wsdiscovery import WSDiscovery -from sdc11073.xml_types import pm_qnames, msg_types +from sdc11073.xml_types import msg_types, pm_qnames if TYPE_CHECKING: from lxml.etree import QName - from sdc11073.wsdiscovery.service import Service + + from sdc11073.loghelper import LoggerAdapter from sdc11073.pysoap.msgreader import ReceivedMessage + from sdc11073.wsdiscovery.service import Service + from sdc11073.xml_types.eventing_types import FilterType ConsumerMdibMethods.DETERMINATIONTIME_WARN_LIMIT = 2.0 -adapter_ip = os.getenv('ref_ip') or '127.0.0.1' -ca_folder = os.getenv('ref_ca') -ssl_passwd = os.getenv('ref_ssl_passwd') or None -search_epr = os.getenv('ref_search_epr') or 'bcd' # 'bcd' is fixed ending in reference_device v2 uuid. numeric_metric_handle = "numeric_metric_0.channel_0.vmd_0.mds_0" alert_condition_handle = "alert_condition_0.vmd_0.mds_1" @@ -48,38 +44,76 @@ set_context_state_handle = "set_context_0.sco.mds_0" +def get_network_adapter() -> network.NetworkAdapter: + """Get network adapter from environment or first loopback.""" + if (ip := os.getenv("ref_ip")) is not None: # noqa: SIM112 + return network.get_adapter_containing_ip(ip) + # get next available loopback adapter + return next(adapter for adapter in network.get_adapters() if adapter.is_loopback) + + +def get_ssl_context() -> sdc11073.certloader.SSLContextContainer | None: + """Get ssl context from environment or None.""" + if (ca_folder := os.getenv("ref_ca")) is None: # noqa: SIM112 + return None + return mk_ssl_contexts_from_folder( + ca_folder, + private_key="user_private_key_encrypted.pem", + certificate="user_certificate_root_signed.pem", + ca_public_key="root_certificate.pem", + cyphers_file=None, + ssl_passwd=os.getenv("ref_ssl_passwd"), # noqa:SIM112 + ) + + +def get_epr() -> uuid.UUID: + """Get epr from environment or default.""" + if (epr := os.getenv("ref_search_epr")) is not None: # noqa: SIM112 + return uuid.UUID(epr) + return uuid.UUID("12345678-6f55-11ea-9697-123456789abc") + + @dataclass class ResultEntry: + """Represents one result entry.""" + verdict: bool | None step: str info: str xtra: str def __str__(self): - verdict_str = {None: 'no result', True: 'passed', False: 'failed'} - return f'{self.step:6s}:{verdict_str[self.verdict]:10s} {self.info}{self.xtra}' + verdict_str = {None: "no result", True: "passed", False: "failed"} + return f"{self.step:6s}:{verdict_str[self.verdict]:10s} {self.info}{self.xtra}" class ResultsCollector: + """Result collector.""" + def __init__(self): self._results: list[ResultEntry] = [] def log_result(self, is_ok: bool | None, step: str, info: str, extra_info: str | None = None): - xtra = f' ({extra_info}) ' if extra_info else '' + """Log the result.""" + xtra = f" ({extra_info}) " if extra_info else "" self._results.append(ResultEntry(is_ok, step, info, xtra)) def print_summary(self): - print('\n### Summary ###') + """Print the summary.""" + print("\n### Summary ###") for r in self._results: print(r) @property - def failed_count(self): + def failed_count(self) -> int: + """Get the amount of failures.""" return len([r for r in self._results if r.verdict is False]) class ConsumerMdibMethodsReferenceTest(ConsumerMdibMethods): - def __init__(self, consumer_mdib, logger): + """Consumer mdib reference test.""" + + def __init__(self, consumer_mdib: ConsumerMdib, logger: LoggerAdapter): super().__init__(consumer_mdib, logger) self.alert_condition_type_concept_updates: list[float] = [] # for test 5a.1 self._last_alert_condition_type_concept_updates = time.monotonic() # timestamp @@ -95,8 +129,9 @@ def _on_episodic_metric_report(self, received_message_data: ReceivedMessage): # The Reference Provider produces at least 5 numeric metric updates in 30 seconds super()._on_episodic_metric_report(received_message_data) - def _on_description_modification_report(self, received_message_data: ReceivedMessage): + def _on_description_modification_report(self, received_message_data: ReceivedMessage): # noqa: C901 """For Test 5a.1 check if the concept description of updated alert condition Type changed. + For Test 5a.2 check if alert condition cause-remedy information changed. """ cls = self._mdib.data_model.msg_types.DescriptionModificationReport @@ -111,28 +146,33 @@ def _on_description_modification_report(self, received_message_data: ReceivedMes old_descriptor = self._mdib.descriptions.handle.get_one(descriptor_container.Handle) # test 5a.1 if descriptor_container.Type.ConceptDescription != old_descriptor.Type.ConceptDescription: - print(f'concept description {descriptor_container.Type.ConceptDescription} <=> ' - f'{old_descriptor.Type.ConceptDescription}') + print( + f"concept description {descriptor_container.Type.ConceptDescription} <=> " + f"{old_descriptor.Type.ConceptDescription}", + ) self.alert_condition_type_concept_updates.append( - now - self._last_alert_condition_type_concept_updates) + now - self._last_alert_condition_type_concept_updates, + ) self._last_alert_condition_type_concept_updates = now # test 5a.2 # (CauseInfo is a list) detected_5a2 = False if len(descriptor_container.CauseInfo) != len(old_descriptor.CauseInfo): - print(f'RemedyInfo no. of CauseInfo {len(descriptor_container.CauseInfo)} <=> ' - f'{len(old_descriptor.CauseInfo)}') + print( + f"RemedyInfo no. of CauseInfo {len(descriptor_container.CauseInfo)} <=> " + f"{len(old_descriptor.CauseInfo)}", + ) detected_5a2 = True else: for i, cause_info in enumerate(descriptor_container.CauseInfo): old_cause_info = old_descriptor.CauseInfo[i] if cause_info.RemedyInfo != old_cause_info.RemedyInfo: - print(f'RemedyInfo {cause_info.RemedyInfo} <=> ' - f'{old_cause_info.RemedyInfo}') + print(f"RemedyInfo {cause_info.RemedyInfo} <=> {old_cause_info.RemedyInfo}") detected_5a2 = True if detected_5a2: self.alert_condition_cause_remedy_updates.append( - now - self._last_alert_condition_cause_remedy_updates) + now - self._last_alert_condition_cause_remedy_updates, + ) self._last_alert_condition_cause_remedy_updates = now elif descriptor_container.is_metric_descriptor: # test 5a.3 @@ -144,79 +184,66 @@ def _on_description_modification_report(self, received_message_data: ReceivedMes super()._on_description_modification_report(received_message_data) -def test_1b_resolve(wsd, my_service) -> (bool, str): +def test_1b_resolve(wsd: WSDiscovery, my_service: Service) -> (bool, str): """Send resolve and check response.""" wsd.clear_remote_services() - wsd._send_resolve(my_service.epr) + wsd._send_resolve(my_service.epr) # noqa: SLF001 time.sleep(3) - if len(wsd._remote_services) == 0: - return False, 'no response' - elif len(wsd._remote_services) > 1: - return False, 'multiple response' - else: - service = wsd._remote_services.get(my_service.epr) - if service.epr != my_service.epr: - return False, 'not the same epr' - else: - return True, 'resolve answered' + if len(wsd._remote_services) == 0: # noqa: SLF001 + return False, "no response" + if len(wsd._remote_services) > 1: # noqa: SLF001 + return False, "multiple response" + service = wsd._remote_services.get(my_service.epr) # noqa: SLF001 + if service.epr != my_service.epr: + return False, "not the same epr" + return True, "resolve answered" def connect_client(my_service: Service) -> SdcConsumer: - if ca_folder: - ssl_contexts = mk_ssl_contexts_from_folder(ca_folder, - cyphers_file=None, - private_key='user_private_key_encrypted.pem', - certificate='user_certificate_root_signed.pem', - ca_public_key='root_certificate.pem', - ssl_passwd=ssl_passwd - ) - else: - ssl_contexts = None - client = SdcConsumer.from_wsd_service(my_service, - ssl_context_container=ssl_contexts, - validate=True) + """Connect sdc consumer.""" + client = SdcConsumer.from_wsd_service(my_service, ssl_context_container=get_ssl_context(), validate=True) client.start_all() return client -def test_min_updates_per_handle(updates_dict, min_updates, node_type_filter=None) -> (bool, str): # True ok +def test_min_updates_per_handle( + updates_dict: dict, + min_updates: int, + node_type_filter: FilterType = None, +) -> (bool, str): # True ok + """Test minimum updates per handle.""" results = [] is_ok = True if len(updates_dict) == 0: is_ok = False - results.append('no updates') + results.append("no updates") else: for k, v in updates_dict.items(): if node_type_filter: - v = [n for n in v if n.NODETYPE == node_type_filter] + v = [n for n in v if node_type_filter == n.NODETYPE] # noqa: PLW2901 if len(v) < min_updates: is_ok = False - results.append(f'Handle {k} only {len(v)} updates, expect >= {min_updates}') - return is_ok, '\n'.join(results) + results.append(f"Handle {k} only {len(v)} updates, expect >= {min_updates}") + return is_ok, "\n".join(results) def test_min_updates_for_type(updates_dict: dict, min_updates: int, q_name_list: list[QName]) -> (bool, str): # True ok + """Verify minimum updates for specified type.""" flat_list = [] for v in updates_dict.values(): flat_list.extend(v) matches = [x for x in flat_list if x.NODETYPE in q_name_list] if len(matches) >= min_updates: - return True, '' - return False, f'expect >= {min_updates}, got {len(matches)} out of {len(flat_list)}' + return True, "" + return False, f"expect >= {min_updates}, got {len(matches)} out of {len(flat_list)}" -# def log_result(is_ok, result_list, step, info, extra_info=None): -# xtra = f' ({extra_info}) ' if extra_info else '' -# if is_ok: -# result_list.append(f'{step} => passed {xtra}{info}') -# else: -# result_list.append(f'{step} => failed {xtra}{info}') - - -def run_ref_test(results_collector: ResultsCollector): - # results = [] - print(f'using adapter address {adapter_ip}') - print('Test step 1: discover device which endpoint ends with "{}"'.format(search_epr)) +def run_ref_test(results_collector: ResultsCollector) -> ResultsCollector: # noqa: PLR0915,PLR0912,C901 + """Run reference test.""" + adapter_ip = get_network_adapter().ip + search_epr = str(get_epr()) + print(f"using adapter address {adapter_ip}") + print(f'Test step 1: discover device which endpoint ends with "{search_epr}"') wsd = WSDiscovery(adapter_ip) wsd.start() @@ -225,27 +252,27 @@ def run_ref_test(results_collector: ResultsCollector): # b) The Reference Provider answers to Probe and Resolve messages # Remark: 1a) is not testable because provider can't be forced to send a hello while this test is running. - step = '1a' - info = 'The Reference Provider sends Hello messages' - results_collector.log_result(None, step, info, extra_info='not testable') + step = "1a" + info = "The Reference Provider sends Hello messages" + results_collector.log_result(None, step, info, extra_info="not testable") - step = '1b.1' - info = 'The Reference Provider answers to Probe messages' + step = "1b.1" + info = "The Reference Provider answers to Probe messages" my_service = None while my_service is None: services = wsd.search_services(types=SdcV1Definitions.MedicalDeviceTypesFilter) - print('found {} services {}'.format(len(services), ', '.join([s.epr for s in services]))) + print("found {} services {}".format(len(services), ", ".join([s.epr for s in services]))) for s in services: if s.epr.endswith(search_epr): my_service = s - print('found service {}'.format(s.epr)) + print(f"found service {s.epr}") break - print('Test step 1 successful: device discovered') + print("Test step 1 successful: device discovered") results_collector.log_result(True, step, info) - step = '1b.2' - info = 'The Reference Provider answers to Resolve messages' - print('Test step 1b: send resolve and check response') + step = "1b.2" + info = "The Reference Provider answers to Resolve messages" + print("Test step 1b: send resolve and check response") is_ok, txt = test_1b_resolve(wsd, my_service) results_collector.log_result(is_ok, step, info, extra_info=txt) @@ -258,62 +285,61 @@ def run_ref_test(results_collector: ResultsCollector): b) The Reference Consumer renews at least one subscription once during the test phase; the Reference Provider grants subscriptions of at most 15 seconds (this allows for the Reference Consumer to verify if auto-renew works)""" - step = '2a' - info = 'The Reference Provider answers to TransferGet' + step = "2a" + info = "The Reference Provider answers to TransferGet" print(step, info) try: client = connect_client(my_service) results_collector.log_result(client.host_description is not None, step, info) - except: + except Exception: # noqa: BLE001 print(traceback.format_exc()) results_collector.log_result(False, step, info) - return # results + return None # results - step = '2b.1' - info = 'the Reference Provider grants subscriptions of at most 15 seconds' + step = "2b.1" + info = "the Reference Provider grants subscriptions of at most 15 seconds" now = time.time() durations = [s.expires_at - now for s in client.subscription_mgr.subscriptions.values()] - print(f'subscription durations = {durations}') - results_collector.log_result(max(durations) <= 15, step, info) - step = '2b.2' - info = 'the Reference Provider grants subscriptions of at most 15 seconds (renew)' - subscription = list(client.subscription_mgr.subscriptions.values())[0] + print(f"subscription durations = {durations}") + results_collector.log_result(max(durations) <= 15, step, info) # noqa: PLR2004 + step = "2b.2" + info = "the Reference Provider grants subscriptions of at most 15 seconds (renew)" + subscription = next(client.subscription_mgr.subscriptions.values()) granted = subscription.renew(30000) - print(f'renew granted = {granted}') - results_collector.log_result(max(durations) <= 15, step, info) + print(f"renew granted = {granted}") + results_collector.log_result(max(durations) <= 15, step, info) # noqa: PLR2004 # 3. Request Response # a) The Reference Provider answers to GetMdib # b) The Reference Provider answers to GetContextStates messages # b.1) The Reference Provider provides at least one location context state - step = '3a' - info = 'The Reference Provider answers to GetMdib' + step = "3a" + info = "The Reference Provider answers to GetMdib" print(step, info) try: mdib = ConsumerMdib(client, extras_cls=ConsumerMdibMethodsReferenceTest) mdib.init_mdib() # throws an exception if provider did not answer to GetMdib results_collector.log_result(True, step, info) - except: + except Exception: # noqa: BLE001 print(traceback.format_exc()) results_collector.log_result(False, step, info) - # results.append(f'{step} => failed') - return # results + return None # results - step = '3b' - info = 'The Reference Provider answers to GetContextStates messages' + step = "3b" + info = "The Reference Provider answers to GetContextStates messages" context_service = client.context_service_client if context_service is None: - results_collector.log_result(False, step, info, extra_info='no context service') + results_collector.log_result(False, step, info, extra_info="no context service") else: try: states = context_service.get_context_states().result.ContextState results_collector.log_result(True, step, info) - except: + except Exception: # noqa: BLE001 print(traceback.format_exc()) - results_collector.log_result(False, step, info, extra_info='exception') - step = '3b.1' - info = 'The Reference Provider provides at least one location context state' - loc_states = [s for s in states if s.NODETYPE == pm_qnames.LocationContextState] + results_collector.log_result(False, step, info, extra_info="exception") + step = "3b.1" + info = "The Reference Provider provides at least one location context state" + loc_states = [s for s in states if pm_qnames.LocationContextState == s.NODETYPE] results_collector.log_result(len(loc_states) > 0, step, info) # 4 State Reports @@ -340,64 +366,46 @@ def run_ref_test(results_collector: ResultsCollector): operational_state_updates = defaultdict(list) description_updates = [] - def on_metric_updates(metrics_by_handle): - """Callback for all metric state updates. - - Writes to numeric_metric_updates or string_metric_updates, depending on type of state. - """ + def on_metric_updates(metrics_by_handle: dict): + """Write to numeric_metric_updates or string_metric_updates, depending on type of state.""" for k, v in metrics_by_handle.items(): - print(f'State {v.NODETYPE.localname} {v.DescriptorHandle}') - if v.NODETYPE == pm_qnames.NumericMetricState: + print(f"State {v.NODETYPE.localname} {v.DescriptorHandle}") + if pm_qnames.NumericMetricState == v.NODETYPE: numeric_metric_updates[k].append(v) - elif v.NODETYPE == pm_qnames.StringMetricState: + elif pm_qnames.StringMetricState == v.NODETYPE: string_metric_updates[k].append(v) - def on_alert_updates(alerts_by_handle): - """Callback for all alert state updates. - - Writes to alert_condition_updates, alert_signal_updates or alert_system_updates, depending on type of state. - """ + def on_alert_updates(alerts_by_handle: dict): + """Write to alert_condition_updates, alert_signal_updates or alert_system_updates, depending on type of state.""" for k, v in alerts_by_handle.items(): - print(f'State {v.NODETYPE.localname} {v.DescriptorHandle}') + print(f"State {v.NODETYPE.localname} {v.DescriptorHandle}") if v.is_alert_condition: alert_condition_updates[k].append(v) elif v.is_alert_signal: alert_signal_updates[k].append(v) - elif v.NODETYPE == pm_qnames.AlertSystemState: + elif pm_qnames.AlertSystemState == v.NODETYPE: alert_system_updates[k].append(v) - def on_component_updates(components_by_handle): - """Callback for all component state updates. - - Writes to component_updates . - """ + def on_component_updates(components_by_handle: dict): + """Write to component_updates.""" for k, v in components_by_handle.items(): - print(f'State {v.NODETYPE.localname} {v.DescriptorHandle}') + print(f"State {v.NODETYPE.localname} {v.DescriptorHandle}") component_updates[k].append(v) - def on_waveform_updates(waveforms_by_handle): - """Callback for all waveform state updates. - - Writes to waveform_updates . - """ + def on_waveform_updates(waveforms_by_handle: dict): + """Write to waveform_updates.""" for k, v in waveforms_by_handle.items(): waveform_updates[k].append(v) - def on_description_modification(description_modification_report): - """Callback for all description modification updates. - - Writes to description_updates . - """ - print('on_description_modification') + def on_description_modification(description_modification_report: dict): + """Write to description_updates.""" + print("on_description_modification") description_updates.append(description_modification_report) - def on_operational_state_updates(operational_states_by_handle): - """Callback for all operational state updates. - - Writes to operational_state_updates . - """ + def on_operational_state_updates(operational_states_by_handle: dict): + """Write to operational_state_updates.""" for k, v in operational_states_by_handle.items(): - print(f'State {v.NODETYPE.localname} {v.DescriptorHandle}') + print(f"State {v.NODETYPE.localname} {v.DescriptorHandle}") operational_state_updates[k].append(v) observableproperties.bind(mdib, metrics_by_handle=on_metric_updates) @@ -410,88 +418,91 @@ def on_operational_state_updates(operational_states_by_handle): # now collect reports sleep_timer = 30 min_updates = 5 - print(f'will wait for {sleep_timer} seconds now, expecting at least {min_updates} updates per Handle') + print(f"will wait for {sleep_timer} seconds now, expecting at least {min_updates} updates per Handle") time.sleep(sleep_timer) # now check report count - step = '4a' - info = 'count numeric metric state updates' + step = "4a" + info = "count numeric metric state updates" print(step, info) is_ok, result = test_min_updates_per_handle(numeric_metric_updates, min_updates) results_collector.log_result(is_ok, step, info) - step = '4b' - info = 'count string metric state updates' + step = "4b" + info = "count string metric state updates" print(step) is_ok, result = test_min_updates_per_handle(string_metric_updates, min_updates) results_collector.log_result(is_ok, step, info) - step = '4c' - info = 'count alert condition updates' + step = "4c" + info = "count alert condition updates" print(step) is_ok, result = test_min_updates_per_handle(alert_condition_updates, min_updates) results_collector.log_result(is_ok, step, info) - step = '4d' - info = ' count alert signal updates' + step = "4d" + info = " count alert signal updates" print(step, info) is_ok, result = test_min_updates_per_handle(alert_signal_updates, min_updates) results_collector.log_result(is_ok, step, info) - step = '4e' - info = 'count alert system self checks' + step = "4e" + info = "count alert system self checks" is_ok, result = test_min_updates_per_handle(alert_system_updates, min_updates) results_collector.log_result(is_ok, step, info) - step = '4f' - info = 'count waveform updates' + step = "4f" + info = "count waveform updates" # 3 waveforms (RealTimeSampleArrayMetric) x 10 messages per second x 100 samples per message print(step, info) is_ok, result = test_min_updates_per_handle(waveform_updates, min_updates) - results_collector.log_result(is_ok, step, info + ' notifications per second') - results_collector.log_result(len(waveform_updates) >= 3, step, info + ' number of waveforms') + results_collector.log_result(is_ok, step, info + " notifications per second") + results_collector.log_result(len(waveform_updates) >= 3, step, info + " number of waveforms") # noqa:PLR2004 expected_samples = 1000 * sleep_timer * 0.9 for handle, reports in waveform_updates.items(): notifications = [n for n in reports if n.MetricValue is not None] samples = sum([len(n.MetricValue.Samples) for n in notifications]) if samples < expected_samples: - results_collector.log_result(False, step, - info + f' waveform {handle} has {samples} samples, expecting {expected_samples}') + results_collector.log_result( + False, + step, + info + f" waveform {handle} has {samples} samples, expecting {expected_samples}", + ) else: - results_collector.log_result(True, step, info + f' waveform {handle} has {samples} samples') + results_collector.log_result(True, step, info + f" waveform {handle} has {samples} samples") pm = mdib.data_model.pm_names pm_types = mdib.data_model.pm_types - step = '4g.1' - info = 'count battery or clock updates' + step = "4g.1" + info = "count battery or clock updates" print(step, info) - is_ok, result = test_min_updates_for_type(component_updates, - min_updates, - [pm.BatteryState, pm.ClockState]) + is_ok, result = test_min_updates_for_type(component_updates, min_updates, [pm.BatteryState, pm.ClockState]) results_collector.log_result(is_ok, step, info) - step = '4g.2' - info = 'count VMD or MDS updates' + step = "4g.2" + info = "count VMD or MDS updates" print(step, info) - is_ok, result = test_min_updates_for_type(component_updates, - min_updates, - [pm.VmdState, pm.MdsState]) + is_ok, result = test_min_updates_for_type(component_updates, min_updates, [pm.VmdState, pm.MdsState]) results_collector.log_result(is_ok, step, info) - step = '4h' - info = 'Enable/Disable operations' + step = "4h" + info = "Enable/Disable operations" print(step, info) - is_ok, result = test_min_updates_for_type(operational_state_updates, - min_updates, - [pm.SetValueOperationState, - pm.SetStringOperationState, - pm.ActivateOperationState, - pm.SetContextStateOperationState, - pm.SetMetricStateOperationState, - pm.SetComponentStateOperationState, - pm.SetAlertStateOperationState]) + is_ok, result = test_min_updates_for_type( + operational_state_updates, + min_updates, + [ + pm.SetValueOperationState, + pm.SetStringOperationState, + pm.ActivateOperationState, + pm.SetContextStateOperationState, + pm.SetMetricStateOperationState, + pm.SetComponentStateOperationState, + pm.SetAlertStateOperationState, + ], + ) results_collector.log_result(is_ok, step, info) # 5 Description Modifications: @@ -504,49 +515,49 @@ def on_operational_state_updates(operational_states_by_handle): # a new handle assigned on each insertion such that containment tree entries are not recycled). # (Tests for the handling of re-insertion of previously inserted objects should be tested additionally) # * Remove the VMD - step = '5a.1' - info = 'Update Alert condition concept description of Type' + step = "5a.1" + info = "Update Alert condition concept description of Type" print(step, info) # verify only that there are Alert Condition Descriptors updated updates = mdib.xtra.alert_condition_type_concept_updates if not updates: - results_collector.log_result(False, step, info, 'no updates') + results_collector.log_result(False, step, info, "no updates") else: max_diff = max(updates) - if max_diff > 10: - results_collector.log_result(False, step, info, f'max dt={max_diff}') + if max_diff > 10: # noqa:PLR2004 + results_collector.log_result(False, step, info, f"max dt={max_diff}") else: - results_collector.log_result(True, step, info, f'{len(updates) - 1} updates, max diff= {max_diff:.1f}') + results_collector.log_result(True, step, info, f"{len(updates) - 1} updates, max diff= {max_diff:.1f}") - step = '5a.2' - info = 'Update Alert condition cause-remedy information' + step = "5a.2" + info = "Update Alert condition cause-remedy information" print(step, info) # verify only that there are remedy infos updated updates = mdib.xtra.alert_condition_cause_remedy_updates if not updates: - results_collector.log_result(False, step, info, 'no updates') + results_collector.log_result(False, step, info, "no updates") else: max_diff = max(updates) - if max_diff > 10: - results_collector.log_result(False, step, info, f'{updates} => max dt={max_diff}') + if max_diff > 10: # noqa:PLR2004 + results_collector.log_result(False, step, info, f"{updates} => max dt={max_diff}") else: - results_collector.log_result(True, step, info, f'{len(updates) - 1} updates, max diff= {max_diff:.1f}') + results_collector.log_result(True, step, info, f"{len(updates) - 1} updates, max diff= {max_diff:.1f}") - step = '5a.3' - info = 'Update Unit of measure' + step = "5a.3" + info = "Update Unit of measure" print(step, info) updates = mdib.xtra.unit_of_measure_updates if not updates: - results_collector.log_result(False, step, info, 'no updates') + results_collector.log_result(False, step, info, "no updates") else: max_diff = max(updates) - if max_diff > 10: - results_collector.log_result(False, step, info, f'max dt={max_diff}') + if max_diff > 10: # noqa: PLR2004 + results_collector.log_result(False, step, info, f"max dt={max_diff}") else: - results_collector.log_result(True, step, info, f'{len(updates) - 1} updates, max diff= {max_diff:.1f}') + results_collector.log_result(True, step, info, f"{len(updates) - 1} updates, max diff= {max_diff:.1f}") - step = '5b' - info = 'Add / remove vmd' + step = "5b" + info = "Add / remove vmd" print(step, info) # verify only that there are Alert Condition Descriptors updated add_found = False @@ -555,14 +566,14 @@ def on_operational_state_updates(operational_states_by_handle): for report_part in report.ReportPart: if report_part.ModificationType == msg_types.DescriptionModificationType.CREATE: for descriptor in report_part.Descriptor: - if descriptor.NODETYPE == pm_qnames.VmdDescriptor: + if pm_qnames.VmdDescriptor == descriptor.NODETYPE: add_found = True if report_part.ModificationType == msg_types.DescriptionModificationType.DELETE: for descriptor in report_part.Descriptor: - if descriptor.NODETYPE == pm_qnames.VmdDescriptor: + if pm_qnames.VmdDescriptor == descriptor.NODETYPE: rm_found = True - results_collector.log_result(add_found, step, info, 'add') - results_collector.log_result(rm_found, step, info, 'remove') + results_collector.log_result(add_found, step, info, "add") + results_collector.log_result(rm_found, step, info, "remove") # 6 Operation invocation # a) (removed) @@ -581,17 +592,17 @@ def on_operational_state_updates(operational_states_by_handle): # * Immediately sends finished # * Action: Alter values of metrics - step = '6b' - info = 'SetContextState' + step = "6b" + info = "SetContextState" print(step, info) - # patients = mdib.context_states.NODETYPE.get(pm.PatientContextState, []) + # patients = mdib.context_states.NODETYPE.get(pm.PatientContextState, []) # noqa: ERA001 patient_context_descriptors = mdib.descriptions.NODETYPE.get(pm.PatientContextDescriptor, []) generated_family_names = [] if len(patient_context_descriptors) == 0: - results_collector.log_result(False, step, info, extra_info='no PatientContextDescriptor') + results_collector.log_result(False, step, info, extra_info="no PatientContextDescriptor") else: try: - for i, p in enumerate(patient_context_descriptors): + for i, p in enumerate(patient_context_descriptors): # noqa: B007 pat = client.context_service_client.mk_proposed_context_object(p.Handle) pat.CoreData.Familyname = uuid.uuid4().hex pat.ContextAssociation = pm_types.ContextAssociation.ASSOCIATED @@ -600,36 +611,48 @@ def on_operational_state_updates(operational_states_by_handle): time.sleep(1) # allow update notification to arrive patients = mdib.context_states.NODETYPE.get(pm_qnames.PatientContextState, []) if len(patients) == 0: - results_collector.log_result(False, step, info, extra_info='no patients found') + results_collector.log_result(False, step, info, extra_info="no patients found") else: all_ok = True for patient in patients: if patient.CoreData.Familyname in generated_family_names: if patient.ContextAssociation != pm_types.ContextAssociation.ASSOCIATED: - results_collector.log_result(False, step, info, - extra_info=f'new patient {patient.CoreData.Familyname} is {patient.ContextAssociation}') - all_ok = False - else: - if patient.ContextAssociation == pm_types.ContextAssociation.ASSOCIATED: - results_collector.log_result(False, step, info, - extra_info=f'old patient {patient.CoreData.Familyname} is {patient.ContextAssociation}') + results_collector.log_result( + False, + step, + info, + extra_info=f"new patient {patient.CoreData.Familyname} is {patient.ContextAssociation}", + ) all_ok = False + elif patient.ContextAssociation == pm_types.ContextAssociation.ASSOCIATED: + results_collector.log_result( + False, + step, + info, + extra_info=f"old patient {patient.CoreData.Familyname} is {patient.ContextAssociation}", + ) + all_ok = False results_collector.log_result(all_ok, step, info) - except Exception as ex: + except Exception as ex: # noqa: BLE001 print(traceback.format_exc()) results_collector.log_result(False, step, info, ex) - step = '6c' + step = "6c" info = 'SetValue: Immediately answers with "finished"' print(step, info) subscriptions = client.subscription_mgr.subscriptions.values() - operation_invoked_subscriptions = [subscr for subscr in subscriptions - if 'OperationInvokedReport' in subscr.short_filter_string] + operation_invoked_subscriptions = [ + subscr for subscr in subscriptions if "OperationInvokedReport" in subscr.short_filter_string + ] if len(operation_invoked_subscriptions) == 0: - results_collector.log_result(False, step, info, 'OperationInvokedReport not subscribed, cannot test') + results_collector.log_result(False, step, info, "OperationInvokedReport not subscribed, cannot test") elif len(operation_invoked_subscriptions) > 1: - results_collector.log_result(False, step, info, - f'found {len(operation_invoked_subscriptions)} OperationInvokedReport subscribed, cannot test') + results_collector.log_result( + False, + step, + info, + f"found {len(operation_invoked_subscriptions)} OperationInvokedReport subscribed, cannot test", + ) else: try: operations = client.mdib.descriptions.NODETYPE.get(pm_qnames.SetValueOperationDescriptor, []) @@ -641,24 +664,36 @@ def on_operational_state_updates(operational_states_by_handle): future_object = client.set_service_client.set_numeric_value(operation.Handle, Decimal(42)) operation_result = future_object.result() if len(operation_result.report_parts) == 0: - results_collector.log_result(False, step, info, 'no notification') + results_collector.log_result(False, step, info, "no notification") elif len(operation_result.report_parts) > 1: - results_collector.log_result(False, step, info, - f'got {len(operation_result.report_parts)} notifications, expect only one') + results_collector.log_result( + False, + step, + info, + f"got {len(operation_result.report_parts)} notifications, expect only one", + ) else: - results_collector.log_result(True, step, info, - f'got {len(operation_result.report_parts)} notifications') + results_collector.log_result( + True, + step, + info, + f"got {len(operation_result.report_parts)} notifications", + ) if operation_result.InvocationInfo.InvocationState != msg_types.InvocationState.FINISHED: - results_collector.log_result(False, step, info, - f'got result {operation_result.InvocationInfo.InvocationState} ' - f'{operation_result.InvocationInfo.InvocationError} ' - f'{operation_result.InvocationInfo.InvocationErrorMessage}') - except Exception as ex: + results_collector.log_result( + False, + step, + info, + f"got result {operation_result.InvocationInfo.InvocationState} " + f"{operation_result.InvocationInfo.InvocationError} " + f"{operation_result.InvocationInfo.InvocationErrorMessage}", + ) + except Exception as ex: # noqa:BLE001 print(traceback.format_exc()) results_collector.log_result(False, step, info, ex) - step = '6d' - info = 'SetString: Initiates a transaction that sends Wait, Start and Finished' + step = "6d" + info = "SetString: Initiates a transaction that sends Wait, Start and Finished" print(step, info) try: operations = client.mdib.descriptions.NODETYPE.get(pm_qnames.SetStringOperationDescriptor, []) @@ -667,34 +702,48 @@ def on_operational_state_updates(operational_states_by_handle): results_collector.log_result(False, step, info, f'found {len(my_ops)} operations with code "67108889"') else: operation = my_ops[0] - future_object = client.set_service_client.set_string(operation.Handle, 'STANDBY') + future_object = client.set_service_client.set_string(operation.Handle, "STANDBY") operation_result = future_object.result() - if len(operation_result.report_parts) < 3: - results_collector.log_result(False, step, info, - f'only {len(operation_result.report_parts)} notification(s)') - elif len(operation_result.report_parts) >= 3: + if len(operation_result.report_parts) < 3: # noqa: PLR2004 + results_collector.log_result( + False, + step, + info, + f"only {len(operation_result.report_parts)} notification(s)", + ) + elif len(operation_result.report_parts) >= 3: # noqa: PLR2004 # check order of operation invoked reports (simple expectation, there could be multiple WAIT in theory) - expectation = [msg_types.InvocationState.WAIT, - msg_types.InvocationState.START, - msg_types.InvocationState.FINISHED] + expectation = [ + msg_types.InvocationState.WAIT, + msg_types.InvocationState.START, + msg_types.InvocationState.FINISHED, + ] inv_states = [p.InvocationInfo.InvocationState for p in operation_result.report_parts] if inv_states != expectation: - results_collector.log_result(False, step, info, f'wrong order {inv_states}') + results_collector.log_result(False, step, info, f"wrong order {inv_states}") else: - results_collector.log_result(True, step, info, - f'got {len(operation_result.report_parts)} notifications') + results_collector.log_result( + True, + step, + info, + f"got {len(operation_result.report_parts)} notifications", + ) if operation_result.InvocationInfo.InvocationState != msg_types.InvocationState.FINISHED: - results_collector.log_result(False, step, info, - f'got result {operation_result.InvocationInfo.InvocationState} ' - f'{operation_result.InvocationInfo.InvocationError} ' - f'{operation_result.InvocationInfo.InvocationErrorMessage}') - - except Exception as ex: + results_collector.log_result( + False, + step, + info, + f"got result {operation_result.InvocationInfo.InvocationState} " + f"{operation_result.InvocationInfo.InvocationError} " + f"{operation_result.InvocationInfo.InvocationErrorMessage}", + ) + + except Exception as ex: # noqa: BLE001 print(traceback.format_exc()) results_collector.log_result(False, step, info, ex) - step = '6e' - info = 'SetMetricStates Immediately answers with finished' + step = "6e" + info = "SetMetricStates Immediately answers with finished" print(step, info) try: operations = client.mdib.descriptions.NODETYPE.get(pm_qnames.SetMetricStateOperationDescriptor, []) @@ -711,51 +760,63 @@ def on_operational_state_updates(operational_states_by_handle): st.MetricValue.Value = Decimal(1) else: st.MetricValue.Value += Decimal(0.1) - future_object = client.set_service_client.set_metric_state(operation.Handle, - [proposed_metric_state1, proposed_metric_state2]) + future_object = client.set_service_client.set_metric_state( + operation.Handle, + [proposed_metric_state1, proposed_metric_state2], + ) operation_result = future_object.result() if len(operation_result.report_parts) == 0: - results_collector.log_result(False, step, info, 'no notification') + results_collector.log_result(False, step, info, "no notification") elif len(operation_result.report_parts) > 1: - results_collector.log_result(False, step, info, - f'got {len(operation_result.report_parts)} notifications, expect only one') + results_collector.log_result( + False, + step, + info, + f"got {len(operation_result.report_parts)} notifications, expect only one", + ) else: - results_collector.log_result(True, step, info, - f'got {len(operation_result.report_parts)} notifications') + results_collector.log_result( + True, + step, + info, + f"got {len(operation_result.report_parts)} notifications", + ) if operation_result.InvocationInfo.InvocationState != msg_types.InvocationState.FINISHED: - results_collector.log_result(False, step, info, - f'got result {operation_result.InvocationInfo.InvocationState} ' - f'{operation_result.InvocationInfo.InvocationError} ' - f'{operation_result.InvocationInfo.InvocationErrorMessage}') - except Exception as ex: + results_collector.log_result( + False, + step, + info, + f"got result {operation_result.InvocationInfo.InvocationState} " + f"{operation_result.InvocationInfo.InvocationError} " + f"{operation_result.InvocationInfo.InvocationErrorMessage}", + ) + except Exception as ex: # noqa: BLE001 print(traceback.format_exc()) results_collector.log_result(False, step, info, ex) - step = '7' - info = 'Graceful shutdown (at least subscriptions are ended; optionally Bye is sent)' + step = "7" + info = "Graceful shutdown (at least subscriptions are ended; optionally Bye is sent)" try: - success = client._subscription_mgr.unsubscribe_all() + success = client._subscription_mgr.unsubscribe_all() # noqa: SLF001 results_collector.log_result(success, step, info) - except Exception as ex: + except Exception as ex: # noqa: BLE001 print(traceback.format_exc()) results_collector.log_result(False, step, info, ex) time.sleep(2) return results -if __name__ == '__main__': - xtra_log_config = os.getenv('ref_xtra_log_cnf') # or None +if __name__ == "__main__": + xtra_log_config = os.getenv("ref_xtra_log_cnf") # noqa:SIM112 import json import logging.config - here = os.path.dirname(__file__) - - with open(os.path.join(here, 'logging_default.json')) as f: + with pathlib.Path(__file__).parent.joinpath("logging_default.json").open() as f: logging_setup = json.load(f) logging.config.dictConfig(logging_setup) if xtra_log_config is not None: - with open(xtra_log_config) as f: + with pathlib.Path(xtra_log_config).open() as f: logging_setup2 = json.load(f) logging.config.dictConfig(logging_setup2) @@ -763,6 +824,4 @@ def on_operational_state_updates(operational_states_by_handle): run_ref_test(results) results.print_summary() - if results.failed_count: - exit(1) - exit(0) + sys.exit(bool(results.failed_count)) diff --git a/examples/ReferenceTestV2/reference_provider_v2.py b/examples/ReferenceTestV2/reference_provider_v2.py index f9ce8214..81ff9d19 100644 --- a/examples/ReferenceTestV2/reference_provider_v2.py +++ b/examples/ReferenceTestV2/reference_provider_v2.py @@ -15,40 +15,75 @@ import json import logging.config import os +import pathlib import traceback +import uuid from decimal import Decimal from time import sleep -from uuid import UUID +from typing import TYPE_CHECKING +import sdc11073 +from sdc11073 import location, network from sdc11073.certloader import mk_ssl_contexts_from_folder -from sdc11073.location import SdcLocation from sdc11073.loghelper import LoggerAdapter from sdc11073.mdib import ProviderMdib, descriptorcontainers -from sdc11073.provider import SdcProvider -from sdc11073.provider import components -from sdc11073.provider.servicesfactory import DPWSHostedService -from sdc11073.provider.servicesfactory import HostedServices, mk_dpws_hosts +from sdc11073.provider import SdcProvider, components +from sdc11073.provider.servicesfactory import DPWSHostedService, HostedServices, mk_dpws_hosts from sdc11073.provider.subscriptionmgr_async import SubscriptionsManagerReferenceParamAsync from sdc11073.pysoap.soapclient_async import SoapClientAsync from sdc11073.roles.waveformprovider import waveforms from sdc11073.wsdiscovery import WSDiscovery -from sdc11073.xml_types import pm_types, pm_qnames +from sdc11073.xml_types import pm_qnames, pm_types from sdc11073.xml_types.dpws_types import ThisDeviceType, ThisModelType -here = os.path.dirname(__file__) -default_mdib_path = os.path.join(here, 'mdib_test_sequence_2_v4(temp).xml') -mdib_path = os.getenv('ref_mdib') or default_mdib_path -xtra_log_config = os.getenv('ref_xtra_log_cnf') +if TYPE_CHECKING: + from sdc11073.provider.components import SdcProviderComponents -My_UUID_str = '12345678-6f55-11ea-9697-123456789bcd' -# these variables define how the device is published on the network: -adapter_ip = os.getenv('ref_ip') or '127.0.0.1' -ca_folder = os.getenv('ref_ca') -ref_fac = os.getenv('ref_fac') or 'r_fac' -ref_poc = os.getenv('ref_poc') or 'r_poc' -ref_bed = os.getenv('ref_bed') or 'r_bed' -ssl_passwd = os.getenv('ref_ssl_passwd') or None +def get_network_adapter() -> network.NetworkAdapter: + """Get network adapter from environment or first loopback.""" + if (ip := os.getenv("ref_ip")) is not None: # noqa: SIM112 + return network.get_adapter_containing_ip(ip) + # get next available loopback adapter + return next(adapter for adapter in network.get_adapters() if adapter.is_loopback) + + +def get_location() -> location.SdcLocation: + """Get location from environment or default.""" + return location.SdcLocation( + fac=os.getenv("ref_fac", default="r_fac"), # noqa: SIM112 + poc=os.getenv("ref_poc", default="r_poc"), # noqa: SIM112 + bed=os.getenv("ref_bed", default="r_bed"), # noqa: SIM112 + ) + + +def get_ssl_context() -> sdc11073.certloader.SSLContextContainer | None: + """Get ssl context from environment or None.""" + if (ca_folder := os.getenv("ref_ca")) is None: # noqa: SIM112 + return None + return mk_ssl_contexts_from_folder( + ca_folder, + private_key="user_private_key_encrypted.pem", + certificate="user_certificate_root_signed.pem", + ca_public_key="root_certificate.pem", + cyphers_file=None, + ssl_passwd=os.getenv("ref_ssl_passwd"), # noqa: SIM112 + ) + + +def get_epr() -> uuid.UUID: + """Get epr from environment or default.""" + if (epr := os.getenv("ref_search_epr")) is not None: # noqa: SIM112 + return uuid.UUID(epr) + return uuid.UUID("12345678-6f55-11ea-9697-123456789abc") + + +def get_mdib_path() -> pathlib.Path: + """Get mdib from environment or default mdib.""" + if mdib_path := os.getenv("ref_mdib"): # noqa:SIM112 + return pathlib.Path(mdib_path) + return pathlib.Path(__file__).parent.joinpath("mdib_test_sequence_2_v4(temp).xml") + numeric_metric_handle = "numeric_metric_0.channel_0.vmd_0.mds_0" string_metric_handle = "string_metric_0.channel_0.vmd_0.mds_0" @@ -56,7 +91,7 @@ alert_signal_handle = "alert_signal_0.mds_0" set_value_handle = "set_value_0.sco.mds_0" set_string_handle = "set_string_0.sco.mds_0" -battery_handle = 'battery_0.mds_0' +battery_handle = "battery_0.mds_0" vmd_handle = "vmd_0.mds_0" mds_handle = "mds_0" USE_REFERENCE_PARAMETERS = False @@ -113,23 +148,29 @@ enable_6e = True -def mk_all_services_except_localization(sdc_provider, components, subscription_managers) -> HostedServices: +def mk_all_services_except_localization( + sdc_provider: SdcProvider, + components: SdcProviderComponents, + subscription_managers: dict, +) -> HostedServices: + """Create all services except localization service.""" # register all services with their endpoint references acc. to structure in components dpws_services, services_by_name = mk_dpws_hosts(sdc_provider, components, DPWSHostedService, subscription_managers) - hosted_services = HostedServices(dpws_services, - services_by_name['GetService'], - set_service=services_by_name.get('SetService'), - context_service=services_by_name.get('ContextService'), - description_event_service=services_by_name.get('DescriptionEventService'), - state_event_service=services_by_name.get('StateEventService'), - waveform_service=services_by_name.get('WaveformService'), - containment_tree_service=services_by_name.get('ContainmentTreeService'), - # localization_service=services_by_name.get('LocalizationService') - ) - return hosted_services - - -def provide_realtime_data(sdc_provider): + return HostedServices( + dpws_services, + services_by_name["GetService"], + set_service=services_by_name.get("SetService"), + context_service=services_by_name.get("ContextService"), + description_event_service=services_by_name.get("DescriptionEventService"), + state_event_service=services_by_name.get("StateEventService"), + waveform_service=services_by_name.get("WaveformService"), + containment_tree_service=services_by_name.get("ContainmentTreeService"), + # localization_service=services_by_name.get('LocalizationService') # noqa: ERA001 + ) + + +def provide_realtime_data(sdc_provider: SdcProvider): + """Provide realtime data.""" waveform_provider = sdc_provider.waveform_provider if waveform_provider is None: return @@ -139,109 +180,118 @@ def provide_realtime_data(sdc_provider): waveform_provider.register_waveform_generator(waveform.Handle, wf_generator) -if __name__ == '__main__': - with open(os.path.join(here, 'logging_default.json')) as f: +if __name__ == "__main__": + with pathlib.Path(__file__).parent.joinpath("logging_default.json") as f: logging_setup = json.load(f) logging.config.dictConfig(logging_setup) + xtra_log_config = os.getenv("ref_xtra_log_cnf") # noqa:SIM112 if xtra_log_config is not None: - with open(xtra_log_config) as f: + with pathlib.Path(xtra_log_config).open() as f: logging_setup2 = json.load(f) logging.config.dictConfig(logging_setup2) - logger = logging.getLogger('sdc') + logger = logging.getLogger("sdc") logger = LoggerAdapter(logger) - logger.info('{}', 'start') + logger.info("%s", "start") + adapter_ip = get_network_adapter().ip wsd = WSDiscovery(adapter_ip) wsd.start() - my_mdib = ProviderMdib.from_mdib_file(mdib_path) - my_uuid = UUID(My_UUID_str) - print("UUID for this device is {}".format(my_uuid)) - loc = SdcLocation(ref_fac, ref_poc, ref_bed) - print("location for this device is {}".format(loc)) - dpwsModel = ThisModelType(manufacturer='sdc11073', - manufacturer_url='www.sdc11073.com', - model_name='TestDevice', - model_number='1.0', - model_url='www.sdc11073.com/model', - presentation_url='www.sdc11073.com/model/presentation') - - dpwsDevice = ThisDeviceType(friendly_name='TestDevice', - firmware_version='Version1', - serial_number='12345') - if ca_folder: - ssl_contexts = mk_ssl_contexts_from_folder(ca_folder, - private_key='user_private_key_encrypted.pem', - certificate='user_certificate_root_signed.pem', - ca_public_key='root_certificate.pem', - cyphers_file=None, - ssl_passwd=ssl_passwd) - else: - ssl_contexts = None + my_mdib = ProviderMdib.from_mdib_file(str(get_mdib_path())) + my_uuid = get_epr() + print(f"UUID for this device is {my_uuid}") + loc = get_location() + print(f"location for this device is {loc}") + dpws_model = ThisModelType( + manufacturer="sdc11073", + manufacturer_url="www.sdc11073.com", + model_name="TestDevice", + model_number="1.0", + model_url="www.sdc11073.com/model", + presentation_url="www.sdc11073.com/model/presentation", + ) + + dpws_device = ThisDeviceType(friendly_name="TestDevice", firmware_version="Version1", serial_number="12345") + ssl_context = get_ssl_context() if USE_REFERENCE_PARAMETERS: - tmp = {'StateEvent': SubscriptionsManagerReferenceParamAsync} + tmp = {"StateEvent": SubscriptionsManagerReferenceParamAsync} specific_components = components.SdcProviderComponents( subscriptions_manager_class=tmp, - hosted_services={'Get': [components.GetService], - 'StateEvent': [components.StateEventService, - components.ContextService, - components.DescriptionEventService, - components.WaveformService], - 'Set': [components.SetService], - 'ContainmentTree': [components.ContainmentTreeService]}, - soap_client_class=SoapClientAsync) + hosted_services={ + "Get": [components.GetService], + "StateEvent": [ + components.StateEventService, + components.ContextService, + components.DescriptionEventService, + components.WaveformService, + ], + "Set": [components.SetService], + "ContainmentTree": [components.ContainmentTreeService], + }, + soap_client_class=SoapClientAsync, + ) else: specific_components = components.SdcProviderComponents( - hosted_services={'Get': [components.GetService], - 'StateEvent': [components.StateEventService, - components.ContextService, - components.DescriptionEventService, - components.WaveformService], - 'Set': [components.SetService], - 'ContainmentTree': [components.ContainmentTreeService]}) - sdc_provider = SdcProvider(wsd, dpwsModel, dpwsDevice, my_mdib, my_uuid, - ssl_context_container=ssl_contexts, - specific_components=specific_components, - max_subscription_duration=15 - ) + hosted_services={ + "Get": [components.GetService], + "StateEvent": [ + components.StateEventService, + components.ContextService, + components.DescriptionEventService, + components.WaveformService, + ], + "Set": [components.SetService], + "ContainmentTree": [components.ContainmentTreeService], + }, + ) + sdc_provider = SdcProvider( + wsd, + dpws_model, + dpws_device, + my_mdib, + my_uuid, + ssl_context_container=ssl_context, + specific_components=specific_components, + max_subscription_duration=15, + ) sdc_provider.start_all() # disable delayed processing for 2 operations if enable_6c: - sdc_provider.get_operation_by_handle('set_value_0.sco.mds_0').delayed_processing = False + sdc_provider.get_operation_by_handle("set_value_0.sco.mds_0").delayed_processing = False if not enable_6d: - sdc_provider.get_operation_by_handle('set_string_0.sco.mds_0').delayed_processing = False + sdc_provider.get_operation_by_handle("set_string_0.sco.mds_0").delayed_processing = False if enable_6e: - sdc_provider.get_operation_by_handle('set_metric_0.sco.vmd_1.mds_0').delayed_processing = False + sdc_provider.get_operation_by_handle("set_metric_0.sco.vmd_1.mds_0").delayed_processing = False - validators = [pm_types.InstanceIdentifier('Validator', extension_string='System')] + validators = [pm_types.InstanceIdentifier("Validator", extension_string="System")] sdc_provider.set_location(loc, validators) if enable_4f: provide_realtime_data(sdc_provider) pm = my_mdib.data_model.pm_names pm_types = my_mdib.data_model.pm_types - patientDescriptorHandle = my_mdib.descriptions.NODETYPE.get(pm.PatientContextDescriptor)[0].Handle + patient_descriptor_handle = my_mdib.descriptions.NODETYPE.get(pm.PatientContextDescriptor)[0].Handle with my_mdib.context_state_transaction() as mgr: - patientContainer = mgr.mk_context_state(patientDescriptorHandle) - patientContainer.CoreData.Givenname = "Given" - patientContainer.CoreData.Middlename = ["Middle"] - patientContainer.CoreData.Familyname = "Familiy" - patientContainer.CoreData.Birthname = "Birthname" - patientContainer.CoreData.Title = "Title" - patientContainer.ContextAssociation = pm_types.ContextAssociation.ASSOCIATED - patientContainer.Validator.extend(validators) + patient_container = mgr.mk_context_state(patient_descriptor_handle) + patient_container.CoreData.Givenname = "Given" + patient_container.CoreData.Middlename = ["Middle"] + patient_container.CoreData.Familyname = "Familiy" + patient_container.CoreData.Birthname = "Birthname" + patient_container.CoreData.Title = "Title" + patient_container.ContextAssociation = pm_types.ContextAssociation.ASSOCIATED + patient_container.Validator.extend(validators) identifiers = [] - patientContainer.Identification = identifiers + patient_container.Identification = identifiers all_descriptors = list(sdc_provider.mdib.descriptions.objects) all_descriptors.sort(key=lambda x: x.Handle) numeric_metric = None string_metric = None - alertCondition = None - alertSignal = None + alert_condition = None + alert_signal = None battery_descriptor = None - activateOperation = None - stringOperation = None - valueOperation = None + activate_operation = None + string_operation = None + value_operation = None # search for descriptors of specific types for one_descriptor in all_descriptors: @@ -250,21 +300,21 @@ def provide_realtime_data(sdc_provider): if one_descriptor.Handle == string_metric_handle: string_metric = one_descriptor if one_descriptor.Handle == alert_condition_handle: - alertCondition = one_descriptor + alert_condition = one_descriptor if one_descriptor.Handle == alert_signal_handle: - alertSignal = one_descriptor + alert_signal = one_descriptor if one_descriptor.Handle == battery_handle: battery_descriptor = one_descriptor if one_descriptor.Handle == set_value_handle: - valueOperation = one_descriptor + value_operation = one_descriptor if one_descriptor.Handle == set_string_handle: - stringOperation = one_descriptor + string_operation = one_descriptor with sdc_provider.mdib.metric_state_transaction() as mgr: - state = mgr.get_state(valueOperation.OperationTarget) + state = mgr.get_state(value_operation.OperationTarget) if not state.MetricValue: state.mk_metric_value() - state = mgr.get_state(stringOperation.OperationTarget) + state = mgr.get_state(string_operation.OperationTarget) if not state.MetricValue: state.mk_metric_value() print("Running forever, CTRL-C to exit") @@ -279,15 +329,16 @@ def provide_realtime_data(sdc_provider): if not state.MetricValue: state.mk_metric_value() if state.MetricValue.Value is None: - state.MetricValue.Value = Decimal('0') + state.MetricValue.Value = Decimal("0") else: state.MetricValue.Value += Decimal(1) if enable_5a3: with sdc_provider.mdib.descriptor_transaction() as mgr: descriptor: descriptorcontainers.AbstractMetricDescriptorContainer = mgr.get_descriptor( - numeric_metric.Handle) - descriptor.Unit.Code = 'code1' if descriptor.Unit.Code == 'code2' else 'code2' - except Exception as ex: + numeric_metric.Handle, + ) + descriptor.Unit.Code = "code1" if descriptor.Unit.Code == "code2" else "code2" + except Exception: # noqa: BLE001 print(traceback.format_exc()) else: print("Numeric Metric not found in MDIB!") @@ -298,27 +349,28 @@ def provide_realtime_data(sdc_provider): state = mgr.get_state(string_metric.Handle) if not state.MetricValue: state.mk_metric_value() - state.MetricValue.Value = f'my string {str_current_value}' + state.MetricValue.Value = f"my string {str_current_value}" str_current_value += 1 - except Exception as ex: + except Exception: # noqa: BLE001 print(traceback.format_exc()) else: print("Numeric Metric not found in MDIB!") - if alertCondition: + if alert_condition: try: if enable_4c: with sdc_provider.mdib.alert_state_transaction() as mgr: - state = mgr.get_state(alertCondition.Handle) + state = mgr.get_state(alert_condition.Handle) state.Presence = not state.Presence - except Exception as ex: + except Exception: # noqa: BLE001 print(traceback.format_exc()) try: with sdc_provider.mdib.descriptor_transaction() as mgr: now = datetime.datetime.now() - text = f'last changed at {now.hour:02d}:{now.minute:02d}:{now.second:02d}' + text = f"last changed at {now.hour:02d}:{now.minute:02d}:{now.second:02d}" descriptor: descriptorcontainers.AlertConditionDescriptorContainer = mgr.get_descriptor( - alertCondition.Handle) + alert_condition.Handle, + ) if enable_5a1: if len(descriptor.Type.ConceptDescription) == 0: descriptor.Type.ConceptDescription.append(pm_types.LocalizedText(text)) @@ -331,22 +383,22 @@ def provide_realtime_data(sdc_provider): descriptor.CauseInfo[0].RemedyInfo.Description.append(pm_types.LocalizedText(text)) else: descriptor.CauseInfo[0].RemedyInfo.Description[0].text = text - except Exception as ex: + except Exception: # noqa: BLE001 print(traceback.format_exc()) else: print("Alert condition not found in MDIB") - if alertSignal: + if alert_signal: try: if enable_4d: with sdc_provider.mdib.alert_state_transaction() as mgr: - state = mgr.get_state(alertSignal.Handle) + state = mgr.get_state(alert_signal.Handle) if state.Slot is None: state.Slot = 1 else: state.Slot += 1 - except Exception as ex: + except Exception: # noqa:BLE001 print(traceback.format_exc()) else: print("Alert signal not found in MDIB") @@ -356,11 +408,11 @@ def provide_realtime_data(sdc_provider): with sdc_provider.mdib.component_state_transaction() as mgr: state = mgr.get_state(battery_descriptor.Handle) if state.Voltage is None: - state.Voltage = pm_types.Measurement(value=Decimal('14.4'), unit=pm_types.CodedValue('xyz')) + state.Voltage = pm_types.Measurement(value=Decimal("14.4"), unit=pm_types.CodedValue("xyz")) else: - state.Voltage.MeasuredValue += Decimal('0.1') - print(f'battery voltage = {state.Voltage.MeasuredValue}') - except Exception as ex: + state.Voltage.MeasuredValue += Decimal("0.1") + print(f"battery voltage = {state.Voltage.MeasuredValue}") + except Exception: # noqa:BLE001 print(traceback.format_exc()) else: print("battery state not found in MDIB") @@ -368,31 +420,33 @@ def provide_realtime_data(sdc_provider): try: with sdc_provider.mdib.component_state_transaction() as mgr: state = mgr.get_state(vmd_handle) - state.OperatingHours = 2 if state.OperatingHours != 2 else 1 - print(f'operating hours = {state.OperatingHours}') - except Exception as ex: + state.OperatingHours = 2 if state.OperatingHours != 2 else 1 # noqa:PLR2004 + print(f"operating hours = {state.OperatingHours}") + except Exception: # noqa:BLE001 print(traceback.format_exc()) try: with sdc_provider.mdib.component_state_transaction() as mgr: state = mgr.get_state(mds_handle) - state.Lang = 'de' if state.Lang != 'de' else 'en' - print(f'mds lang = {state.Lang}') - except Exception as ex: + state.Lang = "de" if state.Lang != "de" else "en" + print(f"mds lang = {state.Lang}") + except Exception: # noqa:BLE001 print(traceback.format_exc()) # add or rm vmd - add_rm_metric_handle = 'add_rm_metric' - add_rm_channel_handle = 'add_rm_channel' - add_rm_vmd_handle = 'add_rm_vmd' - add_rm_mds_handle = 'mds_0' + add_rm_metric_handle = "add_rm_metric" + add_rm_channel_handle = "add_rm_channel" + add_rm_vmd_handle = "add_rm_vmd" + add_rm_mds_handle = "mds_0" vmd_descriptor = sdc_provider.mdib.descriptions.handle.get_one(add_rm_vmd_handle, allow_none=True) if vmd_descriptor is None: vmd = descriptorcontainers.VmdDescriptorContainer(add_rm_vmd_handle, add_rm_mds_handle) channel = descriptorcontainers.ChannelDescriptorContainer(add_rm_channel_handle, add_rm_vmd_handle) - metric = descriptorcontainers.StringMetricDescriptorContainer(add_rm_metric_handle, - add_rm_channel_handle) - metric.Unit = pm_types.CodedValue('123') + metric = descriptorcontainers.StringMetricDescriptorContainer( + add_rm_metric_handle, + add_rm_channel_handle, + ) + metric.Unit = pm_types.CodedValue("123") with sdc_provider.mdib.descriptor_transaction() as mgr: mgr.add_descriptor(vmd) mgr.add_descriptor(channel) @@ -406,11 +460,13 @@ def provide_realtime_data(sdc_provider): # enable disable operation with sdc_provider.mdib.operational_state_transaction() as mgr: - op_state = mgr.get_state('activate_0.sco.mds_0') - op_state.OperatingMode = pm_types.OperatingMode.ENABLED \ - if op_state.OperatingMode == pm_types.OperatingMode.ENABLED \ + op_state = mgr.get_state("activate_0.sco.mds_0") + op_state.OperatingMode = ( + pm_types.OperatingMode.ENABLED + if op_state.OperatingMode == pm_types.OperatingMode.ENABLED else pm_types.OperatingMode.DISABLED - print(f'operation activate_0.sco.mds_0 {op_state.OperatingMode}') + ) + print(f"operation activate_0.sco.mds_0 {op_state.OperatingMode}") sleep(5) except KeyboardInterrupt: From a540916e76c29e3fe8e98ac9664a3e68cd3fcebe Mon Sep 17 00:00:00 2001 From: "Budnick, Leon" Date: Thu, 14 Nov 2024 14:26:47 +0100 Subject: [PATCH 07/16] fix ruff findings --- examples/ReferenceTest/reference_consumer.py | 104 ++++++++----------- 1 file changed, 44 insertions(+), 60 deletions(-) diff --git a/examples/ReferenceTest/reference_consumer.py b/examples/ReferenceTest/reference_consumer.py index dfbf9c32..c1c552ee 100644 --- a/examples/ReferenceTest/reference_consumer.py +++ b/examples/ReferenceTest/reference_consumer.py @@ -1,18 +1,19 @@ +"""Reference test v1.""" + import dataclasses import enum import os +import pathlib import sys import time import traceback -import typing import uuid from collections import defaultdict from concurrent import futures from decimal import Decimal import sdc11073.certloader -from sdc11073 import commlog, network -from sdc11073 import observableproperties +from sdc11073 import commlog, network, observableproperties from sdc11073.certloader import mk_ssl_contexts_from_folder from sdc11073.consumer import SdcConsumer from sdc11073.definitions_sdc import SdcV1Definitions @@ -24,7 +25,7 @@ ConsumerMdibMethods.DETERMINATIONTIME_WARN_LIMIT = 2.0 # ref_discovery_runs indicates the maximum executions of wsdiscovery search services, "0" -> run until service is found -discovery_runs = int(os.getenv('ref_discovery_runs', 0)) # noqa: SIM112 +discovery_runs = int(os.getenv('ref_discovery_runs', "0")) # noqa: SIM112 ENABLE_COMMLOG = True @@ -54,38 +55,22 @@ def get_epr() -> uuid.UUID: return uuid.UUID(epr) return uuid.UUID('12345678-6f55-11ea-9697-123456789abc') -class TestResult(enum.Enum): - """ - Represents the overall test result. - """ - PASSED = 'PASSED' - FAILED = 'FAILED' - -@dataclasses.dataclass -class TestCollector: - overall_test_result: TestResult = TestResult.PASSED - test_messages: typing.List = dataclasses.field(default_factory=list) - - def add_result(self, test_step_message: str, test_step_result: TestResult): - if not isinstance(test_step_result, TestResult): - raise ValueError("Unexpected parameter") - if self.overall_test_result is not TestResult.FAILED: - self.overall_test_result = test_step_result - self.test_messages.append(test_step_message) class TestResult(enum.Enum): - """ - Represents the overall test result. - """ + """Represents the overall test result.""" + PASSED = 'PASSED' FAILED = 'FAILED' @dataclasses.dataclass class TestCollector: + """Test collector.""" + overall_test_result: TestResult = TestResult.PASSED - test_messages: typing.List = dataclasses.field(default_factory=list) + test_messages: list = dataclasses.field(default_factory=list) def add_result(self, test_step_message: str, test_step_result: TestResult): + """Add result to result list.""" if not isinstance(test_step_result, TestResult): raise ValueError("Unexpected parameter") if self.overall_test_result is not TestResult.FAILED: @@ -93,12 +78,13 @@ def add_result(self, test_step_message: str, test_step_result: TestResult): self.test_messages.append(test_step_message) -def run_ref_test() -> TestCollector: +def run_ref_test() -> TestCollector: # noqa: PLR0915,PLR0912,C901 + """Run reference tests.""" test_collector = TestCollector() adapter_ip = get_network_adapter().ip print(f'using adapter address {adapter_ip}') search_epr = get_epr() - print('Test step 1: discover device which endpoint ends with "{}"'.format(search_epr)) + print(f'Test step 1: discover device which endpoint ends with "{search_epr}"') wsd = WSDiscovery(str(adapter_ip)) wsd.start() my_service = None @@ -109,7 +95,7 @@ def run_ref_test() -> TestCollector: for s in services: if s.epr.endswith(str(search_epr)): my_service = s - print('found service {}'.format(s.epr)) + print(f'found service {s.epr}') break discovery_counter += 1 if discovery_runs and discovery_counter >= discovery_runs: @@ -127,7 +113,7 @@ def run_ref_test() -> TestCollector: client.start_all() print('Test step 2 passed: connected to device') test_collector.add_result('### Test 2 ### passed', TestResult.PASSED) - except: + except Exception: # noqa: BLE001 print(traceback.format_exc()) test_collector.add_result('### Test 2 ### failed', TestResult.FAILED) return test_collector @@ -139,7 +125,7 @@ def run_ref_test() -> TestCollector: print('Test step 3&4 passed') test_collector.add_result('### Test 3 ### passed', TestResult.PASSED) test_collector.add_result('### Test 4 ### passed', TestResult.PASSED) - except: + except Exception: # noqa: BLE001 print(traceback.format_exc()) test_collector.add_result('### Test 3 ### failed', TestResult.FAILED) test_collector.add_result('### Test 4 ### failed', TestResult.FAILED) @@ -150,7 +136,7 @@ def run_ref_test() -> TestCollector: print('Test step 5: check that at least one patient context exists') patients = mdib.context_states.NODETYPE.get(pm.PatientContextState, []) if len(patients) > 0: - print('found {} patients, Test step 5 passed'.format(len(patients))) + print(f'found {len(patients)} patients, Test step 5 passed') test_collector.add_result('### Test 5 ### passed', TestResult.PASSED) else: print('found no patients, Test step 5 failed') @@ -160,7 +146,7 @@ def run_ref_test() -> TestCollector: print('Test step 6: check that at least one location context exists') locations = mdib.context_states.NODETYPE.get(pm.LocationContextState, []) if len(locations) > 0: - print('found {} locations, Test step 6 passed'.format(len(locations))) + print(f'found {len(locations)} locations, Test step 6 passed') test_collector.add_result('### Test 6 ### passed', TestResult.PASSED) else: print('found no locations, Test step 6 failed') @@ -170,22 +156,22 @@ def run_ref_test() -> TestCollector: metric_updates = defaultdict(list) alert_updates = defaultdict(list) - def onMetricUpdates(metricsbyhandle): + def _on_metric_updates(metricsbyhandle: dict): print('onMetricUpdates', metricsbyhandle) for k, v in metricsbyhandle.items(): metric_updates[k].append(v) - def onAlertUpdates(alertsbyhandle): + def _on_alert_updates(alertsbyhandle: dict): print('onAlertUpdates', alertsbyhandle) for k, v in alertsbyhandle.items(): alert_updates[k].append(v) - observableproperties.bind(mdib, metrics_by_handle=onMetricUpdates) - observableproperties.bind(mdib, alert_by_handle=onAlertUpdates) + observableproperties.bind(mdib, metrics_by_handle=_on_metric_updates) + observableproperties.bind(mdib, alert_by_handle=_on_alert_updates) sleep_timer = 20 min_updates = sleep_timer // 5 - 1 - print('will wait for {} seconds now, expecting at least {} updates per Handle'.format(sleep_timer, min_updates)) + print(f'will wait for {sleep_timer} seconds now, expecting at least {min_updates} updates per Handle') time.sleep(sleep_timer) print(metric_updates) print(alert_updates) @@ -194,20 +180,20 @@ def onAlertUpdates(alertsbyhandle): else: for k, v in metric_updates.items(): if len(v) < min_updates: - print('found only {} updates for {}, test step 7 failed'.format(len(v), k)) + print(f'found only {len(v)} updates for {k}, test step 7 failed') test_collector.add_result(f'### Test 7 Handle {k} ### failed', TestResult.FAILED) else: - print('found {} updates for {}, test step 7 ok'.format(len(v), k)) + print(f'found {len(v)} updates for {k}, test step 7 ok') test_collector.add_result(f'### Test 7 Handle {k} ### passed', TestResult.PASSED) if len(alert_updates) == 0: test_collector.add_result('### Test 8 ### failed', TestResult.FAILED) else: for k, v in alert_updates.items(): if len(v) < min_updates: - print('found only {} updates for {}, test step 8 failed'.format(len(v), k)) + print(f'found only {len(v)} updates for {k}, test step 8 failed') test_collector.add_result(f'### Test 8 Handle {k} ### failed', TestResult.FAILED) else: - print('found {} updates for {}, test step 8 ok'.format(len(v), k)) + print(f'found {len(v)} updates for {k}, test step 8 ok') test_collector.add_result(f'### Test 8 Handle {k} ### passed', TestResult.PASSED) print('Test step 9: call SetString operation') @@ -220,28 +206,27 @@ def onAlertUpdates(alertsbyhandle): for s in setstring_operations: if s.Handle != setst_handle: continue - print('setString Op ={}'.format(s)) + print(f'setString Op ={s}') try: fut = client.set_service_client.set_string(s.Handle, 'hoppeldipop') try: res = fut.result(timeout=10) print(res) if res.InvocationInfo.InvocationState != InvocationState.FINISHED: - print('set string operation {} did not finish with "Fin":{}'.format(s.Handle, res)) + print(f'set string operation {s.Handle} did not finish with "Fin":{res}') test_collector.add_result('### Test 9(SetString) ### failed', TestResult.FAILED) else: - print('set string operation {} ok:{}'.format(s.Handle, res)) + print(f'set string operation {s.Handle} ok:{res}') test_collector.add_result('### Test 9(SetString) ### passed', TestResult.PASSED) except futures.TimeoutError: print('timeout error') test_collector.add_result('### Test 9(SetString) ### failed', TestResult.FAILED) - except Exception as ex: + except Exception as ex:# noqa: BLE001 print(f'Test 9(SetString): {ex}') test_collector.add_result('### Test 9(SetString) ### failed', TestResult.FAILED) print('Test step 9: call SetValue operation') setvalue_operations = mdib.descriptions.NODETYPE.get(pm.SetValueOperationDescriptor, []) - # print('setvalue_operations', setvalue_operations) setval_handle = 'numeric.ch0.vmd1_sco_0' if len(setvalue_operations) == 0: print('Test step 9 failed, no SetValue operation found') @@ -250,22 +235,22 @@ def onAlertUpdates(alertsbyhandle): for s in setvalue_operations: if s.Handle != setval_handle: continue - print('setNumericValue Op ={}'.format(s)) + print(f'setNumericValue Op ={s}') try: fut = client.set_service_client.set_numeric_value(s.Handle, Decimal('42')) try: res = fut.result(timeout=10) print(res) if res.InvocationInfo.InvocationState != InvocationState.FINISHED: - print('set value operation {} did not finish with "Fin":{}'.format(s.Handle, res)) + print(f'set value operation {s.Handle} did not finish with "Fin":{res}') test_collector.add_result('### Test 9(SetValue) ### failed', TestResult.FAILED) else: - print('set value operation {} ok:{}'.format(s.Handle, res)) + print(f'set value operation {s.Handle} ok:{res}') test_collector.add_result('### Test 9(SetValue) ### passed', TestResult.PASSED) except futures.TimeoutError: print('timeout error') test_collector.add_result('### Test 9(SetValue) ### failed', TestResult.FAILED) - except Exception as ex: + except Exception as ex:# noqa: BLE001 print(f'Test 9(SetValue): {ex}') test_collector.add_result('### Test 9(SetValue) ### failed', TestResult.FAILED) @@ -279,27 +264,27 @@ def onAlertUpdates(alertsbyhandle): for s in activate_operations: if s.Handle != activate_handle: continue - print('activate Op ={}'.format(s)) + print(f'activate Op ={s}') try: fut = client.set_service_client.activate(s.Handle, 'hoppeldipop') try: res = fut.result(timeout=10) print(res) if res.InvocationInfo.InvocationState != InvocationState.FINISHED: - print('activate operation {} did not finish with "Fin":{}'.format(s.Handle, res)) + print(f'activate operation {s.Handle} did not finish with "Fin":{res}') test_collector.add_result('### Test 9(Activate) ### failed', TestResult.FAILED) else: - print('activate operation {} ok:{}'.format(s.Handle, res)) + print(f'activate operation {s.Handle} ok:{res}') test_collector.add_result('### Test 9(Activate) ### passed', TestResult.PASSED) except futures.TimeoutError: print('timeout error') test_collector.add_result('### Test 9(Activate) ### failed', TestResult.FAILED) - except Exception as ex: + except Exception as ex: # noqa: BLE001 print(f'Test 9(Activate): {ex}') test_collector.add_result('### Test 9(Activate) ### failed', TestResult.FAILED) print('Test step 10: cancel all subscriptions') - success = client._subscription_mgr.unsubscribe_all() + success = client._subscription_mgr.unsubscribe_all() # noqa: SLF001 if success: test_collector.add_result('### Test 10(unsubscribe) ### passed', TestResult.PASSED) else: @@ -308,18 +293,17 @@ def onAlertUpdates(alertsbyhandle): return test_collector def main() -> TestCollector: + """Execute reference tests.""" xtra_log_config = os.getenv('ref_xtra_log_cnf') # noqa: SIM112 import json import logging.config - here = os.path.dirname(__file__) - - with open(os.path.join(here, 'logging_default.json')) as f: + with pathlib.Path(__file__).parent.joinpath("logging_default.json").open() as f: logging_setup = json.load(f) logging.config.dictConfig(logging_setup) if xtra_log_config is not None: - with open(xtra_log_config) as f: + with pathlib.Path(xtra_log_config).open() as f: logging_setup2 = json.load(f) logging.config.dictConfig(logging_setup2) comm_logger = commlog.DirectoryLogger(log_folder=r'c:\temp\sdc_refclient_commlog', From 6028cd93cbb1be7adddc8accacbe357e5c8a8530 Mon Sep 17 00:00:00 2001 From: "Budnick, Leon" Date: Thu, 14 Nov 2024 14:28:13 +0100 Subject: [PATCH 08/16] add init --- examples/ReferenceTestV2/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 examples/ReferenceTestV2/__init__.py diff --git a/examples/ReferenceTestV2/__init__.py b/examples/ReferenceTestV2/__init__.py new file mode 100644 index 00000000..e69de29b From be635e38f1373c14ad898aec536d02d2c39d1cbb Mon Sep 17 00:00:00 2001 From: "Budnick, Leon" Date: Thu, 14 Nov 2024 14:30:25 +0100 Subject: [PATCH 09/16] fix ruff findings --- examples/ReferenceTestV2/discoproxyclient.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/ReferenceTestV2/discoproxyclient.py b/examples/ReferenceTestV2/discoproxyclient.py index b093358a..7bca8578 100644 --- a/examples/ReferenceTestV2/discoproxyclient.py +++ b/examples/ReferenceTestV2/discoproxyclient.py @@ -313,7 +313,7 @@ def on_get(self, _: RequestData) -> CreatedMessage: if __name__ == "__main__": def mk_provider( - wsd: DiscoProxyClient, mdib_path: str, uuid_str: str, ssl_contexts: SSLContextContainer + wsd: DiscoProxyClient, mdib_path: str, uuid_str: str, ssl_contexts: SSLContextContainer, ) -> SdcProvider: """Create sdc provider.""" my_mdib = ProviderMdib.from_mdib_file(mdib_path) @@ -357,9 +357,9 @@ def main(): disco_ip = "192.168.30.5:33479" my_ip = "192.168.30.106" my_uuid_str = "12345678-6f55-11ea-9697-123456789bcd" - mdib_path = os.getenv("ref_mdib") or str( - pathlib.Path(__file__).parent.joinpath("mdib_test_sequence_2_v4(temp).xml") - ) # noqa:SIM112 + mdib_path = os.getenv("ref_mdib") or str( # noqa:SIM112 + pathlib.Path(__file__).parent.joinpath("mdib_test_sequence_2_v4(temp).xml"), + ) ref_fac = os.getenv("ref_fac") or "r_fac" # noqa:SIM112 ref_poc = os.getenv("ref_poc") or "r_poc" # noqa:SIM112 ref_bed = os.getenv("ref_bed") or "r_bed" # noqa:SIM112 From 59b530be8283553a1bf8b454450d53a964ec83c4 Mon Sep 17 00:00:00 2001 From: "Budnick, Leon" Date: Fri, 15 Nov 2024 10:41:00 +0100 Subject: [PATCH 10/16] run v2 in ci instead of v1 --- .github/workflows/pat_integration.yml | 4 +- .../ReferenceTestV2/reference_consumer_v2.py | 2 +- .../ReferenceTestV2/reference_provider_v2.py | 7 ++- examples/ReferenceTestV2/run.py | 47 +++++++++++++++++++ 4 files changed, 55 insertions(+), 5 deletions(-) create mode 100644 examples/ReferenceTestV2/run.py diff --git a/.github/workflows/pat_integration.yml b/.github/workflows/pat_integration.yml index cc002993..e827e0ff 100644 --- a/.github/workflows/pat_integration.yml +++ b/.github/workflows/pat_integration.yml @@ -28,10 +28,10 @@ jobs: - name: Run tests with tls enabled if: ${{ matrix.tls_enable }} - run: python -m examples.ReferenceTest.run --tls + run: python -m examples.ReferenceTestV2.run --tls timeout-minutes: 2 - name: Run tests with tls disabled if: ${{ !matrix.tls_enable }} - run: python -m examples.ReferenceTest.run + run: python -m examples.ReferenceTestV2.run timeout-minutes: 2 diff --git a/examples/ReferenceTestV2/reference_consumer_v2.py b/examples/ReferenceTestV2/reference_consumer_v2.py index fb679d3d..9b73042f 100644 --- a/examples/ReferenceTestV2/reference_consumer_v2.py +++ b/examples/ReferenceTestV2/reference_consumer_v2.py @@ -304,7 +304,7 @@ def run_ref_test(results_collector: ResultsCollector) -> ResultsCollector: # no results_collector.log_result(max(durations) <= 15, step, info) # noqa: PLR2004 step = "2b.2" info = "the Reference Provider grants subscriptions of at most 15 seconds (renew)" - subscription = next(client.subscription_mgr.subscriptions.values()) + subscription = list(client.subscription_mgr.subscriptions.values())[0] # noqa: RUF015 granted = subscription.renew(30000) print(f"renew granted = {granted}") results_collector.log_result(max(durations) <= 15, step, info) # noqa: PLR2004 diff --git a/examples/ReferenceTestV2/reference_provider_v2.py b/examples/ReferenceTestV2/reference_provider_v2.py index 81ff9d19..6631a4c0 100644 --- a/examples/ReferenceTestV2/reference_provider_v2.py +++ b/examples/ReferenceTestV2/reference_provider_v2.py @@ -180,8 +180,8 @@ def provide_realtime_data(sdc_provider: SdcProvider): waveform_provider.register_waveform_generator(waveform.Handle, wf_generator) -if __name__ == "__main__": - with pathlib.Path(__file__).parent.joinpath("logging_default.json") as f: +def run_provider(): + with pathlib.Path(__file__).parent.joinpath("logging_default.json").open() as f: logging_setup = json.load(f) logging.config.dictConfig(logging_setup) xtra_log_config = os.getenv("ref_xtra_log_cnf") # noqa:SIM112 @@ -471,3 +471,6 @@ def provide_realtime_data(sdc_provider: SdcProvider): sleep(5) except KeyboardInterrupt: print("Exiting...") + +if __name__ == "__main__": + run_provider() diff --git a/examples/ReferenceTestV2/run.py b/examples/ReferenceTestV2/run.py new file mode 100644 index 00000000..78d3b08e --- /dev/null +++ b/examples/ReferenceTestV2/run.py @@ -0,0 +1,47 @@ +"""Script that executes the plug-a-thon tests.""" + +import os +import pathlib +import platform +import sys +import threading +import uuid + +from sdc11073 import network + +from examples.ReferenceTestV2 import reference_provider_v2, reference_consumer_v2 + + +def setup(tls: bool): + os.environ['ref_search_epr'] = str(uuid.uuid4()) + if platform.system() == 'Darwin': + os.environ['ref_ip'] = next(str(adapter.ip) for adapter in network.get_adapters() if not adapter.is_loopback) + else: + os.environ['ref_ip'] = next(str(adapter.ip) for adapter in network.get_adapters() if adapter.is_loopback) + if tls: + certs_path = pathlib.Path(__file__).parent.parent.joinpath('certs') + assert certs_path.exists() + os.environ['ref_ca'] = str(certs_path) + os.environ['ref_ssl_passwd'] = 'dummypass' + + +def run() -> reference_consumer_v2.ResultsCollector: + threading.Thread(target=reference_provider_v2.run_provider, daemon=True).start() + return reference_consumer_v2.run_ref_test(reference_consumer_v2.ResultsCollector()) + + +def main(tls: bool): + setup(tls) + return run() + + +if __name__ == '__main__': + import argparse + + parser = argparse.ArgumentParser(description='run plug-a-thon tests') + parser.add_argument('--tls', action='store_true', help='Indicates whether tls encryption should be enabled.') + + args = parser.parse_args() + run_results = main(tls=args.tls) + run_results.print_summary() + sys.exit(bool(results.failed_count)) From 82cbeeed6e36722b1b240df48fe3c3b2561006fc Mon Sep 17 00:00:00 2001 From: "Budnick, Leon" Date: Fri, 15 Nov 2024 10:42:36 +0100 Subject: [PATCH 11/16] fix variable --- examples/ReferenceTestV2/run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/ReferenceTestV2/run.py b/examples/ReferenceTestV2/run.py index 78d3b08e..0f4a01a6 100644 --- a/examples/ReferenceTestV2/run.py +++ b/examples/ReferenceTestV2/run.py @@ -44,4 +44,4 @@ def main(tls: bool): args = parser.parse_args() run_results = main(tls=args.tls) run_results.print_summary() - sys.exit(bool(results.failed_count)) + sys.exit(bool(run_results.failed_count)) From b88829a88ea956ea3373256136391b58fd61fc57 Mon Sep 17 00:00:00 2001 From: "Budnick, Leon" Date: Fri, 15 Nov 2024 10:49:02 +0100 Subject: [PATCH 12/16] fix ruff findings --- examples/ReferenceTestV2/run.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/examples/ReferenceTestV2/run.py b/examples/ReferenceTestV2/run.py index 0f4a01a6..20e323bf 100644 --- a/examples/ReferenceTestV2/run.py +++ b/examples/ReferenceTestV2/run.py @@ -7,39 +7,41 @@ import threading import uuid +from examples.ReferenceTestV2 import reference_consumer_v2, reference_provider_v2 from sdc11073 import network -from examples.ReferenceTestV2 import reference_provider_v2, reference_consumer_v2 - def setup(tls: bool): - os.environ['ref_search_epr'] = str(uuid.uuid4()) - if platform.system() == 'Darwin': - os.environ['ref_ip'] = next(str(adapter.ip) for adapter in network.get_adapters() if not adapter.is_loopback) + """Setups the run.""" + os.environ["ref_search_epr"] = str(uuid.uuid4()) # noqa: SIM112 + if platform.system() == "Darwin": + os.environ["ref_ip"] = next(str(adapter.ip) for adapter in network.get_adapters() if not adapter.is_loopback) # noqa: SIM112 else: - os.environ['ref_ip'] = next(str(adapter.ip) for adapter in network.get_adapters() if adapter.is_loopback) + os.environ["ref_ip"] = next(str(adapter.ip) for adapter in network.get_adapters() if adapter.is_loopback) # noqa: SIM112 if tls: - certs_path = pathlib.Path(__file__).parent.parent.joinpath('certs') + certs_path = pathlib.Path(__file__).parent.parent.joinpath("certs") assert certs_path.exists() - os.environ['ref_ca'] = str(certs_path) - os.environ['ref_ssl_passwd'] = 'dummypass' + os.environ["ref_ca"] = str(certs_path) # noqa: SIM112 + os.environ["ref_ssl_passwd"] = "dummypass" # noqa: S105,SIM112 def run() -> reference_consumer_v2.ResultsCollector: + """Run tests.""" threading.Thread(target=reference_provider_v2.run_provider, daemon=True).start() return reference_consumer_v2.run_ref_test(reference_consumer_v2.ResultsCollector()) -def main(tls: bool): +def main(tls: bool) -> reference_consumer_v2.ResultsCollector: + """Setups and run tests.""" setup(tls) return run() -if __name__ == '__main__': +if __name__ == "__main__": import argparse - parser = argparse.ArgumentParser(description='run plug-a-thon tests') - parser.add_argument('--tls', action='store_true', help='Indicates whether tls encryption should be enabled.') + parser = argparse.ArgumentParser(description="run plug-a-thon tests") + parser.add_argument("--tls", action="store_true", help="Indicates whether tls encryption should be enabled.") args = parser.parse_args() run_results = main(tls=args.tls) From 4bc5be73c08b189892ae15c3c870ab8029b35036 Mon Sep 17 00:00:00 2001 From: "Budnick, Leon" Date: Fri, 15 Nov 2024 10:53:14 +0100 Subject: [PATCH 13/16] fix ruff findings --- examples/ReferenceTestV2/reference_provider_v2.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/ReferenceTestV2/reference_provider_v2.py b/examples/ReferenceTestV2/reference_provider_v2.py index 6631a4c0..38fb75a9 100644 --- a/examples/ReferenceTestV2/reference_provider_v2.py +++ b/examples/ReferenceTestV2/reference_provider_v2.py @@ -180,7 +180,8 @@ def provide_realtime_data(sdc_provider: SdcProvider): waveform_provider.register_waveform_generator(waveform.Handle, wf_generator) -def run_provider(): +def run_provider(): # noqa: PLR0915, PLR0912, C901 + """Run provider until KeyboardError is raised.""" with pathlib.Path(__file__).parent.joinpath("logging_default.json").open() as f: logging_setup = json.load(f) logging.config.dictConfig(logging_setup) @@ -263,7 +264,7 @@ def run_provider(): if enable_6e: sdc_provider.get_operation_by_handle("set_metric_0.sco.vmd_1.mds_0").delayed_processing = False - validators = [pm_types.InstanceIdentifier("Validator", extension_string="System")] + validators = [pm_types.InstanceIdentifier("Validator", extension_string="System")] # noqa: F823 sdc_provider.set_location(loc, validators) if enable_4f: provide_realtime_data(sdc_provider) @@ -289,7 +290,6 @@ def run_provider(): alert_condition = None alert_signal = None battery_descriptor = None - activate_operation = None string_operation = None value_operation = None @@ -472,5 +472,6 @@ def run_provider(): except KeyboardInterrupt: print("Exiting...") + if __name__ == "__main__": run_provider() From a38d4d53db2efbf9fb7a5d3481c4f438531a888d Mon Sep 17 00:00:00 2001 From: "Budnick, Leon" Date: Fri, 15 Nov 2024 11:00:18 +0100 Subject: [PATCH 14/16] fix ruff findings --- .github/workflows/pat_integration.yml | 2 +- examples/ReferenceTestV2/reference_provider_v2.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pat_integration.yml b/.github/workflows/pat_integration.yml index e827e0ff..f4afaaf6 100644 --- a/.github/workflows/pat_integration.yml +++ b/.github/workflows/pat_integration.yml @@ -7,7 +7,7 @@ on: - master - v* jobs: - sdc11073_provider_v1: + sdc11073_provider_v2: strategy: matrix: os: [ ubuntu-latest, macos-latest, windows-latest ] diff --git a/examples/ReferenceTestV2/reference_provider_v2.py b/examples/ReferenceTestV2/reference_provider_v2.py index 38fb75a9..19189ed6 100644 --- a/examples/ReferenceTestV2/reference_provider_v2.py +++ b/examples/ReferenceTestV2/reference_provider_v2.py @@ -33,7 +33,7 @@ from sdc11073.pysoap.soapclient_async import SoapClientAsync from sdc11073.roles.waveformprovider import waveforms from sdc11073.wsdiscovery import WSDiscovery -from sdc11073.xml_types import pm_qnames, pm_types +from sdc11073.xml_types import pm_qnames from sdc11073.xml_types.dpws_types import ThisDeviceType, ThisModelType if TYPE_CHECKING: @@ -264,12 +264,12 @@ def run_provider(): # noqa: PLR0915, PLR0912, C901 if enable_6e: sdc_provider.get_operation_by_handle("set_metric_0.sco.vmd_1.mds_0").delayed_processing = False - validators = [pm_types.InstanceIdentifier("Validator", extension_string="System")] # noqa: F823 + pm = my_mdib.data_model.pm_names + pm_types = my_mdib.data_model.pm_types + validators = [pm_types.InstanceIdentifier("Validator", extension_string="System")] sdc_provider.set_location(loc, validators) if enable_4f: provide_realtime_data(sdc_provider) - pm = my_mdib.data_model.pm_names - pm_types = my_mdib.data_model.pm_types patient_descriptor_handle = my_mdib.descriptions.NODETYPE.get(pm.PatientContextDescriptor)[0].Handle with my_mdib.context_state_transaction() as mgr: patient_container = mgr.mk_context_state(patient_descriptor_handle) From 83f89ba8ea91e6809111a720029145b27571f1d8 Mon Sep 17 00:00:00 2001 From: "Budnick, Leon" Date: Fri, 15 Nov 2024 11:04:48 +0100 Subject: [PATCH 15/16] fix ci --- examples/ReferenceTestV2/reference_consumer_v2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/ReferenceTestV2/reference_consumer_v2.py b/examples/ReferenceTestV2/reference_consumer_v2.py index 9b73042f..56f63523 100644 --- a/examples/ReferenceTestV2/reference_consumer_v2.py +++ b/examples/ReferenceTestV2/reference_consumer_v2.py @@ -803,7 +803,7 @@ def on_operational_state_updates(operational_states_by_handle: dict): print(traceback.format_exc()) results_collector.log_result(False, step, info, ex) time.sleep(2) - return results + return results_collector if __name__ == "__main__": From 389396baf722657632f0273b966d099474fcbdee Mon Sep 17 00:00:00 2001 From: "Budnick, Leon" Date: Fri, 15 Nov 2024 13:15:00 +0100 Subject: [PATCH 16/16] use less samples to check if in ci --- .github/workflows/pat_integration.yml | 2 ++ examples/ReferenceTestV2/discoproxyclient.py | 5 ++++- examples/ReferenceTestV2/reference_consumer_v2.py | 14 +++++++++++--- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pat_integration.yml b/.github/workflows/pat_integration.yml index f4afaaf6..84c92a5c 100644 --- a/.github/workflows/pat_integration.yml +++ b/.github/workflows/pat_integration.yml @@ -8,6 +8,8 @@ on: - v* jobs: sdc11073_provider_v2: + env: + EXPECTED_WAVEFORM_SAMPLES_4F: 100 # set to a low value as we cannot control GitHub ci network latency strategy: matrix: os: [ ubuntu-latest, macos-latest, windows-latest ] diff --git a/examples/ReferenceTestV2/discoproxyclient.py b/examples/ReferenceTestV2/discoproxyclient.py index 7bca8578..4c5b0bec 100644 --- a/examples/ReferenceTestV2/discoproxyclient.py +++ b/examples/ReferenceTestV2/discoproxyclient.py @@ -313,7 +313,10 @@ def on_get(self, _: RequestData) -> CreatedMessage: if __name__ == "__main__": def mk_provider( - wsd: DiscoProxyClient, mdib_path: str, uuid_str: str, ssl_contexts: SSLContextContainer, + wsd: DiscoProxyClient, + mdib_path: str, + uuid_str: str, + ssl_contexts: SSLContextContainer, ) -> SdcProvider: """Create sdc provider.""" my_mdib = ProviderMdib.from_mdib_file(mdib_path) diff --git a/examples/ReferenceTestV2/reference_consumer_v2.py b/examples/ReferenceTestV2/reference_consumer_v2.py index 56f63523..d47232d3 100644 --- a/examples/ReferenceTestV2/reference_consumer_v2.py +++ b/examples/ReferenceTestV2/reference_consumer_v2.py @@ -457,9 +457,13 @@ def on_operational_state_updates(operational_states_by_handle: dict): print(step, info) is_ok, result = test_min_updates_per_handle(waveform_updates, min_updates) results_collector.log_result(is_ok, step, info + " notifications per second") - results_collector.log_result(len(waveform_updates) >= 3, step, info + " number of waveforms") # noqa:PLR2004 + results_collector.log_result( + len(waveform_updates) >= 3, # noqa:PLR2004 + step, + info + f" number of waveforms: {len(waveform_updates)}", + ) - expected_samples = 1000 * sleep_timer * 0.9 + expected_samples = int(os.getenv("EXPECTED_WAVEFORM_SAMPLES_4F", 1000 * sleep_timer * 0.9)) for handle, reports in waveform_updates.items(): notifications = [n for n in reports if n.MetricValue is not None] samples = sum([len(n.MetricValue.Samples) for n in notifications]) @@ -470,7 +474,11 @@ def on_operational_state_updates(operational_states_by_handle: dict): info + f" waveform {handle} has {samples} samples, expecting {expected_samples}", ) else: - results_collector.log_result(True, step, info + f" waveform {handle} has {samples} samples") + results_collector.log_result( + True, + step, + info + f" waveform {handle} has more than {expected_samples} samples: {samples}", + ) pm = mdib.data_model.pm_names pm_types = mdib.data_model.pm_types