diff --git a/src/python_testing/TestConformanceSupport.py b/src/python_testing/TestConformanceSupport.py
index f71eb3ed1d531f..1405e5c8c791cc 100644
--- a/src/python_testing/TestConformanceSupport.py
+++ b/src/python_testing/TestConformanceSupport.py
@@ -16,15 +16,23 @@
#
import xml.etree.ElementTree as ElementTree
+from typing import Callable
-from conformance_support import ConformanceDecision, ConformanceException, ConformanceParseParameters, parse_callable_from_xml
-from matter_testing_support import MatterBaseTest, async_test_body, default_matter_test_main
+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 matter_testing_support import MatterBaseTest, default_matter_test_main
from mobly import asserts
+def basic_test(xml: str, cls: Callable) -> None:
+ et = ElementTree.fromstring(xml)
+ xml_callable = parse_basic_callable_from_xml(et)
+ asserts.assert_true(isinstance(xml_callable, cls), "Unexpected class parsed from basic conformance")
+
+
class TestConformanceSupport(MatterBaseTest):
- @async_test_body
- async def setup_class(self):
+ def setup_class(self):
super().setup_class()
# a small feature map
self.feature_names_to_bits = {'AB': 0x01, 'CD': 0x02}
@@ -46,8 +54,7 @@ async def setup_class(self):
self.params = ConformanceParseParameters(
feature_map=self.feature_names_to_bits, attribute_map=self.attribute_names_to_values, command_map=self.command_names_to_values)
- @async_test_body
- async def test_conformance_mandatory(self):
+ def test_conformance_mandatory(self):
xml = ''
et = ElementTree.fromstring(xml)
xml_callable = parse_callable_from_xml(et, self.params)
@@ -55,8 +62,7 @@ async def test_conformance_mandatory(self):
asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.MANDATORY)
asserts.assert_equal(str(xml_callable), 'M')
- @async_test_body
- async def test_conformance_optional(self):
+ def test_conformance_optional(self):
xml = ''
et = ElementTree.fromstring(xml)
xml_callable = parse_callable_from_xml(et, self.params)
@@ -64,8 +70,7 @@ async def test_conformance_optional(self):
asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.OPTIONAL)
asserts.assert_equal(str(xml_callable), 'O')
- @async_test_body
- async def test_conformance_disallowed(self):
+ def test_conformance_disallowed(self):
xml = ''
et = ElementTree.fromstring(xml)
xml_callable = parse_callable_from_xml(et, self.params)
@@ -80,8 +85,7 @@ async def test_conformance_disallowed(self):
asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.DISALLOWED)
asserts.assert_equal(str(xml_callable), 'D')
- @async_test_body
- async def test_conformance_provisional(self):
+ def test_conformance_provisional(self):
xml = ''
et = ElementTree.fromstring(xml)
xml_callable = parse_callable_from_xml(et, self.params)
@@ -89,8 +93,7 @@ async def test_conformance_provisional(self):
asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.PROVISIONAL)
asserts.assert_equal(str(xml_callable), 'P')
- @async_test_body
- async def test_conformance_zigbee(self):
+ def test_conformance_zigbee(self):
xml = ''
et = ElementTree.fromstring(xml)
xml_callable = parse_callable_from_xml(et, self.params)
@@ -98,8 +101,7 @@ async def test_conformance_zigbee(self):
asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.NOT_APPLICABLE)
asserts.assert_equal(str(xml_callable), 'Zigbee')
- @async_test_body
- async def test_conformance_mandatory_on_condition(self):
+ def test_conformance_mandatory_on_condition(self):
xml = (''
''
'')
@@ -151,8 +153,7 @@ async def test_conformance_mandatory_on_condition(self):
# test command in optional and in boolean - this is the same as attribute essentially, so testing every permutation is overkill
- @async_test_body
- async def test_conformance_optional_on_condition(self):
+ def test_conformance_optional_on_condition(self):
# single feature optional
xml = (''
''
@@ -228,8 +229,7 @@ async def test_conformance_optional_on_condition(self):
asserts.assert_equal(xml_callable(0x00, [], c), ConformanceDecision.NOT_APPLICABLE)
asserts.assert_equal(str(xml_callable), '[cmd2]')
- @async_test_body
- async def test_conformance_not_term_mandatory(self):
+ def test_conformance_not_term_mandatory(self):
# single feature not mandatory
xml = (''
''
@@ -288,8 +288,7 @@ async def test_conformance_not_term_mandatory(self):
asserts.assert_equal(xml_callable(0x00, a, []), ConformanceDecision.NOT_APPLICABLE)
asserts.assert_equal(str(xml_callable), '!attr2')
- @async_test_body
- async def test_conformance_not_term_optional(self):
+ def test_conformance_not_term_optional(self):
# single feature not optional
xml = (''
''
@@ -319,8 +318,7 @@ async def test_conformance_not_term_optional(self):
asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.NOT_APPLICABLE)
asserts.assert_equal(str(xml_callable), '[!CD]')
- @async_test_body
- async def test_conformance_and_term(self):
+ def test_conformance_and_term(self):
# and term for features only
xml = (''
''
@@ -370,8 +368,7 @@ async def test_conformance_and_term(self):
asserts.assert_equal(xml_callable(f, a, []), ConformanceDecision.NOT_APPLICABLE)
asserts.assert_equal(str(xml_callable), 'AB & attr2')
- @async_test_body
- async def test_conformance_or_term(self):
+ def test_conformance_or_term(self):
# or term feature only
xml = (''
''
@@ -421,8 +418,7 @@ async def test_conformance_or_term(self):
asserts.assert_equal(xml_callable(f, a, []), ConformanceDecision.NOT_APPLICABLE)
asserts.assert_equal(str(xml_callable), 'AB | attr2')
- @async_test_body
- async def test_conformance_and_term_with_not(self):
+ def test_conformance_and_term_with_not(self):
# and term with not
xml = (''
''
@@ -441,8 +437,7 @@ async def test_conformance_and_term_with_not(self):
asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.NOT_APPLICABLE)
asserts.assert_equal(str(xml_callable), '[!AB & CD]')
- @async_test_body
- async def test_conformance_or_term_with_not(self):
+ def test_conformance_or_term_with_not(self):
# or term with not on second feature
xml = (''
''
@@ -479,8 +474,7 @@ async def test_conformance_or_term_with_not(self):
asserts.assert_equal(xml_callable(f, [], []), ConformanceDecision.NOT_APPLICABLE)
asserts.assert_equal(str(xml_callable), '[!(AB | CD)]')
- @async_test_body
- async def test_conformance_and_term_with_three_terms(self):
+ def test_conformance_and_term_with_three_terms(self):
# and term with three features
xml = (''
''
@@ -519,8 +513,7 @@ async def test_conformance_and_term_with_three_terms(self):
asserts.assert_equal(xml_callable(f, a, c), ConformanceDecision.NOT_APPLICABLE)
asserts.assert_equal(str(xml_callable), '[AB & attr1 & cmd1]')
- @async_test_body
- async def test_conformance_or_term_with_three_terms(self):
+ def test_conformance_or_term_with_three_terms(self):
# or term with three features
xml = (''
''
@@ -671,6 +664,92 @@ def test_conformance_greater(self):
except ConformanceException:
pass
+ def test_basic_conformance(self):
+ basic_test('', mandatory)
+ basic_test('', optional)
+ basic_test('', disallowed)
+ basic_test('', deprecated)
+ basic_test('', provisional)
+ basic_test('', zigbee)
+
+ # feature is not basic so we should get an exception
+ xml = ''
+ et = ElementTree.fromstring(xml)
+ try:
+ parse_basic_callable_from_xml(et)
+ asserts.fail("Unexpected success parsing non-basic conformance")
+ except ConformanceException:
+ pass
+
+ # mandatory tag is basic, but this one is a wrapper, so we should get a TypeError
+ xml = (''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ '')
+ et = ElementTree.fromstring(xml)
+ try:
+ parse_basic_callable_from_xml(et)
+ asserts.fail("Unexpected success parsing mandatory wrapper")
+ except ConformanceException:
+ pass
+
+ def test_device_type_conformance(self):
+ msg = "Unexpected conformance returned for device type"
+ xml = (''
+ ''
+ '')
+ 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)
+
+ xml = (''
+ ''
+ '')
+ et = ElementTree.fromstring(xml)
+ 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)
+
+ # otherwise conforms are allowed
+ xml = (''
+ ''
+ ''
+ '')
+ et = ElementTree.fromstring(xml)
+ 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)
+
+ # 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
+ # thing.
+ # TODO: allow querying conformance for conditional device features
+ # TODO: adjust conformance call function to accept a list of features and evaluate based on that
+ xml = (''
+ ''
+ '')
+ et = ElementTree.fromstring(xml)
+ 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)
+
+ xml = (''
+ ''
+ ''
+ '')
+ 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)
+
if __name__ == "__main__":
default_matter_test_main()
diff --git a/src/python_testing/conformance_support.py b/src/python_testing/conformance_support.py
index 025c1afa678995..c9bdb5830c884b 100644
--- a/src/python_testing/conformance_support.py
+++ b/src/python_testing/conformance_support.py
@@ -39,6 +39,7 @@
COMMAND_TAG = 'command'
CONDITION_TAG = 'condition'
LITERAL_TAG = 'literal'
+ZIGBEE_CONDITION = 'zigbee'
class ConformanceException(Exception):
@@ -137,6 +138,16 @@ def __str__(self):
return str(self.value)
+# Conformance options that apply regardless of the element set of the cluster or device
+BASIC_CONFORMANCE: dict[str, Callable] = {
+ MANDATORY_CONFORM: mandatory(),
+ OPTIONAL_CONFORM: optional(),
+ PROVISIONAL_CONFORM: provisional(),
+ DEPRECATE_CONFORM: deprecated(),
+ DISALLOW_CONFORM: disallowed()
+}
+
+
class feature:
def __init__(self, requiredFeature: uint, code: str):
self.requiredFeature = requiredFeature
@@ -148,7 +159,20 @@ def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_li
return ConformanceDecision.NOT_APPLICABLE
def __str__(self):
- return f'{self.code}'
+ return self.code
+
+
+class device_feature:
+ ''' 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 __str__(self):
+ return self.feature
class attribute:
@@ -162,7 +186,7 @@ def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_li
return ConformanceDecision.NOT_APPLICABLE
def __str__(self):
- return f'{self.name}'
+ return self.name
class command:
@@ -176,7 +200,7 @@ def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_li
return ConformanceDecision.NOT_APPLICABLE
def __str__(self):
- return f'{self.name}'
+ return self.name
def strip_outer_parentheses(inner: str) -> str:
@@ -222,8 +246,11 @@ def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_li
# 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.OPTIONAL or decision == ConformanceDecision.DISALLOWED or decision == ConformanceDecision.PROVISIONAL:
+ if decision == ConformanceDecision.DISALLOWED or decision == 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:
@@ -325,48 +352,23 @@ def __str__(self):
return ', '.join(op_strs)
-def parse_callable_from_xml(element: ElementTree.Element, params: ConformanceParseParameters) -> Callable:
- if len(list(element)) == 0:
- # no subchildren here, so this can only be mandatory, optional, provisional, deprecated, disallowed, feature or attribute
- if element.tag == MANDATORY_CONFORM:
- return mandatory()
- elif element.tag == OPTIONAL_CONFORM:
- return optional()
- elif element.tag == PROVISIONAL_CONFORM:
- return provisional()
- elif element.tag == DEPRECATE_CONFORM:
- return deprecated()
- elif element.tag == DISALLOW_CONFORM:
- return disallowed()
- elif element.tag == FEATURE_TAG:
- try:
- return feature(params.feature_map[element.get('name')], element.get('name'))
- except KeyError:
- raise ConformanceException(f'Conformance specifies feature not in feature table: {element.get("name")}')
- elif element.tag == ATTRIBUTE_TAG:
- # Some command conformance tags are marked as attribute, so if this key isn't in attribute, try command
- name = element.get('name')
- if name in params.attribute_map:
- return attribute(params.attribute_map[name], name)
- elif name in params.command_map:
- return command(params.command_map[name], name)
- else:
- raise ConformanceException(f'Conformance specifies attribute or command not in table: {name}')
- elif element.tag == COMMAND_TAG:
- return command(params.command_map[element.get('name')], element.get('name'))
- elif element.tag == CONDITION_TAG and element.get('name').lower() == 'zigbee':
+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")
+ # This will throw a key error if this is not a basic element key.
+ try:
+ return BASIC_CONFORMANCE[element.tag]
+ except KeyError:
+ if element.tag == CONDITION_TAG and element.get('name').lower() == ZIGBEE_CONDITION:
return zigbee()
elif element.tag == LITERAL_TAG:
return literal(element.get('value'))
else:
raise ConformanceException(
- f'Unexpected xml conformance element with no children {str(element.tag)} {str(element.attrib)}')
+ f'parse_basic_callable_from_xml called for unknown element {str(element.tag)} {str(element.attrib)}')
- # First build the list, then create the callable for this element
- ops = []
- for sub in element:
- ops.append(parse_callable_from_xml(sub, params))
+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
if element.tag == OPTIONAL_CONFORM:
@@ -393,3 +395,55 @@ def parse_callable_from_xml(element: ElementTree.Element, params: ConformancePar
return greater_operation(ops[0], ops[1])
else:
raise ConformanceException(f'Unexpected conformance tag with children {element}')
+
+
+def parse_device_type_callable_from_xml(element: ElementTree.Element) -> Callable:
+ ''' Only allows basic, or wrappers over things that degrade to basic.'''
+ if not list(element):
+ try:
+ return parse_basic_callable_from_xml(element)
+ # For device types ONLY, there are conformances called "attributes" that are essentially just placeholders for conditions in the device library.
+ # For example, temperature controlled cabinet has conditions called "heating" and "cooling". The cluster conditions are dependent on them, but they're not
+ # 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:
+ if element.tag == ATTRIBUTE_TAG or element.tag == CONDITION_TAG or element.tag == FEATURE_TAG:
+ return device_feature(element.attrib['name'])
+ raise
+
+ ops = [parse_device_type_callable_from_xml(sub) for sub in element]
+ return parse_wrapper_callable_from_xml(element, ops)
+
+
+def parse_callable_from_xml(element: ElementTree.Element, params: ConformanceParseParameters) -> Callable:
+ if not list(element):
+ try:
+ return parse_basic_callable_from_xml(element)
+ except ConformanceException:
+ # If we get an exception here, it wasn't a basic type, so move on and check if its
+ # something else.
+ pass
+ if element.tag == FEATURE_TAG:
+ try:
+ return feature(params.feature_map[element.get('name')], element.get('name'))
+ except KeyError:
+ raise ConformanceException(f'Conformance specifies feature not in feature table: {element.get("name")}')
+ elif element.tag == ATTRIBUTE_TAG:
+ # Some command conformance tags are marked as attribute, so if this key isn't in attribute, try command
+ name = element.get('name')
+ if name in params.attribute_map:
+ return attribute(params.attribute_map[name], name)
+ elif name in params.command_map:
+ return command(params.command_map[name], name)
+ else:
+ raise ConformanceException(f'Conformance specifies attribute or command not in table: {name}')
+ elif element.tag == COMMAND_TAG:
+ return command(params.command_map[element.get('name')], element.get('name'))
+ else:
+ raise ConformanceException(
+ f'Unexpected xml conformance element with no children {str(element.tag)} {str(element.attrib)}')
+
+ # First build the list, then create the callable for this element
+ ops = [parse_callable_from_xml(sub, params) for sub in element]
+ return parse_wrapper_callable_from_xml(element, ops)