From 08630f3758aeea33c0bda33ec238bc9d6f5bfbc8 Mon Sep 17 00:00:00 2001 From: C Freeman Date: Thu, 2 Nov 2023 10:56:51 -0400 Subject: [PATCH] Change callables to instances to allow printing (#30149) * Change callables to instances to allow printing If I just write these as callable classes, I can stringify them and we can get a decent print of the expected conformance so that the error messages are more informative and I don't need to keep going back to the spec. Also added tests, and expanded the error messages in the test. * Restyled by isort --------- Co-authored-by: Restyled.io --- .../TC_DeviceBasicComposition.py | 19 +- src/python_testing/TestConformanceSupport.py | 37 ++++ src/python_testing/conformance_support.py | 187 ++++++++++++------ 3 files changed, 183 insertions(+), 60 deletions(-) diff --git a/src/python_testing/TC_DeviceBasicComposition.py b/src/python_testing/TC_DeviceBasicComposition.py index 9d9ed9e75a5285..69a6d6433cdcf5 100644 --- a/src/python_testing/TC_DeviceBasicComposition.py +++ b/src/python_testing/TC_DeviceBasicComposition.py @@ -31,6 +31,7 @@ import chip.clusters.ClusterObjects import chip.tlv from chip.clusters.Attribute import ValueDecodeFailure +from chip.tlv import uint from conformance_support import ConformanceDecision, conformance_allowed from matter_testing_support import (AttributePathLocation, ClusterPathLocation, CommandPathLocation, MatterBaseTest, async_test_body, default_matter_test_main) @@ -1004,6 +1005,14 @@ def test_DESC_2_2(self): self.fail_current_test("Problems with tags lists") def test_spec_conformance(self): + def conformance_str(conformance: Callable, feature_map: uint, feature_dict: dict[str, uint]) -> str: + codes = [] + for mask, details in feature_dict.items(): + if mask & feature_map: + codes.append(details.code) + + return f'Conformance: {str(conformance)}, implemented features: {",".join(codes)}' + success = True # TODO: provisional needs to be an input parameter allow_provisional = True @@ -1051,7 +1060,7 @@ def test_spec_conformance(self): conformance_decision = xml_feature.conformance(feature_map, attribute_list, all_command_list) if conformance_decision == ConformanceDecision.MANDATORY and feature_mask not in feature_masks: self.record_error(self.get_test_name(), location=location, - problem=f'Required feature with mask 0x{f:02x} is not present in feature map') + problem=f'Required feature with mask 0x{f:02x} is not present in feature map. {conformance_str(xml_feature.conformance, feature_map, clusters[cluster_id].features)}') success = False # Attribute conformance checking @@ -1070,14 +1079,14 @@ def test_spec_conformance(self): if not conformance_allowed(conformance_decision, allow_provisional): location = AttributePathLocation(endpoint_id=endpoint_id, cluster_id=cluster_id, attribute_id=attribute_id) self.record_error(self.get_test_name(), location=location, - problem=f'Attribute 0x{attribute_id:02x} is included, but is disallowed by conformance') + problem=f'Attribute 0x{attribute_id:02x} is included, but is disallowed by conformance. {conformance_str(xml_attribute.conformance, feature_map, clusters[cluster_id].features)}') success = False for attribute_id, xml_attribute in clusters[cluster_id].attributes.items(): conformance_decision = xml_attribute.conformance(feature_map, attribute_list, all_command_list) if conformance_decision == ConformanceDecision.MANDATORY and attribute_id not in cluster.keys(): location = AttributePathLocation(endpoint_id=endpoint_id, cluster_id=cluster_id, attribute_id=attribute_id) self.record_error(self.get_test_name(), location=location, - problem=f'Attribute 0x{attribute_id:02x} is required, but is not present on the DUT') + problem=f'Attribute 0x{attribute_id:02x} is required, but is not present on the DUT. {conformance_str(xml_attribute.conformance, feature_map, clusters[cluster_id].features)}') success = False def check_spec_conformance_for_commands(command_type: CommandType) -> bool: @@ -1101,14 +1110,14 @@ def check_spec_conformance_for_commands(command_type: CommandType) -> bool: conformance_decision = xml_command.conformance(feature_map, attribute_list, all_command_list) if not conformance_allowed(conformance_decision, allow_provisional): self.record_error(self.get_test_name(), location=location, - problem=f'Command 0x{command_id:02x} is included, but disallowed by conformance') + problem=f'Command 0x{command_id:02x} is included, but disallowed by conformance. {conformance_str(xml_command.conformance, feature_map, clusters[cluster_id].features)}') success = False for command_id, xml_command in xml_commands_dict.items(): conformance_decision = xml_command.conformance(feature_map, attribute_list, all_command_list) if conformance_decision == ConformanceDecision.MANDATORY and command_id not in command_list: location = CommandPathLocation(endpoint_id=endpoint_id, cluster_id=cluster_id, command_id=command_id) self.record_error(self.get_test_name(), location=location, - problem=f'Command 0x{command_id:02x} is required, but is not present on the DUT') + problem=f'Command 0x{command_id:02x} is required, but is not present on the DUT. {conformance_str(xml_command.conformance, feature_map, clusters[cluster_id].features)}') success = False return success diff --git a/src/python_testing/TestConformanceSupport.py b/src/python_testing/TestConformanceSupport.py index 53f9e885ff9449..788f1025a52fa3 100644 --- a/src/python_testing/TestConformanceSupport.py +++ b/src/python_testing/TestConformanceSupport.py @@ -53,6 +53,7 @@ async def test_conformance_mandatory(self): xml_callable = parse_callable_from_xml(et, self.params) for f in self.feature_maps: asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.MANDATORY) + asserts.assert_equal(str(xml_callable), 'M') @async_test_body async def test_conformance_optional(self): @@ -61,6 +62,7 @@ async def test_conformance_optional(self): xml_callable = parse_callable_from_xml(et, self.params) for f in self.feature_maps: asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.OPTIONAL) + asserts.assert_equal(str(xml_callable), 'O') @async_test_body async def test_conformance_disallowed(self): @@ -69,12 +71,14 @@ async def test_conformance_disallowed(self): xml_callable = parse_callable_from_xml(et, self.params) for f in self.feature_maps: asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.DISALLOWED) + asserts.assert_equal(str(xml_callable), 'X') xml = '' et = ElementTree.fromstring(xml) xml_callable = parse_callable_from_xml(et, self.params) for f in self.feature_maps: asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.DISALLOWED) + asserts.assert_equal(str(xml_callable), 'D') @async_test_body async def test_conformance_provisional(self): @@ -83,6 +87,7 @@ async def test_conformance_provisional(self): xml_callable = parse_callable_from_xml(et, self.params) for f in self.feature_maps: asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.PROVISIONAL) + asserts.assert_equal(str(xml_callable), 'P') @async_test_body async def test_conformance_mandatory_on_condition(self): @@ -96,6 +101,7 @@ async def test_conformance_mandatory_on_condition(self): asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.MANDATORY) else: asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(str(xml_callable), 'AB') xml = ('' '' @@ -107,6 +113,7 @@ async def test_conformance_mandatory_on_condition(self): asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.MANDATORY) else: asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(str(xml_callable), 'CD') # single attribute mandatory xml = ('' @@ -119,6 +126,7 @@ async def test_conformance_mandatory_on_condition(self): asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.MANDATORY) else: asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(str(xml_callable), 'attr1') xml = ('' '' @@ -130,6 +138,7 @@ async def test_conformance_mandatory_on_condition(self): asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.MANDATORY) else: asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(str(xml_callable), 'attr2') # test command in optional and in boolean - this is the same as attribute essentially, so testing every permutation is overkill @@ -146,6 +155,7 @@ async def test_conformance_optional_on_condition(self): asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.OPTIONAL) else: asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(str(xml_callable), '[AB]') xml = ('' '' @@ -157,6 +167,7 @@ async def test_conformance_optional_on_condition(self): asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.OPTIONAL) else: asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(str(xml_callable), '[CD]') # single attribute optional xml = ('' @@ -169,6 +180,7 @@ async def test_conformance_optional_on_condition(self): asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.OPTIONAL) else: asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(str(xml_callable), '[attr1]') xml = ('' '' @@ -180,6 +192,7 @@ async def test_conformance_optional_on_condition(self): asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.OPTIONAL) else: asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(str(xml_callable), '[attr2]') # single command optional xml = ('' @@ -192,6 +205,7 @@ async def test_conformance_optional_on_condition(self): asserts.assert_equal(xml_callable(0x00, [], c), ConformanceDecision.OPTIONAL) else: asserts.assert_equal(xml_callable(0x00, [], c), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(str(xml_callable), '[cmd1]') xml = ('' '' @@ -203,6 +217,7 @@ async def test_conformance_optional_on_condition(self): asserts.assert_equal(xml_callable(0x00, [], c), ConformanceDecision.OPTIONAL) else: asserts.assert_equal(xml_callable(0x00, [], c), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(str(xml_callable), '[cmd2]') @async_test_body async def test_conformance_not_term_mandatory(self): @@ -219,6 +234,7 @@ async def test_conformance_not_term_mandatory(self): asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.MANDATORY) else: asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(str(xml_callable), '!AB') xml = ('' '' @@ -232,6 +248,7 @@ async def test_conformance_not_term_mandatory(self): asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.MANDATORY) else: asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(str(xml_callable), '!CD') # single attribute not mandatory xml = ('' @@ -246,6 +263,7 @@ async def test_conformance_not_term_mandatory(self): asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.MANDATORY) else: asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(str(xml_callable), '!attr1') xml = ('' '' @@ -259,6 +277,7 @@ async def test_conformance_not_term_mandatory(self): asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.MANDATORY) else: asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(str(xml_callable), '!attr2') @async_test_body async def test_conformance_not_term_optional(self): @@ -275,6 +294,7 @@ async def test_conformance_not_term_optional(self): asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.OPTIONAL) else: asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(str(xml_callable), '[!AB]') xml = ('' '' @@ -288,6 +308,7 @@ async def test_conformance_not_term_optional(self): asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.OPTIONAL) else: asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(str(xml_callable), '[!CD]') @async_test_body async def test_conformance_and_term(self): @@ -305,6 +326,7 @@ async def test_conformance_and_term(self): asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.MANDATORY) else: asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(str(xml_callable), 'AB & CD') # and term for attributes only xml = ('' @@ -320,6 +342,7 @@ async def test_conformance_and_term(self): asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.MANDATORY) else: asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(str(xml_callable), 'attr1 & attr2') # and term for feature and attribute xml = ('' @@ -336,6 +359,7 @@ async def test_conformance_and_term(self): asserts.assert_equal(xml_callable(f, a, []), ConformanceDecision.MANDATORY) else: asserts.assert_equal(xml_callable(f, a, []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(str(xml_callable), 'AB & attr2') @async_test_body async def test_conformance_or_term(self): @@ -353,6 +377,7 @@ async def test_conformance_or_term(self): asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.MANDATORY) else: asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(str(xml_callable), 'AB | CD') # or term attribute only xml = ('' @@ -368,6 +393,7 @@ async def test_conformance_or_term(self): asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.MANDATORY) else: asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(str(xml_callable), 'attr1 | attr2') # or term feature and attribute xml = ('' @@ -384,6 +410,7 @@ async def test_conformance_or_term(self): asserts.assert_equal(xml_callable(f, a, []), ConformanceDecision.MANDATORY) else: asserts.assert_equal(xml_callable(f, a, []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(str(xml_callable), 'AB | attr2') @async_test_body async def test_conformance_and_term_with_not(self): @@ -403,6 +430,7 @@ async def test_conformance_and_term_with_not(self): asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.OPTIONAL) else: asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(str(xml_callable), '[!AB & CD]') @async_test_body async def test_conformance_or_term_with_not(self): @@ -422,6 +450,7 @@ async def test_conformance_or_term_with_not(self): asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.MANDATORY) else: asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(str(xml_callable), 'AB | !CD') # not around or term with xml = ('' @@ -439,6 +468,7 @@ async def test_conformance_or_term_with_not(self): asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.OPTIONAL) else: asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(str(xml_callable), '[!(AB | CD)]') @async_test_body async def test_conformance_and_term_with_three_terms(self): @@ -459,6 +489,7 @@ async def test_conformance_and_term_with_three_terms(self): asserts.assert_equal(xml_callable(0x01, [], []), ConformanceDecision.NOT_APPLICABLE) # all features asserts.assert_equal(xml_callable(0x07, [], []), ConformanceDecision.OPTIONAL) + asserts.assert_equal(str(xml_callable), '[AB & CD & EF]') # and term with one of each xml = ('' @@ -477,6 +508,7 @@ async def test_conformance_and_term_with_three_terms(self): asserts.assert_equal(xml_callable(f, a, c), ConformanceDecision.OPTIONAL) else: asserts.assert_equal(xml_callable(f, a, c), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(str(xml_callable), '[AB & attr1 & cmd1]') @async_test_body async def test_conformance_or_term_with_three_terms(self): @@ -496,6 +528,7 @@ async def test_conformance_or_term_with_three_terms(self): asserts.assert_equal(xml_callable(0x01, [], []), ConformanceDecision.OPTIONAL) # all features asserts.assert_equal(xml_callable(0x07, [], []), ConformanceDecision.OPTIONAL) + asserts.assert_equal(str(xml_callable), '[AB | CD | EF]') # or term with one of each xml = ('' @@ -514,6 +547,7 @@ async def test_conformance_or_term_with_three_terms(self): asserts.assert_equal(xml_callable(f, a, c), ConformanceDecision.OPTIONAL) else: asserts.assert_equal(xml_callable(f, a, c), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(str(xml_callable), '[AB | attr1 | cmd1]') def test_conformance_otherwise(self): # AB, O @@ -530,6 +564,7 @@ def test_conformance_otherwise(self): asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.MANDATORY) else: asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.OPTIONAL) + asserts.assert_equal(str(xml_callable), 'AB, O') # AB, [CD] xml = ('' @@ -549,6 +584,7 @@ def test_conformance_otherwise(self): asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.OPTIONAL) else: asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(str(xml_callable), 'AB, [CD]') # AB & !CD, P xml = ('' @@ -569,6 +605,7 @@ def test_conformance_otherwise(self): asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.MANDATORY) else: asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.PROVISIONAL) + asserts.assert_equal(str(xml_callable), 'AB & !CD, P') if __name__ == "__main__": diff --git a/src/python_testing/conformance_support.py b/src/python_testing/conformance_support.py index 2dabb584c9d0f7..1d3f3128ef37cd 100644 --- a/src/python_testing/conformance_support.py +++ b/src/python_testing/conformance_support.py @@ -67,74 +67,131 @@ def conformance_allowed(conformance_decision: ConformanceDecision, allow_provisi return True -def mandatory(feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: - return ConformanceDecision.MANDATORY +class mandatory: + def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: + return ConformanceDecision.MANDATORY + + def __str__(self): + return 'M' + + +class optional: + def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: + return ConformanceDecision.OPTIONAL + def __str__(self): + return 'O' + + +class deprecated: + def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: + return ConformanceDecision.DISALLOWED + + def __str__(self): + return 'D' -def optional(feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: - return ConformanceDecision.OPTIONAL +class disallowed: + def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: + return ConformanceDecision.DISALLOWED -def deprecated(feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: - return ConformanceDecision.DISALLOWED + def __str__(self): + return 'X' -def disallowed(feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: - return ConformanceDecision.DISALLOWED +class provisional: + def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: + return ConformanceDecision.PROVISIONAL + def __str__(self): + return 'P' -def provisional(feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: - return ConformanceDecision.PROVISIONAL +class feature: + def __init__(self, requiredFeature: uint, code: str): + self.requiredFeature = requiredFeature + self.code = code -def feature(requiredFeature: uint) -> Callable: - def feature_inner(feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: - if requiredFeature & feature_map != 0: + def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: + if self.requiredFeature & feature_map != 0: return ConformanceDecision.MANDATORY return ConformanceDecision.NOT_APPLICABLE - return feature_inner + def __str__(self): + return f'{self.code}' + + +class attribute: + def __init__(self, requiredAttribute: uint, name: str): + self.requiredAttribute = requiredAttribute + self.name = name -def attribute(requiredAttribute: uint) -> Callable: - def attribute_inner(feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: - if requiredAttribute in attribute_list: + def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: + if self.requiredAttribute in attribute_list: return ConformanceDecision.MANDATORY return ConformanceDecision.NOT_APPLICABLE - return attribute_inner + def __str__(self): + return f'{self.name}' + + +class command: + def __init__(self, requiredCommand: uint, name: str): + self.requiredCommand = requiredCommand + self.name = name -def command(requiredCommand: uint) -> Callable: - def command_inner(feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: - if requiredCommand in all_command_list: + def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: + if self.requiredCommand in all_command_list: return ConformanceDecision.MANDATORY return ConformanceDecision.NOT_APPLICABLE - return command_inner + + def __str__(self): + return f'{self.name}' -def optional_wrapper(op: Callable) -> Callable: - def optional_wrapper_inner(feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: - decision = op(feature_map, attribute_list, all_command_list) +def strip_outer_parentheses(inner: str) -> str: + if inner[0] == '(' and inner[-1] == ')': + return inner[1:-1] + return inner + + +class optional_wrapper: + def __init__(self, op: Callable): + self.op = op + + def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: + decision = self.op(feature_map, attribute_list, all_command_list) if decision == ConformanceDecision.MANDATORY or decision == ConformanceDecision.OPTIONAL: return ConformanceDecision.OPTIONAL elif decision == ConformanceDecision.NOT_APPLICABLE: return ConformanceDecision.NOT_APPLICABLE else: raise ConformanceException(f'Optional wrapping invalid op {decision}') - return optional_wrapper_inner + + def __str__(self): + return f'[{strip_outer_parentheses(str(self.op))}]' -def mandatory_wrapper(op: Callable) -> Callable: - def mandatory_wrapper_inner(feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: - return op(feature_map, attribute_list, all_command_list) - return mandatory_wrapper_inner +class mandatory_wrapper: + def __init__(self, op: Callable): + self.op = op + def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: + return self.op(feature_map, attribute_list, all_command_list) -def not_operation(op: Callable): - def not_operation_inner(feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: + def __str__(self): + return strip_outer_parentheses(str(self.op)) + + +class not_operation: + def __init__(self, op: Callable): + self.op = op + + def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: # not operations can't be used with anything that returns DISALLOWED # not operations also can't be used with things that are optional # ie, ![AB] doesn't make sense, nor does !O - decision = op(feature_map, attribute_list, all_command_list) + decision = self.op(feature_map, attribute_list, all_command_list) if decision == ConformanceDecision.OPTIONAL or decision == ConformanceDecision.DISALLOWED or decision == ConformanceDecision.PROVISIONAL: raise ConformanceException('NOT operation on optional or disallowed item') elif decision == ConformanceDecision.NOT_APPLICABLE: @@ -143,12 +200,17 @@ def not_operation_inner(feature_map: uint, attribute_list: list[uint], all_comma return ConformanceDecision.NOT_APPLICABLE else: raise ConformanceException('NOT called on item with non-conformance value') - return not_operation_inner + def __str__(self): + return f'!{str(self.op)}' + + +class and_operation: + def __init__(self, op_list: list[Callable]): + self.op_list = op_list -def and_operation(op_list: list[Callable]) -> Callable: - def and_operation_inner(feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: - for op in op_list: + def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: + for op in self.op_list: decision = op(feature_map, attribute_list, all_command_list) # and operations can't happen on optional or disallowed if decision == ConformanceDecision.OPTIONAL or decision == ConformanceDecision.DISALLOWED or decision == ConformanceDecision.PROVISIONAL: @@ -160,12 +222,18 @@ def and_operation_inner(feature_map: uint, attribute_list: list[uint], all_comma else: raise ConformanceException('Oplist item returned non-conformance value') return ConformanceDecision.MANDATORY - return and_operation_inner + def __str__(self): + op_strs = [str(op) for op in self.op_list] + return f'({" & ".join(op_strs)})' + + +class or_operation: + def __init__(self, op_list: list[Callable]): + self.op_list = op_list -def or_operation(op_list: list[Callable]) -> Callable: - def or_operation_inner(feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: - for op in op_list: + def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: + for op in self.op_list: decision = op(feature_map, attribute_list, all_command_list) if decision == ConformanceDecision.DISALLOWED or decision == ConformanceDecision.PROVISIONAL: raise ConformanceException('OR operation on optional or disallowed item') @@ -178,58 +246,67 @@ def or_operation_inner(feature_map: uint, attribute_list: list[uint], all_comman else: raise ConformanceException('Oplist item returned non-conformance value') return ConformanceDecision.NOT_APPLICABLE - return or_operation_inner + + def __str__(self): + op_strs = [str(op) for op in self.op_list] + return f'({" | ".join(op_strs)})' # TODO: add xor operation once it's required # TODO: how would equal and unequal operations work here? -def otherwise(op_list: list[Callable]) -> Callable: - def otherwise_inner(feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: +class otherwise: + def __init__(self, op_list: list[Callable]): + self.op_list = op_list + + def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: # Otherwise operations apply from left to right. If any of them # has a definite decision (optional, mandatory or disallowed), that is the one that applies # Provisional items are meant to be marked as the first item in the list # Deprecated items are either on their own, or follow an O as O,D. # For O,D, optional applies (leftmost), but we should consider some way to warn here as well, # possibly in another function - for op in op_list: + for op in self.op_list: decision = op(feature_map, attribute_list, all_command_list) if decision == ConformanceDecision.NOT_APPLICABLE: continue return decision return ConformanceDecision.NOT_APPLICABLE - return otherwise_inner + + def __str__(self): + op_strs = [strip_outer_parentheses(str(op)) for op in self.op_list] + return ', '.join(op_strs) def parse_callable_from_xml(element: ElementTree.Element, params: ConformanceParseParameters) -> Callable: if len(list(element)) == 0: # no subchildren here, so this can only be mandatory, optional, provisional, deprecated, disallowed, feature or attribute if element.tag == MANDATORY_CONFORM: - return mandatory + return mandatory() elif element.tag == OPTIONAL_CONFORM: - return optional + return optional() elif element.tag == PROVISIONAL_CONFORM: - return provisional + return provisional() elif element.tag == DEPRECATE_CONFORM: - return deprecated + return deprecated() elif element.tag == DISALLOW_CONFORM: - return disallowed + return disallowed() elif element.tag == FEATURE_TAG: try: - return feature(params.feature_map[element.get('name')]) + return feature(params.feature_map[element.get('name')], element.get('name')) except KeyError: raise ConformanceException(f'Conformance specifies feature not in feature table: {element.get("name")}') elif element.tag == ATTRIBUTE_TAG: # Some command conformance tags are marked as attribute, so if this key isn't in attribute, try command name = element.get('name') if name in params.attribute_map: - return attribute(params.attribute_map[name]) + return attribute(params.attribute_map[name], name) elif name in params.command_map: - return command(params.command_map[name]) + return command(params.command_map[name], name) else: raise ConformanceException(f'Conformance specifies attribute or command not in table: {name}') elif element.tag == COMMAND_TAG: - return command(params.command_map[element.get('name')]) + return command(params.command_map[element.get('name')], element.get('name')) else: raise ConformanceException( f'Unexpected xml conformance element with no children {str(element.tag)} {str(element.attrib)}')