Matter SDK Coverage Report
Current view: top level - app/server-cluster/testing - ClusterTester.h (source / functions) Coverage Total Hit
Test: SHA:e021a368d10ac6f3f201c101585146211fdcdaa2 Lines: 89.3 % 112 100
Test Date: 2026-02-13 08:13:38 Functions: 100.0 % 331 331

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

Generated by: LCOV version 2.0-1