|
| 1 | +# |
| 2 | +# Copyright (c) 2023 Project CHIP Authors |
| 3 | +# All rights reserved. |
| 4 | +# |
| 5 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 6 | +# you may not use this file except in compliance with the License. |
| 7 | +# You may obtain a copy of the License at |
| 8 | +# |
| 9 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 10 | +# |
| 11 | +# Unless required by applicable law or agreed to in writing, software |
| 12 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 13 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 14 | +# See the License for the specific language governing permissions and |
| 15 | +# limitations under the License. |
| 16 | +# |
| 17 | + |
| 18 | +import itertools |
| 19 | +import xml.etree.ElementTree as ElementTree |
| 20 | + |
| 21 | +import jinja2 |
| 22 | +from choice_conformance_support import (evaluate_attribute_choice_conformance, evaluate_command_choice_conformance, |
| 23 | + evaluate_feature_choice_conformance) |
| 24 | +from matter_testing_support import MatterBaseTest, ProblemNotice, default_matter_test_main |
| 25 | +from mobly import asserts |
| 26 | +from spec_parsing_support import XmlCluster, add_cluster_data_from_xml |
| 27 | + |
| 28 | +FEATURE_TEMPLATE = '''\ |
| 29 | + <feature bit="{{ id }}" code="{{ name }}" name="{{ name }}" summary="summary"> |
| 30 | + <optionalConform choice="{{ choice }}" more="{{ more }}"> |
| 31 | + {%- if XXX %}' |
| 32 | + <feature name="XXX" /> |
| 33 | + {% endif %} |
| 34 | + </optionalConform> |
| 35 | + </feature> |
| 36 | +''' |
| 37 | + |
| 38 | +ATTRIBUTE_TEMPLATE = ( |
| 39 | + ' <attribute id="{{ id }}" name="{{ name }}" type="uint16">\n' |
| 40 | + ' <optionalConform choice="{{ choice }}" more="{{ more }}">\n' |
| 41 | + ' {% if XXX %}' |
| 42 | + ' <attribute name="XXX" />\n' |
| 43 | + ' {% endif %}' |
| 44 | + ' </optionalConform>\n' |
| 45 | + ' </attribute>\n' |
| 46 | +) |
| 47 | + |
| 48 | +COMMAND_TEMPLATE = ( |
| 49 | + ' <command id="{{ id }}" name="{{ name }}" direction="commandToServer" response="Y">\n' |
| 50 | + ' <optionalConform choice="{{ choice }}" more="{{ more }}">\n' |
| 51 | + ' {% if XXX %}' |
| 52 | + ' <command name="XXX" />\n' |
| 53 | + ' {% endif %}' |
| 54 | + ' </optionalConform>\n' |
| 55 | + ' </command>\n' |
| 56 | +) |
| 57 | + |
| 58 | +CLUSTER_TEMPLATE = ( |
| 59 | + '<cluster xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="types types.xsd cluster cluster.xsd" id="0x0001" name="Test Base" revision="1">\n' |
| 60 | + ' <revisionHistory>\n' |
| 61 | + ' <revision revision="1" summary="Initial version"/>\n' |
| 62 | + ' </revisionHistory>\n' |
| 63 | + ' <clusterIds>\n' |
| 64 | + ' <clusterId id="0x0001" name="Test Base"/>\n' |
| 65 | + ' </clusterIds>\n' |
| 66 | + ' <classification hierarchy="base" role="application" picsCode="BASE" scope="Endpoint"/>\n' |
| 67 | + ' <features>\n' |
| 68 | + ' {{ feature_string }}\n' |
| 69 | + ' </features>\n' |
| 70 | + ' <attributes>\n' |
| 71 | + ' {{ attribute_string}}\n' |
| 72 | + ' </attributes>\n' |
| 73 | + ' <commands>\n' |
| 74 | + ' {{ command_string }}\n' |
| 75 | + ' </commands>\n' |
| 76 | + '</cluster>\n') |
| 77 | + |
| 78 | + |
| 79 | +def _create_elements(template_str: str, base_name: str) -> list[str]: |
| 80 | + xml_str = [] |
| 81 | + |
| 82 | + def add_elements(curr_choice: str, starting_id: int, more: str, XXX: bool): |
| 83 | + for i in range(3): |
| 84 | + element_name = f'{base_name}{curr_choice.upper()*(i+1)}' |
| 85 | + environment = jinja2.Environment() |
| 86 | + template = environment.from_string(template_str) |
| 87 | + xml_str.append(template.render(id=(i + starting_id), name=element_name, choice=curr_choice, more=more, XXX=XXX)) |
| 88 | + add_elements('a', 1, 'false', False) |
| 89 | + add_elements('b', 4, 'true', False) |
| 90 | + add_elements('c', 7, 'false', True) |
| 91 | + add_elements('d', 10, 'true', True) |
| 92 | + |
| 93 | + return xml_str |
| 94 | + |
| 95 | +# TODO: this setup makes my life easy because it assumes that choice conformances apply only within one table |
| 96 | +# if this is not true (ex, you can have choose 1 of a feature or an attribute), then this gets more complex |
| 97 | +# in this case we need to have this test evaluate the same choice conformance value between multiple tables, and all |
| 98 | +# the conformances need to be assessed for the entire element set. |
| 99 | +# I've done it this way specifically so I can hardcode the choice values and test the features, attributes and commands |
| 100 | +# separately, even though I load them all at the start. |
| 101 | + |
| 102 | +# Cluster with choices on all elements |
| 103 | +# 3 of each element with O.a |
| 104 | +# 3 of each element with O.b+ |
| 105 | +# 3 of each element with [XXX].c |
| 106 | +# 3 of each element with [XXX].d+ |
| 107 | +# 1 element named XXX |
| 108 | + |
| 109 | + |
| 110 | +def _create_features(): |
| 111 | + xml = _create_elements(FEATURE_TEMPLATE, 'F') |
| 112 | + xxx = (' <feature bit="13" code="XXX" name="XXX" summary="summary">\n' |
| 113 | + ' <optionalConform />\n' |
| 114 | + ' </feature>\n') |
| 115 | + xml.append(xxx) |
| 116 | + return '\n'.join(xml) |
| 117 | + |
| 118 | + |
| 119 | +def _create_attributes(): |
| 120 | + xml = _create_elements(ATTRIBUTE_TEMPLATE, "attr") |
| 121 | + xxx = (' <attribute id="13" name="XXX" summary="summary">\n' |
| 122 | + ' <optionalConform />\n' |
| 123 | + ' </attribute>\n') |
| 124 | + xml.append(xxx) |
| 125 | + return '\n'.join(xml) |
| 126 | + |
| 127 | + |
| 128 | +def _create_commands(): |
| 129 | + xml = _create_elements(COMMAND_TEMPLATE, 'cmd') |
| 130 | + xxx = (' <command id="13" name="XXX" summary="summary" direction="commandToServer" response="Y">\n' |
| 131 | + ' <optionalConform />\n' |
| 132 | + ' </command>\n') |
| 133 | + xml.append(xxx) |
| 134 | + return '\n'.join(xml) |
| 135 | + |
| 136 | + |
| 137 | +def _create_cluster(): |
| 138 | + environment = jinja2.Environment() |
| 139 | + template = environment.from_string(CLUSTER_TEMPLATE) |
| 140 | + return template.render(feature_string=_create_features(), attribute_string=_create_attributes(), command_string=_create_commands()) |
| 141 | + |
| 142 | + |
| 143 | +class TestConformanceSupport(MatterBaseTest): |
| 144 | + def setup_class(self): |
| 145 | + super().setup_class() |
| 146 | + |
| 147 | + clusters: dict[int, XmlCluster] = {} |
| 148 | + pure_base_clusters: dict[str, XmlCluster] = {} |
| 149 | + ids_by_name: dict[str, int] = {} |
| 150 | + problems: list[ProblemNotice] = [] |
| 151 | + cluster_xml = ElementTree.fromstring(_create_cluster()) |
| 152 | + add_cluster_data_from_xml(cluster_xml, clusters, pure_base_clusters, ids_by_name, problems) |
| 153 | + self.clusters = clusters |
| 154 | + # each element type uses 13 IDs from 1-13 (or bits for the features) and we want to test all the combinations |
| 155 | + num_elements = 13 |
| 156 | + ids = range(1, num_elements + 1) |
| 157 | + self.all_id_combos = [] |
| 158 | + combos = [] |
| 159 | + for r in range(1, num_elements + 1): |
| 160 | + combos.extend(list(itertools.combinations(ids, r))) |
| 161 | + for combo in combos: |
| 162 | + # The first three IDs are all O.a, so we need exactly one for the conformance to be valid |
| 163 | + expected_failures = set() |
| 164 | + if len(set([1, 2, 3]) & set(combo)) != 1: |
| 165 | + expected_failures.add('a') |
| 166 | + if len(set([4, 5, 6]) & set(combo)) < 1: |
| 167 | + expected_failures.add('b') |
| 168 | + # For these, we are checking that choice conformance checkers |
| 169 | + # - Correctly report errors and correct cases when the gating feature is ON |
| 170 | + # - Do not report any errors when the gating features is off. |
| 171 | + # Errors where we incorrectly set disallowed features based on the gating feature are checked |
| 172 | + # elsewhere in the cert test in a comprehensive way. We just want to ensure that we are not |
| 173 | + # incorrectly reporting choice conformance error as well |
| 174 | + if 13 in combo and ((len(set([7, 8, 9]) & set(combo)) != 1)): |
| 175 | + expected_failures.add('c') |
| 176 | + if 13 in combo and (len(set([10, 11, 12]) & set(combo)) < 1): |
| 177 | + expected_failures.add('d') |
| 178 | + |
| 179 | + self.all_id_combos.append((combo, expected_failures)) |
| 180 | + |
| 181 | + def _evaluate_problems(self, problems, expected_failures=list[str]): |
| 182 | + if len(expected_failures) != len(problems): |
| 183 | + print(problems) |
| 184 | + asserts.assert_equal(len(expected_failures), len(problems), 'Unexpected number of choice conformance problems') |
| 185 | + actual_failures = set([p.choice.marker for p in problems]) |
| 186 | + asserts.assert_equal(actual_failures, expected_failures, "Mismatch between failures") |
| 187 | + |
| 188 | + def test_features(self): |
| 189 | + def make_feature_map(combo: tuple[int]) -> int: |
| 190 | + feature_map = 0 |
| 191 | + for bit in combo: |
| 192 | + feature_map += pow(2, bit) |
| 193 | + return feature_map |
| 194 | + |
| 195 | + for combo, expected_failures in self.all_id_combos: |
| 196 | + problems = evaluate_feature_choice_conformance(0, 1, self.clusters, make_feature_map(combo), [], []) |
| 197 | + self._evaluate_problems(problems, expected_failures) |
| 198 | + |
| 199 | + def test_attributes(self): |
| 200 | + for combo, expected_failures in self.all_id_combos: |
| 201 | + problems = evaluate_attribute_choice_conformance(0, 1, self.clusters, 0, list(combo), []) |
| 202 | + self._evaluate_problems(problems, expected_failures) |
| 203 | + |
| 204 | + def test_commands(self): |
| 205 | + for combo, expected_failures in self.all_id_combos: |
| 206 | + problems = evaluate_command_choice_conformance(0, 1, self.clusters, 0, [], list(combo)) |
| 207 | + self._evaluate_problems(problems, expected_failures) |
| 208 | + |
| 209 | + |
| 210 | +if __name__ == "__main__": |
| 211 | + default_matter_test_main() |
0 commit comments