blob: 5714cc0f9877a7e29da83e39705f3a966728008c [file] [log] [blame]
/*
*
* Copyright (c) 2021 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.
*/
/**
* @file
* This file implements unit tests for CHIP Interaction Model Command Interaction
*
*/
#include "app/data-model/NullObject.h"
#include <app-common/zap-generated/cluster-objects.h>
#include <app/AppConfig.h>
#include <app/InteractionModelEngine.h>
#include <app/tests/AppTestContext.h>
#include <controller/InvokeInteraction.h>
#include <lib/core/CHIPCore.h>
#include <lib/core/TLV.h>
#include <lib/core/TLVUtilities.h>
#include <lib/support/ErrorStr.h>
#include <lib/support/UnitTestContext.h>
#include <lib/support/UnitTestRegistration.h>
#include <lib/support/logging/CHIPLogging.h>
#include <messaging/tests/MessagingContext.h>
#include <nlunit-test.h>
#include <protocols/interaction_model/Constants.h>
using TestContext = chip::Test::AppContext;
using namespace chip;
using namespace chip::app;
using namespace chip::app::Clusters;
using namespace chip::Protocols;
namespace {
chip::ClusterStatus kTestSuccessClusterStatus = 1;
chip::ClusterStatus kTestFailureClusterStatus = 2;
constexpr EndpointId kTestEndpointId = 1;
enum ResponseDirective
{
kSendDataResponse,
kSendSuccessStatusCode,
kSendMultipleSuccessStatusCodes,
kSendError,
kSendMultipleErrors,
kSendSuccessStatusCodeWithClusterStatus,
kSendErrorWithClusterStatus,
kAsync,
};
ResponseDirective responseDirective;
CommandHandler::Handle asyncHandle;
} // namespace
namespace chip {
namespace app {
void DispatchSingleClusterCommand(const ConcreteCommandPath & aCommandPath, chip::TLV::TLVReader & aReader,
CommandHandler * apCommandObj)
{
ChipLogDetail(Controller, "Received Cluster Command: Endpoint=%x Cluster=" ChipLogFormatMEI " Command=" ChipLogFormatMEI,
aCommandPath.mEndpointId, ChipLogValueMEI(aCommandPath.mClusterId), ChipLogValueMEI(aCommandPath.mCommandId));
if (aCommandPath.mClusterId == Clusters::UnitTesting::Id &&
aCommandPath.mCommandId == Clusters::UnitTesting::Commands::TestSimpleArgumentRequest::Type::GetCommandId())
{
Clusters::UnitTesting::Commands::TestSimpleArgumentRequest::DecodableType dataRequest;
if (DataModel::Decode(aReader, dataRequest) != CHIP_NO_ERROR)
{
ChipLogError(Controller, "Unable to decode the request");
apCommandObj->AddStatus(aCommandPath, Protocols::InteractionModel::Status::Failure);
return;
}
if (responseDirective == kSendDataResponse)
{
Clusters::UnitTesting::Commands::TestStructArrayArgumentResponse::Type dataResponse;
Clusters::UnitTesting::Structs::NestedStructList::Type nestedStructList[4];
uint8_t i = 0;
for (auto & item : nestedStructList)
{
item.a = i;
item.b = false;
item.c.a = i;
item.c.b = true;
i++;
}
dataResponse.arg1 = nestedStructList;
dataResponse.arg6 = true;
apCommandObj->AddResponse(aCommandPath, dataResponse);
}
else if (responseDirective == kSendSuccessStatusCode)
{
apCommandObj->AddStatus(aCommandPath, Protocols::InteractionModel::Status::Success);
}
else if (responseDirective == kSendMultipleSuccessStatusCodes)
{
// TODO: Right now all but the first AddStatus call fail, so this
// test is not really testing what it should.
for (size_t i = 0; i < 4; ++i)
{
apCommandObj->AddStatus(aCommandPath, Protocols::InteractionModel::Status::Success);
}
// And one failure on the end.
apCommandObj->AddStatus(aCommandPath, Protocols::InteractionModel::Status::Failure);
}
else if (responseDirective == kSendError)
{
apCommandObj->AddStatus(aCommandPath, Protocols::InteractionModel::Status::Failure);
}
else if (responseDirective == kSendMultipleErrors)
{
// TODO: Right now all but the first AddStatus call fail, so this
// test is not really testing what it should.
for (size_t i = 0; i < 4; ++i)
{
apCommandObj->AddStatus(aCommandPath, Protocols::InteractionModel::Status::Failure);
}
}
else if (responseDirective == kSendSuccessStatusCodeWithClusterStatus)
{
apCommandObj->AddClusterSpecificSuccess(aCommandPath, kTestSuccessClusterStatus);
}
else if (responseDirective == kSendErrorWithClusterStatus)
{
apCommandObj->AddClusterSpecificFailure(aCommandPath, kTestFailureClusterStatus);
}
else if (responseDirective == kAsync)
{
asyncHandle = apCommandObj;
}
}
}
InteractionModel::Status ServerClusterCommandExists(const ConcreteCommandPath & aCommandPath)
{
// Mock cluster catalog, only support commands on one cluster on one endpoint.
using InteractionModel::Status;
if (aCommandPath.mEndpointId != kTestEndpointId)
{
return Status::UnsupportedEndpoint;
}
if (aCommandPath.mClusterId != Clusters::UnitTesting::Id)
{
return Status::UnsupportedCluster;
}
return Status::Success;
}
} // namespace app
} // namespace chip
namespace {
class TestCommandInteraction
{
public:
TestCommandInteraction() {}
static void TestDataResponse(nlTestSuite * apSuite, void * apContext);
static void TestSuccessNoDataResponse(nlTestSuite * apSuite, void * apContext);
static void TestMultipleSuccessNoDataResponses(nlTestSuite * apSuite, void * apContext);
static void TestAsyncResponse(nlTestSuite * apSuite, void * apContext);
static void TestFailure(nlTestSuite * apSuite, void * apContext);
static void TestMultipleFailures(nlTestSuite * apSuite, void * apContext);
static void TestSuccessNoDataResponseWithClusterStatus(nlTestSuite * apSuite, void * apContext);
static void TestFailureWithClusterStatus(nlTestSuite * apSuite, void * apContext);
private:
};
void TestCommandInteraction::TestDataResponse(nlTestSuite * apSuite, void * apContext)
{
TestContext & ctx = *static_cast<TestContext *>(apContext);
// We want to send a TestSimpleArgumentRequest::Type, but get a
// TestStructArrayArgumentResponse in return, so need to shadow the actual
// ResponseType that TestSimpleArgumentRequest has.
struct FakeRequest : public Clusters::UnitTesting::Commands::TestSimpleArgumentRequest::Type
{
using ResponseType = Clusters::UnitTesting::Commands::TestStructArrayArgumentResponse::DecodableType;
};
FakeRequest request;
auto sessionHandle = ctx.GetSessionBobToAlice();
bool onSuccessWasCalled = false;
bool onFailureWasCalled = false;
request.arg1 = true;
// Passing of stack variables by reference is only safe because of synchronous completion of the interaction. Otherwise, it's
// not safe to do so.
auto onSuccessCb = [apSuite, &onSuccessWasCalled](const app::ConcreteCommandPath & commandPath, const app::StatusIB & aStatus,
const auto & dataResponse) {
uint8_t i = 0;
auto iter = dataResponse.arg1.begin();
while (iter.Next())
{
auto & item = iter.GetValue();
NL_TEST_ASSERT(apSuite, item.a == i);
NL_TEST_ASSERT(apSuite, item.b == false);
NL_TEST_ASSERT(apSuite, item.c.a == i);
NL_TEST_ASSERT(apSuite, item.c.b == true);
i++;
}
NL_TEST_ASSERT(apSuite, iter.GetStatus() == CHIP_NO_ERROR);
NL_TEST_ASSERT(apSuite, dataResponse.arg6 == true);
onSuccessWasCalled = true;
};
// Passing of stack variables by reference is only safe because of synchronous completion of the interaction. Otherwise, it's
// not safe to do so.
auto onFailureCb = [&onFailureWasCalled](CHIP_ERROR aError) { onFailureWasCalled = true; };
responseDirective = kSendDataResponse;
chip::Controller::InvokeCommandRequest(&ctx.GetExchangeManager(), sessionHandle, kTestEndpointId, request, onSuccessCb,
onFailureCb);
ctx.DrainAndServiceIO();
NL_TEST_ASSERT(apSuite, onSuccessWasCalled && !onFailureWasCalled);
NL_TEST_ASSERT(apSuite, ctx.GetExchangeManager().GetNumActiveExchanges() == 0);
}
void TestCommandInteraction::TestSuccessNoDataResponse(nlTestSuite * apSuite, void * apContext)
{
struct FakeRequest : public Clusters::UnitTesting::Commands::TestSimpleArgumentRequest::Type
{
using ResponseType = DataModel::NullObjectType;
};
TestContext & ctx = *static_cast<TestContext *>(apContext);
FakeRequest request;
auto sessionHandle = ctx.GetSessionBobToAlice();
bool onSuccessWasCalled = false;
bool onFailureWasCalled = false;
bool statusCheck = false;
request.arg1 = true;
// Passing of stack variables by reference is only safe because of synchronous completion of the interaction. Otherwise, it's
// not safe to do so.
auto onSuccessCb = [&onSuccessWasCalled, &statusCheck](const app::ConcreteCommandPath & commandPath,
const app::StatusIB & aStatus, const auto & dataResponse) {
statusCheck = (aStatus.mStatus == Protocols::InteractionModel::Status::Success);
onSuccessWasCalled = true;
};
// Passing of stack variables by reference is only safe because of synchronous completion of the interaction. Otherwise, it's
// not safe to do so.
auto onFailureCb = [&onFailureWasCalled](CHIP_ERROR aError) { onFailureWasCalled = true; };
responseDirective = kSendSuccessStatusCode;
chip::Controller::InvokeCommandRequest(&ctx.GetExchangeManager(), sessionHandle, kTestEndpointId, request, onSuccessCb,
onFailureCb);
ctx.DrainAndServiceIO();
NL_TEST_ASSERT(apSuite, onSuccessWasCalled && !onFailureWasCalled && statusCheck);
NL_TEST_ASSERT(apSuite, ctx.GetExchangeManager().GetNumActiveExchanges() == 0);
}
void TestCommandInteraction::TestMultipleSuccessNoDataResponses(nlTestSuite * apSuite, void * apContext)
{
struct FakeRequest : public Clusters::UnitTesting::Commands::TestSimpleArgumentRequest::Type
{
using ResponseType = DataModel::NullObjectType;
};
TestContext & ctx = *static_cast<TestContext *>(apContext);
FakeRequest request;
auto sessionHandle = ctx.GetSessionBobToAlice();
size_t successCalls = 0;
size_t failureCalls = 0;
bool statusCheck = false;
request.arg1 = true;
// Passing of stack variables by reference is only safe because of synchronous completion of the interaction. Otherwise, it's
// not safe to do so.
auto onSuccessCb = [&successCalls, &statusCheck](const ConcreteCommandPath & commandPath, const StatusIB & aStatus,
const auto & dataResponse) {
statusCheck = (aStatus.mStatus == InteractionModel::Status::Success);
++successCalls;
};
// Passing of stack variables by reference is only safe because of synchronous completion of the interaction. Otherwise, it's
// not safe to do so.
auto onFailureCb = [&failureCalls](CHIP_ERROR aError) { ++failureCalls; };
responseDirective = kSendMultipleSuccessStatusCodes;
Controller::InvokeCommandRequest(&ctx.GetExchangeManager(), sessionHandle, kTestEndpointId, request, onSuccessCb, onFailureCb);
ctx.DrainAndServiceIO();
NL_TEST_ASSERT(apSuite, successCalls == 1 && statusCheck);
NL_TEST_ASSERT(apSuite, failureCalls == 0);
NL_TEST_ASSERT(apSuite, ctx.GetExchangeManager().GetNumActiveExchanges() == 0);
}
void TestCommandInteraction::TestAsyncResponse(nlTestSuite * apSuite, void * apContext)
{
struct FakeRequest : public Clusters::UnitTesting::Commands::TestSimpleArgumentRequest::Type
{
using ResponseType = DataModel::NullObjectType;
};
TestContext & ctx = *static_cast<TestContext *>(apContext);
FakeRequest request;
auto sessionHandle = ctx.GetSessionBobToAlice();
bool onSuccessWasCalled = false;
bool onFailureWasCalled = false;
bool statusCheck = false;
request.arg1 = true;
// Passing of stack variables by reference is only safe because of synchronous completion of the interaction. Otherwise, it's
// not safe to do so.
auto onSuccessCb = [&onSuccessWasCalled, &statusCheck](const app::ConcreteCommandPath & commandPath,
const app::StatusIB & aStatus, const auto & dataResponse) {
statusCheck = (aStatus.mStatus == Protocols::InteractionModel::Status::Success);
onSuccessWasCalled = true;
};
// Passing of stack variables by reference is only safe because of synchronous completion of the interaction. Otherwise, it's
// not safe to do so.
auto onFailureCb = [&onFailureWasCalled](CHIP_ERROR aError) { onFailureWasCalled = true; };
responseDirective = kAsync;
chip::Controller::InvokeCommandRequest(&ctx.GetExchangeManager(), sessionHandle, kTestEndpointId, request, onSuccessCb,
onFailureCb);
ctx.DrainAndServiceIO();
NL_TEST_ASSERT(apSuite, !onSuccessWasCalled && !onFailureWasCalled && !statusCheck);
NL_TEST_ASSERT(apSuite, ctx.GetExchangeManager().GetNumActiveExchanges() == 2);
CommandHandler * commandHandle = asyncHandle.Get();
NL_TEST_ASSERT(apSuite, commandHandle != nullptr);
if (commandHandle == nullptr)
{
return;
}
commandHandle->AddStatus(ConcreteCommandPath(kTestEndpointId, request.GetClusterId(), request.GetCommandId()),
Protocols::InteractionModel::Status::Success);
asyncHandle.Release();
ctx.DrainAndServiceIO();
NL_TEST_ASSERT(apSuite, onSuccessWasCalled && !onFailureWasCalled && statusCheck);
NL_TEST_ASSERT(apSuite, ctx.GetExchangeManager().GetNumActiveExchanges() == 0);
}
void TestCommandInteraction::TestFailure(nlTestSuite * apSuite, void * apContext)
{
TestContext & ctx = *static_cast<TestContext *>(apContext);
Clusters::UnitTesting::Commands::TestSimpleArgumentRequest::Type request;
auto sessionHandle = ctx.GetSessionBobToAlice();
bool onSuccessWasCalled = false;
bool onFailureWasCalled = false;
bool statusCheck = false;
request.arg1 = true;
// Passing of stack variables by reference is only safe because of synchronous completion of the interaction. Otherwise, it's
// not safe to do so.
auto onSuccessCb = [&onSuccessWasCalled](const app::ConcreteCommandPath & commandPath, const app::StatusIB & aStatus,
const auto & dataResponse) { onSuccessWasCalled = true; };
// Passing of stack variables by reference is only safe because of synchronous completion of the interaction. Otherwise, it's
// not safe to do so.
auto onFailureCb = [&onFailureWasCalled, &statusCheck](CHIP_ERROR aError) {
statusCheck = aError.IsIMStatus() && app::StatusIB(aError).mStatus == Protocols::InteractionModel::Status::Failure;
onFailureWasCalled = true;
};
responseDirective = kSendError;
chip::Controller::InvokeCommandRequest(&ctx.GetExchangeManager(), sessionHandle, kTestEndpointId, request, onSuccessCb,
onFailureCb);
ctx.DrainAndServiceIO();
NL_TEST_ASSERT(apSuite, !onSuccessWasCalled && onFailureWasCalled && statusCheck);
NL_TEST_ASSERT(apSuite, ctx.GetExchangeManager().GetNumActiveExchanges() == 0);
}
void TestCommandInteraction::TestMultipleFailures(nlTestSuite * apSuite, void * apContext)
{
struct FakeRequest : public Clusters::UnitTesting::Commands::TestSimpleArgumentRequest::Type
{
using ResponseType = DataModel::NullObjectType;
};
TestContext & ctx = *static_cast<TestContext *>(apContext);
FakeRequest request;
auto sessionHandle = ctx.GetSessionBobToAlice();
size_t successCalls = 0;
size_t failureCalls = 0;
bool statusCheck = false;
request.arg1 = true;
// Passing of stack variables by reference is only safe because of synchronous completion of the interaction. Otherwise, it's
// not safe to do so.
auto onSuccessCb = [&successCalls](const ConcreteCommandPath & commandPath, const StatusIB & aStatus,
const auto & dataResponse) { ++successCalls; };
// Passing of stack variables by reference is only safe because of synchronous completion of the interaction. Otherwise, it's
// not safe to do so.
auto onFailureCb = [&failureCalls, &statusCheck](CHIP_ERROR aError) {
statusCheck = aError.IsIMStatus() && StatusIB(aError).mStatus == InteractionModel::Status::Failure;
++failureCalls;
};
responseDirective = kSendMultipleErrors;
Controller::InvokeCommandRequest(&ctx.GetExchangeManager(), sessionHandle, kTestEndpointId, request, onSuccessCb, onFailureCb);
ctx.DrainAndServiceIO();
NL_TEST_ASSERT(apSuite, successCalls == 0);
NL_TEST_ASSERT(apSuite, failureCalls == 1 && statusCheck);
NL_TEST_ASSERT(apSuite, ctx.GetExchangeManager().GetNumActiveExchanges() == 0);
}
void TestCommandInteraction::TestSuccessNoDataResponseWithClusterStatus(nlTestSuite * apSuite, void * apContext)
{
struct FakeRequest : public Clusters::UnitTesting::Commands::TestSimpleArgumentRequest::Type
{
using ResponseType = DataModel::NullObjectType;
};
TestContext & ctx = *static_cast<TestContext *>(apContext);
FakeRequest request;
auto sessionHandle = ctx.GetSessionBobToAlice();
bool onSuccessWasCalled = false;
bool onFailureWasCalled = false;
bool statusCheck = false;
request.arg1 = true;
// Passing of stack variables by reference is only safe because of synchronous completion of the interaction. Otherwise, it's
// not safe to do so.
auto onSuccessCb = [&onSuccessWasCalled, &statusCheck](const app::ConcreteCommandPath & commandPath,
const app::StatusIB & aStatus, const auto & dataResponse) {
statusCheck = (aStatus.mStatus == Protocols::InteractionModel::Status::Success &&
aStatus.mClusterStatus.Value() == kTestSuccessClusterStatus);
onSuccessWasCalled = true;
};
// Passing of stack variables by reference is only safe because of synchronous completion of the interaction. Otherwise, it's
// not safe to do so.
auto onFailureCb = [&onFailureWasCalled](CHIP_ERROR aError) { onFailureWasCalled = true; };
responseDirective = kSendSuccessStatusCodeWithClusterStatus;
chip::Controller::InvokeCommandRequest(&ctx.GetExchangeManager(), sessionHandle, kTestEndpointId, request, onSuccessCb,
onFailureCb);
ctx.DrainAndServiceIO();
NL_TEST_ASSERT(apSuite, onSuccessWasCalled && !onFailureWasCalled && statusCheck);
NL_TEST_ASSERT(apSuite, ctx.GetExchangeManager().GetNumActiveExchanges() == 0);
}
void TestCommandInteraction::TestFailureWithClusterStatus(nlTestSuite * apSuite, void * apContext)
{
TestContext & ctx = *static_cast<TestContext *>(apContext);
Clusters::UnitTesting::Commands::TestSimpleArgumentRequest::Type request;
auto sessionHandle = ctx.GetSessionBobToAlice();
bool onSuccessWasCalled = false;
bool onFailureWasCalled = false;
bool statusCheck = false;
request.arg1 = true;
// Passing of stack variables by reference is only safe because of synchronous completion of the interaction. Otherwise, it's
// not safe to do so.
auto onSuccessCb = [&onSuccessWasCalled](const app::ConcreteCommandPath & commandPath, const app::StatusIB & aStatus,
const auto & dataResponse) { onSuccessWasCalled = true; };
// Passing of stack variables by reference is only safe because of synchronous completion of the interaction. Otherwise, it's
// not safe to do so.
auto onFailureCb = [&onFailureWasCalled, &statusCheck](CHIP_ERROR aError) {
statusCheck = aError.IsIMStatus();
if (statusCheck)
{
app::StatusIB status(aError);
statusCheck = (status.mStatus == Protocols::InteractionModel::Status::Failure &&
status.mClusterStatus.Value() == kTestFailureClusterStatus);
}
onFailureWasCalled = true;
};
responseDirective = kSendErrorWithClusterStatus;
chip::Controller::InvokeCommandRequest(&ctx.GetExchangeManager(), sessionHandle, kTestEndpointId, request, onSuccessCb,
onFailureCb);
ctx.DrainAndServiceIO();
NL_TEST_ASSERT(apSuite, !onSuccessWasCalled && onFailureWasCalled && statusCheck);
NL_TEST_ASSERT(apSuite, ctx.GetExchangeManager().GetNumActiveExchanges() == 0);
}
// clang-format off
const nlTest sTests[] =
{
NL_TEST_DEF("TestDataResponse", TestCommandInteraction::TestDataResponse),
NL_TEST_DEF("TestSuccessNoDataResponse", TestCommandInteraction::TestSuccessNoDataResponse),
NL_TEST_DEF("TestMultipleSuccessNoDataResponses", TestCommandInteraction::TestMultipleSuccessNoDataResponses),
NL_TEST_DEF("TestAsyncResponse", TestCommandInteraction::TestAsyncResponse),
NL_TEST_DEF("TestFailure", TestCommandInteraction::TestFailure),
NL_TEST_DEF("TestMultipleFailures", TestCommandInteraction::TestMultipleFailures),
NL_TEST_DEF("TestSuccessNoDataResponseWithClusterStatus", TestCommandInteraction::TestSuccessNoDataResponseWithClusterStatus),
NL_TEST_DEF("TestFailureWithClusterStatus", TestCommandInteraction::TestFailureWithClusterStatus),
NL_TEST_SENTINEL()
};
// clang-format on
// clang-format off
nlTestSuite sSuite =
{
"TestCommands",
&sTests[0],
TestContext::Initialize,
TestContext::Finalize
};
// clang-format on
} // namespace
int TestCommandInteractionTest()
{
return chip::ExecuteTestsWithContext<TestContext>(&sSuite);
}
CHIP_REGISTER_TEST_SUITE(TestCommandInteractionTest)