From 15f065b8b78e521a7dca7bbb15d73e8c5baf5ef9 Mon Sep 17 00:00:00 2001 From: Bernd Deichmann <68051229+deichmab-draeger@users.noreply.github.com> Date: Tue, 5 Sep 2023 11:33:40 +0200 Subject: [PATCH] Fix/v1 ext extension (#250) ## Types of changes - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [x] Breaking change (fix or feature that would cause existing functionality to change) ## Checklist: - [x] I have updated the [changelog](../CHANGELOG.md) accordingly. - [x] I have added tests to cover my changes. --- CHANGELOG.md | 1 + src/sdc11073/mdib/containerproperties.py | 97 +++++++-- src/sdc11073/mdib/descriptorcontainers.py | 144 +++++++------ tests/test_descriptorcontainers.py | 12 +- tests/test_pmtypes.py | 243 +++++++++++++++++++++- 5 files changed, 393 insertions(+), 104 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 224f6993..930b1e9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - fixed a bug where coping a xml node would not work if namespace have been defined multiple time [#232](https://github.com/Draegerwerk/sdc11073/issues/232) - getContextState sets correct BindingVersion [#168](https://github.com/Draegerwerk/sdc11073/issues/168). +- comparison of extensions would fail, ExtensionLocalValue type added ### Changed diff --git a/src/sdc11073/mdib/containerproperties.py b/src/sdc11073/mdib/containerproperties.py index 981198ae..f8ccc290 100644 --- a/src/sdc11073/mdib/containerproperties.py +++ b/src/sdc11073/mdib/containerproperties.py @@ -13,6 +13,7 @@ from sdc11073 import isoduration from sdc11073.dataconverters import TimestampConverter, DecimalConverter, IntegerConverter, BooleanConverter, \ DurationConverter, NullConverter +from sdc11073.xmlparsing import copy_node_wo_parent class ElementNotFoundException(Exception): @@ -456,8 +457,48 @@ def updateXMLValue(self, instance, node): subNode.text = value +def _compare_extension(left: etree_.ElementBase, right: etree_.ElementBase) -> bool: + # xml comparison + try: + if left.tag != right.tag: # compare expanded names + return False + if dict(left.attrib) != dict(right.attrib): # unclear how lxml _Attrib compares + return False + except AttributeError: # right side is not an Element type because expected attributes are missing + return False + + # ignore comments + left_children = [child for child in left if not isinstance(child, etree_._Comment)] + right_children = [child for child in right if not isinstance(child, etree_._Comment)] + + if len(left_children) != len(right_children): # compare children count + return False + if len(left_children) == 0 and len(right_children) == 0: + if left.text != right.text: # mixed content is not allowed. only compare text if there are no children + return False + + return all(map(_compare_extension, left_children, right_children)) # compare children but keep order + + +class ExtensionLocalValue(list): + + compare_method = _compare_extension + """may be overwritten by user if a custom comparison behaviour is required""" + + def __eq__(self, other): + try: + if len(self) != len(other): + return False + except TypeError: # len of other cannot be determined + return False + return all(self.__class__.compare_method(left, right) for left, right in zip(self, other)) + + class ExtensionNodeProperty(_PropertyBase): - """ Represents an ext:Extension Element that contains xml tree of any kind.""" + """Represents an ext:Extension Element that contains 0...n child elements of any kind. + + The python representation is an ExtensionLocalValue with list of elements. + """ def __init__(self, subElementNames=None, defaultPyValue=None): if subElementNames is None: @@ -465,36 +506,48 @@ def __init__(self, subElementNames=None, defaultPyValue=None): else: subElementNames.append(namespaces.extTag('Extension')) attrname = '_ext_ext' - super(ExtensionNodeProperty, self).__init__(attrname, subElementNames, defaultPyValue) + super().__init__(attrname, subElementNames, defaultPyValue) self._converter = None + def __set__(self, instance, pyValue): + if not isinstance(pyValue, ExtensionLocalValue): + pyValue = ExtensionLocalValue(pyValue) + super().__set__(instance, pyValue) + + def __get__(self, instance, owner): + """Return a python value, uses the locally stored value.""" + if instance is None: # if called via class + return self + try: + value = getattr(instance, self._localVarName) + except AttributeError: + value = None + if value is None: + value = _PropertyValue(None, ExtensionLocalValue()) + setattr(instance, self._localVarName, value) + return value.py_value + def getPyValueFromNode(self, node): + """Read value from node.""" try: - subNode = self._getElementbyChildNamesList(node, self._subElementNames, createMissingNodes=False) - return _PropertyValue(None, subNode) # subNode is the ext:Extension node + extension_node = self._getElementbyChildNamesList(node, self._subElementNames, createMissingNodes=False) except ElementNotFoundException: - return None + return _PropertyValue(None, ExtensionLocalValue()) + return _PropertyValue(extension_node, ExtensionLocalValue(extension_node[:])) def updateXMLValue(self, instance, node): + """Write value to node. + + The Extension Element is only added if there is at least one element available in local list. + """ try: property_value = getattr(instance, self._localVarName) - except AttributeError: # set to None (it is in the responsibility of the called method to do the right thing) - property_value = None - if property_value is None: - try: - parentNode = self._getElementbyChildNamesList(node, self._subElementNames[:-1], - createMissingNodes=False) - except ElementNotFoundException: - return - subNode = parentNode.find(self._subElementNames[-1]) - if subNode is not None: - parentNode.remove(subNode) - else: - subNode = self._getElementbyChildNamesList(node, self._subElementNames, createMissingNodes=True) - - del subNode[:] # delete all children first - if property_value.py_value is not None: - subNode.extend([copy.copy(n) for n in property_value.py_value]) + except AttributeError: + return # nothing to add + if property_value is None or len(property_value.py_value) == 0: + return # nothing to add + sub_node = self._getElementbyChildNamesList(node, self._subElementNames, createMissingNodes=True) + sub_node.extend(copy_node_wo_parent(x) for x in property_value.py_value) class SubElementProperty(_PropertyBase): diff --git a/src/sdc11073/mdib/descriptorcontainers.py b/src/sdc11073/mdib/descriptorcontainers.py index 5500cd8c..8210a3a9 100644 --- a/src/sdc11073/mdib/descriptorcontainers.py +++ b/src/sdc11073/mdib/descriptorcontainers.py @@ -1,14 +1,15 @@ import inspect from collections import defaultdict + from lxml import etree as etree_ + +from . import containerproperties as cp from .containerbase import ContainerBase -from .. import observableproperties as properties -from ..namespaces import domTag, extTag, siTag, msgTag -from ..namespaces import Prefix_Namespace as Prefix from .. import msgtypes - +from .. import observableproperties as properties from .. import pmtypes -from . import containerproperties as cp +from ..namespaces import Prefix_Namespace as Prefix +from ..namespaces import domTag, extTag, siTag, msgTag class AbstractDescriptorContainer(ContainerBase): @@ -34,7 +35,6 @@ class AbstractDescriptorContainer(ContainerBase): isAlertConditionDescriptor = False isContextDescriptor = False - node = properties.ObservableProperty() # the elementtree node Handle = cp.NodeAttributeProperty('Handle') @@ -74,31 +74,17 @@ def codeId(self): def codingSystem(self): return self.Type.coding.codingSystem if self.Type is not None else None # pylint:disable=no-member - @property - def retrievability(self) -> [msgtypes.Retrievability, None]: - """look for msgTag('Retrievability') in ext:Extension node""" - if self.ext_Extension is None: - return None - retrievability_tag = msgTag('Retrievability') - for elem in self.ext_Extension: - if elem.tag == retrievability_tag: - return msgtypes.Retrievability.fromNode(elem) - return None - - @retrievability.setter - def retrievability(self, retrievability_instance: msgtypes.Retrievability): - """sets msgTag('Retrievability') child node of ext:Extension""" - retrievability_tag = msgTag('Retrievability') - if self.ext_Extension is None: - self.ext_Extension = etree_.Element(extTag('Extension')) - else: - # delete current retrievability info if it exists - for elem in self.ext_Extension: - if elem.tag == retrievability_tag: - self.ext_Extension.remove(elem) - break - node = retrievability_instance.asEtreeNode(retrievability_tag, nsmap=None) - self.ext_Extension.append(node) + def get_retrievability(self): + """Return all retrievability data from Extension.""" + return [msgtypes.Retrievability.fromNode(x) for x in self.ext_Extension if x.tag == msgTag('Retrievability')] + + def set_retrievability(self, retrievabilities): + """Replace all retrievability elements with provided ones in Extension. + + :param retrievabilities: Iterable of msgtypes.Retrievability.""" + for x in [x for x in self.ext_Extension if x.tag == msgTag('Retrievability')]: + self.ext_Extension.remove(x) + self.ext_Extension.extend([r.asEtreeNode(msgTag('Retrievability'), {}) for r in retrievabilities]) def incrementDescriptorVersion(self): if self.DescriptorVersion is None: @@ -125,8 +111,6 @@ def mkNode(self, tag=None, setXsiType=False): def updateNode(self, setXsiType=False): return -# """ deprecated, remove!""" -# self.node = self.mkNode(setXsiType=setXsiType) def connectChildContainers(self, node, containers): ret = self._connectChildNodes(node, containers) @@ -206,7 +190,7 @@ def mkDescriptorNode(self, setXsiType=True, tag=None): """ myTag = tag or self.nodeName node = etree_.Element(myTag, attrib={'Handle': self.handle}, - nsmap = self.nsmapper.partialMap(Prefix.PM, Prefix.XSI)) + nsmap=self.nsmapper.partialMap(Prefix.PM, Prefix.XSI)) self._updateNode(node, setXsiType) order = self._sortedChildNames() self._sortChildNodes(node, order) @@ -243,7 +227,7 @@ def __repr__(self): @classmethod def fromNode(cls, nsmapper, node, parentHandle): obj = cls(nsmapper, - nodeName=None, # will be determined in constructor from node value + nodeName=None, # will be determined in constructor from node value handle=None, # will be determined in constructor from node value parentHandle=parentHandle, node=node) @@ -258,7 +242,6 @@ class AbstractDeviceComponentDescriptorContainer(AbstractDescriptorContainer): _childNodeNames = (domTag('ProductionSpecification'),) - class AbstractComplexDeviceComponentDescriptorContainer(AbstractDeviceComponentDescriptorContainer): _childNodeNames = (domTag('AlertSystem'), domTag('Sco'),) @@ -354,7 +337,7 @@ class AbstractMetricDescriptorContainer(AbstractDescriptorContainer): isMetricDescriptor = True Unit = cp.SubElementProperty([domTag('Unit')], valueClass=pmtypes.CodedValue) BodySite = cp.SubElementListProperty([domTag('BodySite')], cls=pmtypes.CodedValue) - Relation = cp.SubElementListProperty([domTag('Relation')], cls=pmtypes.Relation) # o...n + Relation = cp.SubElementListProperty([domTag('Relation')], cls=pmtypes.Relation) # o...n MetricCategory = cp.NodeAttributeProperty('MetricCategory', defaultPyValue='Unspec') # required DerivationMethod = cp.NodeAttributeProperty('DerivationMethod') # optional # There is an implied value defined, but it is complicated, therefore here not implemented: @@ -367,8 +350,9 @@ class AbstractMetricDescriptorContainer(AbstractDescriptorContainer): DeterminationPeriod = cp.DurationAttributeProperty('DeterminationPeriod') # optional, xsd:duration LifeTimePeriod = cp.DurationAttributeProperty('LifeTimePeriod') # optional, xsd:duration ActivationDuration = cp.DurationAttributeProperty('ActivationDuration') # optional, xsd:duration - _props = ('Unit', 'BodySite', 'Relation', 'MetricCategory', 'DerivationMethod', 'MetricAvailability', 'MaxMeasurementTime', - 'MaxDelayTime', 'DeterminationPeriod', 'LifeTimePeriod', 'ActivationDuration') + _props = ( + 'Unit', 'BodySite', 'Relation', 'MetricCategory', 'DerivationMethod', 'MetricAvailability', + 'MaxMeasurementTime', 'MaxDelayTime', 'DeterminationPeriod', 'LifeTimePeriod', 'ActivationDuration') _childNodeNames = (domTag('Unit'), domTag('BodySite'), domTag('Relation'), @@ -433,12 +417,14 @@ class AbstractOperationDescriptorContainer(AbstractDescriptorContainer): isOperationalDescriptor = True OperationTarget = cp.NodeAttributeProperty('OperationTarget') SafetyReq = cp.SubElementProperty([extTag('Extension'), siTag('SafetyReq')], valueClass=pmtypes.T_SafetyReq) - InvocationEffectiveTimeout = cp.DurationAttributeProperty('InvocationEffectiveTimeout') # optional xsd:duration - MaxTimeToFinish = cp.DurationAttributeProperty('MaxTimeToFinish') # optional xsd:duration - Retriggerable = cp.BooleanAttributeProperty('Retriggerable', impliedPyValue=True) # optional - # AccessLevel can be: Usr (User), CSUsr (Clinical Super User), RO (Responsible Organization), SP (Service Personnel), Oth (Other) + InvocationEffectiveTimeout = cp.DurationAttributeProperty('InvocationEffectiveTimeout') # optional xsd:duration + MaxTimeToFinish = cp.DurationAttributeProperty('MaxTimeToFinish') # optional xsd:duration + Retriggerable = cp.BooleanAttributeProperty('Retriggerable', impliedPyValue=True) # optional + # AccessLevel can be: Usr (User), CSUsr (Clinical Super User), RO (Responsible Organization), + # SP (Service Personnel), Oth (Other) AccessLevel = cp.NodeAttributeProperty('AccessLevel', impliedPyValue='Usr') - _props = ('OperationTarget', 'SafetyReq', 'InvocationEffectiveTimeout', 'MaxTimeToFinish', 'Retriggerable', 'AccessLevel') + _props = ( + 'OperationTarget', 'SafetyReq', 'InvocationEffectiveTimeout', 'MaxTimeToFinish', 'Retriggerable', 'AccessLevel') class SetValueOperationDescriptorContainer(AbstractOperationDescriptorContainer): @@ -446,7 +432,6 @@ class SetValueOperationDescriptorContainer(AbstractOperationDescriptorContainer) STATE_QNAME = domTag('SetValueOperationState') - class SetStringOperationDescriptorContainer(AbstractOperationDescriptorContainer): NODETYPE = domTag('SetStringOperationDescriptor') STATE_QNAME = domTag('SetStringOperationState') @@ -455,7 +440,7 @@ class SetStringOperationDescriptorContainer(AbstractOperationDescriptorContainer class AbstractSetStateOperationDescriptor(AbstractOperationDescriptorContainer): - ModifiableData = cp.SubElementListProperty([domTag('ModifiableData')], cls = pmtypes.ElementWithTextOnly) + ModifiableData = cp.SubElementListProperty([domTag('ModifiableData')], cls=pmtypes.ElementWithTextOnly) _props = ('ModifiableData',) _childNodeNames = (domTag('ModifiableData'),) @@ -483,14 +468,14 @@ class SetAlertStateOperationDescriptorContainer(AbstractSetStateOperationDescrip class ActivateOperationDescriptorContainer(AbstractSetStateOperationDescriptor): NODETYPE = domTag('ActivateOperationDescriptor') STATE_QNAME = domTag('ActivateOperationState') - Argument = cp.SubElementListProperty([domTag('Argument')], cls = pmtypes.Argument) - _props = ('Argument', ) - _childNodeNames = (domTag('Argument'), ) + Argument = cp.SubElementListProperty([domTag('Argument')], cls=pmtypes.Argument) + _props = ('Argument',) + _childNodeNames = (domTag('Argument'),) class AbstractAlertDescriptorContainer(AbstractDescriptorContainer): """AbstractAlertDescriptor acts as a base class for all alert descriptors that contain static alert meta information. - This class has nor specific data.""" + This class has nor specific data.""" isAlertDescriptor = True @@ -516,14 +501,20 @@ class AlertConditionDescriptorContainer(AbstractAlertDescriptorContainer): isAlertConditionDescriptor = True NODETYPE = domTag('AlertConditionDescriptor') STATE_QNAME = domTag('AlertConditionState') - Source = cp.SubElementListProperty([domTag('Source')], cls = pmtypes.ElementWithTextOnly) # a list of 0...n pm:HandleRef elements - CauseInfo = cp.SubElementListProperty([domTag('CauseInfo')], cls = pmtypes.CauseInfo) # a list of 0...n pm:CauseInfo elements - Kind = cp.NodeAttributeProperty('Kind', defaultPyValue=pmtypes.AlertConditionKind.OTHER) # required, type=AlertConditionKind - Priority = cp.NodeAttributeProperty('Priority', defaultPyValue=pmtypes.AlertConditionPriority.NONE) # required, type= AlertConditionPriority - DefaultConditionGenerationDelay = cp.DurationAttributeProperty('DefaultConditionGenerationDelay', impliedPyValue=0) # optional - CanEscalate = cp.NodeAttributeProperty('CanEscalate') # AlertConditionPriority, without 'None' - CanDeescalate = cp.NodeAttributeProperty('CanDeescalate') # AlertConditionPriority, without 'Hi' - _props = ('Source', 'CauseInfo', 'Kind', 'Priority', 'DefaultConditionGenerationDelay', 'CanEscalate', 'CanDeescalate') + Source = cp.SubElementListProperty([domTag('Source')], # a list of 0...n pm:HandleRef elements + cls=pmtypes.ElementWithTextOnly) + CauseInfo = cp.SubElementListProperty([domTag('CauseInfo')], # a list of 0...n pm:CauseInfo elements + cls=pmtypes.CauseInfo) + Kind = cp.NodeAttributeProperty('Kind', # required, type=AlertConditionKind + defaultPyValue=pmtypes.AlertConditionKind.OTHER) + Priority = cp.NodeAttributeProperty('Priority', # required, type= AlertConditionPriority + defaultPyValue=pmtypes.AlertConditionPriority.NONE) + DefaultConditionGenerationDelay = cp.DurationAttributeProperty('DefaultConditionGenerationDelay', + impliedPyValue=0) # optional + CanEscalate = cp.NodeAttributeProperty('CanEscalate') # AlertConditionPriority, without 'None' + CanDeescalate = cp.NodeAttributeProperty('CanDeescalate') # AlertConditionPriority, without 'Hi' + _props = ( + 'Source', 'CauseInfo', 'Kind', 'Priority', 'DefaultConditionGenerationDelay', 'CanEscalate', 'CanDeescalate') _childNodeNames = (domTag('Source'), domTag('CauseInfo')) @@ -534,31 +525,33 @@ class LimitAlertConditionDescriptorContainer(AlertConditionDescriptorContainer): MaxLimits = cp.SubElementProperty([domTag('MaxLimits')], valueClass=pmtypes.Range) AutoLimitSupported = cp.BooleanAttributeProperty('AutoLimitSupported', impliedPyValue=False) _props = ('MaxLimits', 'AutoLimitSupported',) - _childNodeNames = (domTag('MaxLimits'), - ) + _childNodeNames = (domTag('MaxLimits'),) class AlertSignalDescriptorContainer(AbstractAlertDescriptorContainer): isAlertSignalDescriptor = True NODETYPE = domTag('AlertSignalDescriptor') STATE_QNAME = domTag('AlertSignalState') - ConditionSignaled = cp.NodeAttributeProperty('ConditionSignaled') # required, a HandleRef - Manifestation = cp.NodeAttributeProperty('Manifestation') # required, an AlertSignalManifestation ('Aud', 'Vis', 'Tan', 'Oth') - Latching = cp.BooleanAttributeProperty('Latching') # required - DefaultSignalGenerationDelay = cp.DurationAttributeProperty('DefaultSignalGenerationDelay', impliedPyValue=0)# optional, defaults to PT0s ( 0 seconds) - SignalDelegationSupported = cp.BooleanAttributeProperty('SignalDelegationSupported', impliedPyValue=False) # optional, defaults to false - AcknowledgementSupported = cp.BooleanAttributeProperty('AcknowledgementSupported', impliedPyValue=False) # optional, defaults to false - AcknowledgeTimeout = cp.DurationAttributeProperty('AcknowledgeTimeout') # optional - _props = ('ConditionSignaled', 'Manifestation', 'Latching', 'DefaultSignalGenerationDelay', 'SignalDelegationSupported', - 'AcknowledgementSupported', 'AcknowledgeTimeout') - - - + ConditionSignaled = cp.NodeAttributeProperty('ConditionSignaled') # required, a HandleRef + Manifestation = cp.NodeAttributeProperty( + 'Manifestation') # required, an AlertSignalManifestation ('Aud', 'Vis', 'Tan', 'Oth') + Latching = cp.BooleanAttributeProperty('Latching') # required + DefaultSignalGenerationDelay = cp.DurationAttributeProperty('DefaultSignalGenerationDelay', + impliedPyValue=0) # optional, defaults to 0 seconds + SignalDelegationSupported = cp.BooleanAttributeProperty('SignalDelegationSupported', + impliedPyValue=False) # optional, defaults to false + AcknowledgementSupported = cp.BooleanAttributeProperty('AcknowledgementSupported', + impliedPyValue=False) # optional, defaults to false + AcknowledgeTimeout = cp.DurationAttributeProperty('AcknowledgeTimeout') # optional + _props = ('ConditionSignaled', 'Manifestation', 'Latching', 'DefaultSignalGenerationDelay', + 'SignalDelegationSupported', 'AcknowledgementSupported', 'AcknowledgeTimeout') + + class SystemContextDescriptorContainer(AbstractDeviceComponentDescriptorContainer): isSystemContextDescriptor = True NODETYPE = domTag('SystemContextDescriptor') STATE_QNAME = domTag('SystemContextState') - #Child elements are not modeled here + # Child elements are not modeled here _childNodeNames = (domTag('PatientContext'), domTag('LocationContext'), domTag('EnsembleContext'), @@ -591,10 +584,12 @@ class OperatorContextDescriptorContainer(AbstractContextDescriptorContainer): NODETYPE = domTag('OperatorContextDescriptor') STATE_QNAME = domTag('OperatorContextState') + class MeansContextDescriptorContainer(AbstractContextDescriptorContainer): NODETYPE = domTag('MeansContextDescriptor') STATE_QNAME = domTag('MeansContextState') + class EnsembleContextDescriptorContainer(AbstractContextDescriptorContainer): NODETYPE = domTag('EnsembleContextDescriptor') STATE_QNAME = domTag('EnsembleContextState') @@ -646,7 +641,8 @@ class EnsembleContextDescriptorContainer(AbstractContextDescriptorContainer): domTag('SetMetricStateOperationDescriptor'): SetMetricStateOperationDescriptorContainer, domTag('SetComponentStateOperationDescriptor'): SetComponentStateOperationDescriptorContainer, domTag('SetAlertStateOperationDescriptor'): SetAlertStateOperationDescriptorContainer, - } +} + def getContainerClass(qNameType): """ diff --git a/tests/test_descriptorcontainers.py b/tests/test_descriptorcontainers.py index 1dc2c96c..02f23194 100644 --- a/tests/test_descriptorcontainers.py +++ b/tests/test_descriptorcontainers.py @@ -32,23 +32,22 @@ def test_AbstractDescriptorContainer(self): self.assertEqual(dc2.DescriptorVersion, 0) self.assertEqual(dc2.SafetyClassification, 'Inf') self.assertEqual(dc.Type, None) - self.assertEqual(dc.ext_Extension, None) + self.assertEqual(len(dc.ext_Extension), 0) #test update from node dc.DescriptorVersion = 42 dc.SafetyClassification = 'MedA' dc.Type = pmtypes.CodedValue('abc', 'def') - dc.ext_Extension = etree_.Element(namespaces.extTag('Extension')) - etree_.SubElement(dc.ext_Extension, 'foo', attrib={'someattr':'somevalue'}) - etree_.SubElement(dc.ext_Extension, 'bar', attrib={'anotherattr':'differentvalue'}) + dc.ext_Extension = [etree_.Element('foo', attrib={'someattr':'somevalue'})] + dc.ext_Extension.append(etree_.Element('bar', attrib={'anotherattr':'differentvalue'})) retrievability = msgtypes.Retrievability([msgtypes.RetrievabilityInfo(msgtypes.RetrievabilityMethod.GET), msgtypes.RetrievabilityInfo(msgtypes.RetrievabilityMethod.PERIODIC, update_period=42.0) ] ) - dc.retrievability = retrievability + dc.set_retrievability([retrievability]) node = dc.mkNode() dc2.updateDescrFromNode(node) @@ -59,9 +58,8 @@ def test_AbstractDescriptorContainer(self): self.assertEqual(dc.codeId, 'abc') self.assertEqual(dc.codingSystem, 'def') - self.assertEqual(dc2.ext_Extension.tag, namespaces.extTag('Extension')) self.assertEqual(len(dc2.ext_Extension), 3) - self.assertEqual(dc2.retrievability, retrievability) + self.assertEqual(dc2.get_retrievability(), [retrievability]) def test_AbstractMetricDescriptorContainer(self): diff --git a/tests/test_pmtypes.py b/tests/test_pmtypes.py index 0375f5bd..d78f526b 100644 --- a/tests/test_pmtypes.py +++ b/tests/test_pmtypes.py @@ -1,6 +1,8 @@ import unittest - +from unittest import mock +from lxml.etree import fromstring, tostring, QName from sdc11073 import pmtypes +from sdc11073.mdib.containerproperties import ExtensionLocalValue class TestPmtypes(unittest.TestCase): @@ -31,3 +33,242 @@ def test_base_demographics(self): self.assertEqual(bd.Middlename, ['foo']) bd = pmtypes.BaseDemographics(middlenames=['foo', 'bar']) self.assertEqual(bd.Middlename, ['foo', 'bar']) + + +class TestExtensions(unittest.TestCase): + + def test_compare_extensions(self): + xml = b""" + + + + + + + + """ + self.assertNotEqual(fromstring(xml), fromstring(xml)) + inst1 = pmtypes.InstanceIdentifier.fromNode(fromstring(xml)) + inst2 = pmtypes.InstanceIdentifier.fromNode(fromstring(xml)) + self.assertEqual(inst1.ext_Extension, inst2.ext_Extension) + self.assertEqual(inst1, inst2) + + another_xml = b""" + + + + + + + """ + inst2 = pmtypes.InstanceIdentifier.fromNode(fromstring(another_xml)) + self.assertNotEqual(inst1.ext_Extension, inst2.ext_Extension) + self.assertNotEqual(inst1, inst2) + + def test_compare_extension_with_other_types(self): + xml1 = b""" + + + + """ + xml1 = fromstring(xml1) # noqa: S320 + + inst1 = ExtensionLocalValue([xml1]) + self.assertFalse(inst1 == 42) + self.assertFalse(inst1 == [41]) + + def test_element_order(self): + xml1 = b""" + + + + + + + """ + xml2 = b""" + + + + + + + """ + inst1 = pmtypes.InstanceIdentifier.fromNode(fromstring(xml1)) # noqa: S320 + inst2 = pmtypes.InstanceIdentifier.fromNode(fromstring(xml2)) # noqa: S320 + self.assertNotEqual(inst1.ext_Extension, inst2.ext_Extension) + self.assertNotEqual(inst1, inst2) + + def test_attribute_order(self): + xml1 = b""" + + + + + + """ + xml2 = b""" + + + + + + """ + inst1 = pmtypes.InstanceIdentifier.fromNode(fromstring(xml1)) # noqa: S320 + inst2 = pmtypes.InstanceIdentifier.fromNode(fromstring(xml2)) # noqa: S320 + self.assertEqual(inst1.ext_Extension, inst2.ext_Extension) + self.assertEqual(inst1, inst2) + + def test_fails_with_qname(self): + xml1 = fromstring(b""" + + + what:lorem + +""") # noqa: S320 + xml2 = fromstring(b""" + + + who:lorem + +""") # noqa: S320 + self.assertNotEqual(tostring(xml1), tostring(xml2)) + inst1 = ExtensionLocalValue([xml1]) + inst2 = ExtensionLocalValue([xml2]) + self.assertNotEqual(inst1, inst2) + + def test_ignore_not_needed_namespaces(self): + xml1 = fromstring(b""" + +What does this mean? +""") # noqa: S320 + xml2 = fromstring(b""" + +What does this mean? +""") # noqa: S320 + self.assertNotEqual(tostring(xml1), tostring(xml2)) + inst1 = ExtensionLocalValue([xml1]) + inst2 = ExtensionLocalValue([xml2]) + self.assertEqual(inst1, inst2) + + def test_different_length(self): + inst1 = ExtensionLocalValue([mock.MagicMock(), mock.MagicMock()]) + inst2 = ExtensionLocalValue([mock.MagicMock()]) + self.assertNotEqual(inst1, inst2) + + def test_ignore_comments(self): + xml1 = fromstring(b""" + +What does this mean? + +""") # noqa: S320 + xml2 = fromstring(b""" + +What does this mean? +""") # noqa: S320 + inst1 = ExtensionLocalValue([xml1]) + inst2 = ExtensionLocalValue([xml2]) + self.assertEqual(inst1, inst2) + + def test_custom_compare_method(self): + xml1 = b""" + + + + """ + xml2 = b""" + + + + """ + xml1 = fromstring(xml1) # noqa: S320 + xml2 = fromstring(xml2) # noqa: S320 + + inst1 = ExtensionLocalValue([xml1]) + inst2 = ExtensionLocalValue([xml2]) + self.assertNotEqual(inst1, inst2) + + def _my_comparer(_, __): # noqa: ANN001 ANN202 + return True + + orig_method = ExtensionLocalValue.compare_method + ExtensionLocalValue.compare_method = _my_comparer + self.assertEqual(inst1, inst2) + ExtensionLocalValue.compare_method = orig_method + + def test_cdata(self): + xml1 = b""" + + + ]]> + What does this mean? + """ + xml2 = b""" + + + ]]> + What does this mean? + """ + xml1 = fromstring(xml1) # noqa: S320 + xml2 = fromstring(xml2) # noqa: S320 + + inst1 = ExtensionLocalValue([xml1]) + inst2 = ExtensionLocalValue([xml2]) + self.assertEqual(inst1, inst2) + + def test_comparison_subelements(self): + xml1 = b""" + + + + + + + """ + xml2 = b""" + + + + + + + """ + xml1 = fromstring(xml1) # noqa: S320 + xml2 = fromstring(xml2) # noqa: S320 + + inst1 = ExtensionLocalValue([xml1]) + inst2 = ExtensionLocalValue([xml2]) + self.assertNotEqual(inst1, inst2) + + def test_mixed_content_is_ignored(self): + xml1 = fromstring(b""" + + what:lorem +""") # noqa: S320 + xml2 = fromstring(b""" + + + dsafasdf + what:lorem + +""") # noqa: S320 + inst1 = ExtensionLocalValue([xml1]) + inst2 = ExtensionLocalValue([xml2]) + self.assertEqual(inst1, inst2)