|
| 1 | +# |
| 2 | +# Copyright (c) 2025 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 | +# See https://github.com/project-chip/connectedhomeip/blob/master/docs/testing/python.md#defining-the-ci-test-arguments |
| 19 | +# for details about the block below. |
| 20 | +# |
| 21 | +# === BEGIN CI TEST ARGUMENTS === |
| 22 | +# test-runner-runs: |
| 23 | +# run1: |
| 24 | +# app: ${ALL_CLUSTERS_APP} |
| 25 | +# app-args: --discriminator 1234 --KVS kvs1 --trace-to json:${TRACE_APP}.json |
| 26 | +# script-args: > |
| 27 | +# --storage-path admin_storage.json |
| 28 | +# --commissioning-method on-network |
| 29 | +# --discriminator 1234 |
| 30 | +# --passcode 20202021 |
| 31 | +# --PICS src/app/tests/suites/certification/ci-pics-values |
| 32 | +# --string-arg PIXIT.CNET.THREAD_1ST_OPERATIONALDATASET:${THREAD_1ST} |
| 33 | +# --string-arg PIXIT.CNET.THREAD_2ND_OPERATIONALDATASET:${THREAD_2ND} |
| 34 | +# --trace-to json:${TRACE_TEST_JSON}.json |
| 35 | +# --trace-to perfetto:${TRACE_TEST_PERFETTO}.perfetto |
| 36 | +# factory-reset: true |
| 37 | +# quiet: true |
| 38 | +# === END CI TEST ARGUMENTS === |
| 39 | + |
| 40 | +import logging |
| 41 | + |
| 42 | +import chip.clusters as Clusters |
| 43 | +from chip.testing.matter_testing import MatterBaseTest, TestStep, async_test_body, default_matter_test_main |
| 44 | +from mobly import asserts |
| 45 | + |
| 46 | +logger = logging.getLogger(__name__) |
| 47 | + |
| 48 | + |
| 49 | +class TC_CNET_4_12(MatterBaseTest): |
| 50 | + |
| 51 | + CLUSTER_CNET = Clusters.NetworkCommissioning |
| 52 | + CLUSTER_DESC = Clusters.Descriptor |
| 53 | + CLUSTER_CGEN = Clusters.GeneralCommissioning |
| 54 | + failsafe_expiration_seconds = 900 |
| 55 | + default_network_id = 'Thread' |
| 56 | + |
| 57 | + def def_TC_CNET_4_12(self): |
| 58 | + return '[TC-CNET-4.12] [Thread] Verification for ConnectNetwork Command [DUT-Server]' |
| 59 | + |
| 60 | + def pics_TC_CNET_4_12(self): |
| 61 | + """Return the PICS definitions associated with this test.""" |
| 62 | + pics = [ |
| 63 | + "CNET.S" |
| 64 | + ] |
| 65 | + return pics |
| 66 | + |
| 67 | + def steps_TC_CNET_4_12(self) -> list[TestStep]: |
| 68 | + steps = [ |
| 69 | + TestStep('precondition-1', 'DUT supports CNET.S.F01(TH)'), |
| 70 | + TestStep('precondition-2', 'DUT has a Network Commissioning cluster on endpoint PIXIT.CNET.ENDPOINT_THREAD with FeatureMap attribute of 2', is_commissioning=True), |
| 71 | + TestStep('precondition-3', 'DUT is commissioned on PIXIT.CNET.THREAD_1ST_OPERATIONALDATASET'), |
| 72 | + TestStep('precondition-4', 'TH has can communicate to two valid thread PANs: PIXIT.CNET.THREAD_1ST_OPERATIONALDATASET and PIXIT.CNET.THREAD_2ND_OPERATIONALDATASET'), |
| 73 | + TestStep('precondition-5', 'XPANID of PIXIT.CNET.THREAD_1ST_OPERATIONALDATASET is saved as th_xpan and the XPANID of PIXIT.CNET.THREAD_2ND_OPERATIONALDATASET is saved as th_xpan1'), |
| 74 | + TestStep(1, 'TH sends ArmFailSafe command to the DUT with ExpiryLengthSeconds set to 900'), |
| 75 | + TestStep(2, 'TH reads Networks attribute from the DUT and saves the number of entries as NumNetworks'), |
| 76 | + TestStep(3, 'TH saves the index of the Networks list entry from step 2 as Userth_netidx'), |
| 77 | + TestStep(4, '''TH sends RemoveNetwork Command to the DUT |
| 78 | + with NetworkID field set to th_xpan |
| 79 | + and Breadcrumb field set to 1'''), |
| 80 | + TestStep(5, '''TH sends AddOrUpdateThreadNetwork command to the DUT |
| 81 | + with operational dataset field set to PIXIT.CNET.THREAD_2ND_OPERATIONALDATASET |
| 82 | + and Breadcrumb field set to 1'''), |
| 83 | + TestStep(6, 'TH reads Networks attribute from the DUT'), |
| 84 | + TestStep(7, '''TH sends ConnectNetwork command to the DUT |
| 85 | + with NetworkID field set to PIXIT.CNET.THREAD_2ND_OPERATIONALDATASET |
| 86 | + and Breadcrumb field set to 2'''), |
| 87 | + TestStep(8, 'TH discovers and connects to DUT on the PIXIT.CNET.THREAD_2ND_OPERATIONALDATASET operational network'), |
| 88 | + TestStep(9, 'TH reads Breadcrumb attribute from the General Commissioning cluster of the DUT'), |
| 89 | + TestStep(10, 'TH sends ArmFailSafe command to the DUT with ExpiryLengthSeconds set to 0'), |
| 90 | + TestStep(11, 'TH ensures it can communicate on PIXIT.CNET.THREAD_1ST_OPERATIONALDATASET'), |
| 91 | + TestStep(12, 'TH discovers and connects to DUT on the PIXIT.CNET.THREAD_1ST_OPERATIONALDATASET operational network'), |
| 92 | + TestStep(13, 'TH sends ArmFailSafe command to the DUT with ExpiryLengthSeconds set to 900'), |
| 93 | + TestStep(14, '''TH sends RemoveNetwork Command to the DUT with NetworkID field set to th_xpan |
| 94 | + and Breadcrumb field set to 1'''), |
| 95 | + TestStep(15, '''TH sends AddOrUpdateThreadNetwork command to the DUT |
| 96 | + with the OperationalDataset field set to PIXIT.CNET.THREAD_2ND_OPERATIONALDATASET |
| 97 | + and Breadcrumb field set to 1'''), |
| 98 | + TestStep(16, '''TH sends ConnectNetwork command to the DUT |
| 99 | + with NetworkID field set to the extended PAN ID of PIXIT.CNET.THREAD_2ND_OPERATIONALDATASET |
| 100 | + and Breadcrumb field set to 3'''), |
| 101 | + TestStep(17, 'TH discovers and connects to DUT on the PIXIT.CNET.THREAD_2ND_OPERATIONALDATASET operational network'), |
| 102 | + TestStep(18, 'TH reads Breadcrumb attribute from the General Commissioning cluster of the DUT'), |
| 103 | + TestStep(19, 'TH sends the CommissioningComplete command to the DUT'), |
| 104 | + TestStep(20, 'TH reads Networks attribute from the DUT') |
| 105 | + ] |
| 106 | + return steps |
| 107 | + |
| 108 | + @async_test_body |
| 109 | + async def test_TC_CNET_4_12(self): |
| 110 | + |
| 111 | + if self.is_pics_sdk_ci_only: |
| 112 | + logger.info('Test is not running in CI.') |
| 113 | + self.skip_all_remaining_steps('precondition-1') |
| 114 | + return |
| 115 | + |
| 116 | + # Pre-Conditions |
| 117 | + self.step('precondition-1') |
| 118 | + self.step('precondition-2') |
| 119 | + # By running this test from the terminal, it commissions the device. |
| 120 | + logger.info('Pre-Conditions #1: DUT has a Network Commissioning cluster on endpoint PIXIT.CNET.ENDPOINT_THREAD ') |
| 121 | + |
| 122 | + # The FeatureMap attribute value is 2 |
| 123 | + feature_map = await self.read_single_attribute_check_success( |
| 124 | + cluster=self.CLUSTER_CNET, |
| 125 | + attribute=self.CLUSTER_CNET.Attributes.FeatureMap) |
| 126 | + asserts.assert_true(feature_map == 2, |
| 127 | + msg="Verify that feature_map is equal to 1") |
| 128 | + logger.info(f'Pre-Conditions #3: The FeatureMap attribute value is: {feature_map}') |
| 129 | + |
| 130 | + self.step('precondition-3') |
| 131 | + # TODO: Implement precondition-3 |
| 132 | + self.step('precondition-4') |
| 133 | + # TODO: Implement precondition-4 |
| 134 | + self.step('precondition-5') |
| 135 | + # TODO: Implement precondition-5 |
| 136 | + th_xpan = self.user_params.get('PIXIT.CNET.THREAD_1ST_OPERATIONALDATASET', self.default_network_id) |
| 137 | + th_xpan_1 = self.user_params.get('PIXIT.CNET.THREAD_2ND_OPERATIONALDATASET', self.default_network_id) |
| 138 | + |
| 139 | + # Steps |
| 140 | + |
| 141 | + self.step(1) |
| 142 | + cmd = Clusters.GeneralCommissioning.Commands.ArmFailSafe(expiryLengthSeconds=self.failsafe_expiration_seconds, breadcrumb=1) |
| 143 | + resp = await self.send_single_cmd( |
| 144 | + dev_ctrl=self.default_controller, |
| 145 | + node_id=self.dut_node_id, |
| 146 | + cmd=cmd |
| 147 | + ) |
| 148 | + # Verify that the DUT responds with ArmFailSafeResponse with ErrorCode as 'OK'(0) |
| 149 | + asserts.assert_equal(resp.errorCode, Clusters.GeneralCommissioning.Enums.CommissioningErrorEnum.kOk, |
| 150 | + "Failure status returned from arm failsafe") |
| 151 | + logger.info(f'Step #1b - ArmFailSafeResponse with ErrorCode as OK({resp.errorCode})') |
| 152 | + |
| 153 | + self.step(2) |
| 154 | + networks = await self.read_single_attribute_check_success( |
| 155 | + cluster=Clusters.NetworkCommissioning, |
| 156 | + attribute=Clusters.NetworkCommissioning.Attributes.Networks |
| 157 | + ) |
| 158 | + logger.info(f'Step #2: Networks attribute: {networks}') |
| 159 | + |
| 160 | + num_networks = len(networks) |
| 161 | + logger.info(f'Step #2: Number of Networks entries (NumNetworks): {num_networks}') |
| 162 | + asserts.assert_true(num_networks > 0, "Error: No networks found") |
| 163 | + |
| 164 | + # TODO: Implement proper validation to verify the the Networks attribute "NetworkID" and "Connected" |
| 165 | + for cnet in networks: |
| 166 | + if cnet.networkID.decode('utf-8') == th_xpan and cnet.connected: |
| 167 | + network_found = True |
| 168 | + break |
| 169 | + logger.info(f'Step #2: Found network with ID {th_xpan} and connected={network_found}.') |
| 170 | + asserts.assert_true( |
| 171 | + network_found, f"Error: Network with ID {th_xpan} and connected=True not found.") |
| 172 | + |
| 173 | + self.step(3) |
| 174 | + # TODO: Implement proper validation to verify the the Networks index |
| 175 | + userth_netidx = None |
| 176 | + for index, network in enumerate(networks): |
| 177 | + if network['networkID'] == th_xpan: |
| 178 | + userth_netidx = index |
| 179 | + break |
| 180 | + asserts.assert_true(userth_netidx is not None, "") |
| 181 | + logger.info(f'Step #3: Networks attribute: {userth_netidx}') |
| 182 | + |
| 183 | + self.step(4) |
| 184 | + cmd = Clusters.NetworkCommissioning.Commands.RemoveNetwork(networkID=th_xpan, breadcrumb=1) |
| 185 | + resp = await self.send_single_cmd( |
| 186 | + dev_ctrl=self.default_controller, |
| 187 | + node_id=self.dut_node_id, |
| 188 | + cmd=cmd |
| 189 | + ) |
| 190 | + network_index = resp.networkIndex |
| 191 | + logger.info(f'Step #4: RemoveNetwork Status is success ({resp.networkingStatus})') |
| 192 | + logger.info(f'Step #4: Network index: ({network_index})') |
| 193 | + |
| 194 | + # Verify that the DUT responds with Remove Network with NetworkingStatus as 'Success'(0) |
| 195 | + asserts.assert_equal(resp.networkingStatus, Clusters.NetworkCommissioning.Enums.NetworkCommissioningStatusEnum.kSuccess, |
| 196 | + "Failure status returned from ReordeRemove Network") |
| 197 | + asserts.assert_equal(network_index, 0, "The network index is not as expected.") |
| 198 | + |
| 199 | + self.step(5) |
| 200 | + cmd = Clusters.NetworkCommissioning.Commands.AddOrUpdateThreadNetwork(operationalDataset=th_xpan_1, breadcrumb=1) |
| 201 | + resp = await self.send_single_cmd( |
| 202 | + dev_ctrl=self.default_controller, |
| 203 | + node_id=self.dut_node_id, |
| 204 | + cmd=cmd |
| 205 | + ) |
| 206 | + logger.info(f'Step #5: AddOrUpdateThreadNetwork Status is success ({resp.networkingStatus})') |
| 207 | + # Verify that the DUT responds with AddThreadNetwork with NetworkingStatus as 'Success'(0) |
| 208 | + asserts.assert_equal(resp.networkingStatus, Clusters.NetworkCommissioning.Enums.NetworkCommissioningStatusEnum.kSuccess, |
| 209 | + "Failure status returned from AddThreadNetwork") |
| 210 | + debug_text = resp.debugText |
| 211 | + # TODO: Check if None is part of the validation |
| 212 | + asserts.assert_true(debug_text is None or debug_text == '' or len(debug_text) <= 512, |
| 213 | + "debugText must be None, empty or have a maximum length of 512 characters.") |
| 214 | + |
| 215 | + self.step(6) |
| 216 | + networks = await self.read_single_attribute_check_success( |
| 217 | + cluster=Clusters.NetworkCommissioning, |
| 218 | + attribute=Clusters.NetworkCommissioning.Attributes.Networks |
| 219 | + ) |
| 220 | + logger.info(f'Step #6: Networks attribute: {networks}') |
| 221 | + # TODO; Implement the Verify that the Networks attribute list has an entry NetworkID=th_xpan, Connected=FALSE |
| 222 | + # TODO: Why th_xpan? should be th_xpan_1 |
| 223 | + |
| 224 | + self.step(7) |
| 225 | + cmd = Clusters.NetworkCommissioning.Commands.ConnectNetwork(operationalDataset=th_xpan_1, breadcrumb=2) |
| 226 | + resp = await self.send_single_cmd( |
| 227 | + dev_ctrl=self.default_controller, |
| 228 | + node_id=self.dut_node_id, |
| 229 | + cmd=cmd |
| 230 | + ) |
| 231 | + logger.info(f'Step #7: ConnectNetwork Status is success ({resp.networkingStatus})') |
| 232 | + # Verify that the DUT responds with AddThreadNConnectNetworketwork with NetworkingStatus as 'Success'(0) |
| 233 | + asserts.assert_equal(resp.networkingStatus, Clusters.NetworkCommissioning.Enums.NetworkCommissioningStatusEnum.kSuccess, |
| 234 | + "Failure status returned from ConnectNetwork") |
| 235 | + |
| 236 | + self.step(8) |
| 237 | + # TODO: Verify that the TH successfully connects to the DUT from previous step |
| 238 | + |
| 239 | + self.step(9) |
| 240 | + breadcrumb_info = await self.read_single_attribute_check_success( |
| 241 | + cluster=Clusters.GeneralCommissioning, |
| 242 | + attribute=Clusters.GeneralCommissioning.Attributes.Breadcrumb |
| 243 | + ) |
| 244 | + logger.info(f'Step #9: Breadcrumb attribute: {breadcrumb_info}') |
| 245 | + asserts.assert_equal(breadcrumb_info, 1, |
| 246 | + "The Breadcrumb attribute is not 1") |
| 247 | + |
| 248 | + self.step(10) |
| 249 | + cmd = Clusters.GeneralCommissioning.Commands.ArmFailSafe(expiryLengthSeconds=0) |
| 250 | + resp = await self.send_single_cmd( |
| 251 | + dev_ctrl=self.default_controller, |
| 252 | + node_id=self.dut_node_id, |
| 253 | + cmd=cmd |
| 254 | + ) |
| 255 | + # Verify that the DUT responds with ArmFailSafeResponse with ErrorCode as 'OK'(0) |
| 256 | + asserts.assert_equal(resp.errorCode, Clusters.GeneralCommissioning.Enums.CommissioningErrorEnum.kOk, |
| 257 | + "Failure status returned from arm failsafe") |
| 258 | + logger.info(f'Step #10 - ArmFailSafeResponse with ErrorCode as OK({resp.errorCode})') |
| 259 | + |
| 260 | + self.step(11) |
| 261 | + # TODO: Verify that TH can communicate on th_xpan |
| 262 | + |
| 263 | + self.step(12) |
| 264 | + # TODO: Verify that TH can discover and connect on th_xpan |
| 265 | + |
| 266 | + self.step(13) |
| 267 | + cmd = Clusters.GeneralCommissioning.Commands.ArmFailSafe(expiryLengthSeconds=self.failsafe_expiration_seconds) |
| 268 | + resp = await self.send_single_cmd( |
| 269 | + dev_ctrl=self.default_controller, |
| 270 | + node_id=self.dut_node_id, |
| 271 | + cmd=cmd |
| 272 | + ) |
| 273 | + # Verify that the DUT responds with ArmFailSafeResponse with ErrorCode as 'OK'(0) |
| 274 | + asserts.assert_equal(resp.errorCode, Clusters.GeneralCommissioning.Enums.CommissioningErrorEnum.kOk, |
| 275 | + "Failure status returned from arm failsafe") |
| 276 | + logger.info(f'Step #1b - ArmFailSafeResponse with ErrorCode as OK({resp.errorCode})') |
| 277 | + |
| 278 | + self.step(14) |
| 279 | + cmd = Clusters.NetworkCommissioning.Commands.RemoveNetwork(networkID=th_xpan, breadcrumb=1) |
| 280 | + resp = await self.send_single_cmd( |
| 281 | + dev_ctrl=self.default_controller, |
| 282 | + node_id=self.dut_node_id, |
| 283 | + cmd=cmd |
| 284 | + ) |
| 285 | + network_index = resp.networkIndex |
| 286 | + logger.info(f'Step #14: RemoveNetwork Status is success ({resp.networkingStatus})') |
| 287 | + logger.info(f'Step #14: Network index: ({network_index})') |
| 288 | + |
| 289 | + # Verify that the DUT responds with Remove Network with NetworkingStatus as 'Success'(0) |
| 290 | + asserts.assert_equal(resp.networkingStatus, Clusters.NetworkCommissioning.Enums.NetworkCommissioningStatusEnum.kSuccess, |
| 291 | + "Failure status returned from ReordeRemove Network") |
| 292 | + asserts.assert_equal(network_index, 0, "The network index is not as expected.") |
| 293 | + |
| 294 | + self.step(15) |
| 295 | + cmd = Clusters.NetworkCommissioning.Commands.AddOrUpdateThreadNetwork(operationalDataset=th_xpan_1, breadcrumb=3) |
| 296 | + resp = await self.send_single_cmd( |
| 297 | + dev_ctrl=self.default_controller, |
| 298 | + node_id=self.dut_node_id, |
| 299 | + cmd=cmd |
| 300 | + ) |
| 301 | + logger.info(f'Step #15: AddOrUpdateThreadNetwork Status is success ({resp.networkingStatus})') |
| 302 | + # Verify that the DUT responds with AddThreadNetwork with NetworkingStatus as 'Success'(0) |
| 303 | + asserts.assert_equal(resp.networkingStatus, Clusters.NetworkCommissioning.Enums.NetworkCommissioningStatusEnum.kSuccess, |
| 304 | + "Failure status returned from AddThreadNetwork") |
| 305 | + debug_text = resp.debugText |
| 306 | + # TODO: Check if None is part of the validation |
| 307 | + asserts.assert_true(debug_text is None or debug_text == '' or len(debug_text) <= 512, |
| 308 | + "debugText must be None, empty or have a maximum length of 512 characters.") |
| 309 | + |
| 310 | + self.step(16) |
| 311 | + cmd = Clusters.NetworkCommissioning.Commands.ConnectNetwork(operationalDataset=th_xpan_1, breadcrumb=3) |
| 312 | + resp = await self.send_single_cmd( |
| 313 | + dev_ctrl=self.default_controller, |
| 314 | + node_id=self.dut_node_id, |
| 315 | + cmd=cmd |
| 316 | + ) |
| 317 | + logger.info(f'Step #16: ConnectNetwork Status is success ({resp.networkingStatus})') |
| 318 | + # Verify that the DUT responds with AddThreadNConnectNetworketwork with NetworkingStatus as 'Success'(0) |
| 319 | + asserts.assert_equal(resp.networkingStatus, Clusters.NetworkCommissioning.Enums.NetworkCommissioningStatusEnum.kSuccess, |
| 320 | + "Failure status returned from ConnectNetwork") |
| 321 | + |
| 322 | + self.step(17) |
| 323 | + # TODO: Verify that the TH successfully connects to the DUT from previous step |
| 324 | + |
| 325 | + self.step(18) |
| 326 | + breadcrumb_info = await self.read_single_attribute_check_success( |
| 327 | + cluster=Clusters.GeneralCommissioning, |
| 328 | + attribute=Clusters.GeneralCommissioning.Attributes.Breadcrumb |
| 329 | + ) |
| 330 | + logger.info(f'Step #18: Breadcrumb attribute: {breadcrumb_info}') |
| 331 | + asserts.assert_equal(breadcrumb_info, 3, |
| 332 | + "The Breadcrumb attribute is not 3") |
| 333 | + |
| 334 | + self.step(19) |
| 335 | + # TODO: Implement TH sends the CommissioningComplete and CommissioningCompleteResponse with the ErrorCode OK (0) |
| 336 | + |
| 337 | + self.step(20) |
| 338 | + networks = await self.read_single_attribute_check_success( |
| 339 | + cluster=Clusters.NetworkCommissioning, |
| 340 | + attribute=Clusters.NetworkCommissioning.Attributes.Networks |
| 341 | + ) |
| 342 | + logger.info(f'Step #2: Networks attribute: {networks}') |
| 343 | + |
| 344 | + # TODO: Implement proper validation to verify the the Networks attribute "NetworkID" and "Connected" |
| 345 | + for cnet in networks: |
| 346 | + if cnet.networkID.decode('utf-8') == th_xpan_1 and cnet.connected: |
| 347 | + network_found = True |
| 348 | + break |
| 349 | + logger.info(f'Step #2: Found network with ID {th_xpan_1} and connected={network_found}.') |
| 350 | + asserts.assert_true( |
| 351 | + network_found, f"Error: Network with ID {th_xpan_1} and connected=True not found.") |
| 352 | + |
| 353 | + |
| 354 | +if __name__ == "__main__": |
| 355 | + default_matter_test_main() |
0 commit comments