blob: ca41bf088e0fd28ce9f92d4f0170d188973591bc [file] [log] [blame]
#
# Copyright (c) 2023 Project CHIP Authors
# All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import xml.etree.ElementTree as ElementTree
from typing import Callable
from chip.testing.conformance 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 chip.testing.matter_testing import MatterBaseTest, default_matter_test_main
from chip.tlv import uint
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):
def setup_class(self):
super().setup_class()
# a small feature map
self.feature_names_to_bits = {'AB': 0x01, 'CD': 0x02}
# none, AB, CD, AB&CD
self.feature_maps = [0x00, 0x01, 0x02, 0x03]
self.has_ab = [False, True, False, True]
self.has_cd = [False, False, True, True]
self.attribute_names_to_values = {'attr1': 0x00, 'attr2': 0x01}
self.attribute_lists = [[], [0x00], [0x01], [0x00, 0x01]]
self.has_attr1 = [False, True, False, True]
self.has_attr2 = [False, False, True, True]
self.command_names_to_values = {'cmd1': 0x00, 'cmd2': 0x01}
self.cmd_lists = [[], [0x00], [0x01], [0x00, 0x01]]
self.has_cmd1 = [False, True, False, True]
self.has_cmd2 = [False, False, True, True]
self.params = ConformanceParseParameters(
feature_map=self.feature_names_to_bits, attribute_map=self.attribute_names_to_values, command_map=self.command_names_to_values)
def test_conformance_mandatory(self):
xml = '<mandatoryConform />'
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, [], []).decision, ConformanceDecision.MANDATORY)
asserts.assert_equal(str(xml_callable), 'M')
def test_conformance_optional(self):
xml = '<optionalConform />'
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, [], []).decision, ConformanceDecision.OPTIONAL)
asserts.assert_equal(str(xml_callable), 'O')
def test_conformance_disallowed(self):
xml = '<disallowConform />'
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, [], []).decision, ConformanceDecision.DISALLOWED)
asserts.assert_equal(str(xml_callable), 'X')
xml = '<deprecateConform />'
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, [], []).decision, ConformanceDecision.DISALLOWED)
asserts.assert_equal(str(xml_callable), 'D')
def test_conformance_provisional(self):
xml = '<provisionalConform />'
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, [], []).decision, ConformanceDecision.PROVISIONAL)
asserts.assert_equal(str(xml_callable), 'P')
def test_conformance_zigbee(self):
xml = '<condition name="Zigbee"/>'
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, [], []).decision, ConformanceDecision.NOT_APPLICABLE)
asserts.assert_equal(str(xml_callable), 'Zigbee')
def test_conformance_mandatory_on_condition(self):
xml = ('<mandatoryConform>'
'<feature name="AB" />'
'</mandatoryConform>')
et = ElementTree.fromstring(xml)
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, [], []).decision, ConformanceDecision.MANDATORY)
else:
asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.NOT_APPLICABLE)
asserts.assert_equal(str(xml_callable), 'AB')
xml = ('<mandatoryConform>'
'<feature name="CD" />'
'</mandatoryConform>')
et = ElementTree.fromstring(xml)
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, [], []).decision, ConformanceDecision.MANDATORY)
else:
asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.NOT_APPLICABLE)
asserts.assert_equal(str(xml_callable), 'CD')
# single attribute mandatory
xml = ('<mandatoryConform>'
'<attribute name="attr1" />'
'</mandatoryConform>')
et = ElementTree.fromstring(xml)
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, []).decision, ConformanceDecision.MANDATORY)
else:
asserts.assert_equal(xml_callable(0x00, a, []).decision, ConformanceDecision.NOT_APPLICABLE)
asserts.assert_equal(str(xml_callable), 'attr1')
xml = ('<mandatoryConform>'
'<attribute name="attr2" />'
'</mandatoryConform>')
et = ElementTree.fromstring(xml)
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, []).decision, ConformanceDecision.MANDATORY)
else:
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
def test_conformance_optional_on_condition(self):
# single feature optional
xml = ('<optionalConform>'
'<feature name="AB" />'
'</optionalConform>')
et = ElementTree.fromstring(xml)
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, [], []).decision, ConformanceDecision.OPTIONAL)
else:
asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.NOT_APPLICABLE)
asserts.assert_equal(str(xml_callable), '[AB]')
xml = ('<optionalConform>'
'<feature name="CD" />'
'</optionalConform>')
et = ElementTree.fromstring(xml)
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, [], []).decision, ConformanceDecision.OPTIONAL)
else:
asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.NOT_APPLICABLE)
asserts.assert_equal(str(xml_callable), '[CD]')
# single attribute optional
xml = ('<optionalConform>'
'<attribute name="attr1" />'
'</optionalConform>')
et = ElementTree.fromstring(xml)
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, []).decision, ConformanceDecision.OPTIONAL)
else:
asserts.assert_equal(xml_callable(0x00, a, []).decision, ConformanceDecision.NOT_APPLICABLE)
asserts.assert_equal(str(xml_callable), '[attr1]')
xml = ('<optionalConform>'
'<attribute name="attr2" />'
'</optionalConform>')
et = ElementTree.fromstring(xml)
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, []).decision, ConformanceDecision.OPTIONAL)
else:
asserts.assert_equal(xml_callable(0x00, a, []).decision, ConformanceDecision.NOT_APPLICABLE)
asserts.assert_equal(str(xml_callable), '[attr2]')
# single command optional
xml = ('<optionalConform>'
'<command name="cmd1" />'
'</optionalConform>')
et = ElementTree.fromstring(xml)
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).decision, ConformanceDecision.OPTIONAL)
else:
asserts.assert_equal(xml_callable(0x00, [], c).decision, ConformanceDecision.NOT_APPLICABLE)
asserts.assert_equal(str(xml_callable), '[cmd1]')
xml = ('<optionalConform>'
'<command name="cmd2" />'
'</optionalConform>')
et = ElementTree.fromstring(xml)
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).decision, ConformanceDecision.OPTIONAL)
else:
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):
# single feature not mandatory
xml = ('<mandatoryConform>'
'<notTerm>'
'<feature name="AB" />'
'</notTerm>'
'</mandatoryConform>')
et = ElementTree.fromstring(xml)
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, [], []).decision, ConformanceDecision.MANDATORY)
else:
asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.NOT_APPLICABLE)
asserts.assert_equal(str(xml_callable), '!AB')
xml = ('<mandatoryConform>'
'<notTerm>'
'<feature name="CD" />'
'</notTerm>'
'</mandatoryConform>')
et = ElementTree.fromstring(xml)
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, [], []).decision, ConformanceDecision.MANDATORY)
else:
asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.NOT_APPLICABLE)
asserts.assert_equal(str(xml_callable), '!CD')
# single attribute not mandatory
xml = ('<mandatoryConform>'
'<notTerm>'
'<attribute name="attr1" />'
'</notTerm>'
'</mandatoryConform>')
et = ElementTree.fromstring(xml)
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, []).decision, ConformanceDecision.MANDATORY)
else:
asserts.assert_equal(xml_callable(0x00, a, []).decision, ConformanceDecision.NOT_APPLICABLE)
asserts.assert_equal(str(xml_callable), '!attr1')
xml = ('<mandatoryConform>'
'<notTerm>'
'<attribute name="attr2" />'
'</notTerm>'
'</mandatoryConform>')
et = ElementTree.fromstring(xml)
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, []).decision, ConformanceDecision.MANDATORY)
else:
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):
# single feature not optional
xml = ('<optionalConform>'
'<notTerm>'
'<feature name="AB" />'
'</notTerm>'
'</optionalConform>')
et = ElementTree.fromstring(xml)
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, [], []).decision, ConformanceDecision.OPTIONAL)
else:
asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.NOT_APPLICABLE)
asserts.assert_equal(str(xml_callable), '[!AB]')
xml = ('<optionalConform>'
'<notTerm>'
'<feature name="CD" />'
'</notTerm>'
'</optionalConform>')
et = ElementTree.fromstring(xml)
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, [], []).decision, ConformanceDecision.OPTIONAL)
else:
asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.NOT_APPLICABLE)
asserts.assert_equal(str(xml_callable), '[!CD]')
def test_conformance_and_term(self):
# and term for features only
xml = ('<mandatoryConform>'
'<andTerm>'
'<feature name="AB" />'
'<feature name="CD" />'
'</andTerm>'
'</mandatoryConform>')
et = ElementTree.fromstring(xml)
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, [], []).decision, ConformanceDecision.MANDATORY)
else:
asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.NOT_APPLICABLE)
asserts.assert_equal(str(xml_callable), 'AB & CD')
# and term for attributes only
xml = ('<mandatoryConform>'
'<andTerm>'
'<attribute name="attr1" />'
'<attribute name="attr2" />'
'</andTerm>'
'</mandatoryConform>')
et = ElementTree.fromstring(xml)
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, []).decision, ConformanceDecision.MANDATORY)
else:
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
xml = ('<mandatoryConform>'
'<andTerm>'
'<feature name="AB" />'
'<attribute name="attr2" />'
'</andTerm>'
'</mandatoryConform>')
et = ElementTree.fromstring(xml)
xml_callable = parse_callable_from_xml(et, self.params)
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, []).decision, ConformanceDecision.MANDATORY)
else:
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):
# or term feature only
xml = ('<mandatoryConform>'
'<orTerm>'
'<feature name="AB" />'
'<feature name="CD" />'
'</orTerm>'
'</mandatoryConform>')
et = ElementTree.fromstring(xml)
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, [], []).decision, ConformanceDecision.MANDATORY)
else:
asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.NOT_APPLICABLE)
asserts.assert_equal(str(xml_callable), 'AB | CD')
# or term attribute only
xml = ('<mandatoryConform>'
'<orTerm>'
'<attribute name="attr1" />'
'<attribute name="attr2" />'
'</orTerm>'
'</mandatoryConform>')
et = ElementTree.fromstring(xml)
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, []).decision, ConformanceDecision.MANDATORY)
else:
asserts.assert_equal(xml_callable(0x00, a, []).decision, ConformanceDecision.NOT_APPLICABLE)
asserts.assert_equal(str(xml_callable), 'attr1 | attr2')
# or term feature and attribute
xml = ('<mandatoryConform>'
'<orTerm>'
'<feature name="AB" />'
'<attribute name="attr2" />'
'</orTerm>'
'</mandatoryConform>')
et = ElementTree.fromstring(xml)
xml_callable = parse_callable_from_xml(et, self.params)
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, []).decision, ConformanceDecision.MANDATORY)
else:
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):
# and term with not
xml = ('<optionalConform>'
'<andTerm>'
'<notTerm>'
'<feature name="AB" />'
'</notTerm>'
'<feature name="CD" />'
'</andTerm>'
'</optionalConform>')
et = ElementTree.fromstring(xml)
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, [], []).decision, ConformanceDecision.OPTIONAL)
else:
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):
# or term with not on second feature
xml = ('<mandatoryConform>'
'<orTerm>'
'<feature name="AB" />'
'<notTerm>'
'<feature name="CD" />'
'</notTerm>'
'</orTerm>'
'</mandatoryConform>')
et = ElementTree.fromstring(xml)
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, [], []).decision, ConformanceDecision.MANDATORY)
else:
asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.NOT_APPLICABLE)
asserts.assert_equal(str(xml_callable), 'AB | !CD')
# not around or term with
xml = ('<optionalConform>'
'<notTerm>'
'<orTerm>'
'<feature name="AB" />'
'<feature name="CD" />'
'</orTerm>'
'</notTerm>'
'</optionalConform>')
et = ElementTree.fromstring(xml)
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, [], []).decision, ConformanceDecision.OPTIONAL)
else:
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):
# and term with three features
xml = ('<optionalConform>'
'<andTerm>'
'<feature name="AB" />'
'<feature name="CD" />'
'<feature name="EF" />'
'</andTerm>'
'</optionalConform>')
self.feature_names_to_bits['EF'] = 0x04
et = ElementTree.fromstring(xml)
xml_callable = parse_callable_from_xml(et, self.params)
# no features
asserts.assert_equal(xml_callable(0x00, [], []).decision, ConformanceDecision.NOT_APPLICABLE)
# one feature
asserts.assert_equal(xml_callable(0x01, [], []).decision, ConformanceDecision.NOT_APPLICABLE)
# all features
asserts.assert_equal(xml_callable(0x07, [], []).decision, ConformanceDecision.OPTIONAL)
asserts.assert_equal(str(xml_callable), '[AB & CD & EF]')
# and term with one of each
xml = ('<optionalConform>'
'<andTerm>'
'<feature name="AB" />'
'<attribute name="attr1" />'
'<command name="cmd1" />'
'</andTerm>'
'</optionalConform>')
et = ElementTree.fromstring(xml)
xml_callable = parse_callable_from_xml(et, self.params)
for i, f in enumerate(self.feature_maps):
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).decision, ConformanceDecision.OPTIONAL)
else:
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):
# or term with three features
xml = ('<optionalConform>'
'<orTerm>'
'<feature name="AB" />'
'<feature name="CD" />'
'<feature name="EF" />'
'</orTerm>'
'</optionalConform>')
et = ElementTree.fromstring(xml)
xml_callable = parse_callable_from_xml(et, self.params)
# no features
asserts.assert_equal(xml_callable(0x00, [], []).decision, ConformanceDecision.NOT_APPLICABLE)
# one feature
asserts.assert_equal(xml_callable(0x01, [], []).decision, ConformanceDecision.OPTIONAL)
# all features
asserts.assert_equal(xml_callable(0x07, [], []).decision, ConformanceDecision.OPTIONAL)
asserts.assert_equal(str(xml_callable), '[AB | CD | EF]')
# or term with one of each
xml = ('<optionalConform>'
'<orTerm>'
'<feature name="AB" />'
'<attribute name="attr1" />'
'<command name="cmd1" />'
'</orTerm>'
'</optionalConform>')
et = ElementTree.fromstring(xml)
xml_callable = parse_callable_from_xml(et, self.params)
for i, f in enumerate(self.feature_maps):
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).decision, ConformanceDecision.OPTIONAL)
else:
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):
# AB, O
xml = ('<otherwiseConform>'
'<mandatoryConform>'
'<feature name="AB" />'
'</mandatoryConform>'
'<optionalConform />'
'</otherwiseConform>')
et = ElementTree.fromstring(xml)
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, [], []).decision, ConformanceDecision.MANDATORY)
else:
asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.OPTIONAL)
asserts.assert_equal(str(xml_callable), 'AB, O')
# AB, [CD]
xml = ('<otherwiseConform>'
'<mandatoryConform>'
'<feature name="AB" />'
'</mandatoryConform>'
'<optionalConform>'
'<feature name="CD" />'
'</optionalConform>'
'</otherwiseConform>')
et = ElementTree.fromstring(xml)
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, [], []).decision, ConformanceDecision.MANDATORY)
elif self.has_cd[i]:
asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.OPTIONAL)
else:
asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.NOT_APPLICABLE)
asserts.assert_equal(str(xml_callable), 'AB, [CD]')
# AB & !CD, P
xml = ('<otherwiseConform>'
'<mandatoryConform>'
'<andTerm>'
'<feature name="AB" />'
'<notTerm>'
'<feature name="CD" />'
'</notTerm>'
'</andTerm>'
'</mandatoryConform>'
'<provisionalConform />'
'</otherwiseConform>')
et = ElementTree.fromstring(xml)
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, [], []).decision, ConformanceDecision.MANDATORY)
else:
asserts.assert_equal(xml_callable(f, [], []).decision, ConformanceDecision.PROVISIONAL)
asserts.assert_equal(str(xml_callable), 'AB & !CD, P')
def test_conformance_greater(self):
# AB, [CD]
xml = ('<mandatoryConform>'
'<greaterTerm>'
'<attribute name="attr1" />'
'<literal value="1" />'
'</greaterTerm>'
'</mandatoryConform>')
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, [], []).decision, ConformanceDecision.OPTIONAL)
asserts.assert_equal(str(xml_callable), 'attr1 > 1')
# Ensure that we can only have greater terms with exactly 2 value
xml = ('<mandatoryConform>'
'<greaterTerm>'
'<attribute name="attr1" />'
'<attribute name="attr2" />'
'<literal value="1" />'
'</greaterTerm>'
'</mandatoryConform>')
et = ElementTree.fromstring(xml)
try:
xml_callable = parse_callable_from_xml(et, self.params)
asserts.fail("Incorrectly parsed bad greaterTerm XML with > 2 values")
except ConformanceException:
pass
xml = ('<mandatoryConform>'
'<greaterTerm>'
'<attribute name="attr1" />'
'</greaterTerm>'
'</mandatoryConform>')
et = ElementTree.fromstring(xml)
try:
xml_callable = parse_callable_from_xml(et, self.params)
asserts.fail("Incorrectly parsed bad greaterTerm XML with < 2 values")
except ConformanceException:
pass
# Only attributes and literals allowed because arithmetic operations require values
xml = ('<mandatoryConform>'
'<greaterTerm>'
'<feature name="AB" />'
'<literal value="1" />'
'</greaterTerm>'
'</mandatoryConform>')
et = ElementTree.fromstring(xml)
try:
xml_callable = parse_callable_from_xml(et, self.params)
asserts.fail("Incorrectly parsed greater term with feature value")
except ConformanceException:
pass
def test_basic_conformance(self):
basic_test('<mandatoryConform />', mandatory)
basic_test('<optionalConform />', optional)
basic_test('<disallowConform />', disallowed)
basic_test('<deprecateConform />', deprecated)
basic_test('<provisionalConform />', provisional)
basic_test('<condition name="zigbee" />', zigbee)
# feature is not basic so we should get an exception
xml = '<feature name="CD" />'
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 = ('<mandatoryConform>'
'<andTerm>'
'<feature name="AB" />'
'<notTerm>'
'<feature name="CD" />'
'</notTerm>'
'</andTerm>'
'</mandatoryConform>')
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 = ('<mandatoryConform>'
'<condition name="zigbee" />'
'</mandatoryConform>')
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, [], []).decision, ConformanceDecision.NOT_APPLICABLE, msg)
xml = ('<optionalConform>'
'<condition name="zigbee" />'
'</optionalConform>')
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, [], []).decision, ConformanceDecision.NOT_APPLICABLE, msg)
# otherwise conforms are allowed
xml = ('<otherwiseConform>'
'<condition name="zigbee" />'
'<provisionalConform />'
'</otherwiseConform>')
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, [], []).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
# 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 = ('<mandatoryConform>'
'<feature name="CD" />'
'</mandatoryConform>')
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, [], []).decision, ConformanceDecision.OPTIONAL)
xml = ('<otherwiseConform>'
'<feature name="CD" />'
'<condition name="testy" />'
'</otherwiseConform>')
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, [], []).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'<optionalConform {xml_attrs} />'
conformance = self.check_good_choice(xml, f'O.{suffix}')
self.check_decision(more, conformance, 0, [], [])
xml = (f'<optionalConform {xml_attrs}>'
'<feature name="AB" />'
'</optionalConform>')
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'<optionalConform {xml_attrs}>'
'<attribute name="attr1" />'
'</optionalConform>')
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'<optionalConform {xml_attrs}>'
'<command name="cmd1" />'
'</optionalConform>')
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'<optionalConform {xml_attrs}>'
'<orTerm>'
'<feature name="AB" />'
'<feature name="CD" />'
'</orTerm>'
'</optionalConform>')
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'<optionalConform {xml_attrs}>'
'<notTerm>'
'<attribute name="attr1" />'
'</notTerm>'
'</optionalConform>')
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 = ('<otherwiseConform>'
'<attribute name="attr1" />'
f'<optionalConform>'
'<feature name="AB" />'
'</optionalConform>'
f'<optionalConform {xml_attrs} />'
'</otherwiseConform>')
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 = ('<otherwiseConform>'
'<attribute name="attr1" />'
f'<optionalConform {xml_attrs}>'
'<feature name="AB" />'
'</optionalConform>'
f'<optionalConform choice="b">'
'<feature name="CD" />'
'</optionalConform>'
'</otherwiseConform>')
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'<mandatoryConform {xml_attrs} />'
check_bad_choice(xml)
xml = f'<feature name="AB" {xml_attrs} />'
check_bad_choice(xml)
xml = f'<attribute name="attr1" {xml_attrs} />'
check_bad_choice(xml)
xml = f'<command name="cmd1" {xml_attrs} />'
check_bad_choice(xml)
xml = ('<mandatoryConform>'
'<andTerm>'
'<attribute name="attr1"/>'
f'<optionalConform {xml_attrs} />'
'</andTerm>'
'</mandatoryConform>')
check_bad_choice(xml)
xml = ('<mandatoryConform>'
'<orTerm>'
'<attribute name="attr1"/>'
f'<optionalConform {xml_attrs} />'
'</orTerm>'
'</mandatoryConform>')
check_bad_choice(xml)
xml = ('<mandatoryConform>'
'<notTerm>'
f'<optionalConform {xml_attrs} />'
'</notTerm>'
'</mandatoryConform>')
check_bad_choice(xml)
xml = ('<optionalConform>'
f'<feature name="AB" {xml_attrs}/>'
'</optionalConform>')
check_bad_choice(xml)
xml = ('<optionalConform>'
f'<attribute name="attr1" {xml_attrs}/>'
'</optionalConform>')
check_bad_choice(xml)
xml = ('<optionalConform>'
f'<command name="cmd1" {xml_attrs}/>'
'</optionalConform>')
check_bad_choice(xml)
xml = (f'<provisionalConform {xml_attrs}/>')
check_bad_choice(xml)
xml = (f'<disallowConform {xml_attrs}/>')
check_bad_choice(xml)
xml = (f'<deprecateConform {xml_attrs}/>')
check_bad_choice(xml)
if __name__ == "__main__":
default_matter_test_main()