diff --git a/src/app/clusters/diagnostic-logs-server/tests/BUILD.gn b/src/app/clusters/diagnostic-logs-server/tests/BUILD.gn index 714a6af1977a23..f8ea79855ba592 100644 --- a/src/app/clusters/diagnostic-logs-server/tests/BUILD.gn +++ b/src/app/clusters/diagnostic-logs-server/tests/BUILD.gn @@ -29,6 +29,7 @@ chip_test_suite("tests") { public_deps = [ "${chip_root}/src/app/clusters/diagnostic-logs-server", + "${chip_root}/src/app/clusters/testing:testing", "${chip_root}/src/lib/support", "${chip_root}/src/protocols/bdx", ] diff --git a/src/app/clusters/diagnostic-logs-server/tests/TestDiagnosticLogsCluster.cpp b/src/app/clusters/diagnostic-logs-server/tests/TestDiagnosticLogsCluster.cpp index cd3d7e9c6193f3..e0b80fa7a3c218 100644 --- a/src/app/clusters/diagnostic-logs-server/tests/TestDiagnosticLogsCluster.cpp +++ b/src/app/clusters/diagnostic-logs-server/tests/TestDiagnosticLogsCluster.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -36,72 +37,6 @@ using chip::Protocols::InteractionModel::Status; static constexpr EndpointId kRootEndpoint = 0; -class MockCommandHandler : public CommandHandler -{ -public: - ~MockCommandHandler() override {} - - struct ResponseRecord - { - ConcreteCommandPath path; - CommandId commandId; - chip::System::PacketBufferHandle encodedData; - }; - - CHIP_ERROR FallibleAddStatus(const ConcreteCommandPath & aRequestCommandPath, - const Protocols::InteractionModel::ClusterStatusCode & aStatus, - const char * context = nullptr) override - { - return CHIP_NO_ERROR; - } - - void AddStatus(const ConcreteCommandPath & aRequestCommandPath, const Protocols::InteractionModel::ClusterStatusCode & aStatus, - const char * context = nullptr) override - { - CHIP_ERROR err = FallibleAddStatus(aRequestCommandPath, aStatus, context); - VerifyOrDie(err == CHIP_NO_ERROR); - } - - FabricIndex GetAccessingFabricIndex() const override { return mFabricIndex; } - - CHIP_ERROR AddResponseData(const ConcreteCommandPath & aRequestCommandPath, CommandId aResponseCommandId, - const DataModel::EncodableToTLV & aEncodable) override - { - chip::System::PacketBufferHandle handle = chip::MessagePacketBuffer::New(1024); - VerifyOrReturnError(!handle.IsNull(), CHIP_ERROR_NO_MEMORY); - TLV::TLVWriter baseWriter; - baseWriter.Init(handle->Start(), handle->MaxDataLength()); - DataModel::FabricAwareTLVWriter writer(baseWriter, /*fabricIndex*/ 1); - TLV::TLVType ct; - ReturnErrorOnFailure( - static_cast(writer).StartContainer(TLV::AnonymousTag(), TLV::kTLVType_Structure, ct)); - ReturnErrorOnFailure(aEncodable.EncodeTo(writer, TLV::ContextTag(app::CommandDataIB::Tag::kFields))); - ReturnErrorOnFailure(static_cast(writer).EndContainer(ct)); - handle->SetDataLength(static_cast(writer).GetLengthWritten()); - mResponse.path = aRequestCommandPath; - mResponse.commandId = aResponseCommandId; - mResponse.encodedData = std::move(handle); - return CHIP_NO_ERROR; - } - - void AddResponse(const ConcreteCommandPath & aRequestCommandPath, CommandId aResponseCommandId, - const DataModel::EncodableToTLV & aEncodable) override - { - (void) AddResponseData(aRequestCommandPath, aResponseCommandId, aEncodable); - } - - bool IsTimedInvoke() const override { return false; } - void FlushAcksRightAwayOnSlowCommand() override {} - Access::SubjectDescriptor GetSubjectDescriptor() const override { return Access::SubjectDescriptor{}; } - Messaging::ExchangeContext * GetExchangeContext() const override { return nullptr; } - - const ResponseRecord & GetResponse() const { return mResponse; } - -private: - ResponseRecord mResponse; - FabricIndex mFabricIndex = 0; -}; - class MockDelegate : public DiagnosticLogs::DiagnosticLogsProviderDelegate { public: @@ -150,25 +85,11 @@ class MockDelegate : public DiagnosticLogs::DiagnosticLogsProviderDelegate uint16_t bufferSize = 0; }; -static Commands::RetrieveLogsResponse::DecodableType DecodeRetrieveLogsResponse(const MockCommandHandler::ResponseRecord & rec) +static Commands::RetrieveLogsResponse::DecodableType DecodeRetrieveLogsResponse(const Testing::MockCommandHandler & handler) { - TLV::TLVReader reader; - reader.Init(rec.encodedData->Start(), static_cast(rec.encodedData->DataLength())); - - CHIP_ERROR err = reader.Next(); - EXPECT_EQ(err, CHIP_NO_ERROR); - - TLV::TLVReader outer; - err = reader.OpenContainer(outer); - EXPECT_EQ(err, CHIP_NO_ERROR); - - err = outer.Next(); - EXPECT_EQ(err, CHIP_NO_ERROR); - EXPECT_TRUE(IsContextTag(outer.GetTag())); - EXPECT_EQ(TagNumFromTag(outer.GetTag()), chip::to_underlying(CommandDataIB::Tag::kFields)); - Commands::RetrieveLogsResponse::DecodableType decoded; - EXPECT_EQ(decoded.Decode(outer), CHIP_NO_ERROR); + CHIP_ERROR err = handler.DecodeResponse(decoded); + EXPECT_EQ(err, CHIP_NO_ERROR); return decoded; } @@ -188,10 +109,13 @@ TEST_F(TestDiagnosticLogsCluster, ResponsePayload_WithDelegate_Success) diagnosticLogsCluster.SetDelegate(&delegate); const ConcreteCommandPath kPath{ kRootEndpoint, DiagnosticLogs::Id, DiagnosticLogs::Commands::RetrieveLogsRequest::Id }; - MockCommandHandler handler; + Testing::MockCommandHandler handler; diagnosticLogsCluster.HandleLogRequestForResponsePayload(&handler, kPath, DiagnosticLogs::IntentEnum::kEndUserSupport); - EXPECT_EQ(handler.GetResponse().commandId, DiagnosticLogs::Commands::RetrieveLogsResponse::Id); - auto decoded = DecodeRetrieveLogsResponse(handler.GetResponse()); + + // Verify we have exactly one response + EXPECT_EQ(handler.GetResponseCount(), static_cast(1)); + EXPECT_EQ(handler.GetResponseCommandId(), DiagnosticLogs::Commands::RetrieveLogsResponse::Id); + auto decoded = DecodeRetrieveLogsResponse(handler); EXPECT_EQ(decoded.status, DiagnosticLogs::StatusEnum::kSuccess); size_t logContentSize = decoded.logContent.size(); EXPECT_EQ(logContentSize, sizeof(buffer)); @@ -208,11 +132,14 @@ TEST_F(TestDiagnosticLogsCluster, Bdx_WithDelegate_kExhausted) diagnosticLogsCluster.SetDelegate(&delegate); const ConcreteCommandPath kPath{ kRootEndpoint, DiagnosticLogs::Id, DiagnosticLogs::Commands::RetrieveLogsRequest::Id }; - MockCommandHandler handler; + Testing::MockCommandHandler handler; diagnosticLogsCluster.HandleLogRequestForBdx(&handler, kPath, DiagnosticLogs::IntentEnum::kEndUserSupport, MakeOptional(CharSpan::fromCharString("enduser.log"))); - EXPECT_EQ(handler.GetResponse().commandId, DiagnosticLogs::Commands::RetrieveLogsResponse::Id); - auto decoded = DecodeRetrieveLogsResponse(handler.GetResponse()); + + // Verify we have exactly one response + EXPECT_EQ(handler.GetResponseCount(), static_cast(1)); + EXPECT_EQ(handler.GetResponseCommandId(), DiagnosticLogs::Commands::RetrieveLogsResponse::Id); + auto decoded = DecodeRetrieveLogsResponse(handler); EXPECT_EQ(decoded.status, DiagnosticLogs::StatusEnum::kExhausted); size_t logContentSize = decoded.logContent.size(); EXPECT_EQ(logContentSize, sizeof(buffer)); @@ -228,11 +155,14 @@ TEST_F(TestDiagnosticLogsCluster, Bdx_WithDelegate_kExhausted_with_buffer_greate diagnosticLogsCluster.SetDelegate(&delegate); const ConcreteCommandPath kPath{ kRootEndpoint, DiagnosticLogs::Id, DiagnosticLogs::Commands::RetrieveLogsRequest::Id }; - MockCommandHandler handler; + Testing::MockCommandHandler handler; diagnosticLogsCluster.HandleLogRequestForBdx(&handler, kPath, DiagnosticLogs::IntentEnum::kEndUserSupport, MakeOptional(CharSpan::fromCharString("enduser.log"))); - EXPECT_EQ(handler.GetResponse().commandId, DiagnosticLogs::Commands::RetrieveLogsResponse::Id); - auto decoded = DecodeRetrieveLogsResponse(handler.GetResponse()); + + // Verify we have exactly one response + EXPECT_EQ(handler.GetResponseCount(), static_cast(1)); + EXPECT_EQ(handler.GetResponseCommandId(), DiagnosticLogs::Commands::RetrieveLogsResponse::Id); + auto decoded = DecodeRetrieveLogsResponse(handler); EXPECT_EQ(decoded.status, DiagnosticLogs::StatusEnum::kExhausted); size_t logContentSize = decoded.logContent.size(); @@ -245,10 +175,13 @@ TEST_F(TestDiagnosticLogsCluster, ResponsePayload_NoDelegate_NoLogs) DiagnosticLogsCluster diagnosticLogsCluster; const ConcreteCommandPath kPath{ kRootEndpoint, DiagnosticLogs::Id, DiagnosticLogs::Commands::RetrieveLogsRequest::Id }; - MockCommandHandler handler; + Testing::MockCommandHandler handler; diagnosticLogsCluster.HandleLogRequestForResponsePayload(&handler, kPath, DiagnosticLogs::IntentEnum::kEndUserSupport); - EXPECT_EQ(handler.GetResponse().commandId, DiagnosticLogs::Commands::RetrieveLogsResponse::Id); - auto decoded = DecodeRetrieveLogsResponse(handler.GetResponse()); + + // Verify we have exactly one response + EXPECT_EQ(handler.GetResponseCount(), static_cast(1)); + EXPECT_EQ(handler.GetResponseCommandId(), DiagnosticLogs::Commands::RetrieveLogsResponse::Id); + auto decoded = DecodeRetrieveLogsResponse(handler); EXPECT_EQ(decoded.status, DiagnosticLogs::StatusEnum::kNoLogs); } @@ -262,10 +195,13 @@ TEST_F(TestDiagnosticLogsCluster, ResponsePayload_ZeroBufferSize_NoLogs) diagnosticLogsCluster.SetDelegate(&delegate); const ConcreteCommandPath kPath{ kRootEndpoint, DiagnosticLogs::Id, DiagnosticLogs::Commands::RetrieveLogsRequest::Id }; - MockCommandHandler handler; + Testing::MockCommandHandler handler; diagnosticLogsCluster.HandleLogRequestForResponsePayload(&handler, kPath, DiagnosticLogs::IntentEnum::kEndUserSupport); - EXPECT_EQ(handler.GetResponse().commandId, DiagnosticLogs::Commands::RetrieveLogsResponse::Id); - auto decoded = DecodeRetrieveLogsResponse(handler.GetResponse()); + + // Verify we have exactly one response + EXPECT_EQ(handler.GetResponseCount(), static_cast(1)); + EXPECT_EQ(handler.GetResponseCommandId(), DiagnosticLogs::Commands::RetrieveLogsResponse::Id); + auto decoded = DecodeRetrieveLogsResponse(handler); EXPECT_EQ(decoded.status, DiagnosticLogs::StatusEnum::kNoLogs); } @@ -274,11 +210,14 @@ TEST_F(TestDiagnosticLogsCluster, Bdx_NoDelegate_NoLogs) DiagnosticLogsCluster diagnosticLogsCluster; const ConcreteCommandPath kPath{ kRootEndpoint, DiagnosticLogs::Id, DiagnosticLogs::Commands::RetrieveLogsRequest::Id }; - MockCommandHandler handler; + Testing::MockCommandHandler handler; diagnosticLogsCluster.HandleLogRequestForBdx(&handler, kPath, DiagnosticLogs::IntentEnum::kEndUserSupport, MakeOptional(CharSpan::fromCharString("enduser.log"))); - EXPECT_EQ(handler.GetResponse().commandId, DiagnosticLogs::Commands::RetrieveLogsResponse::Id); - auto decoded = DecodeRetrieveLogsResponse(handler.GetResponse()); + + // Verify we have exactly one response + EXPECT_EQ(handler.GetResponseCount(), static_cast(1)); + EXPECT_EQ(handler.GetResponseCommandId(), DiagnosticLogs::Commands::RetrieveLogsResponse::Id); + auto decoded = DecodeRetrieveLogsResponse(handler); EXPECT_EQ(decoded.status, DiagnosticLogs::StatusEnum::kNoLogs); } diff --git a/src/app/clusters/groupcast/tests/TestGroupcastCluster.cpp b/src/app/clusters/groupcast/tests/TestGroupcastCluster.cpp index 33dae75760f875..db62c3148a4bbb 100644 --- a/src/app/clusters/groupcast/tests/TestGroupcastCluster.cpp +++ b/src/app/clusters/groupcast/tests/TestGroupcastCluster.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -39,75 +40,6 @@ using namespace chip::app::Clusters::Groupcast; using chip::app::DataModel::AcceptedCommandEntry; using chip::app::DataModel::AttributeEntry; -class MockCommandHandler : public app::CommandHandler -{ -public: - ~MockCommandHandler() override {} - - struct ResponseRecord - { - app::ConcreteCommandPath path; - CommandId commandId; - System::PacketBufferHandle encodedData; - }; - - CHIP_ERROR FallibleAddStatus(const app::ConcreteCommandPath & aRequestCommandPath, - const Protocols::InteractionModel::ClusterStatusCode & aStatus, - const char * context = nullptr) override - { - return CHIP_NO_ERROR; - } - - void AddStatus(const app::ConcreteCommandPath & aRequestCommandPath, - const Protocols::InteractionModel::ClusterStatusCode & aStatus, const char * context = nullptr) override - { - CHIP_ERROR err = FallibleAddStatus(aRequestCommandPath, aStatus, context); - VerifyOrDie(err == CHIP_NO_ERROR); - } - - FabricIndex GetAccessingFabricIndex() const override { return mFabricIndex; } - - CHIP_ERROR AddResponseData(const app::ConcreteCommandPath & aRequestCommandPath, CommandId aResponseCommandId, - const app::DataModel::EncodableToTLV & aEncodable) override - { - System::PacketBufferHandle handle = MessagePacketBuffer::New(1024); - VerifyOrReturnError(!handle.IsNull(), CHIP_ERROR_NO_MEMORY); - TLV::TLVWriter baseWriter; - baseWriter.Init(handle->Start(), handle->MaxDataLength()); - app::DataModel::FabricAwareTLVWriter writer(baseWriter, mFabricIndex); - TLV::TLVType ct; - ReturnErrorOnFailure( - static_cast(writer).StartContainer(TLV::AnonymousTag(), TLV::kTLVType_Structure, ct)); - ReturnErrorOnFailure(aEncodable.EncodeTo(writer, TLV::ContextTag(app::CommandDataIB::Tag::kFields))); - ReturnErrorOnFailure(static_cast(writer).EndContainer(ct)); - handle->SetDataLength(static_cast(writer).GetLengthWritten()); - mResponse.path = aRequestCommandPath; - mResponse.commandId = aResponseCommandId; - mResponse.encodedData = std::move(handle); - return CHIP_NO_ERROR; - } - - void AddResponse(const app::ConcreteCommandPath & aRequestCommandPath, CommandId aResponseCommandId, - const app::DataModel::EncodableToTLV & aEncodable) override - { - (void) AddResponseData(aRequestCommandPath, aResponseCommandId, aEncodable); - } - - bool IsTimedInvoke() const override { return false; } - void FlushAcksRightAwayOnSlowCommand() override {} - Access::SubjectDescriptor GetSubjectDescriptor() const override { return Access::SubjectDescriptor{}; } - Messaging::ExchangeContext * GetExchangeContext() const override { return nullptr; } - - const ResponseRecord & GetResponse() const { return mResponse; } - - // Optional for test configuration - void SetFabricIndex(FabricIndex index) { mFabricIndex = index; } - -private: - ResponseRecord mResponse; - FabricIndex mFabricIndex = 0; -}; - // initialize memory as ReadOnlyBufferBuilder may allocate struct TestGroupcastCluster : public ::testing::Test { @@ -183,12 +115,13 @@ TEST_F(TestGroupcastCluster, TestJoinGroupCommand) cmdData.gracePeriod = MakeOptional(0U); cmdData.useAuxiliaryACL = MakeOptional(true); - MockCommandHandler cmdHandler; + chip::app::Testing::MockCommandHandler cmdHandler; chip::Test::ClusterTester tester(cluster); - auto result = tester.InvokeCommand(Commands::JoinGroup::Id, cmdData, &cmdHandler); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(result.value().GetStatusCode().GetStatus(), // NOLINT(bugprone-unchecked-optional-access) - Protocols::InteractionModel::Status::Failure); // Currently expect Failure as JoinGroup command returns - // CHIP_ERROR_NOT_IMPLEMENTED + auto result = + tester.Invoke(Commands::JoinGroup::Id, cmdData); + ASSERT_TRUE(result.status.has_value()); + EXPECT_EQ(result.status.value().GetStatusCode().GetStatus(), // NOLINT(bugprone-unchecked-optional-access) + Protocols::InteractionModel::Status::Failure); // Currently expect Failure as JoinGroup command returns + // CHIP_ERROR_NOT_IMPLEMENTED } } // namespace diff --git a/src/app/clusters/push-av-stream-transport-server/tests/BUILD.gn b/src/app/clusters/push-av-stream-transport-server/tests/BUILD.gn index 8577157d38a598..9d0fbd1f44102c 100644 --- a/src/app/clusters/push-av-stream-transport-server/tests/BUILD.gn +++ b/src/app/clusters/push-av-stream-transport-server/tests/BUILD.gn @@ -32,7 +32,7 @@ chip_test_suite("tests") { public_deps = [ "${chip_root}/src/app/clusters/push-av-stream-transport-server", - "${chip_root}/src/app/clusters/testing", + "${chip_root}/src/app/clusters/testing:testing", "${chip_root}/src/app/clusters/tls-client-management-server", "${chip_root}/src/app/tests:helpers", "${chip_root}/src/lib/core:string-builder-adapters", diff --git a/src/app/clusters/push-av-stream-transport-server/tests/TestPushAVStreamTransportCluster.cpp b/src/app/clusters/push-av-stream-transport-server/tests/TestPushAVStreamTransportCluster.cpp index ac8576809156ca..163b814a264b19 100644 --- a/src/app/clusters/push-av-stream-transport-server/tests/TestPushAVStreamTransportCluster.cpp +++ b/src/app/clusters/push-av-stream-transport-server/tests/TestPushAVStreamTransportCluster.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -40,33 +41,11 @@ namespace chip { namespace app { -class MockCommandHandler : public CommandHandler +class MockCommandHandler : public Testing::MockCommandHandler { public: ~MockCommandHandler() override {} - struct ResponseRecord - { - ConcreteCommandPath path; - CommandId commandId; - chip::System::PacketBufferHandle encodedData; - }; - - CHIP_ERROR FallibleAddStatus(const ConcreteCommandPath & aRequestCommandPath, - const Protocols::InteractionModel::ClusterStatusCode & aStatus, - const char * context = nullptr) override - { - mStatuses.push_back({ aRequestCommandPath, aStatus }); - return CHIP_NO_ERROR; - } - - void AddStatus(const ConcreteCommandPath & aRequestCommandPath, const Protocols::InteractionModel::ClusterStatusCode & aStatus, - const char * context = nullptr) override - { - CHIP_ERROR err = FallibleAddStatus(aRequestCommandPath, aStatus, context); - VerifyOrDie(err == CHIP_NO_ERROR); - } - CHIP_ERROR AddClusterSpecificSuccess(const ConcreteCommandPath & aRequestCommandPath, ClusterStatus aClusterStatus) override { return FallibleAddStatus(aRequestCommandPath, @@ -79,69 +58,19 @@ class MockCommandHandler : public CommandHandler Protocols::InteractionModel::ClusterStatusCode::ClusterSpecificFailure(aClusterStatus)); } - FabricIndex GetAccessingFabricIndex() const override { return mFabricIndex; } - - const std::vector & GetResponses() const { return mResponses; } - - CHIP_ERROR AddResponseData(const ConcreteCommandPath & aRequestCommandPath, CommandId aResponseCommandId, - const DataModel::EncodableToTLV & aEncodable) override - { - chip::System::PacketBufferHandle handle = chip::MessagePacketBuffer::New(1024); - VerifyOrReturnError(!handle.IsNull(), CHIP_ERROR_NO_MEMORY); - - TLV::TLVWriter baseWriter; - baseWriter.Init(handle->Start(), handle->MaxDataLength()); - - FabricIndex fabricIndex = 1; - - DataModel::FabricAwareTLVWriter writer(baseWriter, fabricIndex); - - TLV::TLVType containerType; - ReturnErrorOnFailure( - static_cast(writer).StartContainer(TLV::AnonymousTag(), TLV::kTLVType_Structure, containerType)); - ReturnErrorOnFailure(aEncodable.EncodeTo(writer, TLV::ContextTag(chip::app::CommandDataIB::Tag::kFields))); - ReturnErrorOnFailure(static_cast(writer).EndContainer(containerType)); - - handle->SetDataLength(static_cast(writer).GetLengthWritten()); - - mResponses.push_back({ aRequestCommandPath, aResponseCommandId, std::move(handle) }); - return CHIP_NO_ERROR; - } - - void AddResponse(const ConcreteCommandPath & aRequestCommandPath, CommandId aResponseCommandId, - const DataModel::EncodableToTLV & aEncodable) override - { - AddResponseData(aRequestCommandPath, aResponseCommandId, aEncodable); - } - + // Override base class methods for testing bool IsTimedInvoke() const override { return mIsTimedInvoke; } - void FlushAcksRightAwayOnSlowCommand() override { mAcksFlushed = true; } - - Access::SubjectDescriptor GetSubjectDescriptor() const override { return mSubjectDescriptor; } - Messaging::ExchangeContext * GetExchangeContext() const override { return mExchangeContext; } - // Optional for test configuration - void SetFabricIndex(FabricIndex index) { mFabricIndex = index; } + // Configuration methods for testing void SetTimedInvoke(bool isTimed) { mIsTimedInvoke = isTimed; } void SetExchangeContext(Messaging::ExchangeContext * context) { mExchangeContext = context; } private: - struct StatusRecord - { - ConcreteCommandPath path; - Protocols::InteractionModel::ClusterStatusCode status; - }; - - std::vector mResponses; - std::vector mStatuses; - - FabricIndex mFabricIndex = 0; bool mIsTimedInvoke = false; bool mAcksFlushed = false; Messaging::ExchangeContext * mExchangeContext = nullptr; - Access::SubjectDescriptor mSubjectDescriptor; }; static uint8_t gDebugEventBuffer[120]; diff --git a/src/app/clusters/testing/BUILD.gn b/src/app/clusters/testing/BUILD.gn index d210f474eccf52..4ae28934dbcf6b 100644 --- a/src/app/clusters/testing/BUILD.gn +++ b/src/app/clusters/testing/BUILD.gn @@ -21,15 +21,25 @@ source_set("testing") { "AttributeTesting.cpp", "AttributeTesting.h", "ClusterTester.h", + "MockCommandHandler.cpp", + "MockCommandHandler.h", "ValidateGlobalAttributes.h", ] public_deps = [ + "${chip_root}/src/app:command-handler", + "${chip_root}/src/app:paths", + "${chip_root}/src/app/MessageDef:MessageDef", + "${chip_root}/src/app/data-model:data-model", + "${chip_root}/src/app/data-model-provider:core", + "${chip_root}/src/app/data-model-provider:data-model-provider", "${chip_root}/src/app/data-model-provider:metadata", "${chip_root}/src/app/data-model-provider/tests:encode-decode", "${chip_root}/src/app/server-cluster:server-cluster", "${chip_root}/src/app/server-cluster/testing:testing", "${chip_root}/src/lib/core:string-builder-adapters", "${chip_root}/src/lib/support", + "${chip_root}/src/messaging:messaging", + "${chip_root}/src/protocols/interaction_model:interaction_model", ] } diff --git a/src/app/clusters/testing/ClusterTester.h b/src/app/clusters/testing/ClusterTester.h index dc3e2a5b668516..aad26c8bb403a9 100644 --- a/src/app/clusters/testing/ClusterTester.h +++ b/src/app/clusters/testing/ClusterTester.h @@ -19,17 +19,23 @@ #include #include #include +#include #include +#include #include #include #include +#include #include #include #include +#include #include #include #include +#include +#include #include namespace chip { @@ -120,32 +126,76 @@ class ClusterTester return mCluster.WriteAttribute(writeOperation.GetRequest(), decoder); } - // Invoke a command with `data` as arguments. - // The `data` parameter must be of the correct type for the command being invoked. - // Use `app::Clusters::::Commands::::Type` for the `data` parameter to be spec compliant (see the - // comment of the class for usage example). - // Will construct the command path using the first path returned by `GetPaths()` on the cluster. + // Result structure for Invoke operations, containing both status and decoded response. + template + struct InvokeResult + { + std::optional status; + std::optional response; + + // Returns true if the command was successful and response is available + bool IsSuccess() const + { + if constexpr (std::is_same_v) + return status.has_value() && status->IsSuccess(); + else + return status.has_value() && status->IsSuccess() && response.has_value(); + } + }; + + // Invoke a command and return the decoded result. + // The `RequestType`, `ResponseType` type-parameters must be of the correct type for the command being invoked. + // Use `app::Clusters::::Commands::::Type` for the `RequestType` type-parameter to be spec compliant + // Use `app::Clusters::::Commands::::Type::ResponseType` for the `ResponseType` type-parameter to be + // spec compliant Will construct the command path using the first path returned by `GetPaths()` on the cluster. // @returns `CHIP_ERROR_INCORRECT_STATE` if `GetPaths()` doesn't return a list with one path. - template - std::optional InvokeCommand(CommandId commandId, const T & data, - app::CommandHandler * handler) + template + [[nodiscard]] InvokeResult Invoke(chip::CommandId commandId, const RequestType & request) { - VerifyOrReturnError(VerifyClusterPathsValid(), CHIP_ERROR_INCORRECT_STATE); - auto path = mCluster.GetPaths()[0]; - const app::DataModel::InvokeRequest request = { .path = { path.mEndpointId, path.mClusterId, commandId } }; + InvokeResult result; + + const auto & paths = mCluster.GetPaths(); + VerifyOrReturnValue(paths.size() == 1u, result); + + mHandler.ClearResponses(); + mHandler.ClearStatuses(); - constexpr size_t kTlvBufferSize = 128; // Typically CommanderSender will use a TLV of size kMaxSecureSduLengthBytes. For - // now, just use 128 for the unit test. - uint8_t buffer[kTlvBufferSize]; - TLV::TLVWriter tlvWriter; - tlvWriter.Init(buffer); - ReturnErrorOnFailure(data.Encode(tlvWriter, TLV::AnonymousTag())); + const app::DataModel::InvokeRequest invokeRequest = { .path = { paths[0].mEndpointId, paths[0].mClusterId, commandId } }; - TLV::TLVReader tlvReader; - tlvReader.Init(buffer, tlvWriter.GetLengthWritten()); - ReturnErrorOnFailure(tlvReader.Next()); + TLV::TLVWriter writer; + writer.Init(mTlvBuffer); - return mCluster.InvokeCommand(request, tlvReader, handler); + TLV::TLVReader reader; + + VerifyOrReturnValue(request.Encode(writer, TLV::AnonymousTag()) == CHIP_NO_ERROR, result); + VerifyOrReturnValue(writer.Finalize() == CHIP_NO_ERROR, result); + + reader.Init(mTlvBuffer, writer.GetLengthWritten()); + VerifyOrReturnValue(reader.Next(TLV::kTLVType_Structure, TLV::AnonymousTag()) == CHIP_NO_ERROR, result); + + result.status = mCluster.InvokeCommand(invokeRequest, reader, &mHandler); + + // If command was successful and there's a response, decode it (skip for NullObjectType) + if constexpr (!std::is_same_v) + { + if (result.status.has_value() && result.status->IsSuccess() && mHandler.HasResponse()) + { + ResponseType decodedResponse; + CHIP_ERROR decodeError = mHandler.DecodeResponse(decodedResponse); + if (decodeError == CHIP_NO_ERROR) + { + result.response = std::move(decodedResponse); + } + else + { + // Decode failed; reflect error in status and log + result.status = app::DataModel::ActionReturnStatus(decodeError); + ChipLogError(Test, "DecodeResponse failed: %s", decodeError.AsString()); + } + } + } + + return result; } private: @@ -162,6 +212,15 @@ class ClusterTester TestServerClusterContext mTestServerClusterContext{}; app::ServerClusterInterface & mCluster; + + // Buffer size for TLV encoding/decoding of command payloads. + // 256 bytes was chosen as a conservative upper bound for typical command payloads in tests. + // All command payloads used in tests must fit within this buffer; tests with larger payloads will fail. + // If protocol or test requirements change, this value may need to be increased. + static constexpr size_t kTlvBufferSize = 256; + + app::Testing::MockCommandHandler mHandler; + uint8_t mTlvBuffer[kTlvBufferSize]; std::vector> mReadOperations; }; diff --git a/src/app/clusters/testing/MockCommandHandler.cpp b/src/app/clusters/testing/MockCommandHandler.cpp new file mode 100644 index 00000000000000..0c79b8aacaefc9 --- /dev/null +++ b/src/app/clusters/testing/MockCommandHandler.cpp @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2025 Project CHIP Authors + * + * 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. + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace chip { +namespace app { +namespace Testing { + +CHIP_ERROR MockCommandHandler::FallibleAddStatus(const ConcreteCommandPath & aRequestCommandPath, + const Protocols::InteractionModel::ClusterStatusCode & aStatus, + const char * context) +{ + mStatuses.emplace_back(StatusRecord{ aRequestCommandPath, aStatus, context }); + return CHIP_NO_ERROR; +} + +void MockCommandHandler::AddStatus(const ConcreteCommandPath & aRequestCommandPath, + const Protocols::InteractionModel::ClusterStatusCode & aStatus, const char * context) +{ + CHIP_ERROR err = FallibleAddStatus(aRequestCommandPath, aStatus, context); + VerifyOrDie(err == CHIP_NO_ERROR); +} + +CHIP_ERROR MockCommandHandler::AddResponseData(const ConcreteCommandPath & aRequestCommandPath, CommandId aResponseCommandId, + const DataModel::EncodableToTLV & aEncodable) +{ + chip::System::PacketBufferHandle handle = chip::MessagePacketBuffer::New(chip::kMaxAppMessageLen); + VerifyOrReturnError(!handle.IsNull(), CHIP_ERROR_NO_MEMORY); + + TLV::TLVWriter baseWriter; + baseWriter.Init(handle->Start(), handle->MaxDataLength()); + DataModel::FabricAwareTLVWriter writer(baseWriter, mFabricIndex); + TLV::TLVType ct; + + ReturnErrorOnFailure(writer.mTLVWriter.StartContainer(TLV::AnonymousTag(), TLV::kTLVType_Structure, ct)); + ReturnErrorOnFailure(aEncodable.EncodeTo(writer, TLV::ContextTag(app::CommandDataIB::Tag::kFields))); + ReturnErrorOnFailure(writer.mTLVWriter.EndContainer(ct)); + handle->SetDataLength(writer.mTLVWriter.GetLengthWritten()); + + mResponses.emplace_back(ResponseRecord{ aResponseCommandId, std::move(handle), aRequestCommandPath }); + return CHIP_NO_ERROR; +} + +void MockCommandHandler::AddResponse(const ConcreteCommandPath & aRequestCommandPath, CommandId aResponseCommandId, + const DataModel::EncodableToTLV & aEncodable) +{ + CHIP_ERROR err = AddResponseData(aRequestCommandPath, aResponseCommandId, aEncodable); + VerifyOrDie(err == CHIP_NO_ERROR); +} + +CHIP_ERROR MockCommandHandler::GetResponseReader(TLV::TLVReader & reader) const +{ + return GetResponseReader(reader, 0); +} + +CHIP_ERROR MockCommandHandler::GetResponseReader(TLV::TLVReader & reader, size_t index) const +{ + VerifyOrReturnError(!mResponses.empty(), CHIP_ERROR_INCORRECT_STATE); + VerifyOrReturnError(index < mResponses.size(), CHIP_ERROR_INVALID_ARGUMENT); + VerifyOrReturnError(!mResponses[index].encodedData.IsNull(), CHIP_ERROR_INCORRECT_STATE); + + reader.Init(mResponses[index].encodedData->Start(), mResponses[index].encodedData->DataLength()); + ReturnErrorOnFailure(reader.Next(TLV::kTLVType_Structure, TLV::AnonymousTag())); + + TLV::TLVType outerContainer; + ReturnErrorOnFailure(reader.EnterContainer(outerContainer)); + ReturnErrorOnFailure(reader.Next(TLV::kTLVType_Structure, TLV::ContextTag(app::CommandDataIB::Tag::kFields))); + + return CHIP_NO_ERROR; +} + +} // namespace Testing +} // namespace app +} // namespace chip diff --git a/src/app/clusters/testing/MockCommandHandler.h b/src/app/clusters/testing/MockCommandHandler.h new file mode 100644 index 00000000000000..b77319b769198b --- /dev/null +++ b/src/app/clusters/testing/MockCommandHandler.h @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2025 Project CHIP Authors + * + * 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. + */ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace chip { +namespace app { +namespace Testing { + +// Test-only fabric index value for test code. +constexpr FabricIndex kTestFabricIndex = static_cast(151); + +// Mock class that simulates CommandHandler behavior for unit testing, allowing capture and +// verification of responses and statuses without real network interactions. +class MockCommandHandler : public CommandHandler +{ +public: + struct ResponseRecord + { + CommandId commandId; + System::PacketBufferHandle encodedData; + ConcreteCommandPath path; + }; + + struct StatusRecord + { + ConcreteCommandPath path; + Protocols::InteractionModel::ClusterStatusCode status; + const char * context; + }; + + ~MockCommandHandler() override = default; + + CHIP_ERROR FallibleAddStatus(const ConcreteCommandPath & aRequestCommandPath, + const Protocols::InteractionModel::ClusterStatusCode & aStatus, + const char * context = nullptr) override; + + void AddStatus(const ConcreteCommandPath & aRequestCommandPath, const Protocols::InteractionModel::ClusterStatusCode & aStatus, + const char * context = nullptr) override; + + FabricIndex GetAccessingFabricIndex() const override { return mFabricIndex; } + + // Encodes and stores response data, returning error if encoding fails (fallible version for robust test handling). + CHIP_ERROR AddResponseData(const ConcreteCommandPath & aRequestCommandPath, CommandId aResponseCommandId, + const DataModel::EncodableToTLV & aEncodable) override; + + // Encodes and stores response data, without error return (non-fallible version that assumes successful encoding in tests). + void AddResponse(const ConcreteCommandPath & aRequestCommandPath, CommandId aResponseCommandId, + const DataModel::EncodableToTLV & aEncodable) override; + + bool IsTimedInvoke() const override { return false; } + void FlushAcksRightAwayOnSlowCommand() override {} + Access::SubjectDescriptor GetSubjectDescriptor() const override { return Access::SubjectDescriptor{}; } + Messaging::ExchangeContext * GetExchangeContext() const override { return nullptr; } + + // Helper methods to extract response data + bool HasResponse() const { return !mResponses.empty(); } + size_t GetResponseCount() const { return mResponses.size(); } + + // Methods for working with single response (first in array) + CommandId GetResponseCommandId() const { return mResponses.empty() ? 0 : mResponses[0].commandId; } + const ResponseRecord & GetResponse() const { return mResponses[0]; } + + // Methods for working with all responses + const std::vector & GetResponses() const { return mResponses; } + const ResponseRecord & GetResponse(size_t index) const { return mResponses[index]; } + void ClearResponses() { mResponses.clear(); } + + // Helper methods to access stored statuses + bool HasStatus() const { return !mStatuses.empty(); } + const std::vector & GetStatuses() const { return mStatuses; } + const StatusRecord & GetLastStatus() const { return mStatuses.back(); } + void ClearStatuses() { mStatuses.clear(); } + + // Get a TLV reader positioned at the response data fields (first response) + CHIP_ERROR GetResponseReader(TLV::TLVReader & reader) const; + + // Get a TLV reader positioned at the response data fields (specific response) + CHIP_ERROR GetResponseReader(TLV::TLVReader & reader, size_t index) const; + + // Decode response into a specific DecodableType (first response) + template + CHIP_ERROR DecodeResponse(ResponseType & response) const + { + TLV::TLVReader reader; + ReturnErrorOnFailure(GetResponseReader(reader)); + return response.Decode(reader); + } + + // Decode specific response into a specific DecodableType + template + CHIP_ERROR DecodeResponse(ResponseType & response, size_t index) const + { + TLV::TLVReader reader; + ReturnErrorOnFailure(GetResponseReader(reader, index)); + return response.Decode(reader); + } + + // Configuration methods + void SetFabricIndex(FabricIndex index) { mFabricIndex = index; } + +private: + std::vector mResponses; + std::vector mStatuses; + FabricIndex mFabricIndex = kTestFabricIndex; // Default to a clearly test-only fabric index. +}; + +} // namespace Testing +} // namespace app +} // namespace chip