blob: 6f42c38a7789e44598f044e4badf7d86d914c75d [file] [log] [blame]
/*
*
* 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 "DataModelFixtures.h"
#include <access/SubjectDescriptor.h>
#include <app-common/zap-generated/cluster-objects.h>
#include <app-common/zap-generated/ids/Clusters.h>
#include <app/AttributeValueDecoder.h>
#include <app/AttributeValueEncoder.h>
#include <app/ConcreteAttributePath.h>
#include <app/ConcreteClusterPath.h>
#include <app/InteractionModelEngine.h>
#include <app/codegen-data-model-provider/Instance.h>
#include <app/data-model-provider/ActionReturnStatus.h>
#include <lib/core/DataModelTypes.h>
#include <lib/support/logging/TextOnlyLogging.h>
#include <optional>
using namespace chip;
using namespace chip::app;
using namespace chip::app::DataModelTests;
using namespace chip::app::DataModel;
using namespace chip::app::Clusters;
using namespace chip::app::Clusters::UnitTesting;
using namespace chip::Protocols;
namespace chip {
namespace app {
class TestOnlyAttributeValueEncoderAccessor
{
public:
TestOnlyAttributeValueEncoderAccessor(AttributeValueEncoder & encoder) : mEncoder(encoder) {}
AttributeReportIBs::Builder & Builder() { return mEncoder.mAttributeReportIBsBuilder; }
void SetState(const AttributeEncodeState & state) { mEncoder.mEncodeState = state; }
private:
AttributeValueEncoder & mEncoder;
};
class TestOnlyAttributeValueDecoderAccessor
{
public:
TestOnlyAttributeValueDecoderAccessor(AttributeValueDecoder & decoder) : mDecoder(decoder) {}
TLV::TLVReader & GetTlvReader() { return mDecoder.mReader; }
private:
AttributeValueDecoder & mDecoder;
};
namespace DataModelTests {
ScopedChangeOnly<ReadResponseDirective> gReadResponseDirective(ReadResponseDirective::kSendDataResponse);
ScopedChangeOnly<WriteResponseDirective> gWriteResponseDirective(WriteResponseDirective::kSendAttributeSuccess);
ScopedChangeOnly<CommandResponseDirective> gCommandResponseDirective(CommandResponseDirective::kSendSuccessStatusCode);
ScopedChangeOnly<bool> gIsLitIcd(false);
// TODO: usage of a global value that changes as a READ sideffect is problematic for
// dual-read use cases (i.e. during checked ember/datamodel tests)
//
// For now see the hack "change undo" in CustomDataModel::ReadAttribute, however
// overall this is problematic.
uint16_t gInt16uTotalReadCount = 0;
CommandHandler::Handle gAsyncCommandHandle;
} // namespace DataModelTests
static CHIP_ERROR ReadSingleClusterData(const Access::SubjectDescriptor & aSubjectDescriptor, bool aIsFabricFiltered,
const ConcreteReadAttributePath & aPath, AttributeReportIBs::Builder & aAttributeReports,
AttributeEncodeState * apEncoderState)
{
if (aPath.mEndpointId >= chip::Test::kMockEndpointMin)
{
return chip::Test::ReadSingleMockClusterData(aSubjectDescriptor.fabricIndex, aPath, aAttributeReports, apEncoderState);
}
if (gReadResponseDirective == ReadResponseDirective::kSendManyDataResponses ||
gReadResponseDirective == ReadResponseDirective::kSendManyDataResponsesWrongPath)
{
if (aPath.mClusterId != Clusters::UnitTesting::Id || aPath.mAttributeId != Clusters::UnitTesting::Attributes::Boolean::Id)
{
return CHIP_ERROR_INCORRECT_STATE;
}
for (size_t i = 0; i < 4; ++i)
{
ConcreteAttributePath path(aPath);
// Use an incorrect attribute id for some of the responses.
path.mAttributeId = static_cast<AttributeId>(
path.mAttributeId + (i / 2) + (gReadResponseDirective == ReadResponseDirective::kSendManyDataResponsesWrongPath));
AttributeEncodeState state(apEncoderState);
AttributeValueEncoder valueEncoder(aAttributeReports, aSubjectDescriptor, path, kDataVersion, aIsFabricFiltered, state);
ReturnErrorOnFailure(valueEncoder.Encode(true));
}
return CHIP_NO_ERROR;
}
if (gReadResponseDirective == ReadResponseDirective::kSendDataResponse)
{
if (aPath.mClusterId == app::Clusters::UnitTesting::Id &&
aPath.mAttributeId == app::Clusters::UnitTesting::Attributes::ListFabricScoped::Id)
{
AttributeEncodeState state(apEncoderState);
AttributeValueEncoder valueEncoder(aAttributeReports, aSubjectDescriptor, aPath, kDataVersion, aIsFabricFiltered,
state);
return valueEncoder.EncodeList([aSubjectDescriptor](const auto & encoder) -> CHIP_ERROR {
app::Clusters::UnitTesting::Structs::TestFabricScoped::Type val;
val.fabricIndex = aSubjectDescriptor.fabricIndex;
ReturnErrorOnFailure(encoder.Encode(val));
val.fabricIndex = (val.fabricIndex == 1) ? 2 : 1;
ReturnErrorOnFailure(encoder.Encode(val));
return CHIP_NO_ERROR;
});
}
if (aPath.mClusterId == app::Clusters::UnitTesting::Id &&
aPath.mAttributeId == app::Clusters::UnitTesting::Attributes::Int16u::Id)
{
AttributeEncodeState state(apEncoderState);
AttributeValueEncoder valueEncoder(aAttributeReports, aSubjectDescriptor, aPath, kDataVersion, aIsFabricFiltered,
state);
return valueEncoder.Encode(++gInt16uTotalReadCount);
}
if (aPath.mClusterId == kPerpetualClusterId ||
(aPath.mClusterId == app::Clusters::UnitTesting::Id && aPath.mAttributeId == kPerpetualAttributeid))
{
AttributeEncodeState state;
AttributeValueEncoder valueEncoder(aAttributeReports, aSubjectDescriptor, aPath, kDataVersion, aIsFabricFiltered,
state);
CHIP_ERROR err = valueEncoder.EncodeList([](const auto & encoder) -> CHIP_ERROR {
encoder.Encode(static_cast<uint8_t>(1));
return CHIP_ERROR_NO_MEMORY;
});
if (err != CHIP_NO_ERROR)
{
// If the err is not CHIP_NO_ERROR, means the encoding was aborted, then the valueEncoder may save its state.
// The state is used by list chunking feature for now.
if (apEncoderState != nullptr)
{
*apEncoderState = valueEncoder.GetState();
}
return err;
}
}
if (aPath.mClusterId == app::Clusters::IcdManagement::Id &&
aPath.mAttributeId == app::Clusters::IcdManagement::Attributes::OperatingMode::Id)
{
AttributeEncodeState state(apEncoderState);
AttributeValueEncoder valueEncoder(aAttributeReports, aSubjectDescriptor, aPath, kDataVersion, aIsFabricFiltered,
state);
return valueEncoder.Encode(gIsLitIcd ? Clusters::IcdManagement::OperatingModeEnum::kLit
: Clusters::IcdManagement::OperatingModeEnum::kSit);
}
AttributeReportIB::Builder & attributeReport = aAttributeReports.CreateAttributeReport();
ReturnErrorOnFailure(aAttributeReports.GetError());
AttributeDataIB::Builder & attributeData = attributeReport.CreateAttributeData();
ReturnErrorOnFailure(attributeReport.GetError());
Clusters::UnitTesting::Attributes::ListStructOctetString::TypeInfo::Type value;
Clusters::UnitTesting::Structs::TestListStructOctet::Type valueBuf[4];
value = valueBuf;
uint8_t i = 0;
for (auto & item : valueBuf)
{
item.member1 = i;
i++;
}
attributeData.DataVersion(kDataVersion);
ReturnErrorOnFailure(attributeData.GetError());
AttributePathIB::Builder & attributePath = attributeData.CreatePath();
attributePath.Endpoint(aPath.mEndpointId).Cluster(aPath.mClusterId).Attribute(aPath.mAttributeId).EndOfAttributePathIB();
ReturnErrorOnFailure(attributePath.GetError());
ReturnErrorOnFailure(DataModel::Encode(*(attributeData.GetWriter()), TLV::ContextTag(AttributeDataIB::Tag::kData), value));
ReturnErrorOnFailure(attributeData.EndOfAttributeDataIB());
return attributeReport.EndOfAttributeReportIB();
}
for (size_t i = 0; i < (gReadResponseDirective == ReadResponseDirective::kSendTwoDataErrors ? 2 : 1); ++i)
{
AttributeReportIB::Builder & attributeReport = aAttributeReports.CreateAttributeReport();
ReturnErrorOnFailure(aAttributeReports.GetError());
AttributeStatusIB::Builder & attributeStatus = attributeReport.CreateAttributeStatus();
AttributePathIB::Builder & attributePath = attributeStatus.CreatePath();
attributePath.Endpoint(aPath.mEndpointId).Cluster(aPath.mClusterId).Attribute(aPath.mAttributeId).EndOfAttributePathIB();
ReturnErrorOnFailure(attributePath.GetError());
StatusIB::Builder & errorStatus = attributeStatus.CreateErrorStatus();
ReturnErrorOnFailure(attributeStatus.GetError());
errorStatus.EncodeStatusIB(StatusIB(Protocols::InteractionModel::Status::Busy));
attributeStatus.EndOfAttributeStatusIB();
ReturnErrorOnFailure(attributeStatus.GetError());
ReturnErrorOnFailure(attributeReport.EndOfAttributeReportIB());
}
return CHIP_NO_ERROR;
}
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)
{
apCommandObj->AddStatus(aCommandPath, Protocols::InteractionModel::Status::Failure, "Unable to decode the request");
return;
}
if (gCommandResponseDirective == CommandResponseDirective::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 (gCommandResponseDirective == CommandResponseDirective::kSendSuccessStatusCode)
{
apCommandObj->AddStatus(aCommandPath, Protocols::InteractionModel::Status::Success);
}
else if (gCommandResponseDirective == CommandResponseDirective::kSendMultipleSuccessStatusCodes)
{
apCommandObj->AddStatus(aCommandPath, Protocols::InteractionModel::Status::Success,
"No error but testing status success case");
// 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 < 3; ++i)
{
(void) apCommandObj->FallibleAddStatus(aCommandPath, Protocols::InteractionModel::Status::Success,
"No error but testing status success case");
}
// And one failure on the end.
(void) apCommandObj->FallibleAddStatus(aCommandPath, Protocols::InteractionModel::Status::Failure);
}
else if (gCommandResponseDirective == CommandResponseDirective::kSendError)
{
apCommandObj->AddStatus(aCommandPath, Protocols::InteractionModel::Status::Failure);
}
else if (gCommandResponseDirective == CommandResponseDirective::kSendMultipleErrors)
{
apCommandObj->AddStatus(aCommandPath, Protocols::InteractionModel::Status::Failure);
// 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 < 3; ++i)
{
(void) apCommandObj->FallibleAddStatus(aCommandPath, Protocols::InteractionModel::Status::Failure);
}
}
else if (gCommandResponseDirective == CommandResponseDirective::kSendSuccessStatusCodeWithClusterStatus)
{
apCommandObj->AddStatus(
aCommandPath, Protocols::InteractionModel::ClusterStatusCode::ClusterSpecificSuccess(kTestSuccessClusterStatus));
}
else if (gCommandResponseDirective == CommandResponseDirective::kSendErrorWithClusterStatus)
{
apCommandObj->AddStatus(
aCommandPath, Protocols::InteractionModel::ClusterStatusCode::ClusterSpecificFailure(kTestFailureClusterStatus));
}
else if (gCommandResponseDirective == CommandResponseDirective::kAsync)
{
gAsyncCommandHandle = apCommandObj;
}
}
}
CustomDataModel & CustomDataModel::Instance()
{
static CustomDataModel model;
return model;
}
ActionReturnStatus CustomDataModel::ReadAttribute(const ReadAttributeRequest & request, AttributeValueEncoder & encoder)
{
AttributeEncodeState mutableState(&encoder.GetState()); // provide a state copy to start.
Access::SubjectDescriptor subjectDescriptor;
if (request.subjectDescriptor != nullptr)
{
subjectDescriptor = *request.subjectDescriptor;
}
CHIP_ERROR err = ReadSingleClusterData(subjectDescriptor, request.readFlags.Has(ReadFlags::kFabricFiltered), request.path,
TestOnlyAttributeValueEncoderAccessor(encoder).Builder(), &mutableState);
// state must survive CHIP_ERRORs as it is used for chunking
TestOnlyAttributeValueEncoderAccessor(encoder).SetState(mutableState);
return err;
}
ActionReturnStatus CustomDataModel::WriteAttribute(const WriteAttributeRequest & request, AttributeValueDecoder & decoder)
{
static ListIndex listStructOctetStringElementCount = 0;
if (request.path.mDataVersion.HasValue() && request.path.mDataVersion.Value() == kRejectedDataVersion)
{
return InteractionModel::Status::DataVersionMismatch;
}
if (request.path.mClusterId == Clusters::UnitTesting::Id &&
request.path.mAttributeId == Attributes::ListStructOctetString::TypeInfo::GetAttributeId())
{
if (gWriteResponseDirective == WriteResponseDirective::kSendAttributeSuccess)
{
if (!request.path.IsListOperation() || request.path.mListOp == ConcreteDataAttributePath::ListOperation::ReplaceAll)
{
Attributes::ListStructOctetString::TypeInfo::DecodableType value;
ReturnErrorOnFailure(decoder.Decode(value));
auto iter = value.begin();
listStructOctetStringElementCount = 0;
while (iter.Next())
{
auto & item = iter.GetValue();
VerifyOrReturnError(item.member1 == listStructOctetStringElementCount, CHIP_ERROR_INVALID_ARGUMENT);
listStructOctetStringElementCount++;
}
return CHIP_NO_ERROR;
}
if (request.path.mListOp == ConcreteDataAttributePath::ListOperation::AppendItem)
{
Structs::TestListStructOctet::DecodableType item;
ReturnErrorOnFailure(decoder.Decode(item));
VerifyOrReturnError(item.member1 == listStructOctetStringElementCount, CHIP_ERROR_INVALID_ARGUMENT);
listStructOctetStringElementCount++;
return CHIP_NO_ERROR;
}
return CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE;
}
return CHIP_IM_GLOBAL_STATUS(Failure);
}
if (request.path.mClusterId == Clusters::UnitTesting::Id && request.path.mAttributeId == Attributes::ListFabricScoped::Id)
{
// TODO(backwards compatibility): unit tests here undoes the subject descriptor usage
// - original tests were completely bypassing the passed in subject descriptor for this test
// and overriding it with a invalid subject descriptor
// - we do the same here, however this seems somewhat off: decoder.Decode() will fail for list
// items so we could just return the error directly without this extra step
// Mock an invalid Subject Descriptor
AttributeValueDecoder invalidSubjectDescriptorDecoder(TestOnlyAttributeValueDecoderAccessor(decoder).GetTlvReader(),
Access::SubjectDescriptor());
if (!request.path.IsListOperation() || request.path.mListOp == ConcreteDataAttributePath::ListOperation::ReplaceAll)
{
Attributes::ListFabricScoped::TypeInfo::DecodableType value;
ReturnErrorOnFailure(invalidSubjectDescriptorDecoder.Decode(value));
auto iter = value.begin();
while (iter.Next())
{
auto & item = iter.GetValue();
(void) item;
}
}
else if (request.path.mListOp == ConcreteDataAttributePath::ListOperation::AppendItem)
{
Structs::TestFabricScoped::DecodableType item;
ReturnErrorOnFailure(invalidSubjectDescriptorDecoder.Decode(item));
}
else
{
return CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE;
}
return CHIP_NO_ERROR;
}
// Boolean attribute of unit testing cluster triggers "multiple errors" case.
if (request.path.mClusterId == Clusters::UnitTesting::Id &&
request.path.mAttributeId == Attributes::Boolean::TypeInfo::GetAttributeId())
{
// TODO(IMDM): this used to send 4 responses (hence the multiple status)
//
// for (size_t i = 0; i < 4; ++i)
// {
// aWriteHandler->AddStatus(request.path, status);
// }
//
// which are NOT encodable by a simple response. It is unclear how this is
// convertible (if at all): we write path by path only. Having multiple
// responses for the same path within the write code makes no sense
//
// This should NOT be possible anymore when one can only return a single
// status (nobody has access to multiple path status updates at this level)
switch (gWriteResponseDirective)
{
case WriteResponseDirective::kSendMultipleSuccess:
return InteractionModel::Status::Success;
case WriteResponseDirective::kSendMultipleErrors:
return InteractionModel::Status::Failure;
default:
chipDie();
}
}
if (request.path.mClusterId == Clusters::UnitTesting::Id &&
request.path.mAttributeId == Attributes::Int8u::TypeInfo::GetAttributeId())
{
switch (gWriteResponseDirective)
{
case WriteResponseDirective::kSendClusterSpecificSuccess:
return InteractionModel::ClusterStatusCode::ClusterSpecificSuccess(kExampleClusterSpecificSuccess);
case WriteResponseDirective::kSendClusterSpecificFailure:
return InteractionModel::ClusterStatusCode::ClusterSpecificFailure(kExampleClusterSpecificFailure);
default:
// this should not be reached, our tests only set up these for this test case
chipDie();
}
}
return CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE;
}
std::optional<ActionReturnStatus> CustomDataModel::Invoke(const InvokeRequest & request, chip::TLV::TLVReader & input_arguments,
CommandHandler * handler)
{
DispatchSingleClusterCommand(request.path, input_arguments, handler);
return std::nullopt; // handler status is set by the dispatch
}
EndpointId CustomDataModel::FirstEndpoint()
{
return CodegenDataModelProviderInstance()->FirstEndpoint();
}
EndpointId CustomDataModel::NextEndpoint(EndpointId before)
{
return CodegenDataModelProviderInstance()->NextEndpoint(before);
}
std::optional<DataModel::DeviceTypeEntry> CustomDataModel::FirstDeviceType(EndpointId endpoint)
{
return std::nullopt;
}
std::optional<DataModel::DeviceTypeEntry> CustomDataModel::NextDeviceType(EndpointId endpoint,
const DataModel::DeviceTypeEntry & previous)
{
return std::nullopt;
}
ClusterEntry CustomDataModel::FirstCluster(EndpointId endpoint)
{
return CodegenDataModelProviderInstance()->FirstCluster(endpoint);
}
ClusterEntry CustomDataModel::NextCluster(const ConcreteClusterPath & before)
{
return CodegenDataModelProviderInstance()->NextCluster(before);
}
std::optional<ClusterInfo> CustomDataModel::GetClusterInfo(const ConcreteClusterPath & path)
{
return CodegenDataModelProviderInstance()->GetClusterInfo(path);
}
AttributeEntry CustomDataModel::FirstAttribute(const ConcreteClusterPath & cluster)
{
return CodegenDataModelProviderInstance()->FirstAttribute(cluster);
}
AttributeEntry CustomDataModel::NextAttribute(const ConcreteAttributePath & before)
{
return CodegenDataModelProviderInstance()->NextAttribute(before);
}
std::optional<AttributeInfo> CustomDataModel::GetAttributeInfo(const ConcreteAttributePath & path)
{
return CodegenDataModelProviderInstance()->GetAttributeInfo(path);
}
CommandEntry CustomDataModel::FirstAcceptedCommand(const ConcreteClusterPath & cluster)
{
return CodegenDataModelProviderInstance()->FirstAcceptedCommand(cluster);
}
CommandEntry CustomDataModel::NextAcceptedCommand(const ConcreteCommandPath & before)
{
return CodegenDataModelProviderInstance()->NextAcceptedCommand(before);
}
std::optional<CommandInfo> CustomDataModel::GetAcceptedCommandInfo(const ConcreteCommandPath & path)
{
return CodegenDataModelProviderInstance()->GetAcceptedCommandInfo(path);
}
ConcreteCommandPath CustomDataModel::FirstGeneratedCommand(const ConcreteClusterPath & cluster)
{
return CodegenDataModelProviderInstance()->FirstGeneratedCommand(cluster);
}
ConcreteCommandPath CustomDataModel::NextGeneratedCommand(const ConcreteCommandPath & before)
{
return CodegenDataModelProviderInstance()->NextGeneratedCommand(before);
}
} // namespace app
} // namespace chip