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