diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 53195008ba4222..aed711e9906d68 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -582,6 +582,7 @@ jobs: scripts/run_in_python_env.sh out/venv './scripts/tests/TestTimeSyncTrustedTimeSourceRunner.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/test_testing/test_IDM_10_4.py' - name: Uploading core files diff --git a/src/python_testing/TC_DeviceConformance.py b/src/python_testing/TC_DeviceConformance.py index f4785e49e78395..851921df60f0a4 100644 --- a/src/python_testing/TC_DeviceConformance.py +++ b/src/python_testing/TC_DeviceConformance.py @@ -124,12 +124,12 @@ def record_warning(location, problem): record_error(location=location, problem=f'Unknown feature with mask 0x{f:02x}') 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(): - 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)}') @@ -145,16 +145,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)}') @@ -173,13 +173,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 1405e5c8c791cc..3744b6fe5e70d5 100644 --- a/src/python_testing/TestConformanceSupport.py +++ b/src/python_testing/TestConformanceSupport.py @@ -18,9 +18,10 @@ import xml.etree.ElementTree as ElementTree from typing import Callable -from conformance_support import (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 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 from mobly import asserts @@ -59,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): @@ -67,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): @@ -75,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): @@ -90,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): @@ -98,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): @@ -109,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 = ('' @@ -121,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 @@ -134,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 = ('' @@ -146,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 @@ -162,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 = ('' @@ -174,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 @@ -187,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 = ('' @@ -199,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 @@ -212,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 = ('' @@ -224,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): @@ -240,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 = ('' @@ -254,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 @@ -269,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 = ('' @@ -283,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): @@ -299,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 = ('' @@ -313,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): @@ -330,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 @@ -346,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 @@ -363,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): @@ -380,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 @@ -396,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 @@ -413,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): @@ -432,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): @@ -451,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 @@ -469,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): @@ -487,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 @@ -508,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): @@ -525,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 @@ -546,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): @@ -563,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] @@ -581,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 @@ -604,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): @@ -620,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 @@ -705,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 = ('' '' @@ -714,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 = ('' @@ -725,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 @@ -739,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 = ('' '' @@ -748,7 +749,208 @@ 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) + 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 + # - 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: + + 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 = self.check_good_choice(xml, f'O.{suffix}') + self.check_decision(more, conformance, 0, [], []) + + xml = (f'' + '' + '') + conformance = self.check_good_choice(xml, f'[AB].{suffix}') + 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, [], []).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, [], []).decision, ConformanceDecision.NOT_APPLICABLE, msg_not_applicable) + self.check_decision(more, conformance, 0, [], cmd1) + + xml = (f'' + '' + '' + '' + '' + '') + conformance = self.check_good_choice(xml, f'[AB | CD].{suffix}') + 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, attr1, []).decision, ConformanceDecision.NOT_APPLICABLE, msg_not_applicable) + self.check_decision(more, conformance, 0, [], []) + + xml = ('' + '' + f'' + '' + '' + f'' + '') + 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 + self.check_decision(more, conformance, 0, [], []) + # when we have this attribute, we should not have a choice + 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, [], []).decision, ConformanceDecision.OPTIONAL, 'Unexpected conformance') + asserts.assert_equal(conformance(AB, [], []).choice, None, 'Unexpected choice in conformance') + + # - multiple in otherwise [AB].a, [CD].b + xml = ('' + '' + f'' + '' + '' + f'' + '' + '' + '') + conformance = self.check_good_choice(xml, f'attr1, [AB].{suffix}, [CD].b') + 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, []).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, []).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, [], []).decision, ConformanceDecision.OPTIONAL, 'Unexpected conformance') + asserts.assert_equal(conformance(CD, [], []).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: + 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__": diff --git a/src/python_testing/conformance_support.py b/src/python_testing/conformance_support.py index c9bdb5830c884b..6e439f1deb2b4d 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,35 @@ 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: + marker: str + more: bool + + def __str__(self): + more_str = '+' if self.more else '' + return '.' + self.marker + more_str + + +def parse_choice(element: ElementTree.Element) -> Optional[Choice]: + choice = element.get('choice', '') + if not choice: + return None + if element.tag != OPTIONAL_CONFORM: + raise ChoiceConformanceException('Choice conformance on non-optional attribute') + more = element.get('more', 'false') == 'true' + return Choice(choice, more) + + class ConformanceDecision(Enum): MANDATORY = auto() OPTIONAL = auto() @@ -58,6 +87,12 @@ class ConformanceDecision(Enum): PROVISIONAL = auto() +@dataclass +class ConformanceDecisionWithChoice: + decision: ConformanceDecision + choice: Optional[Choice] = None + + @dataclass class ConformanceParseParameters: feature_map: dict[str, uint] @@ -65,68 +100,87 @@ class ConformanceParseParameters: command_map: dict[str, uint] -def conformance_allowed(conformance_decision: ConformanceDecision, allow_provisional: bool): - if conformance_decision == ConformanceDecision.NOT_APPLICABLE or conformance_decision == ConformanceDecision.DISALLOWED: +def conformance_allowed(conformance_decision: ConformanceDecisionWithChoice, allow_provisional: bool): + 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 +class Conformance(Callable): + 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: - def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: - return ConformanceDecision.NOT_APPLICABLE + +class zigbee(Conformance): + 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: - def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: - return ConformanceDecision.MANDATORY +class mandatory(Conformance): + 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' -class optional: - def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: - return ConformanceDecision.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]) -> ConformanceDecisionWithChoice: + return ConformanceDecisionWithChoice(ConformanceDecision.OPTIONAL, self.choice) def __str__(self): - return 'O' + return 'O' + (str(self.choice) if self.choice else '') -class deprecated: - def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: - return ConformanceDecision.DISALLOWED +class deprecated(Conformance): + 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: - def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: - return ConformanceDecision.DISALLOWED +class disallowed(Conformance): + 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: - def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision: - return ConformanceDecision.PROVISIONAL +class provisional(Conformance): + 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' -class literal: +class literal(Conformance): def __init__(self, value: str): self.value = int(value) @@ -148,56 +202,56 @@ def __str__(self): } -class feature: +class feature(Conformance): 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 -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): 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 -class attribute: +class attribute(Conformance): 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 -class command: +class command(Conformance): 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 @@ -209,52 +263,56 @@ 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 - elif decision == ConformanceDecision.NOT_APPLICABLE: - return ConformanceDecision.NOT_APPLICABLE + def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecisionWithChoice: + decision_with_choice = self.op(feature_map, attribute_list, all_command_list) + + if decision_with_choice.decision in [ConformanceDecision.MANDATORY, ConformanceDecision.OPTIONAL]: + return ConformanceDecisionWithChoice(ConformanceDecision.OPTIONAL, self.choice) + 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))}]' + 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 - 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): 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: + 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 - 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 ConformanceDecision.OPTIONAL - elif decision == ConformanceDecision.NOT_APPLICABLE: - return ConformanceDecision.MANDATORY - elif decision == ConformanceDecision.MANDATORY: - return 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_with_choice.decision == ConformanceDecision.MANDATORY: + return ConformanceDecisionWithChoice(ConformanceDecision.NOT_APPLICABLE) else: raise ConformanceException('NOT called on item with non-conformance value') @@ -262,54 +320,58 @@ 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: + 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 ConformanceDecision.NOT_APPLICABLE - 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') - return ConformanceDecision.MANDATORY + return ConformanceDecisionWithChoice(ConformanceDecision.MANDATORY) def __str__(self): op_strs = [str(op) for op in self.op_list] 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: + 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 == ConformanceDecision.MANDATORY: - return ConformanceDecision.MANDATORY - elif decision == ConformanceDecision.OPTIONAL: - return ConformanceDecision.OPTIONAL + elif decision_with_choice.decision in [ConformanceDecision.MANDATORY, ConformanceDecision.OPTIONAL]: + return decision_with_choice 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] 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 @@ -319,21 +381,21 @@ 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)}' -class otherwise: +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 @@ -341,11 +403,11 @@ 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 ConformanceDecision.NOT_APPLICABLE + return decision_with_choice + return ConformanceDecisionWithChoice(ConformanceDecision.NOT_APPLICABLE) def __str__(self): op_strs = [strip_outer_parentheses(str(op)) for op in self.op_list] @@ -354,9 +416,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 +429,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 +473,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 +486,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