Skip to content

Commit 5fbe16d

Browse files
tehampsonrestyled-commits
andauthoredApr 3, 2024
Refactor CommandHandler to be more unittestable (#32467)
* Refactor CommandHandler * Restyle and quick fix * Self review * Add tests * Restyled by whitespace * Restyled by clang-format * Resolve conflict * Restyled by whitespace * Fix CI * Rename CommandResponder to CommandResponseSender for easier reviewing * Restyled by clang-format * Address PR comments * Restyled by whitespace * Restyled by clang-format * Address PR comment * Restyled by clang-format * Address PR comments * Restyled by clang-format * nit fix * Address PR comments --------- Co-authored-by: Restyled.io <commits@restyled.io>
1 parent c172e30 commit 5fbe16d

10 files changed

+707
-463
lines changed
 

‎.github/workflows/lint.yml

+1
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ jobs:
9494
--known-failure app/AttributeAccessToken.h \
9595
--known-failure app/CommandHandler.h \
9696
--known-failure app/CommandHandlerInterface.h \
97+
--known-failure app/CommandResponseSender.h \
9798
--known-failure app/CommandSenderLegacyCallback.h \
9899
--known-failure app/ReadHandler.h \
99100
--known-failure app/reporting/reporting.cpp \

‎src/app/BUILD.gn

+2-1
Original file line numberDiff line numberDiff line change
@@ -281,9 +281,9 @@ static_library("app") {
281281
"ChunkedWriteCallback.cpp",
282282
"ChunkedWriteCallback.h",
283283
"CommandHandler.cpp",
284+
"CommandHandlerExchangeInterface.h",
284285
"CommandResponseHelper.h",
285286
"CommandResponseSender.cpp",
286-
"CommandResponseSender.h",
287287
"DefaultAttributePersistenceProvider.cpp",
288288
"DefaultAttributePersistenceProvider.h",
289289
"DeferredAttributePersistenceProvider.cpp",
@@ -304,6 +304,7 @@ static_library("app") {
304304
# TODO: the following items cannot be included due to interaction-model circularity
305305
# (app depending on im and im including these headers):
306306
# Name with _ so that linter does not recognize it
307+
# "CommandResponseSender._h"
307308
# "CommandHandler._h"
308309
# "ReadHandler._h",
309310
# "WriteHandler._h"

‎src/app/CommandHandler.cpp

+63-101
Large diffs are not rendered by default.

‎src/app/CommandHandler.h

+102-82
Large diffs are not rendered by default.
+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
* Copyright (c) 2024 Project CHIP Authors
3+
* All rights reserved.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
#pragma once
19+
20+
#include <access/SubjectDescriptor.h>
21+
#include <lib/core/DataModelTypes.h>
22+
#include <lib/core/GroupId.h>
23+
#include <lib/core/Optional.h>
24+
#include <messaging/ExchangeContext.h>
25+
#include <protocols/interaction_model/StatusCode.h>
26+
#include <system/SystemPacketBuffer.h>
27+
28+
namespace chip {
29+
namespace app {
30+
31+
/**
32+
* Interface for sending InvokeResponseMessage(s).
33+
*
34+
* Provides information about the associated exchange context.
35+
*
36+
* Design Rationale: This interface enhances unit testability and allows applications to
37+
* customize InvokeResponse behavior. For example, a bridge application might locally execute
38+
* a command using cluster APIs without intending to sending a response on an exchange.
39+
* These cluster APIs require providing an instance of CommandHandler where a status response
40+
* is added (see https://github.com/project-chip/connectedhomeip/issues/32030).
41+
*/
42+
class CommandHandlerExchangeInterface
43+
{
44+
public:
45+
virtual ~CommandHandlerExchangeInterface() = default;
46+
47+
/**
48+
* Get a non-owning pointer to the exchange context the InvokeRequestMessage was
49+
* delivered on.
50+
*
51+
* @return The exchange context. Might be nullptr if no exchange context has been
52+
* assigned or the context has been released. For example, the exchange
53+
* context might not be assigned in unit tests, or if an application wishes
54+
* to locally execute cluster APIs and still receive response data without
55+
* sending it on an exchange.
56+
*/
57+
virtual Messaging::ExchangeContext * GetExchangeContext() const = 0;
58+
59+
// TODO(#30453): Follow up refactor. It should be possible to remove
60+
// GetSubjectDescriptor and GetAccessingFabricIndex, as CommandHandler can get these
61+
// values from ExchangeContext.
62+
63+
/**
64+
* Gets subject descriptor of the exchange.
65+
*
66+
* WARNING: This method should only be called when the caller is certain the
67+
* session has not been evicted.
68+
*/
69+
virtual Access::SubjectDescriptor GetSubjectDescriptor() const = 0;
70+
71+
/**
72+
* Gets accessing fabic index of the exchange.
73+
*
74+
* WARNING: This method should only be called when the caller is certain the
75+
* session has not been evicted.
76+
*/
77+
virtual FabricIndex GetAccessingFabricIndex() const = 0;
78+
/**
79+
* If session for the exchange is a group session, returns its group ID. Otherwise,
80+
* returns a null optional.
81+
*/
82+
virtual Optional<GroupId> GetGroupId() const = 0;
83+
84+
/**
85+
* @brief Called to indicate a slow command is being processed.
86+
*
87+
* Enables the exchange to send whatever transport-level acks might be needed without waiting
88+
* for command processing to complete.
89+
*/
90+
virtual void HandlingSlowCommand() = 0;
91+
92+
/**
93+
* @brief Adds a completed InvokeResponseMessage to the queue for sending to requester.
94+
*
95+
* Called by CommandHandler.
96+
*/
97+
virtual void AddInvokeResponseToSend(System::PacketBufferHandle && aPacket) = 0;
98+
99+
/**
100+
* @brief Called to indicate that an InvokeResponse was dropped.
101+
*
102+
* Called by CommandHandler to relay this information to the requester.
103+
*/
104+
virtual void ResponseDropped() = 0;
105+
};
106+
107+
} // namespace app
108+
} // namespace chip

‎src/app/CommandResponseSender.cpp

+95-15
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,10 @@ CHIP_ERROR CommandResponseSender::OnMessageReceived(Messaging::ExchangeContext *
3838
err = statusError;
3939
VerifyOrExit(err == CHIP_NO_ERROR, failureStatusToSend.SetValue(Status::InvalidAction));
4040

41-
// If SendCommandResponse() fails, we are responsible for closing the exchange,
42-
// as stipulated by the API contract. We fulfill this obligation by not sending
43-
// a message expecting a response on the exchange. Since we are in the middle
44-
// of processing an incoming message, the exchange will close itself once we are
45-
// done processing it, if there is no response to wait for at that point.
4641
err = SendCommandResponse();
42+
// If SendCommandResponse() fails, we must close the exchange. We signal the failure to the
43+
// requester with a StatusResponse ('Failure'). Since we're in the middle of processing an
44+
// incoming message, we close the exchange by indicating that we don't expect a further response.
4745
VerifyOrExit(err == CHIP_NO_ERROR, failureStatusToSend.SetValue(Status::Failure));
4846

4947
bool moreToSend = !mChunks.IsNull();
@@ -81,13 +79,28 @@ void CommandResponseSender::OnResponseTimeout(Messaging::ExchangeContext * apExc
8179
Close();
8280
}
8381

84-
CHIP_ERROR CommandResponseSender::StartSendingCommandResponses()
82+
void CommandResponseSender::StartSendingCommandResponses()
8583
{
86-
VerifyOrReturnError(mState == State::ReadyForInvokeResponses, CHIP_ERROR_INCORRECT_STATE);
87-
// If SendCommandResponse() fails, we are obligated to close the exchange as per the API
88-
// contract. However, this method's contract also stipulates that in the event of our
89-
// failure, the caller bears the responsibility of closing the exchange.
90-
ReturnErrorOnFailure(SendCommandResponse());
84+
VerifyOrDie(mState == State::ReadyForInvokeResponses);
85+
CHIP_ERROR err = SendCommandResponse();
86+
if (err != CHIP_NO_ERROR)
87+
{
88+
ChipLogError(DataManagement, "Failed to send InvokeResponseMessage");
89+
// TODO(#30453): It should be our responsibility to send a Failure StatusResponse to the requestor
90+
// if there is a SessionHandle, but legacy unit tests explicitly check the behavior where
91+
// we do not send any message. Changing this behavior should be done in a standalone
92+
// PR where only that specific change is made. Here is a possible solution that could
93+
// be used that fulfills our responsibility to send a Failure StatusResponse. This causes unit
94+
// tests to start failing.
95+
// ```
96+
// if (mExchangeCtx && mExchangeCtx->HasSessionHandle())
97+
// {
98+
// SendStatusResponse(Status::Failure);
99+
// }
100+
// ```
101+
Close();
102+
return;
103+
}
91104

92105
if (HasMoreToSend())
93106
{
@@ -98,7 +111,31 @@ CHIP_ERROR CommandResponseSender::StartSendingCommandResponses()
98111
{
99112
Close();
100113
}
101-
return CHIP_NO_ERROR;
114+
}
115+
116+
void CommandResponseSender::OnDone(CommandHandler & apCommandObj)
117+
{
118+
if (mState == State::ErrorSentDelayCloseUntilOnDone)
119+
{
120+
// We have already sent a message to the client indicating that we are not expecting
121+
// a response.
122+
Close();
123+
return;
124+
}
125+
StartSendingCommandResponses();
126+
}
127+
128+
void CommandResponseSender::DispatchCommand(CommandHandler & apCommandObj, const ConcreteCommandPath & aCommandPath,
129+
TLV::TLVReader & apPayload)
130+
{
131+
VerifyOrReturn(mpCommandHandlerCallback);
132+
mpCommandHandlerCallback->DispatchCommand(apCommandObj, aCommandPath, apPayload);
133+
}
134+
135+
Status CommandResponseSender::CommandExists(const ConcreteCommandPath & aCommandPath)
136+
{
137+
VerifyOrReturnValue(mpCommandHandlerCallback, Protocols::InteractionModel::Status::UnsupportedCommand);
138+
return mpCommandHandlerCallback->CommandExists(aCommandPath);
102139
}
103140

104141
CHIP_ERROR CommandResponseSender::SendCommandResponse()
@@ -139,6 +176,9 @@ const char * CommandResponseSender::GetStateStr() const
139176

140177
case State::AllInvokeResponsesSent:
141178
return "AllInvokeResponsesSent";
179+
180+
case State::ErrorSentDelayCloseUntilOnDone:
181+
return "ErrorSentDelayCloseUntilOnDone";
142182
}
143183
#endif // CHIP_DETAIL_LOGGING
144184
return "N/A";
@@ -157,12 +197,52 @@ void CommandResponseSender::MoveToState(const State aTargetState)
157197
void CommandResponseSender::Close()
158198
{
159199
MoveToState(State::AllInvokeResponsesSent);
160-
mCloseCalled = true;
161-
if (mResponseSenderDoneCallback)
200+
mpCallback->OnDone(*this);
201+
}
202+
203+
void CommandResponseSender::OnInvokeCommandRequest(Messaging::ExchangeContext * ec, System::PacketBufferHandle && payload,
204+
bool isTimedInvoke)
205+
{
206+
VerifyOrDieWithMsg(ec != nullptr, DataManagement, "Incoming exchange context should not be null");
207+
VerifyOrDieWithMsg(mState == State::ReadyForInvokeResponses, DataManagement, "state should be ReadyForInvokeResponses");
208+
209+
// NOTE: we already know this is an InvokeRequestMessage because we explicitly registered with the
210+
// Exchange Manager for unsolicited InvokeRequestMessages.
211+
mExchangeCtx.Grab(ec);
212+
mExchangeCtx->WillSendMessage();
213+
214+
// Grabbing Handle to prevent mCommandHandler from calling OnDone before OnInvokeCommandRequest returns.
215+
// This allows us to send a StatusResponse error instead of any potentially queued up InvokeResponseMessages.
216+
CommandHandler::Handle workHandle(&mCommandHandler);
217+
Status status = mCommandHandler.OnInvokeCommandRequest(*this, std::move(payload), isTimedInvoke);
218+
if (status != Status::Success)
162219
{
163-
mResponseSenderDoneCallback->mCall(mResponseSenderDoneCallback->mContext);
220+
VerifyOrDie(mState == State::ReadyForInvokeResponses);
221+
SendStatusResponse(status);
222+
// The API contract of OnInvokeCommandRequest requires the CommandResponder instance to outlive
223+
// the CommandHandler. Therefore, we cannot safely call Close() here, even though we have
224+
// finished sending data. Closing must be deferred until the CommandHandler::OnDone callback.
225+
MoveToState(State::ErrorSentDelayCloseUntilOnDone);
164226
}
165227
}
166228

229+
#if CHIP_WITH_NLFAULTINJECTION
230+
231+
void CommandResponseSender::TestOnlyInvokeCommandRequestWithFaultsInjected(Messaging::ExchangeContext * ec,
232+
System::PacketBufferHandle && payload,
233+
bool isTimedInvoke,
234+
CommandHandler::NlFaultInjectionType faultType)
235+
{
236+
VerifyOrDieWithMsg(ec != nullptr, DataManagement, "TH Failure: Incoming exchange context should not be null");
237+
VerifyOrDieWithMsg(mState == State::ReadyForInvokeResponses, DataManagement,
238+
"TH Failure: state should be ReadyForInvokeResponses, issue with TH");
239+
240+
mExchangeCtx.Grab(ec);
241+
mExchangeCtx->WillSendMessage();
242+
243+
mCommandHandler.TestOnlyInvokeCommandRequestWithFaultsInjected(*this, std::move(payload), isTimedInvoke, faultType);
244+
}
245+
#endif // CHIP_WITH_NLFAULTINJECTION
246+
167247
} // namespace app
168248
} // namespace chip

0 commit comments

Comments
 (0)