diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index a8bdfdcdc2c1ec..842699da662e29 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -529,6 +529,7 @@ jobs: scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_ICDManagementCluster.py' scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_IDM_1_2.py' scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_IDM_1_4.py' + scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_IDM_4_2.py' scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_PWRTL_2_1.py' scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_RR_1_1.py' scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_SC_3_6.py' diff --git a/src/python_testing/TC_IDM_4_2.py b/src/python_testing/TC_IDM_4_2.py index 60016ba4dcf20a..b159bb76a1e31c 100644 --- a/src/python_testing/TC_IDM_4_2.py +++ b/src/python_testing/TC_IDM_4_2.py @@ -15,16 +15,24 @@ # limitations under the License. # +# test-runner-runs: run1 +# test-runner-run/run1/app: ${ALL_CLUSTERS_APP} +# test-runner-run/run1/factoryreset: True +# test-runner-run/run1/quiet: True +# test-runner-run/run1/app-args: --discriminator 1234 --KVS kvs1 --trace-to json:${TRACE_APP}.json +# test-runner-run/run1/script-args: --storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --trace-to json:${TRACE_TEST_JSON}.json --trace-to perfetto:${TRACE_TEST_PERFETTO}.perfetto + import copy import logging import time import chip.clusters as Clusters from chip.ChipDeviceCtrl import ChipDeviceController +from chip.clusters import ClusterObjects as ClusterObjects from chip.clusters.Attribute import AttributePath, TypedAttributePath from chip.exceptions import ChipStackError from chip.interaction_model import Status -from matter_testing_support import MatterBaseTest, async_test_body, default_matter_test_main +from matter_testing_support import AttributeChangeCallback, MatterBaseTest, TestStep, async_test_body, default_matter_test_main from mobly import asserts ''' @@ -45,11 +53,37 @@ class TC_IDM_4_2(MatterBaseTest): - ROOT_NODE_ENDPOINT_ID = 0 + def steps_TC_IDM_4_2(self): + return [TestStep(0, "CR1 reads the ServerList attribute from the Descriptor cluster on EP0.", + "If the ICD Management cluster ID (70,0x46) is present, set SUBSCRIPTION_MAX_INTERVAL_PUBLISHER_LIMIT_SEC = IdleModeDuration and min_interval_floor_s to 0, otherwise, set SUBSCRIPTION_MAX_INTERVAL_PUBLISHER_LIMIT_SEC = 60 mins and min_interval_floor_s to 3."), + TestStep(1, "CR1 sends a subscription message to the DUT with MaxIntervalCeiling set to a value greater than subscription_max_interval_publisher_limit_sec. DUT sends a report data action to the TH. CR1 sends a success status response to the DUT. DUT sends a Subscribe Response Message to the CR1 to activate the subscription.", + "Verify on the CR1, a report data message is received. Verify it contains the following data Report data - data of the attribute/event requested earlier. Verify on the CR1 the Subscribe Response has the following fields, SubscriptionId - Verify it is of type uint32. MaxInterval - Verify it is of type uint32. Verify that the MaxInterval is less than or equal to MaxIntervalCeiling."), + TestStep(2, "CR1 sends a subscription message to the DUT with MaxIntervalCeiling set to a value less than subscription_max_interval_publisher_limit_sec. DUT sends a report data action to the CR1. CR1 sends a success status response to the DUT. DUT sends a Subscribe Response Message to the CR1 to activate the subscription.", + "Verify on the CR1, a report data message is received. Verify it contains the following data: Report data - data of the attribute/event requested earlier. Verify on the CR1 the Subscribe Response has the following fields, SubscriptionId - Verify it is of type uint32. MaxInterval - Verify it is of type uint32. Verify that the MaxInterval is less than or equal to SUBSCRIPTION_MAX_INTERVAL_PUBLISHER_LIMIT."), + TestStep(3, "Setup CR2 such that it does not have access to a specific cluster. CR2 sends a subscription message to subscribe to an attribute on that cluster for which it does not have access.", + "Verify that the DUT returns a \"INVALID_ACTION\" status response."), + TestStep(4, "Setup CR2 such that it does not have access to all attributes on a specific cluster and endpoint. CR2 sends a subscription request to subscribe to all attributes for which it does not have access.", + "Verify that the DUT returns a \"INVALID_ACTION\" status response."), + TestStep(5, "Setup CR2 such that it does not have access to an Endpoint. CR2 sends a subscription request to subscribe to all attributes on all clusters on a specific Endpoint for which it does not have access.", + "Verify that the DUT returns a \"INVALID_ACTION\" status response."), + TestStep(6, "Setup CR2 such that it does not have access to the Node. CR2 sends a subscription request to subscribe to all attributes on all clusters on all endpoints on a Node for which it does not have access.", + "Verify that the DUT returns a \"INVALID_ACTION\" status response."), + TestStep(7, "CR1 sends a subscription request action for an attribute with an empty DataVersionFilters field. DUT sends a report data action with the data of the attribute along with the data version. Tear down the subscription for that attribute. Start another subscription with the DataVersionFilter field set to the data version received above.", + "Verify that the subscription is activated between CR1 and DUT."), + TestStep(8, "CR1 sends a subscription request action for an attribute and sets the MinIntervalFloor to min_interval_floor_sec and MaxIntervalCeiling to 10. Activate the Subscription between CR1 and DUT and record the time when the priming ReportDataMessage is received as t_report_sec. Save the returned MaxInterval from the SubscribeResponseMessage as max_interval_sec."), + TestStep(9, "CR1 modifies the attribute which has been subscribed to on the DUT and waits for an incoming ReportDataMessage", + "Verify that t_update - t_report is greater than min_interval_floor_s and less than the ReadClient SubscriptionTimeout (calculated by the ReadClient using max_interval_s and the maximum estimated network delay based on the MRP parameters for retries with backoff)"), + TestStep(10, "CR1 sends a subscription request action for an attribute and set the MinIntervalFloor value to be greater than MaxIntervalCeiling.", + "Verify that the DUT sends an error message and the subscription is not established."), + TestStep(11, "CR1 sends a subscription request to subscribe to a specific global attribute from all clusters on all endpoints.", + "Verify that the Subscription succeeds and the DUT sends back the attribute values for the global attribute."), + TestStep(12, "CR1 sends a subscription request to subscribe to a global attribute on an endpoint on all clusters.", + "Verify that the Subscription succeeds and the DUT sends back the attribute values for the global attribute. Verify no data from other endpoints is sent back."), + TestStep(13, "CR1 sends a subscription request to the DUT with both AttributeRequests and EventRequests as empty.", + "Verify that the Subscription does not succeed and the DUT sends back a Status Response Action with the INVALID_ACTION Status Code") + ] - async def write_acl(self, ctrl, acl, ep=ROOT_NODE_ENDPOINT_ID): - result = await ctrl.WriteAttribute(self.dut_node_id, [(ep, Clusters.AccessControl.Attributes.Acl(acl))]) - asserts.assert_equal(result[ep].Status, Status.Success, "ACL write failed") + ROOT_NODE_ENDPOINT_ID = 0 async def get_descriptor_server_list(self, ctrl, ep=ROOT_NODE_ENDPOINT_ID): return await self.read_single_attribute_check_success( @@ -59,6 +93,14 @@ async def get_descriptor_server_list(self, ctrl, ep=ROOT_NODE_ENDPOINT_ID): attribute=Clusters.Descriptor.Attributes.ServerList ) + async def get_descriptor_parts_list(self, ctrl, ep=ROOT_NODE_ENDPOINT_ID): + return await self.read_single_attribute_check_success( + endpoint=ep, + dev_ctrl=ctrl, + cluster=Clusters.Descriptor, + attribute=Clusters.Descriptor.Attributes.PartsList + ) + async def get_idle_mode_duration_sec(self, ctrl, ep=ROOT_NODE_ENDPOINT_ID): return await self.read_single_attribute_check_success( endpoint=ep, @@ -81,14 +123,18 @@ def verify_attribute_exists(sub, cluster, attribute, ep=ROOT_NODE_ENDPOINT_ID): @staticmethod def get_typed_attribute_path(attribute, ep=ROOT_NODE_ENDPOINT_ID): return TypedAttributePath( - Path=AttributePath( + Path=AttributePath.from_attribute( EndpointId=ep, Attribute=attribute ) ) - async def get_dut_acl(self, ep=ROOT_NODE_ENDPOINT_ID): - sub = await self.default_controller.ReadAttribute( + async def write_dut_acl(self, ctrl, acl, ep=ROOT_NODE_ENDPOINT_ID): + result = await ctrl.WriteAttribute(self.dut_node_id, [(ep, Clusters.AccessControl.Attributes.Acl(acl))]) + asserts.assert_equal(result[ep].Status, Status.Success, "ACL write failed") + + async def get_dut_acl(self, ctrl, ep=ROOT_NODE_ENDPOINT_ID): + sub = await ctrl.ReadAttribute( nodeid=self.dut_node_id, attributes=[(ep, Clusters.AccessControl.Attributes.Acl)], keepSubscriptions=False, @@ -99,11 +145,10 @@ async def get_dut_acl(self, ep=ROOT_NODE_ENDPOINT_ID): return acl_list - async def add_ace_to_dut_acl(self, ctrl, ace): - dut_acl_original = await self.get_dut_acl() + async def add_ace_to_dut_acl(self, ctrl, ace, dut_acl_original): dut_acl = copy.deepcopy(dut_acl_original) dut_acl.append(ace) - await self.write_acl(ctrl=ctrl, acl=dut_acl) + await self.write_dut_acl(ctrl=ctrl, acl=dut_acl) @staticmethod def is_valid_uint32_value(var): @@ -121,8 +166,7 @@ async def test_TC_IDM_4_2(self): cluster_rev_attr_typed_path = self.get_typed_attribute_path(cluster_rev_attr) node_label_attr = Clusters.BasicInformation.Attributes.NodeLabel node_label_attr_path = [(0, node_label_attr)] - node_label_attr_typed_path = self.get_typed_attribute_path(node_label_attr) - SUBSCRIPTION_MAX_INTERVAL_PUBLISHER_LIMIT_SEC = 0 + subscription_max_interval_publisher_limit_sec = 0 INVALID_ACTION_ERROR_CODE = 0x580 # Controller 1 setup @@ -130,6 +174,9 @@ async def test_TC_IDM_4_2(self): # Will write ACL for controller 2 and validate success/error codes CR1: ChipDeviceController = self.default_controller + # Original DUT ACL used for reseting the ACL on some steps + dut_acl_original = await self.get_dut_acl(CR1) + # Controller 2 setup # Subscriber/client with limited access to the DUT # Will validate error status codes @@ -140,8 +187,14 @@ async def test_TC_IDM_4_2(self): paaTrustStorePath=str(self.matter_test_config.paa_trust_store_path), ) - # Read ServerList attribute - self.print_step("0a", "CR1 reads the Descriptor cluster ServerList attribute from EP0") + # *** Step 0 *** + # CR1 reads the ServerList attribute from the Descriptor cluster on EP0. If the ICDManagement cluster ID + # (70,0x46) is present, set SUBSCRIPTION_MAX_INTERVAL_PUBLISHER_LIMIT_SEC = IdleModeDuration and + # min_interval_floor_s to 0, otherwise, set SUBSCRIPTION_MAX_INTERVAL_PUBLISHER_LIMIT_SEC = 60 mins and + # min_interval_floor_s to 3. + self.step(0) + + # Reads the ServerList attribute ep0_servers = await self.get_descriptor_server_list(CR1) # Check if ep0_servers contains the ICD Management cluster ID (0x0046) @@ -151,19 +204,27 @@ async def test_TC_IDM_4_2(self): "CR1 reads from the DUT the IdleModeDuration attribute and sets SUBSCRIPTION_MAX_INTERVAL_PUBLISHER_LIMIT_SEC = IdleModeDuration") idleModeDuration = await self.get_idle_mode_duration_sec(CR1) - - SUBSCRIPTION_MAX_INTERVAL_PUBLISHER_LIMIT_SEC = idleModeDuration + subscription_max_interval_publisher_limit_sec = idleModeDuration + min_interval_floor_sec = 0 else: # Defaulting SUBSCRIPTION_MAX_INTERVAL_PUBLISHER_LIMIT_SEC to 60 minutes - SUBSCRIPTION_MAX_INTERVAL_PUBLISHER_LIMIT_SEC = 60 * 60 + subscription_max_interval_publisher_limit_sec = 60 * 60 + min_interval_floor_sec = 3 + + asserts.assert_greater_equal(subscription_max_interval_publisher_limit_sec, 1, + "SUBSCRIPTION_MAX_INTERVAL_PUBLISHER_LIMIT_SEC must be at least 1") logging.info( - f"Set SUBSCRIPTION_MAX_INTERVAL_PUBLISHER_LIMIT_SEC to {SUBSCRIPTION_MAX_INTERVAL_PUBLISHER_LIMIT_SEC} seconds") + f"Set SUBSCRIPTION_MAX_INTERVAL_PUBLISHER_LIMIT_SEC to {subscription_max_interval_publisher_limit_sec} seconds") # *** Step 1 *** - self.print_step(1, "CR1 sends a subscription message to the DUT with MaxIntervalCeiling set to a value greater than SUBSCRIPTION_MAX_INTERVAL_PUBLISHER_LIMIT_SEC. DUT sends a report data action to the TH. CR1 sends a success status response to the DUT. DUT sends a Subscribe Response Message to the CR1 to activate the subscription.") - min_interval_floor_sec = 1 - max_interval_ceiling_sec = SUBSCRIPTION_MAX_INTERVAL_PUBLISHER_LIMIT_SEC + 5 + # CR1 sends a subscription message to the DUT with MaxIntervalCeiling set to a value greater than + # SUBSCRIPTION_MAX_INTERVAL_PUBLISHER_LIMIT_SEC. DUT sends a report data action to the TH. CR1 sends + # a success status response to the DUT. DUT sends a Subscribe Response Message to the CR1 to activate + # the subscription. + self.step(1) + + max_interval_ceiling_sec = subscription_max_interval_publisher_limit_sec + 5 asserts.assert_greater(max_interval_ceiling_sec, min_interval_floor_sec, "MaxIntervalCeiling must be greater than MinIntervalFloor") @@ -198,9 +259,15 @@ async def test_TC_IDM_4_2(self): sub_cr1_step1.Shutdown() # *** Step 2 *** - self.print_step(2, "CR1 sends a subscription message to the DUT with MaxIntervalCeiling set to a value less than SUBSCRIPTION_MAX_INTERVAL_PUBLISHER_LIMIT_SEC. DUT sends a report data action to the CR1. CR1 sends a success status response to the DUT. DUT sends a Subscribe Response Message to the CR1 to activate the subscription.") - min_interval_floor_sec = 1 - max_interval_ceiling_sec = max(2, SUBSCRIPTION_MAX_INTERVAL_PUBLISHER_LIMIT_SEC - 5) + # CR1 sends a subscription message to the DUT with MaxIntervalCeiling set to a value less than + # subscription_max_interval_publisher_limit_sec. DUT sends a report data action to the CR1. + # CR1 sends a success status response to the DUT. DUT sends a Subscribe Response Message to the + # CR1 to activate the subscription. + self.step(2) + + min_interval_floor_sec = 0 + + max_interval_ceiling_sec = max(1, subscription_max_interval_publisher_limit_sec - 5) asserts.assert_greater(max_interval_ceiling_sec, min_interval_floor_sec, "MaxIntervalCeiling must be greater than MinIntervalFloor") @@ -229,13 +296,18 @@ async def test_TC_IDM_4_2(self): "MaxInterval is not of uint32 type.") # Verify MaxInterval is less than or equal to MaxIntervalCeiling - asserts.assert_less_equal(sub_cr1_step2_max_interval_ceiling_sec, SUBSCRIPTION_MAX_INTERVAL_PUBLISHER_LIMIT_SEC, + asserts.assert_less_equal(sub_cr1_step2_max_interval_ceiling_sec, subscription_max_interval_publisher_limit_sec, "MaxInterval is not less than or equal to SUBSCRIPTION_MAX_INTERVAL_PUBLISHER_LIMIT_SEC") sub_cr1_step2.Shutdown() # *** Step 3 *** - self.print_step(3, "Setup CR2 such that it does not have access to a specific cluster. CR2 sends a subscription message to subscribe to an attribute on that cluster for which it does not have access.") + # Setup CR2 such that it does not have access to a specific cluster. CR2 sends a subscription + # message to subscribe to an attribute on that cluster for which it does not have access. + self.step(3) + + # Setting max_interval_ceiling_sec value for steps 3-8 + max_interval_ceiling_sec = 10 # Limited ACE for controller 2 with single cluster access CR2_limited_ace = Clusters.AccessControl.Structs.AccessControlEntryStruct( @@ -244,18 +316,20 @@ async def test_TC_IDM_4_2(self): targets=[Clusters.AccessControl.Structs.AccessControlTargetStruct(cluster=Clusters.BasicInformation.id)], subjects=[CR2_nodeid]) - self.add_ace_to_dut_acl(CR1, CR2_limited_ace) + # Add limited ACE + await self.add_ace_to_dut_acl(CR1, CR2_limited_ace, dut_acl_original) # Controller 2 tries to subscribe an attribute from a cluster # it doesn't have access to # "INVALID_ACTION" status response expected + try: await CR2.ReadAttribute( nodeid=self.dut_node_id, # Attribute from a cluster controller 2 has no access to attributes=[(0, Clusters.AccessControl.Attributes.Acl)], keepSubscriptions=False, - reportInterval=[3, 3], + reportInterval=(min_interval_floor_sec, max_interval_ceiling_sec), autoResubscribe=False ) asserts.fail("Expected exception not thrown") @@ -265,7 +339,10 @@ async def test_TC_IDM_4_2(self): "Incorrect error response for subscription to unallowed cluster") # *** Step 4 *** - self.print_step(4, "Setup CR2 such that it does not have access to all attributes on a specific cluster and endpoint. CR2 sends a subscription request to subscribe to all attributes for which it does not have access.") + # Setup CR2 such that it does not have access to all attributes on a specific cluster and + # endpoint. CR2 sends a subscription request to subscribe to all attributes for which it + # does not have access. + self.step(4) # Limited ACE for controller 2 with single cluster access and specific endpoint CR2_limited_ace = Clusters.AccessControl.Structs.AccessControlEntryStruct( @@ -273,10 +350,11 @@ async def test_TC_IDM_4_2(self): authMode=Clusters.AccessControl.Enums.AccessControlEntryAuthModeEnum.kCase, targets=[Clusters.AccessControl.Structs.AccessControlTargetStruct( endpoint=1, - cluster=Clusters.BasicInformation.id)], + cluster=Clusters.Descriptor.id)], subjects=[CR2_nodeid]) - self.add_ace_to_dut_acl(CR1, CR2_limited_ace) + # Add limited ACE + await self.add_ace_to_dut_acl(CR1, CR2_limited_ace, dut_acl_original) # Controller 2 tries to subscribe to all attributes from a cluster # it doesn't have access to @@ -287,7 +365,7 @@ async def test_TC_IDM_4_2(self): # Cluster controller 2 has no access to attributes=[(0, Clusters.BasicInformation)], keepSubscriptions=False, - reportInterval=[3, 3], + reportInterval=(min_interval_floor_sec, max_interval_ceiling_sec), autoResubscribe=False ) raise ValueError("Expected exception not thrown") @@ -296,17 +374,30 @@ async def test_TC_IDM_4_2(self): asserts.assert_equal(e.err, INVALID_ACTION_ERROR_CODE, "Incorrect error response for subscription to unallowed cluster") + await self.write_dut_acl(CR1, dut_acl_original) + acl_list = await self.get_dut_acl(CR1) + print(f'acl_list - reset 4: {acl_list}') + # *** Step 5 *** - self.print_step(5, "Setup CR2 such that it does not have access to an Endpoint. CR2 sends a subscription request to subscribe to all attributes on all clusters on a specific Endpoint for which it does not have access.") + # Setup CR2 such that it does not have access to an Endpoint. CR2 sends a subscription + # request to subscribe to all attributes on all clusters on a specific Endpoint for which + # it does not have access. + self.step(5) + + # Get first value of parts list for the endpoint + parts_list = await self.get_descriptor_parts_list(CR1) + asserts.assert_greater(len(parts_list), 0, "Parts list is empty.") + endpoint = parts_list[0] # Limited ACE for controller 2 with endpoint 1 access only to all clusters and all attributes CR2_limited_ace = Clusters.AccessControl.Structs.AccessControlEntryStruct( privilege=Clusters.AccessControl.Enums.AccessControlEntryPrivilegeEnum.kView, authMode=Clusters.AccessControl.Enums.AccessControlEntryAuthModeEnum.kCase, - targets=[Clusters.AccessControl.Structs.AccessControlTargetStruct(endpoint=1)], + targets=[Clusters.AccessControl.Structs.AccessControlTargetStruct(endpoint=endpoint)], subjects=[CR2_nodeid]) - self.add_ace_to_dut_acl(CR1, CR2_limited_ace) + # Add limited ACE + await self.add_ace_to_dut_acl(CR1, CR2_limited_ace, dut_acl_original) # Controller 2 tries to subscribe to all attributes from all clusters # on an endpoint it doesn't have access to @@ -317,7 +408,7 @@ async def test_TC_IDM_4_2(self): # Endpoint controller 2 has no access to attributes=[(0)], keepSubscriptions=False, - reportInterval=[3, 3], + reportInterval=(min_interval_floor_sec, max_interval_ceiling_sec), autoResubscribe=False ) raise ValueError("Expected exception not thrown") @@ -327,14 +418,15 @@ async def test_TC_IDM_4_2(self): "Incorrect error response for subscription to unallowed endpoint") # *** Step 6 *** - self.print_step(6, "Setup CR2 such that it does not have access to the Node. CR2 sends a subscription request to subscribe to all attributes on all clusters on all endpoints on a Node for which it does not have access.") + # Setup CR2 such that it does not have access to the Node. CR2 sends a subscription + # request to subscribe to all attributes on all clusters on all endpoints on a Node + # for which it does not have access. + self.step(6) - # Skip setting an ACE for controller 2 so - # the DUT node rejects subscribing to it + # Skip setting an ACE for controller 2 so the DUT node rejects subscribing to it - # Write original DUT ACL into DUT - dut_acl_original = await self.get_dut_acl() - await self.write_acl(ctrl=CR1, acl=dut_acl_original) + # Restore original DUT ACL + await self.write_dut_acl(CR1, dut_acl_original) # Controller 2 tries to subscribe to all attributes from all clusters # from all endpoints on a node it doesn't have access to @@ -345,7 +437,7 @@ async def test_TC_IDM_4_2(self): nodeid=self.dut_node_id, attributes=[], keepSubscriptions=False, - reportInterval=[3, 3], + reportInterval=(min_interval_floor_sec, max_interval_ceiling_sec), autoResubscribe=False ) raise ValueError("Expected exception not thrown") @@ -355,7 +447,12 @@ async def test_TC_IDM_4_2(self): "Incorrect error response for subscription to unallowed node") # *** Step 7 *** - self.print_step(7, "CR1 sends a subscription request action for an attribute with an empty DataVersionFilters field. DUT sends a report data action with the data of the attribute along with the data version. Tear down the subscription for that attribute. Start another subscription with the DataVersionFilter field set to the data version received above.") + # CR1 sends a subscription request action for an attribute with an empty + # DataVersionFilters field. DUT sends a report data action with the data + # of the attribute along with the data version. Tear down the subscription + # for that attribute. Start another subscription with the DataVersionFilter + # field set to the data version received above. + self.step(7) # Subscribe to attribute with empty dataVersionFilters sub_cr1_empty_dvf = await CR1.ReadAttribute( @@ -376,94 +473,126 @@ async def test_TC_IDM_4_2(self): data_version_filter = [(0, Clusters.BasicInformation, data_version)] # Subscribe to attribute with provided DataVersion - sub_cr1_provided_dvf = await CR1.ReadAttribute( + sub_cr1_step7 = await CR1.ReadAttribute( nodeid=self.dut_node_id, attributes=node_label_attr_path, - reportInterval=(10, 20), + reportInterval=(min_interval_floor_sec, max_interval_ceiling_sec), keepSubscriptions=False, dataVersionFilters=data_version_filter ) # Verify that the subscription is activated between CR1 and DUT - asserts.assert_true(sub_cr1_provided_dvf.subscriptionId, "Subscription not activated") + asserts.assert_true(sub_cr1_step7.subscriptionId, "Subscription not activated") - sub_cr1_provided_dvf.Shutdown() + sub_cr1_step7.Shutdown() # *** Step 8 *** - self.print_step(8, "CR1 sends a subscription request action for an attribute and sets the MinIntervalFloor value to be same as MaxIntervalCeiling. Activate the Subscription between CR1 and DUT. Modify the attribute which has been subscribed to on the DUT.") + # CR1 sends a subscription request action for an attribute and sets the + # MinIntervalFloor to min_interval_floor_sec and MaxIntervalCeiling to 10. + # Activate the Subscription between CR1 and DUT and record the time when + # the priming ReportDataMessage is received as t_report_sec. Save the + # returned MaxInterval from the SubscribeResponseMessage as max_interval_sec. + self.step(8) # Subscribe to attribute - same_min_max_interval_sec = 3 sub_cr1_update_value = await CR1.ReadAttribute( nodeid=self.dut_node_id, attributes=node_label_attr_path, - reportInterval=(same_min_max_interval_sec, same_min_max_interval_sec), + reportInterval=(min_interval_floor_sec, max_interval_ceiling_sec), keepSubscriptions=False ) - # Modify attribute value + # Record the time when the priming ReportDataMessage is received + t_report_sec = time.time() + + # *** Step 9 *** + # CR1 modifies the attribute which has been subscribed to on the DUT + # and waits for an incoming ReportDataMessage + self.step(9) + + # Saving the returned MaxInterval from the SubscribeResponseMessage + min_interval_floor_sec, max_interval_sec = sub_cr1_update_value.GetReportingIntervalsSeconds() + + # Get subscription timeout + subscription_timeout_sec = sub_cr1_update_value.GetSubscriptionTimeoutMs() / 1000 + + # Set Attribute Update Callback + node_label_update_cb = AttributeChangeCallback(node_label_attr) + sub_cr1_update_value.SetAttributeUpdateCallback(node_label_update_cb) + + # Update attribute value new_node_label_write = "NewNodeLabel_011235813" await CR1.WriteAttribute( self.dut_node_id, [(0, node_label_attr(value=new_node_label_write))] ) - # Wait MinIntervalFloor seconds before reading updated attribute value - time.sleep(same_min_max_interval_sec) - new_node_label_read = sub_cr1_update_value.GetAttribute(node_label_attr_typed_path) + node_label_update_cb.wait_for_report() + + # Save the time that the report is received + t_update_sec = time.time() + + # Elapsed time between attribute subscription and write update + t_elapsed_sec = t_update_sec - t_report_sec - # Verify new attribute value after MinIntervalFloor time - asserts.assert_equal(new_node_label_read, new_node_label_write, "Attribute value not updated after write operation.") + # Verify that t_update - t_report is greater than min_interval_floor_s and less than the ReadClient SubscriptionTimeout + asserts.assert_greater(t_elapsed_sec, min_interval_floor_sec, + f"t_update_sec - t_report_sec ({t_elapsed_sec}s) must be greater than min_interval_floor_sec ({min_interval_floor_sec}s)") + asserts.assert_less(t_elapsed_sec, subscription_timeout_sec, + f"t_update_sec - t_report_sec ({t_elapsed_sec}s) must be less than subscription_timeout_sec ({subscription_timeout_sec}s)") sub_cr1_update_value.Shutdown() - # *** Step 9 *** - self.print_step( - 9, "CR1 sends a subscription request action for an attribute and set the MinIntervalFloor value to be greater than MaxIntervalCeiling.") + # *** Step 10 *** + # CR1 sends a subscription request action for an attribute and set the MinIntervalFloor + # value to be greater than MaxIntervalCeiling. + self.step(10) - # Subscribe to attribute with invalid reportInterval arguments, expect and exception + # Subscribe to attribute with invalid reportInterval arguments, expect an error sub_cr1_invalid_intervals = None - try: + with asserts.assert_raises(ChipStackError, "Expected exception wasn't thrown."): sub_cr1_invalid_intervals = await CR1.ReadAttribute( nodeid=self.dut_node_id, attributes=node_label_attr_path, reportInterval=(20, 10), keepSubscriptions=False ) - except ChipStackError: - # Verify no subscription is established - with asserts.assert_raises(AttributeError): - sub_cr1_invalid_intervals.subscriptionId - except Exception: - asserts.fail("Expected exception was not thrown") - # *** Step 10 *** - self.print_step( - 10, "CR1 sends a subscription request to subscribe to a specific global attribute from all clusters on all endpoints.") + # Verify no subscription was established + with asserts.assert_raises(AttributeError): + sub_cr1_invalid_intervals.subscriptionId + + # *** Step 11 *** + # CR1 sends a subscription request to subscribe to a specific global attribute from + # all clusters on all endpoints. + self.step(11) + + # Setting max_interval_ceiling_sec value for steps 11-13 + max_interval_ceiling_sec = 10 # Omitting endpoint to indicate endpoint wildcard cluster_rev_attr_path = [(cluster_rev_attr)] # Subscribe to global attribute - sub_cr1_step10 = await CR1.ReadAttribute( + sub_cr1_step11 = await CR1.ReadAttribute( nodeid=self.dut_node_id, attributes=cluster_rev_attr_path, - reportInterval=(3, 3), + reportInterval=(min_interval_floor_sec, max_interval_ceiling_sec), keepSubscriptions=False ) # Verify that the subscription is activated between CR1 and DUT - asserts.assert_true(sub_cr1_step10.subscriptionId, "Subscription not activated") + asserts.assert_true(sub_cr1_step11.subscriptionId, "Subscription not activated") # Verify attribute came back self.verify_attribute_exists( - sub=sub_cr1_step10, + sub=sub_cr1_step11, cluster=Clusters.BasicInformation, attribute=cluster_rev_attr ) # Verify DUT sends back the attribute values for the global attribute - cluster_revision_attr_value = sub_cr1_step10.GetAttribute(cluster_rev_attr_typed_path) + cluster_revision_attr_value = sub_cr1_step11.GetAttribute(cluster_rev_attr_typed_path) # Verify ClusterRevision is of uint16 type asserts.assert_true(self.is_valid_uint16_value(cluster_revision_attr_value), "ClusterRevision is not of uint16 type.") @@ -471,58 +600,60 @@ async def test_TC_IDM_4_2(self): # Verify valid ClusterRevision value asserts.assert_greater_equal(cluster_revision_attr_value, 0, "Invalid ClusterRevision value.") - sub_cr1_step10.Shutdown() + sub_cr1_step11.Shutdown() - # *** Step 11 *** - self.print_step(11, "CR1 sends a subscription request to subscribe to a global attribute on an endpoint on all clusters.") + # *** Step 12 *** + # CR1 sends a subscription request to subscribe to a global attribute on an endpoint on all clusters. + self.step(12) # Specifying single endpoint 0 requested_ep = 0 cluster_rev_attr_path = [(requested_ep, cluster_rev_attr)] # Subscribe to global attribute - sub_cr1_step11 = await CR1.ReadAttribute( + sub_cr1_step12 = await CR1.ReadAttribute( nodeid=self.dut_node_id, attributes=cluster_rev_attr_path, - reportInterval=(3, 3), + reportInterval=(min_interval_floor_sec, max_interval_ceiling_sec), keepSubscriptions=False ) # Verify that the subscription is activated between CR1 and DUT - asserts.assert_true(sub_cr1_step11.subscriptionId, "Subscription not activated") + asserts.assert_true(sub_cr1_step12.subscriptionId, "Subscription not activated") # Verify attribute came back self.verify_attribute_exists( - sub=sub_cr1_step11, + sub=sub_cr1_step12, cluster=Clusters.BasicInformation, attribute=cluster_rev_attr ) # Verify no data from other endpoints is sent back - attributes = sub_cr1_step11.GetAttributes() + attributes = sub_cr1_step12.GetAttributes() ep_keys = list(attributes.keys()) asserts.assert_true(len(ep_keys) == 1, "More than one endpoint returned, exactly 1 was expected") # Verify DUT sends back the attribute values for the global attribute cluster_rev_attr_typed_path = self.get_typed_attribute_path(cluster_rev_attr) - cluster_revision_attr_value = sub_cr1_step11.GetAttribute(cluster_rev_attr_typed_path) + cluster_revision_attr_value = sub_cr1_step12.GetAttribute(cluster_rev_attr_typed_path) # Verify ClusterRevision is of uint16 type asserts.assert_true(self.is_valid_uint16_value(cluster_revision_attr_value), "ClusterRevision is not of uint16 type.") - sub_cr1_step11.Shutdown() + sub_cr1_step12.Shutdown() - # *** Step 12 *** - self.print_step(12, "CR1 sends a subscription request to the DUT with both AttributeRequests and EventRequests as empty.") + # *** Step 13 *** + # CR1 sends a subscription request to the DUT with both AttributeRequests and EventRequests as empty. + self.step(13) # Attempt a subscription with both AttributeRequests and EventRequests as empty - sub_cr1_step12 = None + sub_cr1_step13 = None try: - sub_cr1_step12 = await CR1.Read( + sub_cr1_step13 = await CR1.Read( nodeid=self.dut_node_id, attributes=[], events=[], - reportInterval=(3, 3) + reportInterval=(min_interval_floor_sec, max_interval_ceiling_sec), ) raise ValueError("Expected exception not thrown") except ChipStackError as e: @@ -532,7 +663,7 @@ async def test_TC_IDM_4_2(self): # Verify no subscription is established with asserts.assert_raises(AttributeError): - sub_cr1_step12.subscriptionId + sub_cr1_step13.subscriptionId except Exception: asserts.fail("Expected exception was not thrown") diff --git a/src/python_testing/matter_testing_support.py b/src/python_testing/matter_testing_support.py index 796397c57e0458..6bdcb5a543a620 100644 --- a/src/python_testing/matter_testing_support.py +++ b/src/python_testing/matter_testing_support.py @@ -52,7 +52,7 @@ from chip import discovery from chip.ChipStack import ChipStack from chip.clusters import ClusterObjects as ClusterObjects -from chip.clusters.Attribute import EventReadResult, SubscriptionTransaction +from chip.clusters.Attribute import EventReadResult, SubscriptionTransaction, TypedAttributePath from chip.exceptions import ChipStackError from chip.interaction_model import InteractionModelError, Status from chip.setup_payload import SetupPayload @@ -266,6 +266,39 @@ def wait_for_event_report(self, expected_event: ClusterObjects.ClusterEvent, tim return res.Data +class AttributeChangeCallback: + def __init__(self, expected_attribute: ClusterObjects.ClusterAttributeDescriptor): + self._output = queue.Queue() + self._expected_attribute = expected_attribute + + def __call__(self, path: TypedAttributePath, transaction: SubscriptionTransaction): + """This is the subscription callback when an attribute is updated. + It checks the passed in attribute is the same as the subscribed to attribute and + then posts it into the queue for later processing.""" + + asserts.assert_equal(path.AttributeType, self._expected_attribute, + f"[AttributeChangeCallback] Attribute mismatch. Expected: {self._expected_attribute}, received: {path.AttributeType}") + logging.debug(f"[AttributeChangeCallback] Attribute update callback for {path.AttributeType}") + q = (path, transaction) + self._output.put(q) + + def wait_for_report(self): + try: + path, transaction = self._output.get(block=True, timeout=10) + except queue.Empty: + asserts.fail( + f"[AttributeChangeCallback] Failed to receive a report for the {self._expected_attribute} attribute change") + + asserts.assert_equal(path.AttributeType, self._expected_attribute, + f"[AttributeChangeCallback] Received incorrect report. Expected: {self._expected_attribute}, received: {path.AttributeType}") + try: + attribute_value = transaction.GetAttribute(path) + logging.info( + f"[AttributeChangeCallback] Got attribute subscription report. Attribute {path.AttributeType}. Updated value: {attribute_value}. SubscriptionId: {transaction.subscriptionId}") + except KeyError: + asserts.fail("[AttributeChangeCallback] Attribute {expected_attribute} not found in returned report") + + class InternalTestRunnerHooks(TestRunnerHooks): def start(self, count: int):