CommandSender: For batch commands, track requests are responded to (#32105)
* CommandSender: For batch commands, track requests are responded to
* Rename
* Restyled by whitespace
* Restyled by clang-format
* Restyled by gn
* Self review updates
* Quick fix
* Quick fix
* Restyled by whitespace
* More fixes
* Fix CI
* Restyled by whitespace
* Restyled by clang-format
* Restyled by gn
* Apply suggestions from code review
Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>
* Address PR comments
* Fix CI and address some PR comments
* Restyled by clang-format
* Update src/app/CommandSender.cpp
Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>
* Update src/app/CommandSender.cpp
Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>
* Address PR comments
* Address comment
---------
Co-authored-by: Restyled.io <commits@restyled.io>
Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>
diff --git a/scripts/tools/check_includes_config.py b/scripts/tools/check_includes_config.py
index 42920c4..6943c26 100644
--- a/scripts/tools/check_includes_config.py
+++ b/scripts/tools/check_includes_config.py
@@ -161,6 +161,9 @@
'src/tracing/json/json_tracing.h': {'fstream', 'unordered_map'},
# Not intended for embedded clients
+ 'src/app/PendingResponseTrackerImpl.h': {'unordered_set'},
+
+ # Not intended for embedded clients
'src/lib/support/jsontlv/JsonToTlv.cpp': {'sstream'},
'src/lib/support/jsontlv/JsonToTlv.h': {'string'},
'src/lib/support/jsontlv/TlvToJson.h': {'string'},
diff --git a/src/app/BUILD.gn b/src/app/BUILD.gn
index 73a0433..c0f3049 100644
--- a/src/app/BUILD.gn
+++ b/src/app/BUILD.gn
@@ -231,6 +231,9 @@
"FailSafeContext.cpp",
"FailSafeContext.h",
"OTAUserConsentCommon.h",
+ "PendingResponseTracker.h",
+ "PendingResponseTrackerImpl.cpp",
+ "PendingResponseTrackerImpl.h",
"ReadHandler.cpp",
"SafeAttributePersistenceProvider.h",
"TimedRequest.cpp",
diff --git a/src/app/CommandSender.cpp b/src/app/CommandSender.cpp
index 7f77d6c..03ce278 100644
--- a/src/app/CommandSender.cpp
+++ b/src/app/CommandSender.cpp
@@ -75,6 +75,9 @@
mTimedRequest(aIsTimedRequest), mUseExtendableCallback(true)
{
assertChipStackLockedByCurrentThread();
+#if CHIP_CONFIG_COMMAND_SENDER_BUILTIN_SUPPORT_FOR_BATCHED_COMMANDS
+ mpPendingResponseTracker = &mNonTestPendingResponseTracker;
+#endif // CHIP_CONFIG_COMMAND_SENDER_BUILTIN_SUPPORT_FOR_BATCHED_COMMANDS
}
CommandSender::~CommandSender()
@@ -222,9 +225,9 @@
if (aPayloadHeader.HasMessageType(MsgType::InvokeCommandResponse))
{
+ mInvokeResponseMessageCount++;
err = ProcessInvokeResponse(std::move(aPayload), moreChunkedMessages);
SuccessOrExit(err);
- mInvokeResponseMessageCount++;
if (moreChunkedMessages)
{
StatusResponse::Send(Status::Success, apExchangeContext, /*aExpectResponse = */ true);
@@ -258,6 +261,10 @@
if (mState != State::AwaitingResponse)
{
+ if (err == CHIP_NO_ERROR)
+ {
+ FlushNoCommandResponse();
+ }
Close();
}
// Else we got a response to a Timed Request and just sent the invoke.
@@ -331,12 +338,25 @@
Close();
}
+void CommandSender::FlushNoCommandResponse()
+{
+ if (mpPendingResponseTracker && mUseExtendableCallback && mCallbackHandle.extendableCallback)
+ {
+ Optional<uint16_t> commandRef = mpPendingResponseTracker->PopPendingResponse();
+ while (commandRef.HasValue())
+ {
+ NoResponseData noResponseData = { commandRef.Value() };
+ mCallbackHandle.extendableCallback->OnNoResponse(this, noResponseData);
+ commandRef = mpPendingResponseTracker->PopPendingResponse();
+ }
+ }
+}
+
void CommandSender::Close()
{
mSuppressResponse = false;
mTimedRequest = false;
MoveToState(State::AwaitingDestruction);
-
OnDoneCallback();
}
@@ -350,10 +370,10 @@
StatusIB statusIB;
{
- bool commandRefExpected = (mFinishedCommandCount > 1);
- bool hasDataResponse = false;
+ bool hasDataResponse = false;
TLV::TLVReader commandDataReader;
Optional<uint16_t> commandRef;
+ bool commandRefExpected = mpPendingResponseTracker && (mpPendingResponseTracker->Count() > 1);
CommandStatusIB::Parser commandStatus;
err = aInvokeResponse.GetStatus(&commandStatus);
@@ -409,6 +429,27 @@
}
ReturnErrorOnFailure(err);
+ if (commandRef.HasValue() && mpPendingResponseTracker != nullptr)
+ {
+ err = mpPendingResponseTracker->Remove(commandRef.Value());
+ if (err != CHIP_NO_ERROR)
+ {
+ // This can happen for two reasons:
+ // 1. The current InvokeResponse is a duplicate (based on its commandRef).
+ // 2. The current InvokeResponse is for a request we never sent (based on its commandRef).
+ // Used when logging errors related to server violating spec.
+ [[maybe_unused]] ScopedNodeId remoteScopedNode;
+ if (mExchangeCtx.Get()->HasSessionHandle())
+ {
+ remoteScopedNode = mExchangeCtx.Get()->GetSessionHandle()->GetPeer();
+ }
+ ChipLogError(DataManagement,
+ "Received Unexpected Response from remote node " ChipLogFormatScopedNodeId ", commandRef=%u",
+ ChipLogValueScopedNodeId(remoteScopedNode), commandRef.Value());
+ return err;
+ }
+ }
+
// When using ExtendableCallbacks, we are adhering to a different API contract where path
// specific errors are sent to the OnResponse callback. For more information on the history
// of this issue please see https://github.com/project-chip/connectedhomeip/issues/30991
@@ -430,17 +471,19 @@
CHIP_ERROR CommandSender::SetCommandSenderConfig(CommandSender::ConfigParameters & aConfigParams)
{
-#if CHIP_CONFIG_SENDING_BATCH_COMMANDS_ENABLED
VerifyOrReturnError(mState == State::Idle, CHIP_ERROR_INCORRECT_STATE);
VerifyOrReturnError(aConfigParams.remoteMaxPathsPerInvoke > 0, CHIP_ERROR_INVALID_ARGUMENT);
+ if (mpPendingResponseTracker != nullptr)
+ {
- mRemoteMaxPathsPerInvoke = aConfigParams.remoteMaxPathsPerInvoke;
- mBatchCommandsEnabled = (aConfigParams.remoteMaxPathsPerInvoke > 1);
+ mRemoteMaxPathsPerInvoke = aConfigParams.remoteMaxPathsPerInvoke;
+ mBatchCommandsEnabled = (aConfigParams.remoteMaxPathsPerInvoke > 1);
+ }
+ else
+ {
+ VerifyOrReturnError(aConfigParams.remoteMaxPathsPerInvoke == 1, CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE);
+ }
return CHIP_NO_ERROR;
-#else
- VerifyOrReturnError(aConfigParams.remoteMaxPathsPerInvoke == 1, CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE);
- return CHIP_NO_ERROR;
-#endif
}
CHIP_ERROR CommandSender::PrepareCommand(const CommandPathParams & aCommandPathParams,
@@ -453,12 +496,19 @@
//
bool canAddAnotherCommand = (mState == State::AddedCommand && mBatchCommandsEnabled && mUseExtendableCallback);
VerifyOrReturnError(mState == State::Idle || canAddAnotherCommand, CHIP_ERROR_INCORRECT_STATE);
- VerifyOrReturnError(mFinishedCommandCount < mRemoteMaxPathsPerInvoke, CHIP_ERROR_MAXIMUM_PATHS_PER_INVOKE_EXCEEDED);
+
+ if (mpPendingResponseTracker != nullptr)
+ {
+ size_t pendingCount = mpPendingResponseTracker->Count();
+ VerifyOrReturnError(pendingCount < mRemoteMaxPathsPerInvoke, CHIP_ERROR_MAXIMUM_PATHS_PER_INVOKE_EXCEEDED);
+ }
if (mBatchCommandsEnabled)
{
+ VerifyOrReturnError(mpPendingResponseTracker != nullptr, CHIP_ERROR_INCORRECT_STATE);
VerifyOrReturnError(aPrepareCommandParams.commandRef.HasValue(), CHIP_ERROR_INVALID_ARGUMENT);
- VerifyOrReturnError(aPrepareCommandParams.commandRef.Value() == mFinishedCommandCount, CHIP_ERROR_INVALID_ARGUMENT);
+ uint16_t commandRef = aPrepareCommandParams.commandRef.Value();
+ VerifyOrReturnError(!mpPendingResponseTracker->IsTracked(commandRef), CHIP_ERROR_INVALID_ARGUMENT);
}
InvokeRequests::Builder & invokeRequests = mInvokeRequestBuilder.GetInvokeRequests();
@@ -482,8 +532,10 @@
{
if (mBatchCommandsEnabled)
{
+ VerifyOrReturnError(mpPendingResponseTracker != nullptr, CHIP_ERROR_INCORRECT_STATE);
VerifyOrReturnError(aFinishCommandParams.commandRef.HasValue(), CHIP_ERROR_INVALID_ARGUMENT);
- VerifyOrReturnError(aFinishCommandParams.commandRef.Value() == mFinishedCommandCount, CHIP_ERROR_INVALID_ARGUMENT);
+ uint16_t commandRef = aFinishCommandParams.commandRef.Value();
+ VerifyOrReturnError(!mpPendingResponseTracker->IsTracked(commandRef), CHIP_ERROR_INVALID_ARGUMENT);
}
return FinishCommandInternal(aFinishCommandParams);
@@ -511,7 +563,10 @@
MoveToState(State::AddedCommand);
- mFinishedCommandCount++;
+ if (mpPendingResponseTracker && aFinishCommandParams.commandRef.HasValue())
+ {
+ mpPendingResponseTracker->Add(aFinishCommandParams.commandRef.Value());
+ }
if (aFinishCommandParams.timedInvokeTimeoutMs.HasValue())
{
diff --git a/src/app/CommandSender.h b/src/app/CommandSender.h
index fa213ac..ef1a5f9 100644
--- a/src/app/CommandSender.h
+++ b/src/app/CommandSender.h
@@ -32,6 +32,7 @@
#include <app/MessageDef/InvokeRequestMessage.h>
#include <app/MessageDef/InvokeResponseMessage.h>
#include <app/MessageDef/StatusIB.h>
+#include <app/PendingResponseTrackerImpl.h>
#include <app/data-model/Encode.h>
#include <lib/core/CHIPCore.h>
#include <lib/core/Optional.h>
@@ -75,6 +76,16 @@
Optional<uint16_t> commandRef;
};
+ // CommandSender::ExtendableCallback::OnNoResponse is public SDK API, so we cannot break
+ // source compatibility for it. To allow for additional values to be added at a future
+ // time without constantly changing the function's declaration parameter list, we are
+ // defining the struct NoResponseData and adding that to the parameter list to allow for
+ // future extendability.
+ struct NoResponseData
+ {
+ uint16_t commandRef;
+ };
+
// CommandSender::ExtendableCallback::OnError is public SDK API, so we cannot break source
// compatibility for it. To allow for additional values to be added at a future time
// without constantly changing the function's declaration parameter list, we are
@@ -127,10 +138,22 @@
* @param[in] apCommandSender The command sender object that initiated the command transaction.
* @param[in] aResponseData Information pertaining to the response.
*/
- ;
virtual void OnResponse(CommandSender * commandSender, const ResponseData & aResponseData) {}
/**
+ * Called for each request that failed to receive a response after the server indicates completion of all requests.
+ *
+ * This callback may be omitted if clients have alternative ways to track non-responses.
+ *
+ * The CommandSender object MUST continue to exist after this call is completed. The application shall wait until it
+ * receives an OnDone call to destroy the object.
+ *
+ * @param apCommandSender The CommandSender object that initiated the transaction.
+ * @param aNoResponseData Details about the request without a response.
+ */
+ virtual void OnNoResponse(CommandSender * commandSender, const NoResponseData & aNoResponseData) {}
+
+ /**
* OnError will be called when a non-path-specific error occurs *after* a successful call to SendCommandRequest().
*
* The CommandSender object MUST continue to exist after this call is completed. The application shall wait until it
@@ -229,12 +252,6 @@
// early in PrepareCommand, even though it's not used until FinishCommand. This proactive
// validation helps prevent unnecessary writing an InvokeRequest into the packet that later
// needs to be undone.
- //
- // Currently, provided commandRefs for the first request must start at 0 and increment by one
- // for each subsequent request. This requirement can be relaxed in the future if a compelling
- // need arises.
- // TODO(#30453): After introducing Request/Response tracking, remove statement above about
- // this currently enforced requirement on commandRefs.
Optional<uint16_t> commandRef;
// If the InvokeRequest needs to be in a state with a started data TLV struct container
bool startDataStruct = false;
@@ -278,6 +295,10 @@
bool endDataStruct = false;
};
+ class TestOnlyMarker
+ {
+ };
+
/*
* Constructor.
*
@@ -287,12 +308,20 @@
*/
CommandSender(Callback * apCallback, Messaging::ExchangeManager * apExchangeMgr, bool aIsTimedRequest = false,
bool aSuppressResponse = false);
- CommandSender(ExtendableCallback * apCallback, Messaging::ExchangeManager * apExchangeMgr, bool aIsTimedRequest = false,
- bool aSuppressResponse = false);
CommandSender(std::nullptr_t, Messaging::ExchangeManager * apExchangeMgr, bool aIsTimedRequest = false,
bool aSuppressResponse = false) :
CommandSender(static_cast<Callback *>(nullptr), apExchangeMgr, aIsTimedRequest, aSuppressResponse)
{}
+ CommandSender(ExtendableCallback * apCallback, Messaging::ExchangeManager * apExchangeMgr, bool aIsTimedRequest = false,
+ bool aSuppressResponse = false);
+ // TODO(#32138): After there is a macro that is always defined for all unit tests, the constructor with
+ // TestOnlyMarker should only be compiled if that macro is defined.
+ CommandSender(TestOnlyMarker aTestMarker, ExtendableCallback * apCallback, Messaging::ExchangeManager * apExchangeMgr,
+ PendingResponseTracker * apPendingResponseTracker, bool aIsTimedRequest = false, bool aSuppressResponse = false) :
+ CommandSender(apCallback, apExchangeMgr, aIsTimedRequest, aSuppressResponse)
+ {
+ mpPendingResponseTracker = apPendingResponseTracker;
+ }
~CommandSender();
/**
@@ -307,11 +336,14 @@
* based on how many paths the remote peer claims to support.
*
* @return CHIP_ERROR_INCORRECT_STATE
- * If device has previously called `PrepareCommand`.
+ * If device has previously called `PrepareCommand`.
* @return CHIP_ERROR_INVALID_ARGUMENT
- * Invalid argument value.
+ * Invalid argument value.
* @return CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE
- * Device has not enabled CHIP_CONFIG_SENDING_BATCH_COMMANDS_ENABLED.
+ * Device has not enabled batch command support. To enable:
+ * 1. Enable the CHIP_CONFIG_COMMAND_SENDER_BUILTIN_SUPPORT_FOR_BATCHED_COMMANDS
+ * configuration option.
+ * 2. Ensure you provide ExtendableCallback.
*/
CHIP_ERROR SetCommandSenderConfig(ConfigParameters & aConfigParams);
@@ -497,6 +529,7 @@
System::PacketBufferHandle && aPayload) override;
void OnResponseTimeout(Messaging::ExchangeContext * apExchangeContext) override;
+ void FlushNoCommandResponse();
//
// Called internally to signal the completion of all work on this object, gracefully close the
// exchange (by calling into the base class) and finally, signal to the application that it's
@@ -580,8 +613,12 @@
chip::System::PacketBufferTLVWriter mCommandMessageWriter;
+#if CHIP_CONFIG_COMMAND_SENDER_BUILTIN_SUPPORT_FOR_BATCHED_COMMANDS
+ PendingResponseTrackerImpl mNonTestPendingResponseTracker;
+#endif // CHIP_CONFIG_COMMAND_SENDER_BUILTIN_SUPPORT_FOR_BATCHED_COMMANDS
+ PendingResponseTracker * mpPendingResponseTracker = nullptr;
+
uint16_t mInvokeResponseMessageCount = 0;
- uint16_t mFinishedCommandCount = 0;
uint16_t mRemoteMaxPathsPerInvoke = 1;
State mState = State::Idle;
diff --git a/src/app/PendingResponseTracker.h b/src/app/PendingResponseTracker.h
new file mode 100644
index 0000000..47bb37d
--- /dev/null
+++ b/src/app/PendingResponseTracker.h
@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) 2024 Project CHIP Authors
+ * All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <lib/core/CHIPError.h>
+#include <lib/core/Optional.h>
+
+namespace chip {
+namespace app {
+
+/**
+ * @brief Interface for tracking responses to outbound InvokeRequests.
+ *
+ * This interface enables clients to:
+ * * Verify that received responses correspond to issued InvokeRequests.
+ * * Detect outstanding responses after the server indicates completion, helpful for identifying response omissions.
+ */
+class PendingResponseTracker
+{
+public:
+ virtual ~PendingResponseTracker() = default;
+
+ /**
+ * Start tracking the given `aCommandRef`
+ *
+ * @return CHIP_ERROR_INVALID_ARGUMENT if `aCommandRef` is already being tracked.
+ */
+ virtual CHIP_ERROR Add(uint16_t aCommandRef) = 0;
+
+ /**
+ * Removes tracking for the given `aCommandRef`
+ *
+ * @return CHIP_ERROR_KEY_NOT_FOUND if aCommandRef is not currently tracked.
+ */
+ virtual CHIP_ERROR Remove(uint16_t aCommandRef) = 0;
+
+ /**
+ * Checks if the given `aCommandRef` is being tracked.
+ */
+ virtual bool IsTracked(uint16_t aCommandRef) = 0;
+
+ /**
+ * Returns the number of pending responses.
+ */
+ virtual size_t Count() = 0;
+
+ /**
+ * Removes a pending response command reference from the tracker.
+ *
+ * Deletes an element from the tracker (order not guaranteed). This function can be called
+ * repeatedly to remove all tracked pending responses.
+ *
+ * @return NullOptional if the tracker is empty.
+ * @return Optional containing the CommandReference of a removed pending response.
+ */
+ virtual Optional<uint16_t> PopPendingResponse() = 0;
+};
+
+} // namespace app
+} // namespace chip
diff --git a/src/app/PendingResponseTrackerImpl.cpp b/src/app/PendingResponseTrackerImpl.cpp
new file mode 100644
index 0000000..72f5fb8
--- /dev/null
+++ b/src/app/PendingResponseTrackerImpl.cpp
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2024 Project CHIP Authors
+ * All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <app/PendingResponseTrackerImpl.h>
+
+#include <lib/support/CodeUtils.h>
+
+namespace chip {
+namespace app {
+
+CHIP_ERROR PendingResponseTrackerImpl::Add(uint16_t aCommandRef)
+{
+ VerifyOrReturnError(!IsTracked(aCommandRef), CHIP_ERROR_INVALID_ARGUMENT);
+ mCommandReferenceSet.insert(aCommandRef);
+ return CHIP_NO_ERROR;
+}
+
+CHIP_ERROR PendingResponseTrackerImpl::Remove(uint16_t aCommandRef)
+{
+ VerifyOrReturnError(IsTracked(aCommandRef), CHIP_ERROR_KEY_NOT_FOUND);
+ mCommandReferenceSet.erase(aCommandRef);
+ return CHIP_NO_ERROR;
+}
+
+bool PendingResponseTrackerImpl::IsTracked(uint16_t aCommandRef)
+{
+ return mCommandReferenceSet.find(aCommandRef) != mCommandReferenceSet.end();
+}
+
+size_t PendingResponseTrackerImpl::Count()
+{
+ return mCommandReferenceSet.size();
+}
+
+Optional<uint16_t> PendingResponseTrackerImpl::PopPendingResponse()
+{
+ if (Count() == 0)
+ {
+ return NullOptional;
+ }
+ uint16_t commandRef = *mCommandReferenceSet.begin();
+ mCommandReferenceSet.erase(mCommandReferenceSet.begin());
+ return MakeOptional(commandRef);
+}
+
+} // namespace app
+} // namespace chip
diff --git a/src/app/PendingResponseTrackerImpl.h b/src/app/PendingResponseTrackerImpl.h
new file mode 100644
index 0000000..8d6ade2
--- /dev/null
+++ b/src/app/PendingResponseTrackerImpl.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2024 Project CHIP Authors
+ * All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <unordered_set>
+
+#include <app/PendingResponseTracker.h>
+
+namespace chip {
+namespace app {
+
+/**
+ * @brief An implementation of PendingResponseTracker.
+ */
+class PendingResponseTrackerImpl : public PendingResponseTracker
+{
+public:
+ CHIP_ERROR Add(uint16_t aCommandRef) override;
+ CHIP_ERROR Remove(uint16_t aCommandRef) override;
+ bool IsTracked(uint16_t aCommandRef) override;
+ size_t Count() override;
+ Optional<uint16_t> PopPendingResponse() override;
+
+private:
+ std::unordered_set<uint16_t> mCommandReferenceSet;
+};
+
+} // namespace app
+} // namespace chip
diff --git a/src/app/tests/BUILD.gn b/src/app/tests/BUILD.gn
index 4dad961..be78dd8 100644
--- a/src/app/tests/BUILD.gn
+++ b/src/app/tests/BUILD.gn
@@ -147,6 +147,7 @@
"TestNumericAttributeTraits.cpp",
"TestOperationalStateClusterObjects.cpp",
"TestPendingNotificationMap.cpp",
+ "TestPendingResponseTrackerImpl.cpp",
"TestPowerSourceCluster.cpp",
"TestReadInteraction.cpp",
"TestReportingEngine.cpp",
diff --git a/src/app/tests/TestCommandInteraction.cpp b/src/app/tests/TestCommandInteraction.cpp
index d67b389..6aab66e 100644
--- a/src/app/tests/TestCommandInteraction.cpp
+++ b/src/app/tests/TestCommandInteraction.cpp
@@ -85,7 +85,8 @@
constexpr CommandId kTestCommandIdFillResponseMessage = 7;
constexpr CommandId kTestNonExistCommandId = 0;
-const app::CommandHandler::TestOnlyMarker kThisIsForTestOnly;
+const app::CommandHandler::TestOnlyMarker kCommandHandlerTestOnlyMarker;
+const app::CommandSender::TestOnlyMarker kCommandSenderTestOnlyMarker;
} // namespace
namespace app {
@@ -328,7 +329,8 @@
static void TestCommandSenderLegacyCallbackUnsupportedCommand(nlTestSuite * apSuite, void * apContext);
static void TestCommandSenderExtendableCallbackUnsupportedCommand(nlTestSuite * apSuite, void * apContext);
static void TestCommandSenderLegacyCallbackBuildingBatchCommandFails(nlTestSuite * apSuite, void * apContext);
- static void TestCommandSenderExtendableCallbackBuildingBatchCommandFails(nlTestSuite * apSuite, void * apContext);
+ static void TestCommandSenderExtendableCallbackBuildingBatchDuplicateCommandRefFails(nlTestSuite * apSuite, void * apContext);
+ static void TestCommandSenderExtendableCallbackBuildingBatchCommandSuccess(nlTestSuite * apSuite, void * apContext);
static void TestCommandSenderCommandSuccessResponseFlow(nlTestSuite * apSuite, void * apContext);
static void TestCommandSenderCommandAsyncSuccessResponseFlow(nlTestSuite * apSuite, void * apContext);
static void TestCommandSenderCommandFailureResponseFlow(nlTestSuite * apSuite, void * apContext);
@@ -1271,8 +1273,12 @@
prepareCommandParams.SetStartDataStruct(true).SetCommandRef(0);
finishCommandParams.SetEndDataStruct(true).SetCommandRef(0);
- commandSender.mBatchCommandsEnabled = true;
- commandSender.mRemoteMaxPathsPerInvoke = 2;
+ CommandSender::ConfigParameters config;
+ config.SetRemoteMaxPathsPerInvoke(2);
+ err = commandSender.SetCommandSenderConfig(config);
+ NL_TEST_ASSERT(apSuite, err == CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE);
+ // Even though we got an error saying invalid argument we are going to attempt
+ // to add two commands.
auto commandPathParams = MakeTestCommandPath();
err = commandSender.PrepareCommand(commandPathParams, prepareCommandParams);
@@ -1291,20 +1297,25 @@
NL_TEST_ASSERT(apSuite, ctx.GetExchangeManager().GetNumActiveExchanges() == 0);
}
-void TestCommandInteraction::TestCommandSenderExtendableCallbackBuildingBatchCommandFails(nlTestSuite * apSuite, void * apContext)
+void TestCommandInteraction::TestCommandSenderExtendableCallbackBuildingBatchDuplicateCommandRefFails(nlTestSuite * apSuite,
+ void * apContext)
{
TestContext & ctx = *static_cast<TestContext *>(apContext);
CHIP_ERROR err = CHIP_NO_ERROR;
mockCommandSenderExtendedDelegate.ResetCounter();
- app::CommandSender commandSender(&mockCommandSenderExtendedDelegate, &ctx.GetExchangeManager());
+ PendingResponseTrackerImpl pendingResponseTracker;
+ app::CommandSender commandSender(kCommandSenderTestOnlyMarker, &mockCommandSenderExtendedDelegate, &ctx.GetExchangeManager(),
+ &pendingResponseTracker);
app::CommandSender::PrepareCommandParameters prepareCommandParams;
app::CommandSender::FinishCommandParameters finishCommandParams;
+
+ CommandSender::ConfigParameters config;
+ config.SetRemoteMaxPathsPerInvoke(2);
+ err = commandSender.SetCommandSenderConfig(config);
+ NL_TEST_ASSERT(apSuite, err == CHIP_NO_ERROR);
+
prepareCommandParams.SetStartDataStruct(true).SetCommandRef(0);
finishCommandParams.SetEndDataStruct(true).SetCommandRef(0);
-
- commandSender.mBatchCommandsEnabled = true;
- commandSender.mRemoteMaxPathsPerInvoke = 2;
-
auto commandPathParams = MakeTestCommandPath();
err = commandSender.PrepareCommand(commandPathParams, prepareCommandParams);
NL_TEST_ASSERT(apSuite, err == CHIP_NO_ERROR);
@@ -1314,8 +1325,47 @@
err = commandSender.FinishCommand(finishCommandParams);
NL_TEST_ASSERT(apSuite, err == CHIP_NO_ERROR);
// Preparing second command.
- prepareCommandParams.SetCommandRef(1);
- finishCommandParams.SetCommandRef(1);
+ prepareCommandParams.SetCommandRef(0);
+ err = commandSender.PrepareCommand(commandPathParams, prepareCommandParams);
+ NL_TEST_ASSERT(apSuite, err == CHIP_ERROR_INVALID_ARGUMENT);
+
+ NL_TEST_ASSERT(apSuite, GetNumActiveHandlerObjects() == 0);
+ NL_TEST_ASSERT(apSuite, ctx.GetExchangeManager().GetNumActiveExchanges() == 0);
+}
+
+void TestCommandInteraction::TestCommandSenderExtendableCallbackBuildingBatchCommandSuccess(nlTestSuite * apSuite, void * apContext)
+{
+ TestContext & ctx = *static_cast<TestContext *>(apContext);
+ CHIP_ERROR err = CHIP_NO_ERROR;
+ mockCommandSenderExtendedDelegate.ResetCounter();
+ PendingResponseTrackerImpl pendingResponseTracker;
+ app::CommandSender commandSender(kCommandSenderTestOnlyMarker, &mockCommandSenderExtendedDelegate, &ctx.GetExchangeManager(),
+ &pendingResponseTracker);
+ app::CommandSender::PrepareCommandParameters prepareCommandParams;
+ app::CommandSender::FinishCommandParameters finishCommandParams;
+
+ CommandSender::ConfigParameters config;
+ config.SetRemoteMaxPathsPerInvoke(2);
+ err = commandSender.SetCommandSenderConfig(config);
+ NL_TEST_ASSERT(apSuite, err == CHIP_NO_ERROR);
+
+ // The specific values chosen here are arbitrary. This test primarily verifies that we can
+ // use a larger command reference value followed by a smaller one for subsequent command.
+ uint16_t firstCommandRef = 40;
+ uint16_t secondCommandRef = 2;
+ prepareCommandParams.SetStartDataStruct(true).SetCommandRef(firstCommandRef);
+ finishCommandParams.SetEndDataStruct(true).SetCommandRef(firstCommandRef);
+ auto commandPathParams = MakeTestCommandPath();
+ err = commandSender.PrepareCommand(commandPathParams, prepareCommandParams);
+ NL_TEST_ASSERT(apSuite, err == CHIP_NO_ERROR);
+ chip::TLV::TLVWriter * writer = commandSender.GetCommandDataIBTLVWriter();
+ err = writer->PutBoolean(chip::TLV::ContextTag(1), true);
+ NL_TEST_ASSERT(apSuite, err == CHIP_NO_ERROR);
+ err = commandSender.FinishCommand(finishCommandParams);
+ NL_TEST_ASSERT(apSuite, err == CHIP_NO_ERROR);
+ // Preparing second command.
+ prepareCommandParams.SetCommandRef(secondCommandRef);
+ finishCommandParams.SetCommandRef(secondCommandRef);
err = commandSender.PrepareCommand(commandPathParams, prepareCommandParams);
NL_TEST_ASSERT(apSuite, err == CHIP_NO_ERROR);
writer = commandSender.GetCommandDataIBTLVWriter();
@@ -1650,7 +1700,7 @@
}
BasicCommandPathRegistry<4> mBasicCommandPathRegistry;
- CommandHandler commandHandler(kThisIsForTestOnly, &mockCommandHandlerDelegate, &mBasicCommandPathRegistry);
+ CommandHandler commandHandler(kCommandHandlerTestOnlyMarker, &mockCommandHandlerDelegate, &mBasicCommandPathRegistry);
TestExchangeDelegate delegate;
auto exchange = ctx.NewExchangeToAlice(&delegate, false);
commandHandler.mResponseSender.SetExchangeContext(exchange);
@@ -1724,7 +1774,7 @@
}
BasicCommandPathRegistry<4> mBasicCommandPathRegistry;
- CommandHandler commandHandler(kThisIsForTestOnly, &mockCommandHandlerDelegate, &mBasicCommandPathRegistry);
+ CommandHandler commandHandler(kCommandHandlerTestOnlyMarker, &mockCommandHandlerDelegate, &mBasicCommandPathRegistry);
TestExchangeDelegate delegate;
auto exchange = ctx.NewExchangeToAlice(&delegate, false);
commandHandler.mResponseSender.SetExchangeContext(exchange);
@@ -1756,7 +1806,7 @@
nlTestSuite * apSuite, void * apContext)
{
BasicCommandPathRegistry<4> mBasicCommandPathRegistry;
- CommandHandler commandHandler(kThisIsForTestOnly, &mockCommandHandlerDelegate, &mBasicCommandPathRegistry);
+ CommandHandler commandHandler(kCommandHandlerTestOnlyMarker, &mockCommandHandlerDelegate, &mBasicCommandPathRegistry);
commandHandler.mReserveSpaceForMoreChunkMessages = true;
ConcreteCommandPath requestCommandPath1 = { kTestEndpointId, kTestClusterId, kTestCommandIdFillResponseMessage };
ConcreteCommandPath requestCommandPath2 = { kTestEndpointId, kTestClusterId, kTestCommandIdCommandSpecificResponse };
@@ -1782,7 +1832,7 @@
nlTestSuite * apSuite, void * apContext)
{
BasicCommandPathRegistry<4> mBasicCommandPathRegistry;
- CommandHandler commandHandler(kThisIsForTestOnly, &mockCommandHandlerDelegate, &mBasicCommandPathRegistry);
+ CommandHandler commandHandler(kCommandHandlerTestOnlyMarker, &mockCommandHandlerDelegate, &mBasicCommandPathRegistry);
commandHandler.mReserveSpaceForMoreChunkMessages = true;
ConcreteCommandPath requestCommandPath1 = { kTestEndpointId, kTestClusterId, kTestCommandIdFillResponseMessage };
ConcreteCommandPath requestCommandPath2 = { kTestEndpointId, kTestClusterId, kTestCommandIdCommandSpecificResponse };
@@ -1808,7 +1858,7 @@
void * apContext)
{
BasicCommandPathRegistry<4> mBasicCommandPathRegistry;
- CommandHandler commandHandler(kThisIsForTestOnly, &mockCommandHandlerDelegate, &mBasicCommandPathRegistry);
+ CommandHandler commandHandler(kCommandHandlerTestOnlyMarker, &mockCommandHandlerDelegate, &mBasicCommandPathRegistry);
commandHandler.mReserveSpaceForMoreChunkMessages = true;
ConcreteCommandPath requestCommandPath1 = { kTestEndpointId, kTestClusterId, kTestCommandIdFillResponseMessage };
ConcreteCommandPath requestCommandPath2 = { kTestEndpointId, kTestClusterId, kTestCommandIdCommandSpecificResponse };
@@ -1902,7 +1952,8 @@
NL_TEST_DEF("TestCommandSenderLegacyCallbackUnsupportedCommand", chip::app::TestCommandInteraction::TestCommandSenderLegacyCallbackUnsupportedCommand),
NL_TEST_DEF("TestCommandSenderExtendableCallbackUnsupportedCommand", chip::app::TestCommandInteraction::TestCommandSenderExtendableCallbackUnsupportedCommand),
NL_TEST_DEF("TestCommandSenderLegacyCallbackBuildingBatchCommandFails", chip::app::TestCommandInteraction::TestCommandSenderLegacyCallbackBuildingBatchCommandFails),
- NL_TEST_DEF("TestCommandSenderExtendableCallbackBuildingBatchCommandFails", chip::app::TestCommandInteraction::TestCommandSenderExtendableCallbackBuildingBatchCommandFails),
+ NL_TEST_DEF("TestCommandSenderExtendableCallbackBuildingBatchDuplicateCommandRefFails", chip::app::TestCommandInteraction::TestCommandSenderExtendableCallbackBuildingBatchDuplicateCommandRefFails),
+ NL_TEST_DEF("TestCommandSenderExtendableCallbackBuildingBatchCommandSuccess", chip::app::TestCommandInteraction::TestCommandSenderExtendableCallbackBuildingBatchCommandSuccess),
NL_TEST_DEF("TestCommandSenderCommandSuccessResponseFlow", chip::app::TestCommandInteraction::TestCommandSenderCommandSuccessResponseFlow),
NL_TEST_DEF("TestCommandSenderCommandAsyncSuccessResponseFlow", chip::app::TestCommandInteraction::TestCommandSenderCommandAsyncSuccessResponseFlow),
NL_TEST_DEF("TestCommandSenderCommandSpecificResponseFlow", chip::app::TestCommandInteraction::TestCommandSenderCommandSpecificResponseFlow),
diff --git a/src/app/tests/TestPendingResponseTrackerImpl.cpp b/src/app/tests/TestPendingResponseTrackerImpl.cpp
new file mode 100644
index 0000000..da62394
--- /dev/null
+++ b/src/app/tests/TestPendingResponseTrackerImpl.cpp
@@ -0,0 +1,135 @@
+/*
+ * Copyright (c) 2024 Project CHIP Authors
+ * All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <app/PendingResponseTrackerImpl.h>
+#include <lib/support/UnitTestRegistration.h>
+
+#include <algorithm>
+#include <nlunit-test.h>
+#include <vector>
+
+namespace {
+
+using namespace chip;
+
+void TestPendingResponseTracker_FillEntireTracker(nlTestSuite * inSuite, void * inContext)
+{
+ chip::app::PendingResponseTrackerImpl pendingResponseTracker;
+ for (uint16_t commandRef = 0; commandRef < std::numeric_limits<uint16_t>::max(); commandRef++)
+ {
+ NL_TEST_ASSERT(inSuite, false == pendingResponseTracker.IsTracked(commandRef));
+ NL_TEST_ASSERT(inSuite, CHIP_NO_ERROR == pendingResponseTracker.Add(commandRef));
+ NL_TEST_ASSERT(inSuite, true == pendingResponseTracker.IsTracked(commandRef));
+ }
+
+ NL_TEST_ASSERT(inSuite, std::numeric_limits<uint16_t>::max() == pendingResponseTracker.Count());
+
+ for (uint16_t commandRef = 0; commandRef < std::numeric_limits<uint16_t>::max(); commandRef++)
+ {
+ NL_TEST_ASSERT(inSuite, CHIP_NO_ERROR == pendingResponseTracker.Remove(commandRef));
+ NL_TEST_ASSERT(inSuite, false == pendingResponseTracker.IsTracked(commandRef));
+ }
+ NL_TEST_ASSERT(inSuite, 0 == pendingResponseTracker.Count());
+}
+
+void TestPendingResponseTracker_FillSingleEntryInTracker(nlTestSuite * inSuite, void * inContext)
+{
+ chip::app::PendingResponseTrackerImpl pendingResponseTracker;
+
+ // The value 40 is arbitrary; any value would work for this purpose.
+ uint16_t commandRefToSet = 40;
+ NL_TEST_ASSERT(inSuite, CHIP_NO_ERROR == pendingResponseTracker.Add(commandRefToSet));
+
+ for (uint16_t commandRef = 0; commandRef < std::numeric_limits<uint16_t>::max(); commandRef++)
+ {
+ bool expectedIsSetResult = (commandRef == commandRefToSet);
+ NL_TEST_ASSERT(inSuite, expectedIsSetResult == pendingResponseTracker.IsTracked(commandRef));
+ }
+}
+
+void TestPendingResponseTracker_RemoveNonExistentEntryInTrackerFails(nlTestSuite * inSuite, void * inContext)
+{
+ chip::app::PendingResponseTrackerImpl pendingResponseTracker;
+
+ // The value 40 is arbitrary; any value would work for this purpose.
+ uint16_t commandRef = 40;
+ NL_TEST_ASSERT(inSuite, false == pendingResponseTracker.IsTracked(commandRef));
+ NL_TEST_ASSERT(inSuite, CHIP_ERROR_KEY_NOT_FOUND == pendingResponseTracker.Remove(commandRef));
+}
+
+void TestPendingResponseTracker_AddingSecondEntryFails(nlTestSuite * inSuite, void * inContext)
+{
+ chip::app::PendingResponseTrackerImpl pendingResponseTracker;
+
+ // The value 40 is arbitrary; any value would work for this purpose.
+ uint16_t commandRef = 40;
+ NL_TEST_ASSERT(inSuite, false == pendingResponseTracker.IsTracked(commandRef));
+ NL_TEST_ASSERT(inSuite, CHIP_NO_ERROR == pendingResponseTracker.Add(commandRef));
+ NL_TEST_ASSERT(inSuite, true == pendingResponseTracker.IsTracked(commandRef));
+ NL_TEST_ASSERT(inSuite, CHIP_ERROR_INVALID_ARGUMENT == pendingResponseTracker.Add(commandRef));
+}
+
+void TestPendingResponseTracker_PopFindsAllPendingRequests(nlTestSuite * inSuite, void * inContext)
+{
+ chip::app::PendingResponseTrackerImpl pendingResponseTracker;
+
+ // The specific values in requestsToAdd are not significant; they are chosen arbitrarily for testing purposes.
+ std::vector<uint16_t> requestsToAdd = { 0, 50, 2, 2000 };
+ for (const uint16_t & commandRef : requestsToAdd)
+ {
+ NL_TEST_ASSERT(inSuite, false == pendingResponseTracker.IsTracked(commandRef));
+ NL_TEST_ASSERT(inSuite, CHIP_NO_ERROR == pendingResponseTracker.Add(commandRef));
+ NL_TEST_ASSERT(inSuite, true == pendingResponseTracker.IsTracked(commandRef));
+ }
+
+ NL_TEST_ASSERT(inSuite, requestsToAdd.size() == pendingResponseTracker.Count());
+
+ for (size_t i = 0; i < requestsToAdd.size(); i++)
+ {
+ auto commandRef = pendingResponseTracker.PopPendingResponse();
+ NL_TEST_ASSERT(inSuite, true == commandRef.HasValue());
+ bool expectedCommandRef = std::find(requestsToAdd.begin(), requestsToAdd.end(), commandRef.Value()) != requestsToAdd.end();
+ NL_TEST_ASSERT(inSuite, true == expectedCommandRef);
+ }
+ NL_TEST_ASSERT(inSuite, 0 == pendingResponseTracker.Count());
+ auto commandRef = pendingResponseTracker.PopPendingResponse();
+ NL_TEST_ASSERT(inSuite, false == commandRef.HasValue());
+}
+
+} // namespace
+
+#define NL_TEST_DEF_FN(fn) NL_TEST_DEF("Test " #fn, fn)
+/**
+ * Test Suite. It lists all the test functions.
+ */
+static const nlTest sTests[] = { NL_TEST_DEF_FN(TestPendingResponseTracker_FillEntireTracker),
+ NL_TEST_DEF_FN(TestPendingResponseTracker_FillSingleEntryInTracker),
+ NL_TEST_DEF_FN(TestPendingResponseTracker_RemoveNonExistentEntryInTrackerFails),
+ NL_TEST_DEF_FN(TestPendingResponseTracker_AddingSecondEntryFails),
+ NL_TEST_DEF_FN(TestPendingResponseTracker_PopFindsAllPendingRequests),
+ NL_TEST_SENTINEL() };
+
+int TestPendingResponseTracker()
+{
+ nlTestSuite theSuite = { "CHIP PendingResponseTrackerImpl tests", &sTests[0], nullptr, nullptr };
+
+ // Run test suite against one context.
+ nlTestRunner(&theSuite, nullptr);
+ return nlTestRunnerStats(&theSuite);
+}
+
+CHIP_REGISTER_TEST_SUITE(TestPendingResponseTracker)
diff --git a/src/lib/core/BUILD.gn b/src/lib/core/BUILD.gn
index 2761137..9163caf 100644
--- a/src/lib/core/BUILD.gn
+++ b/src/lib/core/BUILD.gn
@@ -68,7 +68,7 @@
"CHIP_CONFIG_BIG_ENDIAN_TARGET=${chip_target_is_big_endian}",
"CHIP_CONFIG_TLV_VALIDATE_CHAR_STRING_ON_WRITE=${chip_tlv_validate_char_string_on_write}",
"CHIP_CONFIG_TLV_VALIDATE_CHAR_STRING_ON_READ=${chip_tlv_validate_char_string_on_read}",
- "CHIP_CONFIG_SENDING_BATCH_COMMANDS_ENABLED=${chip_enable_sending_batch_commands}",
+ "CHIP_CONFIG_COMMAND_SENDER_BUILTIN_SUPPORT_FOR_BATCHED_COMMANDS=${chip_enable_sending_batch_commands}",
]
visibility = [ ":chip_config_header" ]
diff --git a/src/lib/core/CHIPConfig.h b/src/lib/core/CHIPConfig.h
index 09131ae..2de6a66 100644
--- a/src/lib/core/CHIPConfig.h
+++ b/src/lib/core/CHIPConfig.h
@@ -1703,12 +1703,15 @@
#endif
/**
- * @def CHIP_CONFIG_SENDING_BATCH_COMMANDS_ENABLED
+ * @def CHIP_CONFIG_COMMAND_SENDER_BUILTIN_SUPPORT_FOR_BATCHED_COMMANDS
*
- * @brief Device supports sending multiple batch commands in a single Invoke Request Message.
+ * @brief CommandSender will use built-in support to enable sending batch commands in a single Invoke Request Message.
+ *
+ * **Important:** This feature is code and RAM intensive. Enable only on platforms where these
+ * resources are not constrained.
*/
-#ifndef CHIP_CONFIG_SENDING_BATCH_COMMANDS_ENABLED
-#define CHIP_CONFIG_SENDING_BATCH_COMMANDS_ENABLED 0
+#ifndef CHIP_CONFIG_COMMAND_SENDER_BUILTIN_SUPPORT_FOR_BATCHED_COMMANDS
+#define CHIP_CONFIG_COMMAND_SENDER_BUILTIN_SUPPORT_FOR_BATCHED_COMMANDS 0
#endif
/**