Matter SDK Coverage Report
Current view: top level - app/server-cluster/testing - ClusterTester.h (source / functions) Coverage Total Hit
Test: SHA:2a48c1efeab1c0f76f3adb3a0940b0f7de706453 Lines: 89.3 % 112 100
Test Date: 2026-01-31 08:14:20 Functions: 100.0 % 262 262

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

Generated by: LCOV version 2.0-1