diff --git a/src/python_testing/TC_CNET_4_3.py b/src/python_testing/TC_CNET_4_3.py new file mode 100644 index 00000000000000..ddabd5ad00f550 --- /dev/null +++ b/src/python_testing/TC_CNET_4_3.py @@ -0,0 +1,139 @@ +# +# Copyright (c) 2025 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. +# + +# See https://github.com/project-chip/connectedhomeip/blob/master/docs/testing/python.md#defining-the-ci-test-arguments +# for details about the block below. +# +# === 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: > +# --endpoint 0 +# --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 +# factory-reset: true +# quiet: true +# === END CI TEST ARGUMENTS === + +import chip.clusters as Clusters +import test_plan_support +from chip.clusters.Types import NullValue +from chip.testing import matter_asserts +from chip.testing.matter_testing import MatterBaseTest, TestStep, default_matter_test_main, has_feature, run_if_endpoint_matches +from mobly import asserts + + +class TC_CNET_4_3(MatterBaseTest): + + def desc_TC_CNET_4_3(self) -> str: + return "[TC-CNET-4.3] [Ethernet] Verification for attributes check [DUT-Server]" + + def steps_TC_CNET_4_3(self) -> list[TestStep]: + steps = [ + TestStep(1, test_plan_support.commission_if_required(), "", is_commissioning=True), + TestStep(2, "TH reads Descriptor Cluster from the DUT with EP0 TH reads ServerList from the DUT", + "Verify for the presence of an element with value 49 (0x0031) in the ServerList"), + TestStep(3, "TH reads the MaxNetworks attribute from the DUT", + "Verify that MaxNetworks attribute value is within a range of 1 to 255"), + TestStep(4, "TH reads the Networks attribute list from the DUT", + "Verify that each element in the Networks attribute list has the following fields: 'NetworkID', 'connected'.\n\ + NetworkID field is of type octstr with a length range 1 to 32 \n\ + The connected field is of type bool \n\ + Verify that only one entry has connected status as TRUE \n\ + Verify that the number of entries in the Networks attribute is less than or equal to 'MaxNetworksValue'"), + TestStep(5, "TH reads InterfaceEnabled attribute from the DUT", "Verify that InterfaceEnabled attribute value is true"), + TestStep(6, "TH reads LastNetworkingStatus attribute from the DUT", + "LastNetworkingStatus attribute value will be within any one of the following values \ + Success, NetworkNotFound, OutOfRange, RegulatoryError, UnknownError, null"), + TestStep(7, "TH reads the LastNetworkID attribute from the DUT", + "Verify that LastNetworkID attribute matches the NetworkID value of one of the entries in the Networks attribute list"), + TestStep(8, "TH reads the LastConnectErrorValue attribute from the DUT", + "Verify that LastConnectErrorValue attribute value is null") + ] + return steps + + @run_if_endpoint_matches(has_feature(Clusters.NetworkCommissioning, + Clusters.NetworkCommissioning.Bitmaps.Feature.kEthernetNetworkInterface)) + async def test_TC_CNET_4_3(self): + # Commissioning already done + self.step(1) + + self.step(2) + server_list = await self.read_single_attribute_check_success( + cluster=Clusters.Descriptor, + attribute=Clusters.Descriptor.Attributes.ServerList) + asserts.assert_true(49 in server_list, + msg="Verify for the presence of an element with value 49 (0x0031) in the ServerList") + + self.step(3) + max_networks_count = await self.read_single_attribute_check_success( + cluster=Clusters.NetworkCommissioning, + attribute=Clusters.NetworkCommissioning.Attributes.MaxNetworks) + matter_asserts.assert_int_in_range(max_networks_count, min_value=1, max_value=255, description="MaxNetworks") + + self.step(4) + networks = await self.read_single_attribute_check_success( + cluster=Clusters.NetworkCommissioning, + attribute=Clusters.NetworkCommissioning.Attributes.Networks) + asserts.assert_true(networks, "NetworkInfoStruct list should not be empty") + matter_asserts.assert_list_element_type(networks, Clusters.NetworkCommissioning.Structs.NetworkInfoStruct, + "All elements in list are of type NetworkInfoStruct") + matter_asserts.assert_all(networks, lambda x: isinstance(x.networkID, bytes) and 1 <= len(x.networkID) <= 32, + "NetworkID field is an octet string within a length range 1 to 32") + connected_networks_count = sum(map(lambda x: x.connected, networks)) + asserts.assert_equal(connected_networks_count, 1, "Verify that only one entry has connected status as TRUE") + asserts.assert_less_equal(len(networks), max_networks_count, + "Number of entries in the Networks attribute is less than or equal to 'MaxNetworksValue'") + + self.step(5) + interface_enabled = await self.read_single_attribute_check_success( + cluster=Clusters.NetworkCommissioning, + attribute=Clusters.NetworkCommissioning.Attributes.InterfaceEnabled) + asserts.assert_true(interface_enabled, "Verify that InterfaceEnabled attribute value is true") + + self.step(6) + last_networking_status = await self.read_single_attribute_check_success( + cluster=Clusters.NetworkCommissioning, + attribute=Clusters.NetworkCommissioning.Attributes.LastNetworkingStatus) + expected_status = Clusters.NetworkCommissioning.Enums.NetworkCommissioningStatusEnum.kSuccess + asserts.assert_is(last_networking_status, expected_status, "Verify that LastNetworkingStatus attribute value is success") + + self.step(7) + last_network_id = await self.read_single_attribute_check_success( + cluster=Clusters.NetworkCommissioning, + attribute=Clusters.NetworkCommissioning.Attributes.LastNetworkID) + matching_networks_count = sum(map(lambda x: x.networkID == last_network_id, networks)) + asserts.assert_equal(matching_networks_count, 1, + "Verify that LastNetworkID attribute matches the NetworkID value of one of the entries") + asserts.assert_true(isinstance(last_network_id, bytes) and 1 <= len(last_network_id) <= 32, + "Verify LastNetworkID attribute value will be of type octstr with a length range of 1 to 32") + + self.step(8) + last_connect_error_value = await self.read_single_attribute_check_success( + cluster=Clusters.NetworkCommissioning, + attribute=Clusters.NetworkCommissioning.Attributes.LastConnectErrorValue) + asserts.assert_is(last_connect_error_value, NullValue, "Verify that LastConnectErrorValue attribute value is null") + + +if __name__ == "__main__": + default_matter_test_main() diff --git a/src/python_testing/matter_testing_infrastructure/chip/testing/matter_asserts.py b/src/python_testing/matter_testing_infrastructure/chip/testing/matter_asserts.py index 078e4030d5d62c..bfbc1b0e59b2b0 100644 --- a/src/python_testing/matter_testing_infrastructure/chip/testing/matter_asserts.py +++ b/src/python_testing/matter_testing_infrastructure/chip/testing/matter_asserts.py @@ -2,7 +2,7 @@ Matter-specific assertions building on top of Mobly asserts. """ -from typing import Any, List, Optional, Type, TypeVar +from typing import Any, Callable, List, Optional, Type, TypeVar from mobly import asserts @@ -158,6 +158,23 @@ def assert_list(value: Any, description: str, min_length: Optional[int] = None, f"{description} must not exceed {max_length} elements") +def assert_all(value: List[T], condition: Callable[[T], bool], description: str) -> None: + """ + Asserts that all elements in the list satisfy the provided condition. + + Args: + value (List[T]): The list of elements to check. + condition (Callable[[T], bool]): A function that takes an element from value and returns True if it meets the condition, False otherwise. + description: User-defined description for error messages + + Raises: + AssertionError: If any element in the list does not satisfy the condition. + """ + assert_list(value, description) + for i, item in enumerate(value): + asserts.assert_true(condition(item), f"Element at index {i} does not satisfy the condition: {description}") + + def assert_list_element_type(value: List[Any], expected_type: Type[T], description: str, allow_empty: bool = False) -> None: """ Asserts that all elements in the list are of the expected type. diff --git a/src/python_testing/matter_testing_infrastructure/chip/testing/test_matter_asserts.py b/src/python_testing/matter_testing_infrastructure/chip/testing/test_matter_asserts.py index 98b4233c679f8d..b38423cc3dd187 100644 --- a/src/python_testing/matter_testing_infrastructure/chip/testing/test_matter_asserts.py +++ b/src/python_testing/matter_testing_infrastructure/chip/testing/test_matter_asserts.py @@ -164,6 +164,18 @@ def test_assert_list(self): with self.assertRaises(signals.TestFailure): matter_asserts.assert_list([1, 2, 3], "test_max_length", max_length=2) + def test_assert_all(self): + """Test assert_all with valid and invalid values.""" + # Valid cases + matter_asserts.assert_all([], lambda x: isinstance(x, str), "empty list") + matter_asserts.assert_all([1, 2, 3], lambda x: isinstance(x, int), "list of ints") + + # Invalid cases + with self.assertRaises(signals.TestFailure): + matter_asserts.assert_all([1, 2, 'a'], lambda x: isinstance(x, int), "mixed types") + with self.assertRaises(signals.TestFailure): + matter_asserts.assert_all("not a list", lambda x: True, "not a list") + def test_assert_list_element_type(self): """Test assert_list_element_type with valid and invalid values.""" # Valid cases diff --git a/src/python_testing/test_metadata.yaml b/src/python_testing/test_metadata.yaml index 5bd91ecf542152..e56c2daf1d0877 100644 --- a/src/python_testing/test_metadata.yaml +++ b/src/python_testing/test_metadata.yaml @@ -3,6 +3,11 @@ not_automated: - name: MinimalRepresentation.py reason: Code/Test not being used or not shared code for any other tests + - name: TC_CNET_4_3.py + reason: + network commissioning cluster does not return expected values on linux + example apps - + https://github.com/project-chip/connectedhomeip/issues/37824 - name: TC_CNET_4_4.py reason: It has no CI execution block, is not executed in CI - name: TC_DGGEN_3_2.py