From dc18c992527482d64a381fe370805b4346024ac0 Mon Sep 17 00:00:00 2001 From: cecille Date: Tue, 11 Jun 2024 15:49:47 -0400 Subject: [PATCH 1/8] Choice conformance: Add conformance parsing --- src/python_testing/TestConformanceSupport.py | 208 ++++++++++++++++++- src/python_testing/conformance_support.py | 127 ++++++++--- 2 files changed, 299 insertions(+), 36 deletions(-) diff --git a/src/python_testing/TestConformanceSupport.py b/src/python_testing/TestConformanceSupport.py index 1405e5c8c791cc..3590baf67050b7 100644 --- a/src/python_testing/TestConformanceSupport.py +++ b/src/python_testing/TestConformanceSupport.py @@ -18,7 +18,9 @@ import xml.etree.ElementTree as ElementTree from typing import Callable -from conformance_support import (ConformanceDecision, ConformanceException, ConformanceParseParameters, deprecated, disallowed, +from chip.tlv import uint + +from conformance_support import (Choice, Conformance, ConformanceDecision, ConformanceException, ConformanceParseParameters, deprecated, disallowed, mandatory, optional, parse_basic_callable_from_xml, parse_callable_from_xml, parse_device_type_callable_from_xml, provisional, zigbee) from matter_testing_support import MatterBaseTest, default_matter_test_main @@ -749,7 +751,209 @@ def test_device_type_conformance(self): xml_callable = parse_device_type_callable_from_xml(et) asserts.assert_equal(str(xml_callable), 'CD, testy', msg) asserts.assert_equal(xml_callable(0, [], []), ConformanceDecision.OPTIONAL) - + + + def test_choice_conformance(self): + # Choice conformances can appear on: + # - base optional O.a + # - base optional feature [AB].a + # - base optional attribute [attr1].a + # - base optional command [cmd1].a + # - optional wrapper of complex feature [AB | CD].a, [!attr1].a + # - otherwise conformance attr1, [AB], O.a / attr1, [AB].a, O + # - multiple in otherwise [AB].a, [CD].b + # + # Choice conformances are disallowed on: + # - mandatory M.a + # - mandatory feature AB.a + # - mandatory attribute attr1.a + # - mandatory command cmd1.a + # - AND expressions (attr1 & O.a) + # - OR expressions (attr1 | O.a) + # - NOT expressions (!O.a) + # - internal expressions [AB.a], [attr1.a], [cmd1.a] + # - provisional P.a + # - disallowed X.a + # - deprecated D.a + + choices = [('a+', 'choice="a" more="true"', True), ('a', 'choice="a"', False)] + for suffix, xml_attrs, more in choices: + def check_good_choice(xml: str, conformance_str: str) -> Conformance: + msg = 'Bad choice conformance string' + et = ElementTree.fromstring(xml) + xml_callable = parse_callable_from_xml(et, self.params) + asserts.assert_equal(str(xml_callable), conformance_str, msg) + return xml_callable + + def check_decision(conformance: Conformance, feature_map: uint, attr_list: list[uint], cmd_list: list[uint]): + decision = conformance(feature_map, attr_list, cmd_list) + asserts.assert_true(decision.get_choice(), 'Expected choice conformance on decision, but did not get one') + asserts.assert_equal(decision.get_choice().choice, 'a', 'Unexpected conformance string returned') + asserts.assert_equal(decision.get_choice().more, more, "Unexpected more on choice") + + AB = self.feature_names_to_bits['AB'] + attr1 = [self.attribute_names_to_values['attr1']] + cmd1 = [self.command_names_to_values['cmd1']] + + msg_not_applicable = "Expected NOT_APPLICABLE conformance" + xml = f'' + conformance = check_good_choice(xml, f'O.{suffix}') + check_decision(conformance, 0, [], []) + + xml = (f'' + '' + '') + conformance = check_good_choice(xml, f'[AB].{suffix}') + asserts.assert_equal(conformance(0, [], []), ConformanceDecision.NOT_APPLICABLE, msg_not_applicable) + check_decision(conformance, AB, [], []) + + xml = (f'' + '' + '') + conformance = check_good_choice(xml, f'[attr1].{suffix}') + asserts.assert_equal(conformance(0, [], []), ConformanceDecision.NOT_APPLICABLE, msg_not_applicable) + check_decision(conformance, 0, attr1, []) + + xml = (f'' + '' + '') + conformance = check_good_choice(xml, f'[cmd1].{suffix}') + asserts.assert_equal(conformance(0, [], []), ConformanceDecision.NOT_APPLICABLE, msg_not_applicable) + check_decision(conformance, 0, [], cmd1) + + xml = (f'' + '' + '' + '' + '' + '') + conformance = check_good_choice(xml, f'[AB | CD].{suffix}') + asserts.assert_equal(conformance(0, [], []), ConformanceDecision.NOT_APPLICABLE, msg_not_applicable) + check_decision(conformance, AB, [], []) + + xml = (f'' + '' + '' + '' + '') + conformance = check_good_choice(xml, f'[!attr1].{suffix}') + asserts.assert_equal(conformance(0, attr1, []), ConformanceDecision.NOT_APPLICABLE, msg_not_applicable) + check_decision(conformance, 0, [], []) + + xml = ('' + '' + f'' + '' + '' + f'' + '') + conformance = check_good_choice(xml, f'attr1, [AB], O.{suffix}') + # with no features or attributes, this should end up as O.a, so there should be a choice + check_decision(conformance, 0, [], []) + # when we have this attribute, we should not have a choice + asserts.assert_equal(conformance(0, attr1, []), ConformanceDecision.MANDATORY, 'Unexpected conformance') + asserts.assert_equal(conformance(0, attr1, []).get_choice(), None, 'Unexpected choice in conformance') + # when we have only this feature, we should not have a choice + asserts.assert_equal(conformance(AB, [], []), ConformanceDecision.OPTIONAL, 'Unexpected conformance') + asserts.assert_equal(conformance(AB, [], []).get_choice(), None, 'Unexpected choice in conformance') + + # - multiple in otherwise [AB].a, [CD].b + xml = ('' + '' + f'' + '' + '' + f'' + '' + '' + '') + conformance = check_good_choice(xml, f'attr1, [AB].{suffix}, [CD].b') + asserts.assert_equal(conformance(0, [], []), ConformanceDecision.NOT_APPLICABLE, msg_not_applicable) + # when we have this attribute, we should not have a choice + asserts.assert_equal(conformance(0, attr1, []), ConformanceDecision.MANDATORY, 'Unexpected conformance') + asserts.assert_equal(conformance(0, attr1, []).get_choice(), None, 'Unexpected choice in conformance') + # When it's just AB, we should have a choice + check_decision(conformance, AB, [], []) + # When we have both the attribute and AB, we should not have a choice + asserts.assert_equal(conformance(0, attr1, []), ConformanceDecision.MANDATORY, 'Unexpected conformance') + asserts.assert_equal(conformance(0, attr1, []).get_choice(), None, 'Unexpected choice in conformance') + # When we have AB and CD, we should be using the AB choice + CD = self.feature_names_to_bits['CD'] + ABCD = AB | CD + check_decision(conformance, ABCD, [], []) + # When we just have CD, we still have a choice, but the string should be b + asserts.assert_equal(conformance(CD, [], []), ConformanceDecision.OPTIONAL, 'Unexpected conformance') + asserts.assert_equal(conformance(CD, [], []).get_choice(), Choice('b', False), 'Unexpected choice in conformance') + + + # Ones that should throw exceptions + def check_bad_choice(xml: str): + msg = f'Choice conformance string should cause exception, but did not: {xml}' + et = ElementTree.fromstring(xml) + try: + xml_callable = parse_callable_from_xml(et, self.params) + asserts.fail(msg) + except ConformanceException: + pass + xml = f'' + check_bad_choice(xml) + + xml = f'' + check_bad_choice(xml) + + xml = f'' + check_bad_choice(xml) + + xml = f'' + check_bad_choice(xml) + + xml = ('' + '' + '' + f'' + '' + '') + check_bad_choice(xml) + + xml = ('' + '' + '' + f'' + '' + '') + check_bad_choice(xml) + + + xml = ('' + '' + f'' + '' + '') + check_bad_choice(xml) + + xml = ('' + f'' + '') + check_bad_choice(xml) + + xml = ('' + f'' + '') + check_bad_choice(xml) + + xml = ('' + f'' + '') + check_bad_choice(xml) + + xml = (f'') + check_bad_choice(xml) + + xml = (f'') + check_bad_choice(xml) + + xml = (f'') + check_bad_choice(xml) if __name__ == "__main__": default_matter_test_main() diff --git a/src/python_testing/conformance_support.py b/src/python_testing/conformance_support.py index c9bdb5830c884b..971e484f0adf56 100644 --- a/src/python_testing/conformance_support.py +++ b/src/python_testing/conformance_support.py @@ -18,7 +18,7 @@ import xml.etree.ElementTree as ElementTree from dataclasses import dataclass from enum import Enum, auto -from typing import Callable +from typing import Callable, Optional from chip.tlv import uint @@ -50,6 +50,33 @@ def __str__(self): return f"ConformanceException({self.msg})" +class ChoiceConformanceException(ConformanceException): + def __str__(self): + return f'ChoiceExceptions({self.msg})' + + +class BasicConformanceException(ConformanceException): + pass + + +@dataclass(frozen=True) +class Choice: + choice: str + more: bool + + def __str__(self): + more_str = '+' if self.more else '' + return '.' + self.choice + more_str + + +def parse_choice(element: ElementTree.Element) -> Optional[Choice]: + choice = element.get('choice', '') + more = element.get('more', 'false') == 'true' + if choice and element.tag != OPTIONAL_CONFORM: + raise ChoiceConformanceException('Choice conformance on non-optional attribute') + return Choice(choice, more) if choice else None + + class ConformanceDecision(Enum): MANDATORY = auto() OPTIONAL = auto() @@ -57,6 +84,16 @@ class ConformanceDecision(Enum): DISALLOWED = auto() PROVISIONAL = auto() + def set_choice(self, choice: Choice): + self.choice = choice + return self + + def get_choice(self) -> Optional[Choice]: + try: + return self.choice + except AttributeError: + return None + @dataclass class ConformanceParseParameters: @@ -78,7 +115,14 @@ def is_disallowed(conformance: Callable): return conformance(0, [], []) == ConformanceDecision.DISALLOWED -class zigbee: +@dataclass +class Conformance(Callable): + def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: + raise ConformanceException('Base conformance called') + choice: Optional[Choice] = None + + +class zigbee(Conformance): def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: return ConformanceDecision.NOT_APPLICABLE @@ -86,7 +130,7 @@ def __str__(self): return "Zigbee" -class mandatory: +class mandatory(Conformance): def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: return ConformanceDecision.MANDATORY @@ -94,15 +138,18 @@ def __str__(self): return 'M' -class optional: +class optional(Conformance): + def __init__(self, choice: Optional[Choice] = None): + self.choice = choice + def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: - return ConformanceDecision.OPTIONAL + return ConformanceDecision.OPTIONAL.set_choice(self.choice) def __str__(self): - return 'O' + return 'O' + (str(self.choice) if self.choice else '') -class deprecated: +class deprecated(Conformance): def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: return ConformanceDecision.DISALLOWED @@ -110,7 +157,7 @@ def __str__(self): return 'D' -class disallowed: +class disallowed(Conformance): def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: return ConformanceDecision.DISALLOWED @@ -118,7 +165,7 @@ def __str__(self): return 'X' -class provisional: +class provisional(Conformance): def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: return ConformanceDecision.PROVISIONAL @@ -126,7 +173,7 @@ def __str__(self): return 'P' -class literal: +class literal(Conformance): def __init__(self, value: str): self.value = int(value) @@ -148,7 +195,7 @@ def __str__(self): } -class feature: +class feature(Conformance): def __init__(self, requiredFeature: uint, code: str): self.requiredFeature = requiredFeature self.code = code @@ -162,7 +209,7 @@ def __str__(self): return self.code -class device_feature: +class device_feature(Conformance): ''' This is different than element feature because device types use "features" that aren't reported anywhere''' def __init__(self, feature: str): @@ -175,7 +222,7 @@ def __str__(self): return self.feature -class attribute: +class attribute(Conformance): def __init__(self, requiredAttribute: uint, name: str): self.requiredAttribute = requiredAttribute self.name = name @@ -189,7 +236,7 @@ def __str__(self): return self.name -class command: +class command(Conformance): def __init__(self, requiredCommand: uint, name: str): self.requiredCommand = requiredCommand self.name = name @@ -209,24 +256,26 @@ def strip_outer_parentheses(inner: str) -> str: return inner -class optional_wrapper: - def __init__(self, op: Callable): +class optional_wrapper(Conformance): + def __init__(self, op: Callable, choice: Optional[Choice] = None): self.op = op + self.choice = choice 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 + + if decision in [ConformanceDecision.MANDATORY, ConformanceDecision.OPTIONAL]: + return ConformanceDecision.OPTIONAL.set_choice(self.choice) elif decision == ConformanceDecision.NOT_APPLICABLE: return ConformanceDecision.NOT_APPLICABLE else: raise ConformanceException(f'Optional wrapping invalid op {decision}') def __str__(self): - return f'[{strip_outer_parentheses(str(self.op))}]' + return f'[{strip_outer_parentheses(str(self.op))}]' + (str(self.choice) if self.choice else '') -class mandatory_wrapper: +class mandatory_wrapper(Conformance): def __init__(self, op: Callable): self.op = op @@ -237,8 +286,10 @@ def __str__(self): return strip_outer_parentheses(str(self.op)) -class not_operation: +class not_operation(Conformance): def __init__(self, op: Callable): + if op.choice: + raise ChoiceConformanceException('NOT operation called on choice conformance') self.op = op def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: @@ -262,8 +313,11 @@ def __str__(self): return f'!{str(self.op)}' -class and_operation: +class and_operation(Conformance): def __init__(self, op_list: list[Callable]): + for op in op_list: + if op.choice: + raise ChoiceConformanceException('AND operation with internal choice conformance') self.op_list = op_list def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: @@ -285,8 +339,11 @@ def __str__(self): return f'({" & ".join(op_strs)})' -class or_operation: +class or_operation(Conformance): def __init__(self, op_list: list[Callable]): + for op in op_list: + if op.choice: + raise ChoiceConformanceException('AND operation with internal choice conformance') self.op_list = op_list def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: @@ -296,10 +353,8 @@ def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_li raise ConformanceException('OR operation on optional or disallowed item') elif decision == ConformanceDecision.NOT_APPLICABLE: continue - elif decision == ConformanceDecision.MANDATORY: - return ConformanceDecision.MANDATORY - elif decision == ConformanceDecision.OPTIONAL: - return ConformanceDecision.OPTIONAL + elif decision in [ConformanceDecision.MANDATORY, ConformanceDecision.OPTIONAL]: + return decision else: raise ConformanceException('Oplist item returned non-conformance value') return ConformanceDecision.NOT_APPLICABLE @@ -309,7 +364,7 @@ def __str__(self): return f'({" | ".join(op_strs)})' -class greater_operation: +class greater_operation(Conformance): def _type_ok(self, op: Callable): return type(op) == attribute or type(op) == literal @@ -329,7 +384,7 @@ def __str__(self): return f'{str(self.op1)} > {str(self.op2)}' -class otherwise: +class otherwise(Conformance): def __init__(self, op_list: list[Callable]): self.op_list = op_list @@ -354,9 +409,12 @@ def __str__(self): def parse_basic_callable_from_xml(element: ElementTree.Element) -> Callable: if list(element): - raise ConformanceException("parse_basic_callable_from_xml called for XML element with children") + raise BasicConformanceException("parse_basic_callable_from_xml called for XML element with children") # This will throw a key error if this is not a basic element key. try: + choice = parse_choice(element) + if choice and element.tag == OPTIONAL_CONFORM: + return optional(choice) return BASIC_CONFORMANCE[element.tag] except KeyError: if element.tag == CONDITION_TAG and element.get('name').lower() == ZIGBEE_CONDITION: @@ -364,17 +422,18 @@ def parse_basic_callable_from_xml(element: ElementTree.Element) -> Callable: elif element.tag == LITERAL_TAG: return literal(element.get('value')) else: - raise ConformanceException( + raise BasicConformanceException( f'parse_basic_callable_from_xml called for unknown element {str(element.tag)} {str(element.attrib)}') def parse_wrapper_callable_from_xml(element: ElementTree.Element, ops: list[Callable]) -> Callable: # optional can be a wrapper as well as a standalone # This can be any of the boolean operations, optional or otherwise + choice = parse_choice(element) if element.tag == OPTIONAL_CONFORM: if len(ops) > 1: raise ConformanceException(f'OPTIONAL term found with more than one subelement {list(element)}') - return optional_wrapper(ops[0]) + return optional_wrapper(ops[0], choice) elif element.tag == MANDATORY_CONFORM: if len(ops) > 1: raise ConformanceException(f'MANDATORY term found with more than one subelement {list(element)}') @@ -407,7 +466,7 @@ def parse_device_type_callable_from_xml(element: ElementTree.Element) -> Callabl # actually exposed anywhere ON the device other than through the presence of the cluster. So for now, treat any attribute conditions that are cluster conditions # as just optional, because it's optional to implement any device type feature. # Device types also have some marked as "condition" that are similarly optional - except ConformanceException: + except BasicConformanceException: if element.tag == ATTRIBUTE_TAG or element.tag == CONDITION_TAG or element.tag == FEATURE_TAG: return device_feature(element.attrib['name']) raise @@ -420,7 +479,7 @@ def parse_callable_from_xml(element: ElementTree.Element, params: ConformancePar if not list(element): try: return parse_basic_callable_from_xml(element) - except ConformanceException: + except BasicConformanceException: # If we get an exception here, it wasn't a basic type, so move on and check if its # something else. pass From 766df0cb3d7ce11024e511d06e7137c4811ad53f Mon Sep 17 00:00:00 2001 From: cecille Date: Tue, 11 Jun 2024 15:52:15 -0400 Subject: [PATCH 2/8] Add to tests.yaml --- .github/workflows/tests.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index dccd846affddc1..0f177236de503d 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -563,6 +563,7 @@ jobs: scripts/run_in_python_env.sh out/venv './src/python_testing/test_testing/test_TC_DA_1_2.py' scripts/run_in_python_env.sh out/venv './src/python_testing/test_testing/test_TC_ICDM_2_1.py' scripts/run_in_python_env.sh out/venv 'python3 ./src/python_testing/TestIdChecks.py' + scripts/run_in_python_env.sh out/venv 'python3 .src/python_testing/TestConformanceSupport.py' - name: Uploading core files uses: actions/upload-artifact@v4 From 5b8b745daae8518cf95ce9fa49193fb934dff84a Mon Sep 17 00:00:00 2001 From: "Restyled.io" Date: Tue, 11 Jun 2024 19:56:42 +0000 Subject: [PATCH 3/8] Restyled by autopep8 --- src/python_testing/TestConformanceSupport.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/python_testing/TestConformanceSupport.py b/src/python_testing/TestConformanceSupport.py index 3590baf67050b7..09df48e874832a 100644 --- a/src/python_testing/TestConformanceSupport.py +++ b/src/python_testing/TestConformanceSupport.py @@ -751,7 +751,6 @@ def test_device_type_conformance(self): xml_callable = parse_device_type_callable_from_xml(et) asserts.assert_equal(str(xml_callable), 'CD, testy', msg) asserts.assert_equal(xml_callable(0, [], []), ConformanceDecision.OPTIONAL) - def test_choice_conformance(self): # Choice conformances can appear on: @@ -885,8 +884,8 @@ def check_decision(conformance: Conformance, feature_map: uint, attr_list: list[ asserts.assert_equal(conformance(CD, [], []), ConformanceDecision.OPTIONAL, 'Unexpected conformance') asserts.assert_equal(conformance(CD, [], []).get_choice(), Choice('b', False), 'Unexpected choice in conformance') - # Ones that should throw exceptions + def check_bad_choice(xml: str): msg = f'Choice conformance string should cause exception, but did not: {xml}' et = ElementTree.fromstring(xml) @@ -923,7 +922,6 @@ def check_bad_choice(xml: str): '') check_bad_choice(xml) - xml = ('' '' f'' @@ -955,5 +953,6 @@ def check_bad_choice(xml: str): xml = (f'') check_bad_choice(xml) + if __name__ == "__main__": default_matter_test_main() From 83eb2d3ed2972602b4bb80ea5e934df1f876757a Mon Sep 17 00:00:00 2001 From: "Restyled.io" Date: Tue, 11 Jun 2024 19:56:43 +0000 Subject: [PATCH 4/8] Restyled by isort --- src/python_testing/TestConformanceSupport.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/python_testing/TestConformanceSupport.py b/src/python_testing/TestConformanceSupport.py index 09df48e874832a..b9ee94f017d03e 100644 --- a/src/python_testing/TestConformanceSupport.py +++ b/src/python_testing/TestConformanceSupport.py @@ -19,10 +19,9 @@ from typing import Callable from chip.tlv import uint - -from conformance_support import (Choice, Conformance, ConformanceDecision, ConformanceException, ConformanceParseParameters, deprecated, disallowed, - mandatory, optional, parse_basic_callable_from_xml, parse_callable_from_xml, - parse_device_type_callable_from_xml, provisional, zigbee) +from conformance_support import (Choice, Conformance, ConformanceDecision, ConformanceException, ConformanceParseParameters, + deprecated, disallowed, mandatory, optional, parse_basic_callable_from_xml, + parse_callable_from_xml, parse_device_type_callable_from_xml, provisional, zigbee) from matter_testing_support import MatterBaseTest, default_matter_test_main from mobly import asserts From 68f1eae082fbcbe65a12bbd2343beedfadda2050 Mon Sep 17 00:00:00 2001 From: cecille Date: Fri, 14 Jun 2024 10:00:11 -0400 Subject: [PATCH 5/8] linter --- src/python_testing/TestConformanceSupport.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/python_testing/TestConformanceSupport.py b/src/python_testing/TestConformanceSupport.py index b9ee94f017d03e..69ff30f0925f68 100644 --- a/src/python_testing/TestConformanceSupport.py +++ b/src/python_testing/TestConformanceSupport.py @@ -889,7 +889,7 @@ def check_bad_choice(xml: str): msg = f'Choice conformance string should cause exception, but did not: {xml}' et = ElementTree.fromstring(xml) try: - xml_callable = parse_callable_from_xml(et, self.params) + parse_callable_from_xml(et, self.params) asserts.fail(msg) except ConformanceException: pass From bf48ab9a7762e0a55f53ec368fcb62252b604941 Mon Sep 17 00:00:00 2001 From: C Freeman Date: Tue, 2 Jul 2024 11:18:32 -0400 Subject: [PATCH 6/8] Update .github/workflows/tests.yaml --- .github/workflows/tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 0f177236de503d..830d5ee4bff9eb 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -563,7 +563,7 @@ jobs: scripts/run_in_python_env.sh out/venv './src/python_testing/test_testing/test_TC_DA_1_2.py' scripts/run_in_python_env.sh out/venv './src/python_testing/test_testing/test_TC_ICDM_2_1.py' scripts/run_in_python_env.sh out/venv 'python3 ./src/python_testing/TestIdChecks.py' - scripts/run_in_python_env.sh out/venv 'python3 .src/python_testing/TestConformanceSupport.py' + scripts/run_in_python_env.sh out/venv 'python3 ./src/python_testing/TestConformanceSupport.py' - name: Uploading core files uses: actions/upload-artifact@v4 From 0ec0545e0d3a9bf3213f2b412df72e8d264b663a Mon Sep 17 00:00:00 2001 From: cecille Date: Thu, 4 Jul 2024 16:49:33 -0400 Subject: [PATCH 7/8] wrap enum to contain choice, address review comments --- src/python_testing/TestConformanceSupport.py | 68 +++++----- src/python_testing/conformance_support.py | 124 +++++++++++-------- 2 files changed, 103 insertions(+), 89 deletions(-) diff --git a/src/python_testing/TestConformanceSupport.py b/src/python_testing/TestConformanceSupport.py index 69ff30f0925f68..43d523e3bc6d76 100644 --- a/src/python_testing/TestConformanceSupport.py +++ b/src/python_testing/TestConformanceSupport.py @@ -751,6 +751,18 @@ def test_device_type_conformance(self): asserts.assert_equal(str(xml_callable), 'CD, testy', msg) asserts.assert_equal(xml_callable(0, [], []), ConformanceDecision.OPTIONAL) + def check_good_choice(self, xml: str, conformance_str: str) -> Conformance: + et = ElementTree.fromstring(xml) + xml_callable = parse_callable_from_xml(et, self.params) + asserts.assert_equal(str(xml_callable), conformance_str, 'Bad choice conformance string') + return xml_callable + + def check_decision(self, more_expected: bool, conformance: Conformance, feature_map: uint, attr_list: list[uint], cmd_list: list[uint]): + decision = conformance(feature_map, attr_list, cmd_list) + asserts.assert_true(decision.choice, 'Expected choice conformance on decision, but did not get one') + asserts.assert_equal(decision.choice.marker, 'a', 'Unexpected conformance string returned') + asserts.assert_equal(decision.choice.more, more_expected, "Unexpected more on choice") + def test_choice_conformance(self): # Choice conformances can appear on: # - base optional O.a @@ -776,18 +788,6 @@ def test_choice_conformance(self): choices = [('a+', 'choice="a" more="true"', True), ('a', 'choice="a"', False)] for suffix, xml_attrs, more in choices: - def check_good_choice(xml: str, conformance_str: str) -> Conformance: - msg = 'Bad choice conformance string' - et = ElementTree.fromstring(xml) - xml_callable = parse_callable_from_xml(et, self.params) - asserts.assert_equal(str(xml_callable), conformance_str, msg) - return xml_callable - - def check_decision(conformance: Conformance, feature_map: uint, attr_list: list[uint], cmd_list: list[uint]): - decision = conformance(feature_map, attr_list, cmd_list) - asserts.assert_true(decision.get_choice(), 'Expected choice conformance on decision, but did not get one') - asserts.assert_equal(decision.get_choice().choice, 'a', 'Unexpected conformance string returned') - asserts.assert_equal(decision.get_choice().more, more, "Unexpected more on choice") AB = self.feature_names_to_bits['AB'] attr1 = [self.attribute_names_to_values['attr1']] @@ -795,29 +795,29 @@ def check_decision(conformance: Conformance, feature_map: uint, attr_list: list[ msg_not_applicable = "Expected NOT_APPLICABLE conformance" xml = f'' - conformance = check_good_choice(xml, f'O.{suffix}') - check_decision(conformance, 0, [], []) + conformance = self.check_good_choice(xml, f'O.{suffix}') + self.check_decision(more, conformance, 0, [], []) xml = (f'' '' '') - conformance = check_good_choice(xml, f'[AB].{suffix}') + conformance = self.check_good_choice(xml, f'[AB].{suffix}') asserts.assert_equal(conformance(0, [], []), ConformanceDecision.NOT_APPLICABLE, msg_not_applicable) - check_decision(conformance, AB, [], []) + self.check_decision(more, conformance, AB, [], []) xml = (f'' '' '') - conformance = check_good_choice(xml, f'[attr1].{suffix}') + conformance = self.check_good_choice(xml, f'[attr1].{suffix}') asserts.assert_equal(conformance(0, [], []), ConformanceDecision.NOT_APPLICABLE, msg_not_applicable) - check_decision(conformance, 0, attr1, []) + self.check_decision(more, conformance, 0, attr1, []) xml = (f'' '' '') - conformance = check_good_choice(xml, f'[cmd1].{suffix}') + conformance = self.check_good_choice(xml, f'[cmd1].{suffix}') asserts.assert_equal(conformance(0, [], []), ConformanceDecision.NOT_APPLICABLE, msg_not_applicable) - check_decision(conformance, 0, [], cmd1) + self.check_decision(more, conformance, 0, [], cmd1) xml = (f'' '' @@ -825,18 +825,18 @@ def check_decision(conformance: Conformance, feature_map: uint, attr_list: list[ '' '' '') - conformance = check_good_choice(xml, f'[AB | CD].{suffix}') + conformance = self.check_good_choice(xml, f'[AB | CD].{suffix}') asserts.assert_equal(conformance(0, [], []), ConformanceDecision.NOT_APPLICABLE, msg_not_applicable) - check_decision(conformance, AB, [], []) + self.check_decision(more, conformance, AB, [], []) xml = (f'' '' '' '' '') - conformance = check_good_choice(xml, f'[!attr1].{suffix}') + conformance = self.check_good_choice(xml, f'[!attr1].{suffix}') asserts.assert_equal(conformance(0, attr1, []), ConformanceDecision.NOT_APPLICABLE, msg_not_applicable) - check_decision(conformance, 0, [], []) + self.check_decision(more, conformance, 0, [], []) xml = ('' '' @@ -845,15 +845,15 @@ def check_decision(conformance: Conformance, feature_map: uint, attr_list: list[ '' f'' '') - conformance = check_good_choice(xml, f'attr1, [AB], O.{suffix}') + conformance = self.check_good_choice(xml, f'attr1, [AB], O.{suffix}') # with no features or attributes, this should end up as O.a, so there should be a choice - check_decision(conformance, 0, [], []) + self.check_decision(more, conformance, 0, [], []) # when we have this attribute, we should not have a choice asserts.assert_equal(conformance(0, attr1, []), ConformanceDecision.MANDATORY, 'Unexpected conformance') - asserts.assert_equal(conformance(0, attr1, []).get_choice(), None, 'Unexpected choice in conformance') + asserts.assert_equal(conformance(0, attr1, []).choice, None, 'Unexpected choice in conformance') # when we have only this feature, we should not have a choice asserts.assert_equal(conformance(AB, [], []), ConformanceDecision.OPTIONAL, 'Unexpected conformance') - asserts.assert_equal(conformance(AB, [], []).get_choice(), None, 'Unexpected choice in conformance') + asserts.assert_equal(conformance(AB, [], []).choice, None, 'Unexpected choice in conformance') # - multiple in otherwise [AB].a, [CD].b xml = ('' @@ -865,23 +865,23 @@ def check_decision(conformance: Conformance, feature_map: uint, attr_list: list[ '' '' '') - conformance = check_good_choice(xml, f'attr1, [AB].{suffix}, [CD].b') + conformance = self.check_good_choice(xml, f'attr1, [AB].{suffix}, [CD].b') asserts.assert_equal(conformance(0, [], []), ConformanceDecision.NOT_APPLICABLE, msg_not_applicable) # when we have this attribute, we should not have a choice asserts.assert_equal(conformance(0, attr1, []), ConformanceDecision.MANDATORY, 'Unexpected conformance') - asserts.assert_equal(conformance(0, attr1, []).get_choice(), None, 'Unexpected choice in conformance') + asserts.assert_equal(conformance(0, attr1, []).choice, None, 'Unexpected choice in conformance') # When it's just AB, we should have a choice - check_decision(conformance, AB, [], []) + self.check_decision(more, conformance, AB, [], []) # When we have both the attribute and AB, we should not have a choice asserts.assert_equal(conformance(0, attr1, []), ConformanceDecision.MANDATORY, 'Unexpected conformance') - asserts.assert_equal(conformance(0, attr1, []).get_choice(), None, 'Unexpected choice in conformance') + asserts.assert_equal(conformance(0, attr1, []).choice, None, 'Unexpected choice in conformance') # When we have AB and CD, we should be using the AB choice CD = self.feature_names_to_bits['CD'] ABCD = AB | CD - check_decision(conformance, ABCD, [], []) + self.check_decision(more, conformance, ABCD, [], []) # When we just have CD, we still have a choice, but the string should be b asserts.assert_equal(conformance(CD, [], []), ConformanceDecision.OPTIONAL, 'Unexpected conformance') - asserts.assert_equal(conformance(CD, [], []).get_choice(), Choice('b', False), 'Unexpected choice in conformance') + asserts.assert_equal(conformance(CD, [], []).choice, Choice('b', False), 'Unexpected choice in conformance') # Ones that should throw exceptions diff --git a/src/python_testing/conformance_support.py b/src/python_testing/conformance_support.py index 971e484f0adf56..48c8d75b880155 100644 --- a/src/python_testing/conformance_support.py +++ b/src/python_testing/conformance_support.py @@ -61,20 +61,22 @@ class BasicConformanceException(ConformanceException): @dataclass(frozen=True) class Choice: - choice: str + marker: str more: bool def __str__(self): more_str = '+' if self.more else '' - return '.' + self.choice + more_str + return '.' + self.marker + more_str def parse_choice(element: ElementTree.Element) -> Optional[Choice]: choice = element.get('choice', '') - more = element.get('more', 'false') == 'true' - if choice and element.tag != OPTIONAL_CONFORM: + if not choice: + return None + if element.tag != OPTIONAL_CONFORM: raise ChoiceConformanceException('Choice conformance on non-optional attribute') - return Choice(choice, more) if choice else None + more = element.get('more', 'false') == 'true' + return Choice(choice, more) class ConformanceDecision(Enum): @@ -84,15 +86,18 @@ class ConformanceDecision(Enum): DISALLOWED = auto() PROVISIONAL = auto() - def set_choice(self, choice: Choice): - self.choice = choice - return self - def get_choice(self) -> Optional[Choice]: - try: - return self.choice - except AttributeError: - return None +@dataclass +class ConformanceDecisionWithChoice: + decision: ConformanceDecision + choice: Optional[Choice] = None + + def __eq__(self, other): + if isinstance(other, self.__class__): + return self.decision == other.decision and self.choice == other.choice + if isinstance(other, ConformanceDecision): + return self.decision == other + return False @dataclass @@ -102,7 +107,7 @@ class ConformanceParseParameters: command_map: dict[str, uint] -def conformance_allowed(conformance_decision: ConformanceDecision, allow_provisional: bool): +def conformance_allowed(conformance_decision: ConformanceDecisionWithChoice, allow_provisional: bool): if conformance_decision == ConformanceDecision.NOT_APPLICABLE or conformance_decision == ConformanceDecision.DISALLOWED: return False if conformance_decision == ConformanceDecision.PROVISIONAL: @@ -117,22 +122,31 @@ def is_disallowed(conformance: Callable): @dataclass class Conformance(Callable): - def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: + def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecisionWithChoice: + ''' Evaluates the conformance of a specific cluster or device type element. + + feature_map: The feature_map for the given cluster for which this conformance applies. Used to evaluate feature conformances + attribute_list: The attribute list for the given cluster for which this conformance applied. Used to evaluate attribute conformances + all_command_list: combined list of accepted and generated command IDs for the cluster. Used to evaluate command conformances + + Returns: ConformanceDevisionWithChoice + Raises: ConformanceException if the conformance is invalid + ''' raise ConformanceException('Base conformance called') choice: Optional[Choice] = None class zigbee(Conformance): - def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: - return ConformanceDecision.NOT_APPLICABLE + def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecisionWithChoice: + return ConformanceDecisionWithChoice(ConformanceDecision.NOT_APPLICABLE) def __str__(self): return "Zigbee" class mandatory(Conformance): - def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: - return ConformanceDecision.MANDATORY + def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecisionWithChoice: + return ConformanceDecisionWithChoice(ConformanceDecision.MANDATORY) def __str__(self): return 'M' @@ -142,32 +156,32 @@ class optional(Conformance): def __init__(self, choice: Optional[Choice] = None): self.choice = choice - def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: - return ConformanceDecision.OPTIONAL.set_choice(self.choice) + def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecisionWithChoice: + return ConformanceDecisionWithChoice(ConformanceDecision.OPTIONAL, self.choice) def __str__(self): return 'O' + (str(self.choice) if self.choice else '') class deprecated(Conformance): - def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: - return ConformanceDecision.DISALLOWED + def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecisionWithChoice: + return ConformanceDecisionWithChoice(ConformanceDecision.DISALLOWED) def __str__(self): return 'D' class disallowed(Conformance): - def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: - return ConformanceDecision.DISALLOWED + def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecisionWithChoice: + return ConformanceDecisionWithChoice(ConformanceDecision.DISALLOWED) def __str__(self): return 'X' class provisional(Conformance): - def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: - return ConformanceDecision.PROVISIONAL + def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecisionWithChoice: + return ConformanceDecisionWithChoice(ConformanceDecision.PROVISIONAL) def __str__(self): return 'P' @@ -200,10 +214,10 @@ def __init__(self, requiredFeature: uint, code: str): self.requiredFeature = requiredFeature self.code = code - def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: + def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecisionWithChoice: if self.requiredFeature & feature_map != 0: - return ConformanceDecision.MANDATORY - return ConformanceDecision.NOT_APPLICABLE + return ConformanceDecisionWithChoice(ConformanceDecision.MANDATORY) + return ConformanceDecisionWithChoice(ConformanceDecision.NOT_APPLICABLE) def __str__(self): return self.code @@ -215,8 +229,8 @@ class device_feature(Conformance): def __init__(self, feature: str): self.feature = feature - def __call__(self, feature_map: uint = 0, attribute_list: list[uint] = [], all_command_list: list[uint] = []) -> ConformanceDecision: - return ConformanceDecision.OPTIONAL + def __call__(self, feature_map: uint = 0, attribute_list: list[uint] = [], all_command_list: list[uint] = []) -> ConformanceDecisionWithChoice: + return ConformanceDecisionWithChoice(ConformanceDecision.OPTIONAL) def __str__(self): return self.feature @@ -227,10 +241,10 @@ def __init__(self, requiredAttribute: uint, name: str): self.requiredAttribute = requiredAttribute self.name = name - def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: + def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecisionWithChoice: if self.requiredAttribute in attribute_list: - return ConformanceDecision.MANDATORY - return ConformanceDecision.NOT_APPLICABLE + return ConformanceDecisionWithChoice(ConformanceDecision.MANDATORY) + return ConformanceDecisionWithChoice(ConformanceDecision.NOT_APPLICABLE) def __str__(self): return self.name @@ -241,10 +255,10 @@ def __init__(self, requiredCommand: uint, name: str): self.requiredCommand = requiredCommand self.name = name - def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: + def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecisionWithChoice: if self.requiredCommand in all_command_list: - return ConformanceDecision.MANDATORY - return ConformanceDecision.NOT_APPLICABLE + return ConformanceDecisionWithChoice(ConformanceDecision.MANDATORY) + return ConformanceDecisionWithChoice(ConformanceDecision.NOT_APPLICABLE) def __str__(self): return self.name @@ -261,13 +275,13 @@ def __init__(self, op: Callable, choice: Optional[Choice] = None): self.op = op self.choice = choice - def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: + def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecisionWithChoice: decision = self.op(feature_map, attribute_list, all_command_list) if decision in [ConformanceDecision.MANDATORY, ConformanceDecision.OPTIONAL]: - return ConformanceDecision.OPTIONAL.set_choice(self.choice) + return ConformanceDecisionWithChoice(ConformanceDecision.OPTIONAL, self.choice) elif decision == ConformanceDecision.NOT_APPLICABLE: - return ConformanceDecision.NOT_APPLICABLE + return decision else: raise ConformanceException(f'Optional wrapping invalid op {decision}') @@ -279,7 +293,7 @@ class mandatory_wrapper(Conformance): def __init__(self, op: Callable): self.op = op - def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: + def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecisionWithChoice: return self.op(feature_map, attribute_list, all_command_list) def __str__(self): @@ -292,7 +306,7 @@ def __init__(self, op: Callable): raise ChoiceConformanceException('NOT operation called on choice conformance') self.op = op - def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: + def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecisionWithChoice: # 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 @@ -301,11 +315,11 @@ def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_li raise ConformanceException('NOT operation on optional or disallowed item') # Features in device types degrade to optional so a not operation here is still optional because we don't have any way to verify the features since they're not exposed anywhere elif decision == ConformanceDecision.OPTIONAL: - return ConformanceDecision.OPTIONAL + return decision elif decision == ConformanceDecision.NOT_APPLICABLE: - return ConformanceDecision.MANDATORY + return ConformanceDecisionWithChoice(ConformanceDecision.MANDATORY) elif decision == ConformanceDecision.MANDATORY: - return ConformanceDecision.NOT_APPLICABLE + return ConformanceDecisionWithChoice(ConformanceDecision.NOT_APPLICABLE) else: raise ConformanceException('NOT called on item with non-conformance value') @@ -320,19 +334,19 @@ def __init__(self, op_list: list[Callable]): raise ChoiceConformanceException('AND operation with internal choice conformance') self.op_list = op_list - def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: + def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecisionWithChoice: 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: raise ConformanceException('AND operation on optional or disallowed item') elif decision == ConformanceDecision.NOT_APPLICABLE: - return ConformanceDecision.NOT_APPLICABLE + return decision elif decision == ConformanceDecision.MANDATORY: continue else: raise ConformanceException('Oplist item returned non-conformance value') - return ConformanceDecision.MANDATORY + return ConformanceDecisionWithChoice(ConformanceDecision.MANDATORY) def __str__(self): op_strs = [str(op) for op in self.op_list] @@ -346,7 +360,7 @@ def __init__(self, op_list: list[Callable]): raise ChoiceConformanceException('AND operation with internal choice conformance') self.op_list = op_list - def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: + def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecisionWithChoice: for op in self.op_list: decision = op(feature_map, attribute_list, all_command_list) if decision == ConformanceDecision.DISALLOWED or decision == ConformanceDecision.PROVISIONAL: @@ -357,7 +371,7 @@ def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_li return decision else: raise ConformanceException('Oplist item returned non-conformance value') - return ConformanceDecision.NOT_APPLICABLE + return ConformanceDecisionWithChoice(ConformanceDecision.NOT_APPLICABLE) def __str__(self): op_strs = [str(op) for op in self.op_list] @@ -374,11 +388,11 @@ def __init__(self, op1: Callable, op2: Callable): self.op1 = op1 self.op2 = op2 - def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: + def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecisionWithChoice: # For now, this is fully optional, need to implement this properly later, but it requires access to the actual attribute values # We need to reach into the attribute, but can't use it directly because the attribute callable is an EXISTENCE check and # the arithmetic functions require a value. - return ConformanceDecision.OPTIONAL + return ConformanceDecisionWithChoice(ConformanceDecision.OPTIONAL) def __str__(self): return f'{str(self.op1)} > {str(self.op2)}' @@ -388,7 +402,7 @@ class otherwise(Conformance): 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: + def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecisionWithChoice: # 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 @@ -400,7 +414,7 @@ def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_li if decision == ConformanceDecision.NOT_APPLICABLE: continue return decision - return ConformanceDecision.NOT_APPLICABLE + return ConformanceDecisionWithChoice(ConformanceDecision.NOT_APPLICABLE) def __str__(self): op_strs = [strip_outer_parentheses(str(op)) for op in self.op_list] From d14311fc62aaf345b312986e6f8d89c5ea4aea2f Mon Sep 17 00:00:00 2001 From: cecille Date: Mon, 8 Jul 2024 14:51:19 -0400 Subject: [PATCH 8/8] remove equality operator and check the member directly --- src/python_testing/TC_DeviceConformance.py | 24 +-- src/python_testing/TestConformanceSupport.py | 180 +++++++++---------- src/python_testing/conformance_support.py | 61 +++---- 3 files changed, 129 insertions(+), 136 deletions(-) diff --git a/src/python_testing/TC_DeviceConformance.py b/src/python_testing/TC_DeviceConformance.py index 42fddd7e31ee4b..4ad768289b36a7 100644 --- a/src/python_testing/TC_DeviceConformance.py +++ b/src/python_testing/TC_DeviceConformance.py @@ -119,14 +119,14 @@ def record_warning(location, problem): if cluster_id in ignore_features and f in ignore_features[cluster_id]: continue xml_feature = self.xml_clusters[cluster_id].features[f] - conformance_decision = xml_feature.conformance(feature_map, attribute_list, all_command_list) - if not conformance_allowed(conformance_decision, allow_provisional): + conformance_decision_with_choice = xml_feature.conformance(feature_map, attribute_list, all_command_list) + if not conformance_allowed(conformance_decision_with_choice, allow_provisional): record_error(location=location, problem=f'Disallowed feature with mask 0x{f:02x}') for feature_mask, xml_feature in self.xml_clusters[cluster_id].features.items(): if cluster_id in ignore_features and feature_mask in ignore_features[cluster_id]: continue - conformance_decision = xml_feature.conformance(feature_map, attribute_list, all_command_list) - if conformance_decision == ConformanceDecision.MANDATORY and feature_mask not in feature_masks: + conformance_decision_with_choice = xml_feature.conformance(feature_map, attribute_list, all_command_list) + if conformance_decision_with_choice.decision == ConformanceDecision.MANDATORY and feature_mask not in feature_masks: record_error( location=location, problem=f'Required feature with mask 0x{f:02x} is not present in feature map. {conformance_str(xml_feature.conformance, feature_map, self.xml_clusters[cluster_id].features)}') @@ -142,16 +142,16 @@ def record_warning(location, problem): record_error(location=location, problem='Standard attribute found on device, but not in spec') continue xml_attribute = self.xml_clusters[cluster_id].attributes[attribute_id] - conformance_decision = xml_attribute.conformance(feature_map, attribute_list, all_command_list) - if not conformance_allowed(conformance_decision, allow_provisional): + conformance_decision_with_choice = xml_attribute.conformance(feature_map, attribute_list, all_command_list) + if not conformance_allowed(conformance_decision_with_choice, allow_provisional): location = AttributePathLocation(endpoint_id=endpoint_id, cluster_id=cluster_id, attribute_id=attribute_id) record_error( location=location, problem=f'Attribute 0x{attribute_id:02x} is included, but is disallowed by conformance. {conformance_str(xml_attribute.conformance, feature_map, self.xml_clusters[cluster_id].features)}') for attribute_id, xml_attribute in self.xml_clusters[cluster_id].attributes.items(): if cluster_id in ignore_attributes and attribute_id in ignore_attributes[cluster_id]: continue - conformance_decision = xml_attribute.conformance(feature_map, attribute_list, all_command_list) - if conformance_decision == ConformanceDecision.MANDATORY and attribute_id not in cluster.keys(): + conformance_decision_with_choice = xml_attribute.conformance(feature_map, attribute_list, all_command_list) + if conformance_decision_with_choice.decision == ConformanceDecision.MANDATORY and attribute_id not in cluster.keys(): location = AttributePathLocation(endpoint_id=endpoint_id, cluster_id=cluster_id, attribute_id=attribute_id) record_error( location=location, problem=f'Attribute 0x{attribute_id:02x} is required, but is not present on the DUT. {conformance_str(xml_attribute.conformance, feature_map, self.xml_clusters[cluster_id].features)}') @@ -170,13 +170,13 @@ def check_spec_conformance_for_commands(command_type: CommandType): record_error(location=location, problem='Standard command found on device, but not in spec') continue xml_command = xml_commands_dict[command_id] - conformance_decision = xml_command.conformance(feature_map, attribute_list, all_command_list) - if not conformance_allowed(conformance_decision, allow_provisional): + conformance_decision_with_choice = xml_command.conformance(feature_map, attribute_list, all_command_list) + if not conformance_allowed(conformance_decision_with_choice, allow_provisional): record_error( location=location, problem=f'Command 0x{command_id:02x} is included, but disallowed by conformance. {conformance_str(xml_command.conformance, feature_map, self.xml_clusters[cluster_id].features)}') 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: + conformance_decision_with_choice = xml_command.conformance(feature_map, attribute_list, all_command_list) + if conformance_decision_with_choice.decision == ConformanceDecision.MANDATORY and command_id not in command_list: location = CommandPathLocation(endpoint_id=endpoint_id, cluster_id=cluster_id, command_id=command_id) record_error( location=location, problem=f'Command 0x{command_id:02x} is required, but is not present on the DUT. {conformance_str(xml_command.conformance, feature_map, self.xml_clusters[cluster_id].features)}') diff --git a/src/python_testing/TestConformanceSupport.py b/src/python_testing/TestConformanceSupport.py index 43d523e3bc6d76..3744b6fe5e70d5 100644 --- a/src/python_testing/TestConformanceSupport.py +++ b/src/python_testing/TestConformanceSupport.py @@ -60,7 +60,7 @@ def test_conformance_mandatory(self): 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.MANDATORY) + asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.MANDATORY) asserts.assert_equal(str(xml_callable), 'M') def test_conformance_optional(self): @@ -68,7 +68,7 @@ def test_conformance_optional(self): 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.OPTIONAL) + asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.OPTIONAL) asserts.assert_equal(str(xml_callable), 'O') def test_conformance_disallowed(self): @@ -76,14 +76,14 @@ def test_conformance_disallowed(self): 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(xml_callable(f, [], []).decision, 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(xml_callable(f, [], []).decision, ConformanceDecision.DISALLOWED) asserts.assert_equal(str(xml_callable), 'D') def test_conformance_provisional(self): @@ -91,7 +91,7 @@ def test_conformance_provisional(self): 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.PROVISIONAL) + asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.PROVISIONAL) asserts.assert_equal(str(xml_callable), 'P') def test_conformance_zigbee(self): @@ -99,7 +99,7 @@ def test_conformance_zigbee(self): 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.NOT_APPLICABLE) + asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.NOT_APPLICABLE) asserts.assert_equal(str(xml_callable), 'Zigbee') def test_conformance_mandatory_on_condition(self): @@ -110,9 +110,9 @@ def test_conformance_mandatory_on_condition(self): xml_callable = parse_callable_from_xml(et, self.params) for i, f in enumerate(self.feature_maps): if self.has_ab[i]: - asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.MANDATORY) + asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.MANDATORY) else: - asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.NOT_APPLICABLE) asserts.assert_equal(str(xml_callable), 'AB') xml = ('' @@ -122,9 +122,9 @@ def test_conformance_mandatory_on_condition(self): xml_callable = parse_callable_from_xml(et, self.params) for i, f in enumerate(self.feature_maps): if self.has_cd[i]: - asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.MANDATORY) + asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.MANDATORY) else: - asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.NOT_APPLICABLE) asserts.assert_equal(str(xml_callable), 'CD') # single attribute mandatory @@ -135,9 +135,9 @@ def test_conformance_mandatory_on_condition(self): xml_callable = parse_callable_from_xml(et, self.params) for i, a in enumerate(self.attribute_lists): if self.has_attr1[i]: - asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.MANDATORY) + asserts.assert_equal(xml_callable(0x00, a, []).decision, ConformanceDecision.MANDATORY) else: - asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(xml_callable(0x00, a, []).decision, ConformanceDecision.NOT_APPLICABLE) asserts.assert_equal(str(xml_callable), 'attr1') xml = ('' @@ -147,9 +147,9 @@ def test_conformance_mandatory_on_condition(self): xml_callable = parse_callable_from_xml(et, self.params) for i, a in enumerate(self.attribute_lists): if self.has_attr2[i]: - asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.MANDATORY) + asserts.assert_equal(xml_callable(0x00, a, []).decision, ConformanceDecision.MANDATORY) else: - asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(xml_callable(0x00, a, []).decision, 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 @@ -163,9 +163,9 @@ def test_conformance_optional_on_condition(self): xml_callable = parse_callable_from_xml(et, self.params) for i, f in enumerate(self.feature_maps): if self.has_ab[i]: - asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.OPTIONAL) + asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.OPTIONAL) else: - asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.NOT_APPLICABLE) asserts.assert_equal(str(xml_callable), '[AB]') xml = ('' @@ -175,9 +175,9 @@ def test_conformance_optional_on_condition(self): xml_callable = parse_callable_from_xml(et, self.params) for i, f in enumerate(self.feature_maps): if self.has_cd[i]: - asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.OPTIONAL) + asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.OPTIONAL) else: - asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.NOT_APPLICABLE) asserts.assert_equal(str(xml_callable), '[CD]') # single attribute optional @@ -188,9 +188,9 @@ def test_conformance_optional_on_condition(self): xml_callable = parse_callable_from_xml(et, self.params) for i, a in enumerate(self.attribute_lists): if self.has_attr1[i]: - asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.OPTIONAL) + asserts.assert_equal(xml_callable(0x00, a, []).decision, ConformanceDecision.OPTIONAL) else: - asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(xml_callable(0x00, a, []).decision, ConformanceDecision.NOT_APPLICABLE) asserts.assert_equal(str(xml_callable), '[attr1]') xml = ('' @@ -200,9 +200,9 @@ def test_conformance_optional_on_condition(self): xml_callable = parse_callable_from_xml(et, self.params) for i, a in enumerate(self.attribute_lists): if self.has_attr2[i]: - asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.OPTIONAL) + asserts.assert_equal(xml_callable(0x00, a, []).decision, ConformanceDecision.OPTIONAL) else: - asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(xml_callable(0x00, a, []).decision, ConformanceDecision.NOT_APPLICABLE) asserts.assert_equal(str(xml_callable), '[attr2]') # single command optional @@ -213,9 +213,9 @@ def test_conformance_optional_on_condition(self): xml_callable = parse_callable_from_xml(et, self.params) for i, c in enumerate(self.cmd_lists): if self.has_cmd1[i]: - asserts.assert_equal(xml_callable(0x00, [], c), ConformanceDecision.OPTIONAL) + asserts.assert_equal(xml_callable(0x00, [], c).decision, ConformanceDecision.OPTIONAL) else: - asserts.assert_equal(xml_callable(0x00, [], c), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(xml_callable(0x00, [], c).decision, ConformanceDecision.NOT_APPLICABLE) asserts.assert_equal(str(xml_callable), '[cmd1]') xml = ('' @@ -225,9 +225,9 @@ def test_conformance_optional_on_condition(self): xml_callable = parse_callable_from_xml(et, self.params) for i, c in enumerate(self.cmd_lists): if self.has_cmd2[i]: - asserts.assert_equal(xml_callable(0x00, [], c), ConformanceDecision.OPTIONAL) + asserts.assert_equal(xml_callable(0x00, [], c).decision, ConformanceDecision.OPTIONAL) else: - asserts.assert_equal(xml_callable(0x00, [], c), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(xml_callable(0x00, [], c).decision, ConformanceDecision.NOT_APPLICABLE) asserts.assert_equal(str(xml_callable), '[cmd2]') def test_conformance_not_term_mandatory(self): @@ -241,9 +241,9 @@ def test_conformance_not_term_mandatory(self): xml_callable = parse_callable_from_xml(et, self.params) for i, f in enumerate(self.feature_maps): if not self.has_ab[i]: - asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.MANDATORY) + asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.MANDATORY) else: - asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.NOT_APPLICABLE) asserts.assert_equal(str(xml_callable), '!AB') xml = ('' @@ -255,9 +255,9 @@ def test_conformance_not_term_mandatory(self): xml_callable = parse_callable_from_xml(et, self.params) for i, f in enumerate(self.feature_maps): if not self.has_cd[i]: - asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.MANDATORY) + asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.MANDATORY) else: - asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.NOT_APPLICABLE) asserts.assert_equal(str(xml_callable), '!CD') # single attribute not mandatory @@ -270,9 +270,9 @@ def test_conformance_not_term_mandatory(self): xml_callable = parse_callable_from_xml(et, self.params) for i, a in enumerate(self.attribute_lists): if not self.has_attr1[i]: - asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.MANDATORY) + asserts.assert_equal(xml_callable(0x00, a, []).decision, ConformanceDecision.MANDATORY) else: - asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(xml_callable(0x00, a, []).decision, ConformanceDecision.NOT_APPLICABLE) asserts.assert_equal(str(xml_callable), '!attr1') xml = ('' @@ -284,9 +284,9 @@ def test_conformance_not_term_mandatory(self): xml_callable = parse_callable_from_xml(et, self.params) for i, a in enumerate(self.attribute_lists): if not self.has_attr2[i]: - asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.MANDATORY) + asserts.assert_equal(xml_callable(0x00, a, []).decision, ConformanceDecision.MANDATORY) else: - asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(xml_callable(0x00, a, []).decision, ConformanceDecision.NOT_APPLICABLE) asserts.assert_equal(str(xml_callable), '!attr2') def test_conformance_not_term_optional(self): @@ -300,9 +300,9 @@ def test_conformance_not_term_optional(self): xml_callable = parse_callable_from_xml(et, self.params) for i, f in enumerate(self.feature_maps): if not self.has_ab[i]: - asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.OPTIONAL) + asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.OPTIONAL) else: - asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.NOT_APPLICABLE) asserts.assert_equal(str(xml_callable), '[!AB]') xml = ('' @@ -314,9 +314,9 @@ def test_conformance_not_term_optional(self): xml_callable = parse_callable_from_xml(et, self.params) for i, f in enumerate(self.feature_maps): if not self.has_cd[i]: - asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.OPTIONAL) + asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.OPTIONAL) else: - asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.NOT_APPLICABLE) asserts.assert_equal(str(xml_callable), '[!CD]') def test_conformance_and_term(self): @@ -331,9 +331,9 @@ def test_conformance_and_term(self): xml_callable = parse_callable_from_xml(et, self.params) for i, f in enumerate(self.feature_maps): if self.has_ab[i] and self.has_cd[i]: - asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.MANDATORY) + asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.MANDATORY) else: - asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.NOT_APPLICABLE) asserts.assert_equal(str(xml_callable), 'AB & CD') # and term for attributes only @@ -347,9 +347,9 @@ def test_conformance_and_term(self): xml_callable = parse_callable_from_xml(et, self.params) for i, a in enumerate(self.attribute_lists): if self.has_attr1[i] and self.has_attr2[i]: - asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.MANDATORY) + asserts.assert_equal(xml_callable(0x00, a, []).decision, ConformanceDecision.MANDATORY) else: - asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(xml_callable(0x00, a, []).decision, ConformanceDecision.NOT_APPLICABLE) asserts.assert_equal(str(xml_callable), 'attr1 & attr2') # and term for feature and attribute @@ -364,9 +364,9 @@ def test_conformance_and_term(self): for i, f in enumerate(self.feature_maps): for j, a in enumerate(self.attribute_lists): if self.has_ab[i] and self.has_attr2[j]: - asserts.assert_equal(xml_callable(f, a, []), ConformanceDecision.MANDATORY) + asserts.assert_equal(xml_callable(f, a, []).decision, ConformanceDecision.MANDATORY) else: - asserts.assert_equal(xml_callable(f, a, []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(xml_callable(f, a, []).decision, ConformanceDecision.NOT_APPLICABLE) asserts.assert_equal(str(xml_callable), 'AB & attr2') def test_conformance_or_term(self): @@ -381,9 +381,9 @@ def test_conformance_or_term(self): xml_callable = parse_callable_from_xml(et, self.params) for i, f in enumerate(self.feature_maps): if self.has_ab[i] or self.has_cd[i]: - asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.MANDATORY) + asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.MANDATORY) else: - asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.NOT_APPLICABLE) asserts.assert_equal(str(xml_callable), 'AB | CD') # or term attribute only @@ -397,9 +397,9 @@ def test_conformance_or_term(self): xml_callable = parse_callable_from_xml(et, self.params) for i, a in enumerate(self.attribute_lists): if self.has_attr1[i] or self.has_attr2[i]: - asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.MANDATORY) + asserts.assert_equal(xml_callable(0x00, a, []).decision, ConformanceDecision.MANDATORY) else: - asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(xml_callable(0x00, a, []).decision, ConformanceDecision.NOT_APPLICABLE) asserts.assert_equal(str(xml_callable), 'attr1 | attr2') # or term feature and attribute @@ -414,9 +414,9 @@ def test_conformance_or_term(self): for i, f in enumerate(self.feature_maps): for j, a in enumerate(self.attribute_lists): if self.has_ab[i] or self.has_attr2[j]: - asserts.assert_equal(xml_callable(f, a, []), ConformanceDecision.MANDATORY) + asserts.assert_equal(xml_callable(f, a, []).decision, ConformanceDecision.MANDATORY) else: - asserts.assert_equal(xml_callable(f, a, []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(xml_callable(f, a, []).decision, ConformanceDecision.NOT_APPLICABLE) asserts.assert_equal(str(xml_callable), 'AB | attr2') def test_conformance_and_term_with_not(self): @@ -433,9 +433,9 @@ def test_conformance_and_term_with_not(self): xml_callable = parse_callable_from_xml(et, self.params) for i, f in enumerate(self.feature_maps): if not self.has_ab[i] and self.has_cd[i]: - asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.OPTIONAL) + asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.OPTIONAL) else: - asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.NOT_APPLICABLE) asserts.assert_equal(str(xml_callable), '[!AB & CD]') def test_conformance_or_term_with_not(self): @@ -452,9 +452,9 @@ def test_conformance_or_term_with_not(self): xml_callable = parse_callable_from_xml(et, self.params) for i, f in enumerate(self.feature_maps): if self.has_ab[i] or not self.has_cd[i]: - asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.MANDATORY) + asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.MANDATORY) else: - asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.NOT_APPLICABLE) asserts.assert_equal(str(xml_callable), 'AB | !CD') # not around or term with @@ -470,9 +470,9 @@ def test_conformance_or_term_with_not(self): xml_callable = parse_callable_from_xml(et, self.params) for i, f in enumerate(self.feature_maps): if not (self.has_ab[i] or self.has_cd[i]): - asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.OPTIONAL) + asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.OPTIONAL) else: - asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.NOT_APPLICABLE) asserts.assert_equal(str(xml_callable), '[!(AB | CD)]') def test_conformance_and_term_with_three_terms(self): @@ -488,11 +488,11 @@ def test_conformance_and_term_with_three_terms(self): et = ElementTree.fromstring(xml) xml_callable = parse_callable_from_xml(et, self.params) # no features - asserts.assert_equal(xml_callable(0x00, [], []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(xml_callable(0x00, [], []).decision, ConformanceDecision.NOT_APPLICABLE) # one feature - asserts.assert_equal(xml_callable(0x01, [], []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(xml_callable(0x01, [], []).decision, ConformanceDecision.NOT_APPLICABLE) # all features - asserts.assert_equal(xml_callable(0x07, [], []), ConformanceDecision.OPTIONAL) + asserts.assert_equal(xml_callable(0x07, [], []).decision, ConformanceDecision.OPTIONAL) asserts.assert_equal(str(xml_callable), '[AB & CD & EF]') # and term with one of each @@ -509,9 +509,9 @@ def test_conformance_and_term_with_three_terms(self): for j, a in enumerate(self.attribute_lists): for k, c in enumerate(self.cmd_lists): if self.has_ab[i] and self.has_attr1[j] and self.has_cmd1[k]: - asserts.assert_equal(xml_callable(f, a, c), ConformanceDecision.OPTIONAL) + asserts.assert_equal(xml_callable(f, a, c).decision, ConformanceDecision.OPTIONAL) else: - asserts.assert_equal(xml_callable(f, a, c), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(xml_callable(f, a, c).decision, ConformanceDecision.NOT_APPLICABLE) asserts.assert_equal(str(xml_callable), '[AB & attr1 & cmd1]') def test_conformance_or_term_with_three_terms(self): @@ -526,11 +526,11 @@ def test_conformance_or_term_with_three_terms(self): et = ElementTree.fromstring(xml) xml_callable = parse_callable_from_xml(et, self.params) # no features - asserts.assert_equal(xml_callable(0x00, [], []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(xml_callable(0x00, [], []).decision, ConformanceDecision.NOT_APPLICABLE) # one feature - asserts.assert_equal(xml_callable(0x01, [], []), ConformanceDecision.OPTIONAL) + asserts.assert_equal(xml_callable(0x01, [], []).decision, ConformanceDecision.OPTIONAL) # all features - asserts.assert_equal(xml_callable(0x07, [], []), ConformanceDecision.OPTIONAL) + asserts.assert_equal(xml_callable(0x07, [], []).decision, ConformanceDecision.OPTIONAL) asserts.assert_equal(str(xml_callable), '[AB | CD | EF]') # or term with one of each @@ -547,9 +547,9 @@ def test_conformance_or_term_with_three_terms(self): for j, a in enumerate(self.attribute_lists): for k, c in enumerate(self.cmd_lists): if self.has_ab[i] or self.has_attr1[j] or self.has_cmd1[k]: - asserts.assert_equal(xml_callable(f, a, c), ConformanceDecision.OPTIONAL) + asserts.assert_equal(xml_callable(f, a, c).decision, ConformanceDecision.OPTIONAL) else: - asserts.assert_equal(xml_callable(f, a, c), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(xml_callable(f, a, c).decision, ConformanceDecision.NOT_APPLICABLE) asserts.assert_equal(str(xml_callable), '[AB | attr1 | cmd1]') def test_conformance_otherwise(self): @@ -564,9 +564,9 @@ def test_conformance_otherwise(self): xml_callable = parse_callable_from_xml(et, self.params) for i, f in enumerate(self.feature_maps): if self.has_ab[i]: - asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.MANDATORY) + asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.MANDATORY) else: - asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.OPTIONAL) + asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.OPTIONAL) asserts.assert_equal(str(xml_callable), 'AB, O') # AB, [CD] @@ -582,11 +582,11 @@ def test_conformance_otherwise(self): xml_callable = parse_callable_from_xml(et, self.params) for i, f in enumerate(self.feature_maps): if self.has_ab[i]: - asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.MANDATORY) + asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.MANDATORY) elif self.has_cd[i]: - asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.OPTIONAL) + asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.OPTIONAL) else: - asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.NOT_APPLICABLE) + asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.NOT_APPLICABLE) asserts.assert_equal(str(xml_callable), 'AB, [CD]') # AB & !CD, P @@ -605,9 +605,9 @@ def test_conformance_otherwise(self): xml_callable = parse_callable_from_xml(et, self.params) for i, f in enumerate(self.feature_maps): if self.has_ab[i] and not self.has_cd[i]: - asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.MANDATORY) + asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.MANDATORY) else: - asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.PROVISIONAL) + asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.PROVISIONAL) asserts.assert_equal(str(xml_callable), 'AB & !CD, P') def test_conformance_greater(self): @@ -621,7 +621,7 @@ def test_conformance_greater(self): et = ElementTree.fromstring(xml) xml_callable = parse_callable_from_xml(et, self.params) # TODO: switch this to check greater than once the update to the base is done (#33422) - asserts.assert_equal(xml_callable(0x00, [], []), ConformanceDecision.OPTIONAL) + asserts.assert_equal(xml_callable(0x00, [], []).decision, ConformanceDecision.OPTIONAL) asserts.assert_equal(str(xml_callable), 'attr1 > 1') # Ensure that we can only have greater terms with exactly 2 value @@ -706,7 +706,7 @@ def test_device_type_conformance(self): et = ElementTree.fromstring(xml) xml_callable = parse_device_type_callable_from_xml(et) asserts.assert_equal(str(xml_callable), 'Zigbee', msg) - asserts.assert_equal(xml_callable(0, [], []), ConformanceDecision.NOT_APPLICABLE, msg) + asserts.assert_equal(xml_callable(0, [], []).decision, ConformanceDecision.NOT_APPLICABLE, msg) xml = ('' '' @@ -715,7 +715,7 @@ def test_device_type_conformance(self): xml_callable = parse_device_type_callable_from_xml(et) # expect no exception here asserts.assert_equal(str(xml_callable), '[Zigbee]', msg) - asserts.assert_equal(xml_callable(0, [], []), ConformanceDecision.NOT_APPLICABLE, msg) + asserts.assert_equal(xml_callable(0, [], []).decision, ConformanceDecision.NOT_APPLICABLE, msg) # otherwise conforms are allowed xml = ('' @@ -726,7 +726,7 @@ def test_device_type_conformance(self): xml_callable = parse_device_type_callable_from_xml(et) # expect no exception here asserts.assert_equal(str(xml_callable), 'Zigbee, P', msg) - asserts.assert_equal(xml_callable(0, [], []), ConformanceDecision.PROVISIONAL, msg) + asserts.assert_equal(xml_callable(0, [], []).decision, ConformanceDecision.PROVISIONAL, msg) # Device type conditions or features don't correspond to anything in the spec, so the XML takes a best # guess as to what they are. We should be able to parse features, conditions, attributes as the same @@ -740,7 +740,7 @@ def test_device_type_conformance(self): xml_callable = parse_device_type_callable_from_xml(et) asserts.assert_equal(str(xml_callable), 'CD', msg) # Device features are always optional (at least for now), even though we didn't pass this feature in - asserts.assert_equal(xml_callable(0, [], []), ConformanceDecision.OPTIONAL) + asserts.assert_equal(xml_callable(0, [], []).decision, ConformanceDecision.OPTIONAL) xml = ('' '' @@ -749,7 +749,7 @@ def test_device_type_conformance(self): et = ElementTree.fromstring(xml) xml_callable = parse_device_type_callable_from_xml(et) asserts.assert_equal(str(xml_callable), 'CD, testy', msg) - asserts.assert_equal(xml_callable(0, [], []), ConformanceDecision.OPTIONAL) + asserts.assert_equal(xml_callable(0, [], []).decision, ConformanceDecision.OPTIONAL) def check_good_choice(self, xml: str, conformance_str: str) -> Conformance: et = ElementTree.fromstring(xml) @@ -802,21 +802,21 @@ def test_choice_conformance(self): '' '') conformance = self.check_good_choice(xml, f'[AB].{suffix}') - asserts.assert_equal(conformance(0, [], []), ConformanceDecision.NOT_APPLICABLE, msg_not_applicable) + asserts.assert_equal(conformance(0, [], []).decision, ConformanceDecision.NOT_APPLICABLE, msg_not_applicable) self.check_decision(more, conformance, AB, [], []) xml = (f'' '' '') conformance = self.check_good_choice(xml, f'[attr1].{suffix}') - asserts.assert_equal(conformance(0, [], []), ConformanceDecision.NOT_APPLICABLE, msg_not_applicable) + asserts.assert_equal(conformance(0, [], []).decision, ConformanceDecision.NOT_APPLICABLE, msg_not_applicable) self.check_decision(more, conformance, 0, attr1, []) xml = (f'' '' '') conformance = self.check_good_choice(xml, f'[cmd1].{suffix}') - asserts.assert_equal(conformance(0, [], []), ConformanceDecision.NOT_APPLICABLE, msg_not_applicable) + asserts.assert_equal(conformance(0, [], []).decision, ConformanceDecision.NOT_APPLICABLE, msg_not_applicable) self.check_decision(more, conformance, 0, [], cmd1) xml = (f'' @@ -826,7 +826,7 @@ def test_choice_conformance(self): '' '') conformance = self.check_good_choice(xml, f'[AB | CD].{suffix}') - asserts.assert_equal(conformance(0, [], []), ConformanceDecision.NOT_APPLICABLE, msg_not_applicable) + asserts.assert_equal(conformance(0, [], []).decision, ConformanceDecision.NOT_APPLICABLE, msg_not_applicable) self.check_decision(more, conformance, AB, [], []) xml = (f'' @@ -835,7 +835,7 @@ def test_choice_conformance(self): '' '') conformance = self.check_good_choice(xml, f'[!attr1].{suffix}') - asserts.assert_equal(conformance(0, attr1, []), ConformanceDecision.NOT_APPLICABLE, msg_not_applicable) + asserts.assert_equal(conformance(0, attr1, []).decision, ConformanceDecision.NOT_APPLICABLE, msg_not_applicable) self.check_decision(more, conformance, 0, [], []) xml = ('' @@ -849,10 +849,10 @@ def test_choice_conformance(self): # with no features or attributes, this should end up as O.a, so there should be a choice self.check_decision(more, conformance, 0, [], []) # when we have this attribute, we should not have a choice - asserts.assert_equal(conformance(0, attr1, []), ConformanceDecision.MANDATORY, 'Unexpected conformance') + asserts.assert_equal(conformance(0, attr1, []).decision, ConformanceDecision.MANDATORY, 'Unexpected conformance') asserts.assert_equal(conformance(0, attr1, []).choice, None, 'Unexpected choice in conformance') # when we have only this feature, we should not have a choice - asserts.assert_equal(conformance(AB, [], []), ConformanceDecision.OPTIONAL, 'Unexpected conformance') + asserts.assert_equal(conformance(AB, [], []).decision, ConformanceDecision.OPTIONAL, 'Unexpected conformance') asserts.assert_equal(conformance(AB, [], []).choice, None, 'Unexpected choice in conformance') # - multiple in otherwise [AB].a, [CD].b @@ -866,21 +866,21 @@ def test_choice_conformance(self): '' '') conformance = self.check_good_choice(xml, f'attr1, [AB].{suffix}, [CD].b') - asserts.assert_equal(conformance(0, [], []), ConformanceDecision.NOT_APPLICABLE, msg_not_applicable) + asserts.assert_equal(conformance(0, [], []).decision, ConformanceDecision.NOT_APPLICABLE, msg_not_applicable) # when we have this attribute, we should not have a choice - asserts.assert_equal(conformance(0, attr1, []), ConformanceDecision.MANDATORY, 'Unexpected conformance') + asserts.assert_equal(conformance(0, attr1, []).decision, ConformanceDecision.MANDATORY, 'Unexpected conformance') asserts.assert_equal(conformance(0, attr1, []).choice, None, 'Unexpected choice in conformance') # When it's just AB, we should have a choice self.check_decision(more, conformance, AB, [], []) # When we have both the attribute and AB, we should not have a choice - asserts.assert_equal(conformance(0, attr1, []), ConformanceDecision.MANDATORY, 'Unexpected conformance') + asserts.assert_equal(conformance(0, attr1, []).decision, ConformanceDecision.MANDATORY, 'Unexpected conformance') asserts.assert_equal(conformance(0, attr1, []).choice, None, 'Unexpected choice in conformance') # When we have AB and CD, we should be using the AB choice CD = self.feature_names_to_bits['CD'] ABCD = AB | CD self.check_decision(more, conformance, ABCD, [], []) # When we just have CD, we still have a choice, but the string should be b - asserts.assert_equal(conformance(CD, [], []), ConformanceDecision.OPTIONAL, 'Unexpected conformance') + asserts.assert_equal(conformance(CD, [], []).decision, ConformanceDecision.OPTIONAL, 'Unexpected conformance') asserts.assert_equal(conformance(CD, [], []).choice, Choice('b', False), 'Unexpected choice in conformance') # Ones that should throw exceptions diff --git a/src/python_testing/conformance_support.py b/src/python_testing/conformance_support.py index 48c8d75b880155..6e439f1deb2b4d 100644 --- a/src/python_testing/conformance_support.py +++ b/src/python_testing/conformance_support.py @@ -92,13 +92,6 @@ class ConformanceDecisionWithChoice: decision: ConformanceDecision choice: Optional[Choice] = None - def __eq__(self, other): - if isinstance(other, self.__class__): - return self.decision == other.decision and self.choice == other.choice - if isinstance(other, ConformanceDecision): - return self.decision == other - return False - @dataclass class ConformanceParseParameters: @@ -108,16 +101,16 @@ class ConformanceParseParameters: def conformance_allowed(conformance_decision: ConformanceDecisionWithChoice, allow_provisional: bool): - if conformance_decision == ConformanceDecision.NOT_APPLICABLE or conformance_decision == ConformanceDecision.DISALLOWED: + if conformance_decision.decision in [ConformanceDecision.NOT_APPLICABLE, ConformanceDecision.DISALLOWED]: return False - if conformance_decision == ConformanceDecision.PROVISIONAL: + if conformance_decision.decision == ConformanceDecision.PROVISIONAL: return allow_provisional return True def is_disallowed(conformance: Callable): # Deprecated and disallowed conformances will come back as disallowed regardless of the implemented features / attributes / etc. - return conformance(0, [], []) == ConformanceDecision.DISALLOWED + return conformance(0, [], []).decision == ConformanceDecision.DISALLOWED @dataclass @@ -276,14 +269,14 @@ def __init__(self, op: Callable, choice: Optional[Choice] = None): self.choice = choice def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecisionWithChoice: - decision = self.op(feature_map, attribute_list, all_command_list) + decision_with_choice = self.op(feature_map, attribute_list, all_command_list) - if decision in [ConformanceDecision.MANDATORY, ConformanceDecision.OPTIONAL]: + if decision_with_choice.decision in [ConformanceDecision.MANDATORY, ConformanceDecision.OPTIONAL]: return ConformanceDecisionWithChoice(ConformanceDecision.OPTIONAL, self.choice) - elif decision == ConformanceDecision.NOT_APPLICABLE: - return decision + elif decision_with_choice.decision == ConformanceDecision.NOT_APPLICABLE: + return decision_with_choice else: - raise ConformanceException(f'Optional wrapping invalid op {decision}') + raise ConformanceException(f'Optional wrapping invalid op {decision_with_choice}') def __str__(self): return f'[{strip_outer_parentheses(str(self.op))}]' + (str(self.choice) if self.choice else '') @@ -310,15 +303,15 @@ def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_li # 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 = self.op(feature_map, attribute_list, all_command_list) - if decision == ConformanceDecision.DISALLOWED or decision == ConformanceDecision.PROVISIONAL: + decision_with_choice = self.op(feature_map, attribute_list, all_command_list) + if decision_with_choice.decision in [ConformanceDecision.DISALLOWED, ConformanceDecision.PROVISIONAL]: raise ConformanceException('NOT operation on optional or disallowed item') # Features in device types degrade to optional so a not operation here is still optional because we don't have any way to verify the features since they're not exposed anywhere - elif decision == ConformanceDecision.OPTIONAL: - return decision - elif decision == ConformanceDecision.NOT_APPLICABLE: + elif decision_with_choice.decision == ConformanceDecision.OPTIONAL: + return decision_with_choice + elif decision_with_choice.decision == ConformanceDecision.NOT_APPLICABLE: return ConformanceDecisionWithChoice(ConformanceDecision.MANDATORY) - elif decision == ConformanceDecision.MANDATORY: + elif decision_with_choice.decision == ConformanceDecision.MANDATORY: return ConformanceDecisionWithChoice(ConformanceDecision.NOT_APPLICABLE) else: raise ConformanceException('NOT called on item with non-conformance value') @@ -336,13 +329,13 @@ def __init__(self, op_list: list[Callable]): def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecisionWithChoice: for op in self.op_list: - decision = op(feature_map, attribute_list, all_command_list) + decision_with_choice = 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: + if decision_with_choice.decision in [ConformanceDecision.OPTIONAL, ConformanceDecision.DISALLOWED, ConformanceDecision.PROVISIONAL]: raise ConformanceException('AND operation on optional or disallowed item') - elif decision == ConformanceDecision.NOT_APPLICABLE: - return decision - elif decision == ConformanceDecision.MANDATORY: + elif decision_with_choice.decision == ConformanceDecision.NOT_APPLICABLE: + return decision_with_choice + elif decision_with_choice.decision == ConformanceDecision.MANDATORY: continue else: raise ConformanceException('Oplist item returned non-conformance value') @@ -362,13 +355,13 @@ def __init__(self, op_list: list[Callable]): def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecisionWithChoice: for op in self.op_list: - decision = op(feature_map, attribute_list, all_command_list) - if decision == ConformanceDecision.DISALLOWED or decision == ConformanceDecision.PROVISIONAL: + decision_with_choice = op(feature_map, attribute_list, all_command_list) + if decision_with_choice.decision in [ConformanceDecision.DISALLOWED, ConformanceDecision.PROVISIONAL]: raise ConformanceException('OR operation on optional or disallowed item') - elif decision == ConformanceDecision.NOT_APPLICABLE: + elif decision_with_choice.decision == ConformanceDecision.NOT_APPLICABLE: continue - elif decision in [ConformanceDecision.MANDATORY, ConformanceDecision.OPTIONAL]: - return decision + elif decision_with_choice.decision in [ConformanceDecision.MANDATORY, ConformanceDecision.OPTIONAL]: + return decision_with_choice else: raise ConformanceException('Oplist item returned non-conformance value') return ConformanceDecisionWithChoice(ConformanceDecision.NOT_APPLICABLE) @@ -410,10 +403,10 @@ def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_li # For O,D, optional applies (leftmost), but we should consider some way to warn here as well, # possibly in another function for op in self.op_list: - decision = op(feature_map, attribute_list, all_command_list) - if decision == ConformanceDecision.NOT_APPLICABLE: + decision_with_choice = op(feature_map, attribute_list, all_command_list) + if decision_with_choice.decision == ConformanceDecision.NOT_APPLICABLE: continue - return decision + return decision_with_choice return ConformanceDecisionWithChoice(ConformanceDecision.NOT_APPLICABLE) def __str__(self):