diff --git a/src/python_testing/TC_TCCM_1_2.py b/src/python_testing/TC_TCCM_1_2.py new file mode 100644 index 00000000000000..8173d6705106b0 --- /dev/null +++ b/src/python_testing/TC_TCCM_1_2.py @@ -0,0 +1,108 @@ +# +# 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. +# + +# === BEGIN CI TEST ARGUMENTS === +# test-runner-runs: +# run1: +# app: ${ALL_CLUSTERS_APP} +# app-args: > +# --discriminator 1234 +# --KVS kvs1 +# --trace-to json:${TRACE_APP}.json +# script-args: > +# --storage-path admin_storage.json +# --commissioning-method on-network +# --discriminator 1234 +# --passcode 20202021 +# --endpoint 1 +# --trace-to json:${TRACE_TEST_JSON}.json +# --trace-to perfetto:${TRACE_TEST_PERFETTO}.perfetto +# factory-reset: true +# quiet: true +# === END CI TEST ARGUMENTS === + + +import chip.clusters as Clusters +from chip.testing.matter_testing import MatterBaseTest, TestStep, async_test_body, default_matter_test_main +from modebase_cluster_check import ModeBaseClusterChecks + +CLUSTER = Clusters.RefrigeratorAndTemperatureControlledCabinetMode + + +class TC_TCCM_1_2(MatterBaseTest, ModeBaseClusterChecks): + + def __init__(self, *args): + MatterBaseTest.__init__(self, *args) + ModeBaseClusterChecks.__init__(self, + modebase_derived_cluster=CLUSTER) + + def desc_TC_TCCM_1_2(self) -> str: + return "[TC-TCCM-1.2] Cluster attributes with DUT as Server" + + def steps_TC_TCCM_1_2(self) -> list[TestStep]: + steps = [ + TestStep(1, "Commissioning, already done", is_commissioning=True), + TestStep(2, "TH reads from the DUT the SupportedModes attribute."), + TestStep(3, "TH reads from the DUT the CurrentMode attribute."), + TestStep(4, "TH reads from the DUT the OnMode attribute."), + TestStep(5, "TH reads from the DUT the StartUpMode attribute.") + ] + return steps + + def pics_TC_TCCM_1_2(self) -> list[str]: + pics = [ + "TCCM.S" + ] + return pics + + @async_test_body + async def test_TC_TCCM_1_2(self): + + # Setup common mode check + endpoint = self.get_endpoint(default=1) + + self.step(1) + + self.step(2) + # Verify common checks for Mode Base as described in the TC-TCCM-1.2 + supported_modes = await self.check_supported_modes_and_labels(endpoint=endpoint) + # According to the spec, there should be at least one RapidCool or RapidFreeze tag in + # the ones supported. + additional_tags = [CLUSTER.Enums.ModeTag.kRapidCool, + CLUSTER.Enums.ModeTag.kRapidFreeze] + self.check_tags_in_lists(supported_modes=supported_modes, required_tags=additional_tags) + + self.step(3) + # Verify that the CurrentMode attribute has a valid value. + mode = self.cluster.Attributes.CurrentMode + await self.read_and_check_mode(endpoint=endpoint, mode=mode, supported_modes=supported_modes) + + self.step(4) + # Verify that the OnMode attribute has a valid value or null. + mode = self.cluster.Attributes.OnMode + await self.read_and_check_mode(endpoint=endpoint, mode=mode, + supported_modes=supported_modes, is_nullable=True) + + self.step(5) + # Verify that the StartUpMode has a valid value or null + mode = self.cluster.Attributes.StartUpMode + await self.read_and_check_mode(endpoint=endpoint, mode=mode, + supported_modes=supported_modes, is_nullable=True) + + +if __name__ == "__main__": + default_matter_test_main() diff --git a/src/python_testing/modebase_cluster_check.py b/src/python_testing/modebase_cluster_check.py new file mode 100644 index 00000000000000..eb11d34436a02d --- /dev/null +++ b/src/python_testing/modebase_cluster_check.py @@ -0,0 +1,158 @@ +# +# Copyright (c) 2023 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. +# + +import logging + +from chip.clusters.Types import NullValue +from mobly import asserts + +logger = logging.getLogger(__name__) + +# Maximum value for ModeTags according to specs is 16bits. +MAX_MODE_TAG = 0xFFFF +# According to specs, the specific MfgTags should be defined in the range 0x8000 - 0xBFFF +START_MFGTAGS_RANGE = 0x8000 +END_MFGTAGS_RANGE = 0xBFFF + + +class ModeBaseClusterChecks: + """ Class that holds the common Mode checks between TCs + + Several TCs have similar checks in place for functionality that is common among them. + This class holds most of this common functionality to avoid duplicating code with the same validations. + + Link to spec: + https://github.com/CHIP-Specifications/chip-test-plans/blob/master/src/cluster/modebase_common.adoc + + + Attributes: + modebase_derived_cluster: A reference to the cluster to be tested, it should be a derived from the Mode Base cluster. + """ + + def __init__(self, modebase_derived_cluster): + self.mode_tags = [tag.value for tag in modebase_derived_cluster.Enums.ModeTag] + self.cluster = modebase_derived_cluster + self.attributes = modebase_derived_cluster.Attributes + + async def check_supported_modes_and_labels(self, endpoint): + """ Verifies the device supported modes and labels. + + Checks that the SupportedModes attribute has the expected structure and values like: + - Between 2 and 255 entries. + - The Mode values of all entries are unique. + - The Label values of all entries are unique. + + Args: + endpoint: The endpoint used for the requests to the cluster. + + Returns: + A list of ModeOptionStruct supported by the cluster. + """ + # Get the supported modes + supported_modes = await self.read_single_attribute_check_success(endpoint=endpoint, + cluster=self.cluster, + attribute=self.attributes.SupportedModes) + + # Check if the list of supported modes is larger than 2 + asserts.assert_greater_equal(len(supported_modes), 2, "SupportedModes must have at least 2 entries!") + # Check that supported modes are less than 255 + asserts.assert_less_equal(len(supported_modes), 255, "SupportedModes must have at most 255 entries!") + + # Check for repeated labels or modes + labels = set() + modes = set() + for mode_option_struct in supported_modes: + # Verify that the modes in all ModeOptionStruct in SupportedModes are unique. + if mode_option_struct.mode in modes: + asserts.fail("SupportedModes can't have repeated Mode values") + else: + modes.add(mode_option_struct.mode) + # Verify that the labels in all ModeOptionStruct in SupportedModes are unique. + if mode_option_struct.label in labels: + asserts.fail("SupportedModes can't have repeated Label values") + else: + labels.add(mode_option_struct.label) + + return supported_modes + + def check_tags_in_lists(self, supported_modes, required_tags=None): + """ Validates the ModeTags values. + + This function evaluates the ModeTags of each ModeOptionStruct: + - Should have at least one tag. + - Should be maximum 16bits in size. + - Should be a Mfg tag or one of the supported ones (either common or specific). + - Should have at least one common or specific tag. + - If defined, verify that at least one of the "required_tags" exists. + + Args: + supported_modes: A list of ModeOptionStruct. + required_tags: List of tags that are required according to the cluster spec. + """ + # Verify the ModeTags on each ModeOptionStruct + for mode_option_struct in supported_modes: + # Shuld have at least one entry + if len(mode_option_struct.modeTags) == 0: + asserts.fail("The ModeTags field should have at least one entry.") + + # Check each ModelTag + at_least_one_common_or_derived = False + for tag in mode_option_struct.modeTags: + # Value should not larger than 16bits + if not (0 <= tag.value <= MAX_MODE_TAG): + asserts.fail("Tag should not be larger than 16bits.") + + # Check if is tag is common, derived or mfg. + is_mfg = (START_MFGTAGS_RANGE <= tag.value <= END_MFGTAGS_RANGE) + if not (is_mfg or tag.value in self.mode_tags): + asserts.fail("Mode tag value is not a common, derived or vendor tag.") + + # Confirm if tag is common or derived. + if not is_mfg: + at_least_one_common_or_derived = True + + if not at_least_one_common_or_derived: + asserts.fail("There should be at least one common or derived tag on each ModeOptionsStruct") + + if required_tags: + has_required_tags = False + for mode_options_struct in supported_modes: + has_required_tags = any(tag.value in required_tags for tag in mode_options_struct.modeTags) + if has_required_tags: + break + asserts.assert_true(has_required_tags, "No ModeOptionsStruct has the required tags.") + + async def read_and_check_mode(self, endpoint, mode, supported_modes, is_nullable=False): + """Evaluates the current mode + + This functions checks if the requested mode attribute has a valid value from the SupportedModes, + supports optional nullable values. + + Args: + endpoint: The endpoint used for the requests to the cluster. + mode: Mode that will be verified. + supported_modes: A list of ModeOptionStruct. + is_nullable: Optional argument to indicate if the tested mode allows NullValue + """ + mode_value = await self.read_single_attribute_check_success(endpoint=endpoint, + cluster=self.cluster, + attribute=mode) + supported_modes_dut = {mode_option_struct.mode for mode_option_struct in supported_modes} + is_valid = mode_value in supported_modes_dut + if is_nullable and mode_value == NullValue: + is_valid = True + asserts.assert_true(is_valid, f"{mode} not supported") diff --git a/src/python_testing/test_metadata.yaml b/src/python_testing/test_metadata.yaml index dd5f5e3af49b5f..850a6ee756615d 100644 --- a/src/python_testing/test_metadata.yaml +++ b/src/python_testing/test_metadata.yaml @@ -77,6 +77,8 @@ not_automated: reason: Shared code for TC_*, not a standalone test - name: test_plan_table_generator.py reason: Code/Test not being used or not shared code for any other tests + - name: modebase_cluster_check.py + reason: Shared code for Modebase derived clusters, not a standalone test. # This is a list of slow tests (just arbitrarily picked around 20 seconds) # used in some script reporting for "be patient" messages as well as potentially