|
19 | 19 |
|
20 | 20 | import chip.clusters as Clusters
|
21 | 21 | from global_attribute_ids import GlobalAttributeIds
|
22 |
| -from matter_testing_support import MatterBaseTest, default_matter_test_main |
| 22 | +from matter_testing_support import MatterBaseTest, ProblemNotice, default_matter_test_main |
23 | 23 | from mobly import asserts
|
24 |
| -from spec_parsing_support import ClusterParser, XmlCluster |
| 24 | +from spec_parsing_support import (ClusterParser, XmlCluster, add_cluster_data_from_xml, check_clusters_for_unknown_commands, |
| 25 | + combine_derived_clusters_with_base) |
25 | 26 |
|
26 | 27 | # TODO: improve the test coverage here
|
27 | 28 | # https://github.com/project-chip/connectedhomeip/issues/30958
|
@@ -77,6 +78,120 @@ def get_access_enum_from_string(access_str: str) -> Clusters.AccessControl.Enums
|
77 | 78 | asserts.fail("Unknown access string")
|
78 | 79 |
|
79 | 80 |
|
| 81 | +BASE_CLUSTER_XML_STR = ( |
| 82 | + '<cluster xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="types types.xsd cluster cluster.xsd" id="" name="Test Base" revision="1">' |
| 83 | + ' <revisionHistory>' |
| 84 | + ' <revision revision="1" summary="Initial version"/>' |
| 85 | + ' </revisionHistory>' |
| 86 | + ' <classification hierarchy="base" role="application" picsCode="BASE" scope="Endpoint"/>' |
| 87 | + ' <features>' |
| 88 | + ' <feature bit="0" code="DEPONOFF" name="OnOff" summary="Dependency with the OnOff cluster">' |
| 89 | + ' <optionalConform/>' |
| 90 | + ' </feature>' |
| 91 | + ' </features>' |
| 92 | + ' <attributes>' |
| 93 | + ' <attribute id="0x0000" name="SupportedModes" type="list" default="MS">' |
| 94 | + ' <entry type="ModeOptionStruct"/>' |
| 95 | + ' <access read="true" readPrivilege="view"/>' |
| 96 | + ' <quality changeOmitted="false" nullable="false" scene="false" persistence="fixed" reportable="false"/>' |
| 97 | + ' <mandatoryConform/>' |
| 98 | + ' <constraint type="countBetween" from="2" to="255"/>' |
| 99 | + ' </attribute>' |
| 100 | + ' <attribute id="0x0001" name="CurrentMode" type="uint8" default="MS">' |
| 101 | + ' <access read="true" readPrivilege="view"/>' |
| 102 | + ' <quality changeOmitted="false" nullable="false" scene="true" persistence="nonVolatile" reportable="false"/>' |
| 103 | + ' <mandatoryConform/>' |
| 104 | + ' <constraint type="desc"/>' |
| 105 | + ' </attribute>' |
| 106 | + ' <attribute id="0x0002" name="StartUpMode" type="uint8" default="MS">' |
| 107 | + ' <access read="true" write="true" readPrivilege="view" writePrivilege="operate"/>' |
| 108 | + ' <quality changeOmitted="false" nullable="true" scene="false" persistence="nonVolatile" reportable="false"/>' |
| 109 | + ' <optionalConform/>' |
| 110 | + ' <constraint type="desc"/>' |
| 111 | + ' </attribute>' |
| 112 | + ' <attribute id="0x0003" name="OnMode" type="uint8" default="null">' |
| 113 | + ' <access read="true" write="true" readPrivilege="view" writePrivilege="operate"/>' |
| 114 | + ' <quality changeOmitted="false" nullable="true" scene="false" persistence="nonVolatile" reportable="false"/>' |
| 115 | + ' <mandatoryConform>' |
| 116 | + ' <feature name="DEPONOFF"/>' |
| 117 | + ' </mandatoryConform>' |
| 118 | + ' <constraint type="desc"/>' |
| 119 | + ' </attribute>' |
| 120 | + ' </attributes>' |
| 121 | + ' <commands>' |
| 122 | + ' <command id="0x00" name="ChangeToMode" response="ChangeToModeResponse">' |
| 123 | + ' <access invokePrivilege="operate"/>' |
| 124 | + ' <mandatoryConform/>' |
| 125 | + ' <field id="0" name="NewMode" type="uint8">' |
| 126 | + ' <mandatoryConform/>' |
| 127 | + ' <constraint type="desc"/>' |
| 128 | + ' </field>' |
| 129 | + ' </command>' |
| 130 | + ' <command id="0x01" name="ChangeToModeResponse" direction="responseFromServer">' |
| 131 | + ' <access invokePrivilege="operate"/>' |
| 132 | + ' <mandatoryConform/>' |
| 133 | + ' <field id="0" name="Status" type="enum8">' |
| 134 | + ' <enum>' |
| 135 | + ' <item from="0x00" to="0x3F" name="CommonCodes" summary="Common standard values defined in the generic Mode Base cluster specification.">' |
| 136 | + ' <mandatoryConform/>' |
| 137 | + ' </item>' |
| 138 | + ' </enum>' |
| 139 | + ' <mandatoryConform/>' |
| 140 | + ' <constraint type="desc"/>' |
| 141 | + ' </field>' |
| 142 | + ' <field id="1" name="StatusText" type="string">' |
| 143 | + ' <constraint type="maxLength" value="64"/>' |
| 144 | + ' </field>' |
| 145 | + ' </command>' |
| 146 | + ' </commands>' |
| 147 | + '</cluster>') |
| 148 | + |
| 149 | +DERIVED_CLUSTER_XML_STR = ( |
| 150 | + '<cluster xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="types types.xsd cluster cluster.xsd" id="0xFFFF" name="Test Derived" revision="1">' |
| 151 | + ' <revisionHistory>' |
| 152 | + ' <revision revision="1" summary="Initial Release"/>' |
| 153 | + ' </revisionHistory>' |
| 154 | + ' <classification hierarchy="derived" baseCluster="Test Base" role="application" picsCode="MWOM" scope="Endpoint"/>' |
| 155 | + ' <attributes>' |
| 156 | + ' <attribute id="0x0000" name="SupportedModes">' |
| 157 | + ' <mandatoryConform/>' |
| 158 | + ' </attribute>' |
| 159 | + ' <attribute id="0x0002" name="StartUpMode">' |
| 160 | + ' <disallowConform/>' |
| 161 | + ' </attribute>' |
| 162 | + ' <attribute id="0x0003" name="OnMode">' |
| 163 | + ' <disallowConform/>' |
| 164 | + ' </attribute>' |
| 165 | + ' </attributes>' |
| 166 | + ' <commands>' |
| 167 | + ' <command id="0x00" name="ChangeToMode" direction="commandToClient">' |
| 168 | + ' <access invokePrivilege="operate"/>' |
| 169 | + ' <disallowConform/>' |
| 170 | + ' </command>' |
| 171 | + ' <command id="0x01" name="ChangeToModeResponse" direction="commandToClient">' |
| 172 | + ' <access invokePrivilege="operate"/>' |
| 173 | + ' <disallowConform/>' |
| 174 | + ' </command>' |
| 175 | + ' </commands>' |
| 176 | + '</cluster>' |
| 177 | +) |
| 178 | + |
| 179 | +CLUSTER_WITH_UNKNOWN_COMMAND = ( |
| 180 | + '<cluster xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="types types.xsd cluster cluster.xsd" id="0xFFFE" name="Test Unknown Command" revision="1">' |
| 181 | + ' <revisionHistory>' |
| 182 | + ' <revision revision="1" summary="Initial version"/>' |
| 183 | + ' </revisionHistory>' |
| 184 | + ' <classification hierarchy="base" role="application" picsCode="BASE" scope="Endpoint"/>' |
| 185 | + ' <commands>' |
| 186 | + ' <command id="0x00" name="ChangeToMode" direction="commandToClient">' |
| 187 | + ' <access invokePrivilege="operate"/>' |
| 188 | + ' <mandatoryConform/>' |
| 189 | + ' </command>' |
| 190 | + ' </commands>' |
| 191 | + '</cluster>' |
| 192 | +) |
| 193 | + |
| 194 | + |
80 | 195 | class TestSpecParsingSupport(MatterBaseTest):
|
81 | 196 | def test_spec_parsing_access(self):
|
82 | 197 | strs = [None, 'view', 'operate', 'manage', 'admin']
|
@@ -106,6 +221,81 @@ def test_write_optional(self):
|
106 | 221 | asserts.assert_equal(xml_cluster.attributes[ATTRIBUTE_ID].write_optional,
|
107 | 222 | write_support == 'optional', "Unexpected write_optional value")
|
108 | 223 |
|
| 224 | + def test_derived_clusters(self): |
| 225 | + clusters: dict[int, XmlCluster] = {} |
| 226 | + pure_base_clusters: dict[str, XmlCluster] = {} |
| 227 | + ids_by_name: dict[str, int] = {} |
| 228 | + problems: list[ProblemNotice] = [] |
| 229 | + base_cluster_xml = ElementTree.fromstring(BASE_CLUSTER_XML_STR) |
| 230 | + derived_cluster_xml = ElementTree.fromstring(DERIVED_CLUSTER_XML_STR) |
| 231 | + expected_global_attrs = [GlobalAttributeIds.FEATURE_MAP_ID, GlobalAttributeIds.ATTRIBUTE_LIST_ID, |
| 232 | + GlobalAttributeIds.ACCEPTED_COMMAND_LIST_ID, GlobalAttributeIds.GENERATED_COMMAND_LIST_ID, GlobalAttributeIds.CLUSTER_REVISION_ID] |
| 233 | + |
| 234 | + add_cluster_data_from_xml(base_cluster_xml, clusters, pure_base_clusters, ids_by_name, problems) |
| 235 | + add_cluster_data_from_xml(derived_cluster_xml, clusters, pure_base_clusters, ids_by_name, problems) |
| 236 | + |
| 237 | + asserts.assert_equal(len(clusters), 1, "Unexpected number of clusters") |
| 238 | + asserts.assert_equal(len(pure_base_clusters), 1, "Unexpected number of pure base clusters") |
| 239 | + asserts.assert_equal(len(ids_by_name), 1, "Unexpected number of IDs per name") |
| 240 | + asserts.assert_equal(len(problems), 0, "Unexpected number of problems") |
| 241 | + asserts.assert_equal(ids_by_name["Test Derived"], 0xFFFF, "Test derived name not added to IDs") |
| 242 | + |
| 243 | + asserts.assert_true(0xFFFF in clusters, "Derived ID not found in clusters") |
| 244 | + asserts.assert_equal(set(clusters[0xFFFF].attributes.keys()), set( |
| 245 | + [0, 2, 3] + expected_global_attrs), "Unexpected attribute list") |
| 246 | + asserts.assert_equal(set(clusters[0xFFFF].accepted_commands.keys()), set([]), "Unexpected accepted commands") |
| 247 | + asserts.assert_equal(set(clusters[0xFFFF].generated_commands.keys()), set([]), "Unexpected generated commands") |
| 248 | + |
| 249 | + asserts.assert_true("Test Base" in pure_base_clusters, "Base ID not found in derived clusters") |
| 250 | + asserts.assert_equal(set(pure_base_clusters["Test Base"].attributes.keys()), set( |
| 251 | + [0, 1, 2, 3] + expected_global_attrs), "Unexpected attribute list") |
| 252 | + asserts.assert_equal(set(pure_base_clusters["Test Base"].accepted_commands.keys()), |
| 253 | + set([0]), "Unexpected accepted commands") |
| 254 | + asserts.assert_equal(set(pure_base_clusters["Test Base"].generated_commands.keys()), |
| 255 | + set([1]), "Unexpected generated commands") |
| 256 | + asserts.assert_equal(str(pure_base_clusters["Test Base"].accepted_commands[0].conformance), |
| 257 | + "M", "Unexpected conformance on base accepted command") |
| 258 | + asserts.assert_equal(str(pure_base_clusters["Test Base"].generated_commands[1].conformance), |
| 259 | + "M", "Unexpected conformance on base generated command") |
| 260 | + |
| 261 | + asserts.assert_equal(len(pure_base_clusters["Test Base"].unknown_commands), |
| 262 | + 0, "Unexpected number of unknown commands in base") |
| 263 | + asserts.assert_equal(len(clusters[0xFFFF].unknown_commands), 2, "Unexpected number of unknown commands in derived cluster") |
| 264 | + |
| 265 | + combine_derived_clusters_with_base(clusters, pure_base_clusters, ids_by_name) |
| 266 | + # Ensure the base-only attribute (1) was added to the derived cluster |
| 267 | + asserts.assert_equal(set(clusters[0xFFFF].attributes.keys()), set( |
| 268 | + [0, 1, 2, 3] + expected_global_attrs), "Unexpected attribute list") |
| 269 | + # Ensure the conformance overrides from the derived cluster are on the attributes |
| 270 | + asserts.assert_equal(str(clusters[0xFFFF].attributes[0].conformance), "M", "Unexpected conformance on attribute 0") |
| 271 | + asserts.assert_equal(str(clusters[0xFFFF].attributes[1].conformance), "M", "Unexpected conformance on attribute 1") |
| 272 | + asserts.assert_equal(str(clusters[0xFFFF].attributes[2].conformance), "X", "Unexpected conformance on attribute 2") |
| 273 | + asserts.assert_equal(str(clusters[0xFFFF].attributes[3].conformance), "X", "Unexpected conformance on attribute 3") |
| 274 | + |
| 275 | + # Ensure both the accepted and generated command overrides work |
| 276 | + asserts.assert_true(set(clusters[0xFFFF].accepted_commands.keys()), |
| 277 | + set([0]), "Unexpected accepted command list after merge") |
| 278 | + asserts.assert_true(set(clusters[0xFFFF].generated_commands.keys()), set([1]), |
| 279 | + "Unexpected generated command list after merge") |
| 280 | + asserts.assert_equal(str(clusters[0xFFFF].accepted_commands[0].conformance), |
| 281 | + "X", "Unexpected conformance on accepted commands") |
| 282 | + asserts.assert_equal(str(clusters[0xFFFF].generated_commands[1].conformance), |
| 283 | + "X", "Unexpected conformance on generated commands") |
| 284 | + asserts.assert_equal(len(clusters[0xFFFF].unknown_commands), 0, "Unexpected number of unknown commands after merge") |
| 285 | + |
| 286 | + def test_missing_command_direction(self): |
| 287 | + clusters: dict[int, XmlCluster] = {} |
| 288 | + pure_base_clusters: dict[str, XmlCluster] = {} |
| 289 | + ids_by_name: dict[str, int] = {} |
| 290 | + problems: list[ProblemNotice] = [] |
| 291 | + cluster_xml = ElementTree.fromstring(CLUSTER_WITH_UNKNOWN_COMMAND) |
| 292 | + |
| 293 | + add_cluster_data_from_xml(cluster_xml, clusters, pure_base_clusters, ids_by_name, problems) |
| 294 | + check_clusters_for_unknown_commands(clusters, problems) |
| 295 | + asserts.assert_equal(len(problems), 1, "Unexpected number of problems found") |
| 296 | + asserts.assert_equal(problems[0].location.cluster_id, 0xFFFE, "Unexpected problem location (cluster id)") |
| 297 | + asserts.assert_equal(problems[0].location.command_id, 0, "Unexpected problem location (command id)") |
| 298 | + |
109 | 299 |
|
110 | 300 | if __name__ == "__main__":
|
111 | 301 | default_matter_test_main()
|
0 commit comments