From 927c81857ae9288ffab4ffbd529979412f8cc5f1 Mon Sep 17 00:00:00 2001 From: C Freeman Date: Tue, 20 Aug 2024 21:31:40 -0400 Subject: [PATCH] TC-IDM-10.2: Add check for MACL (#35086) * TC-IDM-10.2: Add check for MACL Test: unit tests. Note this has not been tested against the example app as it is still under development. * Update src/python_testing/TC_DeviceConformance.py Co-authored-by: Tennessee Carmel-Veilleux * Update src/python_testing/TestConformanceTest.py --------- Co-authored-by: Tennessee Carmel-Veilleux --- .github/workflows/tests.yaml | 1 + src/python_testing/TC_DeviceConformance.py | 23 ++++ src/python_testing/TestConformanceTest.py | 131 +++++++++++++++++++++ src/python_testing/execute_python_tests.py | 1 + 4 files changed, 156 insertions(+) create mode 100644 src/python_testing/TestConformanceTest.py diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index dbbda0aa36580f..d60ffb185de022 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -514,6 +514,7 @@ jobs: scripts/run_in_python_env.sh out/venv 'python3 ./src/python_testing/TestIdChecks.py' scripts/run_in_python_env.sh out/venv 'python3 ./src/python_testing/TestSpecParsingDeviceType.py' scripts/run_in_python_env.sh out/venv 'python3 ./src/python_testing/TestConformanceSupport.py' + scripts/run_in_python_env.sh out/venv 'python3 ./src/python_testing/TestConformanceTest.py' scripts/run_in_python_env.sh out/venv 'python3 ./src/python_testing/TestChoiceConformanceSupport.py' scripts/run_in_python_env.sh out/venv 'python3 ./src/python_testing/test_testing/test_IDM_10_4.py' scripts/run_in_python_env.sh out/venv 'python3 ./src/python_testing/test_testing/test_TC_SC_7_1.py' diff --git a/src/python_testing/TC_DeviceConformance.py b/src/python_testing/TC_DeviceConformance.py index 3cc57e2b018ab6..611d7f54a82b62 100644 --- a/src/python_testing/TC_DeviceConformance.py +++ b/src/python_testing/TC_DeviceConformance.py @@ -50,6 +50,22 @@ async def setup_class_helper(self): self.xml_device_types, problems = build_xml_device_types() self.problems.extend(problems) + def _get_device_type_id(self, device_type_name: str) -> int: + id = [id for id, dt in self.xml_device_types.items() if dt.name.lower() == device_type_name.lower()] + if len(id) != 1: + self.fail_current_test(f"Unable to find {device_type_name} device type") + return id[0] + + def _has_nim(self): + nim_id = self._get_device_type_id('network infrastructure manager') + for endpoint in self.endpoints_tlv.values(): + desc = Clusters.Descriptor + device_types = [dt.deviceType for dt in endpoint[desc.id][desc.Attributes.DeviceTypeList.attribute_id]] + if nim_id in device_types: + # TODO: it's unclear if this needs to be present on every endpoint. Right now, this assumes one is sufficient. + return True + return False + def check_conformance(self, ignore_in_progress: bool, is_ci: bool): problems = [] success = True @@ -125,6 +141,13 @@ def record_warning(location, problem): for f in feature_masks: location = AttributePathLocation(endpoint_id=endpoint_id, cluster_id=cluster_id, attribute_id=GlobalAttributeIds.FEATURE_MAP_ID) + if cluster_id == Clusters.AccessControl.id and f == Clusters.AccessControl.Bitmaps.Feature.kManagedDevice: + # Managed ACL is treated as a special case because it is only allowed if other endpoints support NIM and disallowed otherwise. + if not self._has_nim(): + record_error( + location=location, problem="MACL feature is disallowed if the Network Infrastructure Manager device type is not present") + continue + if f not in self.xml_clusters[cluster_id].features.keys(): record_error(location=location, problem=f'Unknown feature with mask 0x{f:02x}') continue diff --git a/src/python_testing/TestConformanceTest.py b/src/python_testing/TestConformanceTest.py new file mode 100644 index 00000000000000..a656e228bf716f --- /dev/null +++ b/src/python_testing/TestConformanceTest.py @@ -0,0 +1,131 @@ +# +# Copyright (c) 2024 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. +# + +from typing import Any + +import chip.clusters as Clusters +from conformance_support import ConformanceDecision +from global_attribute_ids import GlobalAttributeIds +from matter_testing_support import MatterBaseTest, async_test_body, default_matter_test_main +from mobly import asserts +from spec_parsing_support import build_xml_clusters, build_xml_device_types +from TC_DeviceConformance import DeviceConformanceTests + + +def is_mandatory(conformance): + return conformance(0, [], []).decision == ConformanceDecision.MANDATORY + + +class TestConformanceSupport(MatterBaseTest, DeviceConformanceTests): + def setup_class(self): + self.xml_clusters, self.problems = build_xml_clusters() + self.xml_device_types, problems = build_xml_device_types() + self.problems.extend(problems) + + def _create_minimal_cluster(self, cluster_id: int) -> dict[int, Any]: + attrs = {} + attrs[GlobalAttributeIds.FEATURE_MAP_ID] = 0 + + mandatory_attributes = [id for id, a in self.xml_clusters[cluster_id].attributes.items() if is_mandatory(a.conformance)] + for m in mandatory_attributes: + # dummy versions - we're not using the values in this test + attrs[m] = 0 + attrs[GlobalAttributeIds.ATTRIBUTE_LIST_ID] = mandatory_attributes + mandatory_accepted_commands = [id for id, a in self.xml_clusters[cluster_id].accepted_commands.items() + if is_mandatory(a.conformance)] + attrs[GlobalAttributeIds.ACCEPTED_COMMAND_LIST_ID] = mandatory_accepted_commands + mandatory_generated_commands = [id for id, a in self.xml_clusters[cluster_id].generated_commands.items() + if is_mandatory(a.conformance)] + attrs[GlobalAttributeIds.GENERATED_COMMAND_LIST_ID] = mandatory_generated_commands + attrs[GlobalAttributeIds.CLUSTER_REVISION_ID] = self.xml_clusters[cluster_id].revision + return attrs + + def _create_minimal_dt(self, device_type_id: int) -> dict[int, dict[int, Any]]: + ''' Creates the internals of an endpoint_tlv with the minimal set of clusters, with the minimal set of attributes and commands. Global attributes only. + Does NOT take into account overrides yet. + ''' + endpoint_tlv = {} + required_servers = [id for id, c in self.xml_device_types[device_type_id].server_clusters.items() + if is_mandatory(c.conformance)] + required_clients = [id for id, c in self.xml_device_types[device_type_id].client_clusters.items() + if is_mandatory(c.conformance)] + device_type_revision = self.xml_device_types[device_type_id].revision + + for s in required_servers: + endpoint_tlv[s] = self._create_minimal_cluster(s) + + # Descriptor + attr = Clusters.Descriptor.Attributes + attrs = {} + attrs[attr.FeatureMap.attribute_id] = 0 + attrs[attr.AcceptedCommandList.attribute_id] = [] + attrs[attr.GeneratedCommandList.attribute_id] = [] + attrs[attr.ClusterRevision.attribute_id] = self.xml_clusters[Clusters.Descriptor.id].revision + attrs[attr.DeviceTypeList.attribute_id] = [ + Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=device_type_id, revision=device_type_revision)] + attrs[attr.ServerList.attribute_id] = required_servers + attrs[attr.ClientList.attribute_id] = required_clients + attrs[attr.PartsList.attribute_id] = [] + attrs[attr.AttributeList.attribute_id] = [] + attrs[attr.AttributeList.attribute_id] = list(attrs.keys()) + + endpoint_tlv[Clusters.Descriptor.id] = attrs + return endpoint_tlv + + def add_macl(self, root_endpoint: dict[int, dict[int, Any]]): + ac = Clusters.AccessControl + root_endpoint[ac.id][ac.Attributes.FeatureMap.attribute_id] = ac.Bitmaps.Feature.kManagedDevice + root_endpoint[ac.id][ac.Attributes.Arl.attribute_id] = [] + root_endpoint[ac.id][ac.Attributes.CommissioningARL.attribute_id] = [] + root_endpoint[ac.id][ac.Attributes.AttributeList.attribute_id].extend([ + ac.Attributes.Arl.attribute_id, ac.Attributes.CommissioningARL.attribute_id]) + root_endpoint[ac.id][ac.Attributes.AcceptedCommandList.attribute_id].append(ac.Commands.ReviewFabricRestrictions.command_id) + root_endpoint[ac.id][ac.Attributes.GeneratedCommandList.attribute_id].append( + ac.Commands.ReviewFabricRestrictionsResponse.command_id) + + @async_test_body + async def test_macl_handling(self): + nim_id = self._get_device_type_id('network infrastructure manager') + root_node_id = self._get_device_type_id('root node') + on_off_id = self._get_device_type_id('On/Off Light') + + root = self._create_minimal_dt(device_type_id=root_node_id) + nim = self._create_minimal_dt(device_type_id=nim_id) + self.endpoints_tlv = {0: root, 1: nim} + asserts.assert_true(self._has_nim(), "Did not find NIM in generated device") + + success, problems = self.check_conformance(ignore_in_progress=False, is_ci=False) + self.problems.extend(problems) + asserts.assert_true(success, "Unexpected failure parsing minimal dt") + + self.add_macl(root) + # A MACL is allowed when there is a NIM, so this should succeed as well + success, problems = self.check_conformance(ignore_in_progress=False, is_ci=False) + self.problems.extend(problems) + asserts.assert_true(success, "Unexpected failure with NIM and MACL") + + # A MACL is not allowed when there is no NIM + self.endpoints_tlv[1] = self._create_minimal_dt(device_type_id=on_off_id) + success, problems = self.check_conformance(ignore_in_progress=False, is_ci=False) + self.problems.extend(problems) + asserts.assert_false(success, "Unexpected success with On/Off and MACL") + + # TODO: what happens if there is a NIM and a non-NIM endpoint? + + +if __name__ == "__main__": + default_matter_test_main() diff --git a/src/python_testing/execute_python_tests.py b/src/python_testing/execute_python_tests.py index ad3fc83c910bf8..86d88e5aa1c7a5 100644 --- a/src/python_testing/execute_python_tests.py +++ b/src/python_testing/execute_python_tests.py @@ -87,6 +87,7 @@ def main(search_directory, env_file): "TestChoiceConformanceSupport.py", "TC_DEMTestBase.py", "choice_conformance_support.py", + "TestConformanceTest.py", # Unit test of the conformance test (TC_DeviceConformance) - does not run against an app. "TestIdChecks.py", "TestSpecParsingDeviceType.py", "TestMatterTestingSupport.py",