Matter SDK Coverage Report
Current view: top level - app/server-cluster/testing - ClusterTester.h (source / functions) Coverage Total Hit
Test: SHA:e98a48c2e59f85a25417956e1d105721433aa5d1 Lines: 91.6 % 95 87
Test Date: 2026-01-09 16:53:50 Functions: 100.0 % 98 98

            Line data    Source code
       1              : /*
       2              :  *    Copyright (c) 2025 Project CHIP Authors
       3              :  *
       4              :  *    Licensed under the Apache License, Version 2.0 (the "License");
       5              :  *    you may not use this file except in compliance with the License.
       6              :  *    You may obtain a copy of the License at
       7              :  *
       8              :  *        http://www.apache.org/licenses/LICENSE-2.0
       9              :  *
      10              :  *    Unless required by applicable law or agreed to in writing, software
      11              :  *    distributed under the License is distributed on an "AS IS" BASIS,
      12              :  *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      13              :  *    See the License for the specific language governing permissions and
      14              :  *    limitations under the License.
      15              :  */
      16              : 
      17              : #pragma once
      18              : #include "FabricTestFixture.h"
      19              : #include <app/AttributeValueDecoder.h>
      20              : #include <app/AttributeValueEncoder.h>
      21              : #include <app/CommandHandler.h>
      22              : #include <app/ConcreteAttributePath.h>
      23              : #include <app/ConcreteClusterPath.h>
      24              : #include <app/ConcreteCommandPath.h>
      25              : #include <app/ConcreteEventPath.h>
      26              : #include <app/data-model-provider/ActionReturnStatus.h>
      27              : #include <app/data-model-provider/MetadataTypes.h>
      28              : #include <app/data-model-provider/tests/ReadTesting.h>
      29              : #include <app/data-model-provider/tests/WriteTesting.h>
      30              : #include <app/data-model/List.h>
      31              : #include <app/data-model/NullObject.h>
      32              : #include <app/server-cluster/ServerClusterInterface.h>
      33              : #include <app/server-cluster/testing/MockCommandHandler.h>
      34              : #include <app/server-cluster/testing/TestServerClusterContext.h>
      35              : #include <clusters/shared/Attributes.h>
      36              : #include <credentials/FabricTable.h>
      37              : #include <credentials/PersistentStorageOpCertStore.h>
      38              : #include <crypto/CHIPCryptoPAL.h>
      39              : #include <lib/core/CHIPError.h>
      40              : #include <lib/core/CHIPPersistentStorageDelegate.h>
      41              : #include <lib/core/DataModelTypes.h>
      42              : #include <lib/core/TLVReader.h>
      43              : #include <lib/support/ReadOnlyBuffer.h>
      44              : #include <lib/support/Span.h>
      45              : #include <memory>
      46              : #include <optional>
      47              : #include <type_traits>
      48              : #include <vector>
      49              : 
      50              : namespace chip {
      51              : namespace Testing {
      52              : // Helper class for testing clusters.
      53              : //
      54              : // This class ensures that data read by attribute is referencing valid memory for all
      55              : // read requests until the ClusterTester object goes out of scope. (for the case where the underlying read references a list or
      56              : // string that points to TLV data).
      57              : //
      58              : // Read/Write of all attribute types should work, but make sure to use ::Type for encoding
      59              : // and ::DecodableType for decoding structure types.
      60              : //
      61              : // Example of usage:
      62              : //
      63              : // ExampleCluster cluster(someEndpointId);
      64              : //
      65              : // // Possibly steps to setup the cluster
      66              : //
      67              : // ClusterTester tester(cluster);
      68              : // app::Clusters::ExampleCluster::Attributes::FeatureMap::TypeInfo::DecodableType features;
      69              : // ASSERT_EQ(tester.ReadAttribute(FeatureMap::Id, features), CHIP_NO_ERROR);
      70              : //
      71              : // app::Clusters::ExampleCluster::Attributes::ExampleListAttribute::TypeInfo::DecodableType list;
      72              : // ASSERT_EQ(tester.ReadAttribute(LabelList::Id, list), CHIP_NO_ERROR);
      73              : // auto it = list.begin();
      74              : // while (it.Next())
      75              : // {
      76              : //     ASSERT_GT(it.GetValue().label.size(), 0u);
      77              : // }
      78              : //
      79              : class ClusterTester
      80              : {
      81              : public:
      82          178 :     ClusterTester(app::ServerClusterInterface & cluster) : mCluster(cluster), mFabricTestFixture(nullptr) {}
      83              : 
      84              :     // Constructor with FabricHelper
      85              :     ClusterTester(app::ServerClusterInterface & cluster, FabricTestFixture * fabricHelper) :
      86              :         mCluster(cluster), mFabricTestFixture(fabricHelper)
      87              :     {}
      88              : 
      89           51 :     app::ServerClusterContext & GetServerClusterContext() { return mTestServerClusterContext.Get(); }
      90              : 
      91              :     // Read attribute into `out` parameter.
      92              :     // The `out` parameter must be of the correct type for the attribute being read.
      93              :     // Use `app::Clusters::<ClusterName>::Attributes::<AttributeName>::TypeInfo::DecodableType` for the `out` parameter to be spec
      94              :     // compliant (see the comment of the class for usage example).
      95              :     // Will construct the attribute path using the first path returned by `GetPaths()` on the cluster.
      96              :     // @returns `CHIP_ERROR_INCORRECT_STATE` if `GetPaths()` doesn't return a list with one path.
      97              :     // @returns `CHIP_IM_GLOBAL_STATUS(UnsupportedAttribute)` if the attribute is not present in AttributeList.
      98              :     template <typename T>
      99          249 :     app::DataModel::ActionReturnStatus ReadAttribute(AttributeId attr_id, T & out)
     100              :     {
     101          249 :         VerifyOrReturnError(VerifyClusterPathsValid(), CHIP_ERROR_INCORRECT_STATE);
     102              : 
     103              :         // Verify that the attribute is present in AttributeList before attempting to read it.
     104              :         // This ensures tests match real-world behavior where the Interaction Model checks AttributeList first.
     105          249 :         auto checkStatus = VerifyAttributeInAttributeList(attr_id);
     106          249 :         VerifyOrReturnError(checkStatus.IsSuccess(), checkStatus);
     107              : 
     108          246 :         auto path = mCluster.GetPaths()[0];
     109              : 
     110              :         // Store the read operation in a vector<std::unique_ptr<...>> to ensure its lifetime
     111              :         // using std::unique_ptr because ReadOperation is non-copyable and non-movable
     112              :         // vector reallocation is not an issue since we store unique_ptrs
     113          246 :         std::unique_ptr<chip::Testing::ReadOperation> readOperation =
     114              :             std::make_unique<chip::Testing::ReadOperation>(path.mEndpointId, path.mClusterId, attr_id);
     115              : 
     116          246 :         mReadOperations.push_back(std::move(readOperation));
     117          246 :         chip::Testing::ReadOperation & readOperationRef = *mReadOperations.back().get();
     118              : 
     119          246 :         Access::SubjectDescriptor subjectDescriptor{ .fabricIndex = mHandler.GetAccessingFabricIndex() };
     120          246 :         readOperationRef.SetSubjectDescriptor(subjectDescriptor);
     121              : 
     122          246 :         std::unique_ptr<app::AttributeValueEncoder> encoder = readOperationRef.StartEncoding();
     123          246 :         app::DataModel::ActionReturnStatus status           = mCluster.ReadAttribute(readOperationRef.GetRequest(), *encoder);
     124          246 :         VerifyOrReturnError(status.IsSuccess(), status);
     125          243 :         ReturnErrorOnFailure(readOperationRef.FinishEncoding());
     126              : 
     127          243 :         std::vector<chip::Testing::DecodedAttributeData> attributeData;
     128          243 :         ReturnErrorOnFailure(readOperationRef.GetEncodedIBs().Decode(attributeData));
     129          243 :         VerifyOrReturnError(attributeData.size() == 1u, CHIP_ERROR_INCORRECT_STATE);
     130              : 
     131          243 :         return app::DataModel::Decode(attributeData[0].dataReader, out);
     132          246 :     }
     133              : 
     134              :     // Write attribute from `value` parameter.
     135              :     // The `value` parameter must be of the correct type for the attribute being written.
     136              :     // Use `app::Clusters::<ClusterName>::Attributes::<AttributeName>::TypeInfo::Type` for the `value` parameter to be spec
     137              :     // compliant (see the comment of the class for usage example).
     138              :     // Will construct the attribute path using the first path returned by `GetPaths()` on the cluster.
     139              :     // @returns `CHIP_ERROR_INCORRECT_STATE` if `GetPaths()` doesn't return a list with one path.
     140              :     // @returns `CHIP_IM_GLOBAL_STATUS(UnsupportedAttribute)` if the attribute is not present in AttributeList.
     141              :     template <typename T>
     142           59 :     app::DataModel::ActionReturnStatus WriteAttribute(AttributeId attr, const T & value)
     143              :     {
     144           59 :         const auto & paths = mCluster.GetPaths();
     145              : 
     146           59 :         VerifyOrReturnError(paths.size() == 1u, CHIP_ERROR_INCORRECT_STATE);
     147              : 
     148              :         // Verify that the attribute is present in AttributeList before attempting to write it.
     149              :         // This ensures tests match real-world behavior where the Interaction Model checks AttributeList first.
     150           59 :         auto checkStatus = VerifyAttributeInAttributeList(attr);
     151           59 :         VerifyOrReturnError(checkStatus.IsSuccess(), checkStatus);
     152              : 
     153           59 :         app::ConcreteAttributePath path(paths[0].mEndpointId, paths[0].mClusterId, attr);
     154           59 :         chip::Testing::WriteOperation writeOp(path);
     155              : 
     156              :         // Create a stable object on the stack
     157           59 :         Access::SubjectDescriptor subjectDescriptor{ .fabricIndex = mHandler.GetAccessingFabricIndex() };
     158           59 :         writeOp.SetSubjectDescriptor(subjectDescriptor);
     159              : 
     160              :         uint8_t buffer[1024];
     161           59 :         TLV::TLVWriter writer;
     162           59 :         writer.Init(buffer);
     163              : 
     164              :         // - DataModel::Encode(integral, enum, etc.) for simple types.
     165              :         // - DataModel::Encode(List<X>) for lists (which iterates and calls Encode on elements).
     166              :         // - DataModel::Encode(Struct) for non-fabric-scoped structs.
     167              :         // - Note: For attribute writes, DataModel::EncodeForWrite is usually preferred for fabric-scoped types,
     168              :         //         but the generic DataModel::Encode often works as a top-level function.
     169              :         //         If you use EncodeForWrite, you ensure fabric-scoped list items are handled correctly:
     170              : 
     171              :         if constexpr (app::DataModel::IsFabricScoped<T>::value)
     172              :         {
     173            3 :             ReturnErrorOnFailure(chip::app::DataModel::EncodeForWrite(writer, TLV::AnonymousTag(), value));
     174              :         }
     175              :         else
     176              :         {
     177           56 :             ReturnErrorOnFailure(chip::app::DataModel::Encode(writer, TLV::AnonymousTag(), value));
     178              :         }
     179              : 
     180           59 :         TLV::TLVReader reader;
     181           59 :         reader.Init(buffer, writer.GetLengthWritten());
     182           59 :         ReturnErrorOnFailure(reader.Next());
     183              : 
     184           59 :         app::AttributeValueDecoder decoder(reader, *writeOp.GetRequest().subjectDescriptor);
     185              : 
     186           59 :         return mCluster.WriteAttribute(writeOp.GetRequest(), decoder);
     187              :     }
     188              : 
     189              :     // Result structure for Invoke operations, containing both status and decoded response.
     190              :     template <typename ResponseType>
     191              :     struct InvokeResult
     192              :     {
     193              :         std::optional<app::DataModel::ActionReturnStatus> status;
     194              :         std::optional<ResponseType> response;
     195              : 
     196              :         // Returns true if the command was successful and response is available
     197          148 :         bool IsSuccess() const
     198              :         {
     199              :             if constexpr (std::is_same_v<ResponseType, app::DataModel::NullObjectType>)
     200           45 :                 return status.has_value() && status->IsSuccess();
     201              :             else
     202          103 :                 return status.has_value() && status->IsSuccess() && response.has_value();
     203              :         }
     204              :     };
     205              : 
     206              :     // Invoke a command and return the decoded result.
     207              :     // The `request` parameter must be of the correct type for the command being invoked.
     208              :     // Use `app::Clusters::<ClusterName>::Commands::<CommandName>::Type` for the `request` parameter to be spec compliant
     209              :     // Will construct the command path using the first path returned by `GetPaths()` on the cluster.
     210              :     // @returns `CHIP_ERROR_INCORRECT_STATE` if `GetPaths()` doesn't return a list with one path.
     211              :     template <typename RequestType, typename ResponseType = typename RequestType::ResponseType>
     212          185 :     [[nodiscard]] InvokeResult<ResponseType> Invoke(chip::CommandId commandId, const RequestType & request)
     213              :     {
     214          185 :         InvokeResult<ResponseType> result;
     215              : 
     216          185 :         const auto & paths = mCluster.GetPaths();
     217          185 :         VerifyOrReturnValue(paths.size() == 1u, result);
     218              : 
     219          185 :         mHandler.ClearResponses();
     220          185 :         mHandler.ClearStatuses();
     221              : 
     222          185 :         const app::DataModel::InvokeRequest invokeRequest = { .path = { paths[0].mEndpointId, paths[0].mClusterId, commandId } };
     223              : 
     224          185 :         TLV::TLVWriter writer;
     225          185 :         writer.Init(mTlvBuffer);
     226              : 
     227          185 :         TLV::TLVReader reader;
     228              : 
     229          370 :         VerifyOrReturnValue(request.Encode(writer, TLV::AnonymousTag()) == CHIP_NO_ERROR, result);
     230          370 :         VerifyOrReturnValue(writer.Finalize() == CHIP_NO_ERROR, result);
     231              : 
     232          185 :         reader.Init(mTlvBuffer, writer.GetLengthWritten());
     233          370 :         VerifyOrReturnValue(reader.Next(TLV::kTLVType_Structure, TLV::AnonymousTag()) == CHIP_NO_ERROR, result);
     234              : 
     235          185 :         result.status = mCluster.InvokeCommand(invokeRequest, reader, &mHandler);
     236              : 
     237              :         // If InvokeCommand returned nullopt, it means the command implementation handled the response.
     238              :         // We need to check the mock handler for a data response or a status response.
     239          185 :         if (!result.status.has_value())
     240              :         {
     241          139 :             if (mHandler.HasResponse())
     242              :             {
     243              :                 // A data response was added, so the command is successful.
     244          139 :                 result.status = app::DataModel::ActionReturnStatus(CHIP_NO_ERROR);
     245              :             }
     246            0 :             else if (mHandler.HasStatus())
     247              :             {
     248              :                 // A status response was added. Use the last one.
     249            0 :                 result.status = app::DataModel::ActionReturnStatus(mHandler.GetLastStatus().status);
     250              :             }
     251              :             else
     252              :             {
     253              :                 // Neither response nor status was provided; this is unexpected.
     254              :                 // This would happen either in error (as mentioned here) or if the command is supposed
     255              :                 // to be handled asynchronously. ClusterTester does not support such asynchronous processing.
     256            0 :                 result.status = app::DataModel::ActionReturnStatus(CHIP_ERROR_INCORRECT_STATE);
     257            0 :                 ChipLogError(
     258              :                     Test, "InvokeCommand returned nullopt, but neither HasResponse nor HasStatus is true. Setting error status.");
     259              :             }
     260              :         }
     261              : 
     262              :         // If command was successful and there's a response, decode it (skip for NullObjectType)
     263              :         if constexpr (!std::is_same_v<ResponseType, app::DataModel::NullObjectType>)
     264              :         {
     265          139 :             if (result.status.has_value() && result.status->IsSuccess() && mHandler.HasResponse())
     266              :             {
     267          139 :                 ResponseType decodedResponse;
     268          139 :                 CHIP_ERROR decodeError = mHandler.DecodeResponse(decodedResponse);
     269          278 :                 if (decodeError == CHIP_NO_ERROR)
     270              :                 {
     271          139 :                     result.response = std::move(decodedResponse);
     272              :                 }
     273              :                 else
     274              :                 {
     275              :                     // Decode failed; reflect error in status and log
     276            0 :                     result.status = app::DataModel::ActionReturnStatus(decodeError);
     277            0 :                     ChipLogError(Test, "DecodeResponse failed: %s", decodeError.AsString());
     278              :                 }
     279              :             }
     280              :         }
     281              : 
     282          185 :         return result;
     283              :     }
     284              : 
     285              :     // convenience method: most requests have a `GetCommandId` (and GetClusterId() as well).
     286              :     template <typename RequestType, typename ResponseType = typename RequestType::ResponseType>
     287           11 :     [[nodiscard]] InvokeResult<ResponseType> Invoke(const RequestType & request)
     288              :     {
     289           11 :         return Invoke(RequestType::GetCommandId(), request);
     290              :     }
     291              : 
     292              :     // Returns the next generated event from the event generator in the test server cluster context
     293            1 :     std::optional<LogOnlyEvents::EventInformation> GetNextGeneratedEvent()
     294              :     {
     295            1 :         return mTestServerClusterContext.EventsGenerator().GetNextEvent();
     296              :     }
     297              : 
     298            8 :     std::vector<app::AttributePathParams> & GetDirtyList() { return mTestServerClusterContext.ChangeListener().DirtyList(); }
     299              : 
     300           65 :     void SetFabricIndex(FabricIndex fabricIndex) { mHandler.SetFabricIndex(fabricIndex); }
     301              : 
     302              :     FabricTestFixture * GetFabricHelper() { return mFabricTestFixture; }
     303              : 
     304              : private:
     305          249 :     bool VerifyClusterPathsValid()
     306              :     {
     307          249 :         auto paths = mCluster.GetPaths();
     308          249 :         if (paths.size() != 1)
     309              :         {
     310            0 :             ChipLogError(Test, "cluster.GetPaths() did not return exactly one path");
     311            0 :             return false;
     312              :         }
     313          249 :         return true;
     314              :     }
     315              : 
     316              :     // Verifies that an attribute is present in the cluster's AttributeList.
     317              :     // @returns CHIP_NO_ERROR if the attribute is found in AttributeList.
     318              :     // @returns CHIP_IM_GLOBAL_STATUS(UnsupportedAttribute) if the attribute is not found in AttributeList.
     319              :     // @returns error status if Attributes cannot be retrieved.
     320          308 :     app::DataModel::ActionReturnStatus VerifyAttributeInAttributeList(AttributeId attr_id)
     321              :     {
     322          308 :         auto path = mCluster.GetPaths()[0];
     323              : 
     324              :         // Get the list of attributes from the cluster's metadata
     325          308 :         ReadOnlyBufferBuilder<app::DataModel::AttributeEntry> builder;
     326          308 :         CHIP_ERROR err = mCluster.Attributes(path, builder);
     327          308 :         ReturnErrorOnFailure(err);
     328              : 
     329          308 :         ReadOnlyBuffer<app::DataModel::AttributeEntry> attributeEntries = builder.TakeBuffer();
     330              : 
     331              :         // Check if the requested attribute ID is present in the attribute list
     332         1076 :         for (const auto & entry : attributeEntries)
     333              :         {
     334         1073 :             if (entry.attributeId == attr_id)
     335              :             {
     336          305 :                 return CHIP_NO_ERROR;
     337              :             }
     338              :         }
     339              : 
     340              :         // Attribute not found in AttributeList
     341            3 :         return CHIP_IM_GLOBAL_STATUS(UnsupportedAttribute);
     342          308 :     }
     343              : 
     344              :     TestServerClusterContext mTestServerClusterContext{};
     345              :     app::ServerClusterInterface & mCluster;
     346              : 
     347              :     // Buffer size for TLV encoding/decoding of command payloads.
     348              :     // 256 bytes was chosen as a conservative upper bound for typical command payloads in tests.
     349              :     // All command payloads used in tests must fit within this buffer; tests with larger payloads will fail.
     350              :     // If protocol or test requirements change, this value may need to be increased.
     351              :     static constexpr size_t kTlvBufferSize = 256;
     352              : 
     353              :     chip::Testing::MockCommandHandler mHandler;
     354              :     uint8_t mTlvBuffer[kTlvBufferSize];
     355              :     std::vector<std::unique_ptr<ReadOperation>> mReadOperations;
     356              : 
     357              :     FabricTestFixture * mFabricTestFixture;
     358              : };
     359              : 
     360              : } // namespace Testing
     361              : } // namespace chip
        

Generated by: LCOV version 2.0-1