diff --git a/examples/chef/chef.py b/examples/chef/chef.py index 6176c6e8c280ac..f681f3a059f4fc 100755 --- a/examples/chef/chef.py +++ b/examples/chef/chef.py @@ -878,7 +878,7 @@ def main() -> int: """)) if options.do_clean: shell.run_cmd("rm -rf out") - shell.run_cmd("gn gen out") + shell.run_cmd("gn gen --export-compile-commands out") shell.run_cmd("ninja -C out") # diff --git a/src/BUILD.gn b/src/BUILD.gn index 27f449dcf8e543..e2557535d9d72d 100644 --- a/src/BUILD.gn +++ b/src/BUILD.gn @@ -50,7 +50,6 @@ if (chip_build_tests) { chip_test_group("tests") { deps = [] tests = [ - "${chip_root}/src/app/codegen-data-model/tests", "${chip_root}/src/app/data-model-interface/tests", "${chip_root}/src/access/tests", "${chip_root}/src/crypto/tests", @@ -82,7 +81,15 @@ if (chip_build_tests) { if (current_os != "zephyr" && current_os != "mbed" && chip_device_platform != "efr32") { + # Avoid these items from "one single binary" test executions. Once tests + # are split, we can re-visit this (and likely many others) + # + # In particular: + # "app/codegen-data-model/tests" contains symbols for ember mocks which + # are used by other tests + tests += [ + "${chip_root}/src/app/codegen-data-model/tests", "${chip_root}/src/setup_payload/tests", "${chip_root}/src/transport/raw/tests", ] diff --git a/src/app/codegen-data-model/BUILD.gn b/src/app/codegen-data-model/BUILD.gn index 418983a2fc8b8f..5803f01a37778e 100644 --- a/src/app/codegen-data-model/BUILD.gn +++ b/src/app/codegen-data-model/BUILD.gn @@ -20,6 +20,7 @@ import("//build_overrides/chip.gni") # # Use `model.gni` to get access to: # CodegenDataModel.cpp +# CodegenDataModel_Read.cpp # CodegenDataModel.h # # The above list of files exists to satisfy the "dependency linter" diff --git a/src/app/codegen-data-model/CodegenDataModel.cpp b/src/app/codegen-data-model/CodegenDataModel.cpp index e1597dbdc4f8b4..d46deeeddaa1e0 100644 --- a/src/app/codegen-data-model/CodegenDataModel.cpp +++ b/src/app/codegen-data-model/CodegenDataModel.cpp @@ -231,13 +231,6 @@ bool CodegenDataModel::EmberCommandListIterator::Exists(const CommandId * list, return (*mCurrentHint == toCheck); } -CHIP_ERROR CodegenDataModel::ReadAttribute(const InteractionModel::ReadAttributeRequest & request, - InteractionModel::ReadState & state, AttributeValueEncoder & encoder) -{ - // TODO: this needs an implementation - return CHIP_ERROR_NOT_IMPLEMENTED; -} - CHIP_ERROR CodegenDataModel::WriteAttribute(const InteractionModel::WriteAttributeRequest & request, AttributeValueDecoder & decoder) { diff --git a/src/app/codegen-data-model/CodegenDataModel.h b/src/app/codegen-data-model/CodegenDataModel.h index f8d997451bdace..b65f38b9155e73 100644 --- a/src/app/codegen-data-model/CodegenDataModel.h +++ b/src/app/codegen-data-model/CodegenDataModel.h @@ -68,8 +68,7 @@ class CodegenDataModel : public chip::app::InteractionModel::DataModel /// Generic model implementations CHIP_ERROR Shutdown() override { return CHIP_NO_ERROR; } - CHIP_ERROR ReadAttribute(const InteractionModel::ReadAttributeRequest & request, InteractionModel::ReadState & state, - AttributeValueEncoder & encoder) override; + CHIP_ERROR ReadAttribute(const InteractionModel::ReadAttributeRequest & request, AttributeValueEncoder & encoder) override; CHIP_ERROR WriteAttribute(const InteractionModel::WriteAttributeRequest & request, AttributeValueDecoder & decoder) override; CHIP_ERROR Invoke(const InteractionModel::InvokeRequest & request, chip::TLV::TLVReader & input_arguments, InteractionModel::InvokeReply & reply) override; diff --git a/src/app/codegen-data-model/CodegenDataModel_Read.cpp b/src/app/codegen-data-model/CodegenDataModel_Read.cpp new file mode 100644 index 00000000000000..04265d37f8623f --- /dev/null +++ b/src/app/codegen-data-model/CodegenDataModel_Read.cpp @@ -0,0 +1,372 @@ +/* + * 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 "lib/core/CHIPError.h" +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace chip { +namespace app { +namespace { +using namespace chip::app::Compatibility::Internal; + +// Fetch the source for the given attribute path: either a cluster (for global ones) or attribute +// path. +// +// if returning a CHIP_ERROR, it will NEVER be CHIP_NO_ERROR. +std::variant +FindAttributeMetadata(const ConcreteAttributePath & aPath) +{ + for (auto & attr : GlobalAttributesNotInMetadata) + { + + if (attr == aPath.mAttributeId) + { + const EmberAfCluster * cluster = emberAfFindServerCluster(aPath.mEndpointId, aPath.mClusterId); + if (cluster == nullptr) + { + return (emberAfFindEndpointType(aPath.mEndpointId) == nullptr) ? CHIP_IM_GLOBAL_STATUS(UnsupportedEndpoint) + : CHIP_IM_GLOBAL_STATUS(UnsupportedCluster); + } + + return cluster; + } + } + const EmberAfAttributeMetadata * metadata = + emberAfLocateAttributeMetadata(aPath.mEndpointId, aPath.mClusterId, aPath.mAttributeId); + + if (metadata == nullptr) + { + const EmberAfEndpointType * type = emberAfFindEndpointType(aPath.mEndpointId); + if (type == nullptr) + { + return CHIP_IM_GLOBAL_STATUS(UnsupportedEndpoint); + } + + const EmberAfCluster * cluster = emberAfFindClusterInType(type, aPath.mClusterId, CLUSTER_MASK_SERVER); + if (cluster == nullptr) + { + return CHIP_IM_GLOBAL_STATUS(UnsupportedCluster); + } + + // Since we know the attribute is unsupported and the endpoint/cluster are + // OK, this is the only option left. + return CHIP_IM_GLOBAL_STATUS(UnsupportedAttribute); + } + + return metadata; +} + +/// Attempts to read via an attribute access interface (AAI) +/// +/// If it returns a CHIP_ERROR, then this is a FINAL result (i.e. either failure or success). +/// +/// If it returns std::nullopt, then there is no AAI to handle the given path +/// and processing should figure out the value otherwise (generally from other ember data) +std::optional TryReadViaAccessInterface(const ConcreteAttributePath & path, AttributeAccessInterface * aai, + AttributeValueEncoder & encoder) +{ + // Processing can happen only if an attribute access interface actually exists.. + if (aai == nullptr) + { + return std::nullopt; + } + + CHIP_ERROR err = aai->Read(path, encoder); + + if (err != CHIP_NO_ERROR) + { + // Implementation of 8.4.3.2 of the spec for path expansion + if (path.mExpanded && (err == CHIP_IM_GLOBAL_STATUS(UnsupportedRead))) + { + return CHIP_NO_ERROR; + } + + return err; + } + + // If the encoder tried to encode, then a value should have been written. + // - if encode, assume DONE (i.e. FINAL CHIP_NO_ERROR) + // - if no encode, say that processing must continue + return encoder.TriedEncode() ? std::make_optional(CHIP_NO_ERROR) : std::nullopt; +} + +/// Metadata of what a ember/pascal short string means (prepended by a u8 length) +struct ShortPascalString +{ + using LengthType = uint8_t; + static constexpr LengthType kNullLength = 0xFF; +}; + +/// Metadata of what a ember/pascal LONG string means (prepended by a u16 length) +struct LongPascalString +{ + using LengthType = uint16_t; + static constexpr LengthType kNullLength = 0xFFFF; +}; + +// ember assumptions ... should just work +static_assert(sizeof(ShortPascalString::LengthType) == 1); +static_assert(sizeof(LongPascalString::LengthType) == 2); + +/// Given a ByteSpan containing data from ember, interpret it +/// as a span of type OUT (i.e. ByteSpan or CharSpan) given a ENCODING +/// where ENCODING is Short or Long pascal strings. +template +std::optional ExtractEmberString(ByteSpan data) +{ + typename ENCODING::LengthType len; + + // Ember storage format for pascal-prefix data is specifically "native byte order", + // hence the use of memcpy. + VerifyOrDie(sizeof(len) <= data.size()); + memcpy(&len, data.data(), sizeof(len)); + + if (len == ENCODING::kNullLength) + { + return std::nullopt; + } + + VerifyOrDie(static_cast(len + sizeof(len)) <= data.size()); + return std::make_optional(reinterpret_cast(data.data() + sizeof(len)), len); +} + +/// Encode a value inside `encoder` +/// +/// The value encoded will be of type T (e.g. CharSpan or ByteSpan) and it will be decoded +/// via the given ENCODING (i.e. ShortPascalString or LongPascalString) +/// +/// isNullable defines if the value of NULL is allowed to be encoded. +template +CHIP_ERROR EncodeStringLike(ByteSpan data, bool isNullable, AttributeValueEncoder & encoder) +{ + std::optional value = ExtractEmberString(data); + if (!value.has_value()) + { + if (isNullable) + { + return encoder.EncodeNull(); + } + return CHIP_ERROR_INCORRECT_STATE; + } + + // encode value as-is + return encoder.Encode(*value); +} + +/// Encodes a numeric data value of type T from the given ember-encoded buffer `data`. +/// +/// isNullable defines if the value of NULL is allowed to be encoded. +template +CHIP_ERROR EncodeFromSpan(ByteSpan data, bool isNullable, AttributeValueEncoder & encoder) +{ + typename NumericAttributeTraits::StorageType value; + + VerifyOrReturnError(data.size() >= sizeof(value), CHIP_ERROR_INVALID_ARGUMENT); + memcpy(&value, data.data(), sizeof(value)); + + if (isNullable && NumericAttributeTraits::IsNullValue(value)) + { + return encoder.EncodeNull(); + } + + if (!NumericAttributeTraits::CanRepresentValue(isNullable, value)) + { + return CHIP_ERROR_INCORRECT_STATE; + } + + return encoder.Encode(NumericAttributeTraits::StorageToWorking(value)); +} + +/// Converts raw ember data from `data` into the encoder +/// +/// Uses the attribute `metadata` to determine how the data is encoded into `data` and +/// write a suitable value into `encoder`. +CHIP_ERROR EncodeEmberValue(ByteSpan data, const EmberAfAttributeMetadata * metadata, AttributeValueEncoder & encoder) +{ + VerifyOrReturnError(metadata != nullptr, CHIP_ERROR_INVALID_ARGUMENT); + + const bool isNullable = metadata->IsNullable(); + + switch (AttributeBaseType(metadata->attributeType)) + { + case ZCL_NO_DATA_ATTRIBUTE_TYPE: // No data + return encoder.EncodeNull(); + case ZCL_BOOLEAN_ATTRIBUTE_TYPE: // Boolean + return EncodeFromSpan(data, isNullable, encoder); + case ZCL_INT8U_ATTRIBUTE_TYPE: // Unsigned 8-bit integer + return EncodeFromSpan(data, isNullable, encoder); + case ZCL_INT16U_ATTRIBUTE_TYPE: // Unsigned 16-bit integer + return EncodeFromSpan(data, isNullable, encoder); + case ZCL_INT24U_ATTRIBUTE_TYPE: // Unsigned 24-bit integer + return EncodeFromSpan>(data, isNullable, encoder); + case ZCL_INT32U_ATTRIBUTE_TYPE: // Unsigned 32-bit integer + return EncodeFromSpan(data, isNullable, encoder); + case ZCL_INT40U_ATTRIBUTE_TYPE: // Unsigned 40-bit integer + return EncodeFromSpan>(data, isNullable, encoder); + case ZCL_INT48U_ATTRIBUTE_TYPE: // Unsigned 48-bit integer + return EncodeFromSpan>(data, isNullable, encoder); + case ZCL_INT56U_ATTRIBUTE_TYPE: // Unsigned 56-bit integer + return EncodeFromSpan>(data, isNullable, encoder); + case ZCL_INT64U_ATTRIBUTE_TYPE: // Unsigned 64-bit integer + return EncodeFromSpan(data, isNullable, encoder); + case ZCL_INT8S_ATTRIBUTE_TYPE: // Signed 8-bit integer + return EncodeFromSpan(data, isNullable, encoder); + case ZCL_INT16S_ATTRIBUTE_TYPE: // Signed 16-bit integer + return EncodeFromSpan(data, isNullable, encoder); + case ZCL_INT24S_ATTRIBUTE_TYPE: // Signed 24-bit integer + return EncodeFromSpan>(data, isNullable, encoder); + case ZCL_INT32S_ATTRIBUTE_TYPE: // Signed 32-bit integer + return EncodeFromSpan(data, isNullable, encoder); + case ZCL_INT40S_ATTRIBUTE_TYPE: // Signed 40-bit integer + return EncodeFromSpan>(data, isNullable, encoder); + case ZCL_INT48S_ATTRIBUTE_TYPE: // Signed 48-bit integer + return EncodeFromSpan>(data, isNullable, encoder); + case ZCL_INT56S_ATTRIBUTE_TYPE: // Signed 56-bit integer + return EncodeFromSpan>(data, isNullable, encoder); + case ZCL_INT64S_ATTRIBUTE_TYPE: // Signed 64-bit integer + return EncodeFromSpan(data, isNullable, encoder); + case ZCL_SINGLE_ATTRIBUTE_TYPE: // 32-bit float + return EncodeFromSpan(data, isNullable, encoder); + case ZCL_DOUBLE_ATTRIBUTE_TYPE: // 64-bit float + return EncodeFromSpan(data, isNullable, encoder); + case ZCL_CHAR_STRING_ATTRIBUTE_TYPE: // Char string + return EncodeStringLike(data, isNullable, encoder); + case ZCL_LONG_CHAR_STRING_ATTRIBUTE_TYPE: + return EncodeStringLike(data, isNullable, encoder); + case ZCL_OCTET_STRING_ATTRIBUTE_TYPE: // Octet string + return EncodeStringLike(data, isNullable, encoder); + case ZCL_LONG_OCTET_STRING_ATTRIBUTE_TYPE: + return EncodeStringLike(data, isNullable, encoder); + default: + ChipLogError(DataManagement, "Attribute type 0x%x not handled", static_cast(metadata->attributeType)); + return CHIP_IM_GLOBAL_STATUS(UnsupportedRead); + } +} + +} // namespace + +/// separated-out ReadAttribute implementation (given existing complexity) +/// +/// Generally will: +/// - validate ACL (only for non-internal requests) +/// - Try to read attribute via the AttributeAccessInterface +/// - Try to read the value from ember RAM storage +CHIP_ERROR CodegenDataModel::ReadAttribute(const InteractionModel::ReadAttributeRequest & request, AttributeValueEncoder & encoder) +{ + ChipLogDetail(DataManagement, + "Reading attribute: Cluster=" ChipLogFormatMEI " Endpoint=0x%x AttributeId=" ChipLogFormatMEI " (expanded=%d)", + ChipLogValueMEI(request.path.mClusterId), request.path.mEndpointId, ChipLogValueMEI(request.path.mAttributeId), + request.path.mExpanded); + + // ACL check for non-internal requests + if (!request.operationFlags.Has(InteractionModel::OperationFlags::kInternal)) + { + ReturnErrorCodeIf(!request.subjectDescriptor.has_value(), CHIP_ERROR_INVALID_ARGUMENT); + + Access::RequestPath requestPath{ .cluster = request.path.mClusterId, .endpoint = request.path.mEndpointId }; + CHIP_ERROR err = Access::GetAccessControl().Check(*request.subjectDescriptor, requestPath, + RequiredPrivilege::ForReadAttribute(request.path)); + if (err != CHIP_NO_ERROR) + { + // Implementation of 8.4.3.2 of the spec for path expansion + if (request.path.mExpanded && (err == CHIP_ERROR_ACCESS_DENIED)) + { + return CHIP_NO_ERROR; + } + return err; + } + } + + auto metadata = FindAttributeMetadata(request.path); + + // Explicit failure in finding a suitable metadata + if (const CHIP_ERROR * err = std::get_if(&metadata)) + { + VerifyOrDie(*err != CHIP_NO_ERROR); + return *err; + } + + // Read via AAI + std::optional aai_result; + if (const EmberAfCluster ** cluster = std::get_if(&metadata)) + { + Compatibility::GlobalAttributeReader aai(*cluster); + aai_result = TryReadViaAccessInterface(request.path, &aai, encoder); + } + else + { + aai_result = TryReadViaAccessInterface( + request.path, GetAttributeAccessOverride(request.path.mEndpointId, request.path.mClusterId), encoder); + } + ReturnErrorCodeIf(aai_result.has_value(), *aai_result); + + if (!std::holds_alternative(metadata)) + { + // if we only got a cluster, this was for a global attribute. We cannot read ember attributes + // at this point, so give up (although GlobalAttributeReader should have returned something here). + chipDie(); + } + const EmberAfAttributeMetadata * attributeMetadata = std::get(metadata); + + // At this point, we have to use ember directly to read the data. + EmberAfAttributeSearchRecord record; + record.endpoint = request.path.mEndpointId; + record.clusterId = request.path.mClusterId; + record.attributeId = request.path.mAttributeId; + Protocols::InteractionModel::Status status = emAfReadOrWriteAttribute( + &record, &attributeMetadata, gEmberAttributeIOBufferSpan.data(), static_cast(gEmberAttributeIOBufferSpan.size()), + /* write = */ false); + + if (status != Protocols::InteractionModel::Status::Success) + { + return CHIP_ERROR_IM_GLOBAL_STATUS_VALUE(status); + } + + return EncodeEmberValue(gEmberAttributeIOBufferSpan, attributeMetadata, encoder); +} + +} // namespace app +} // namespace chip diff --git a/src/app/codegen-data-model/model.gni b/src/app/codegen-data-model/model.gni index c8dce5617b9a7b..3be7b2d2610513 100644 --- a/src/app/codegen-data-model/model.gni +++ b/src/app/codegen-data-model/model.gni @@ -24,12 +24,13 @@ import("//build_overrides/chip.gni") # As a result, the files here are NOT a source_set or similar because they cannot # be cleanly built as a stand-alone and instead have to be imported as part of # a different data model or compilation unit. -codegen_interaction_model_SOURCES = [ +codegen_data_model_SOURCES = [ "${chip_root}/src/app/codegen-data-model/CodegenDataModel.h", "${chip_root}/src/app/codegen-data-model/CodegenDataModel.cpp", + "${chip_root}/src/app/codegen-data-model/CodegenDataModel_Read.cpp", ] -codegen_interaction_model_PUBLIC_DEPS = [ +codegen_data_model_PUBLIC_DEPS = [ "${chip_root}/src/app/common:attribute-type", "${chip_root}/src/app/data-model-interface", ] diff --git a/src/app/codegen-data-model/tests/AttributeReportIBEncodeDecode.cpp b/src/app/codegen-data-model/tests/AttributeReportIBEncodeDecode.cpp new file mode 100644 index 00000000000000..29088d0cdc55e1 --- /dev/null +++ b/src/app/codegen-data-model/tests/AttributeReportIBEncodeDecode.cpp @@ -0,0 +1,130 @@ +/* + * 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 "AttributeReportIBEncodeDecode.h" + +#include +#include + +using namespace chip::app; + +namespace chip { +namespace Test { + +CHIP_ERROR DecodedAttributeData::DecodeFrom(const AttributeDataIB::Parser & parser) +{ + ReturnErrorOnFailure(parser.GetDataVersion(&dataVersion)); + + AttributePathIB::Parser pathParser; + ReturnErrorOnFailure(parser.GetPath(&pathParser)); + ReturnErrorOnFailure(pathParser.GetConcreteAttributePath(attributePath, AttributePathIB::ValidateIdRanges::kNo)); + ReturnErrorOnFailure(parser.GetData(&dataReader)); + + return CHIP_NO_ERROR; +} + +CHIP_ERROR DecodeAttributeReportIBs(ByteSpan data, std::vector & decoded_items) +{ + // Espected data format: + // CONTAINER (anonymous) + // 0x01 => Array (i.e. report data ib) + // ReportIB* + // + // Generally this is VERY hard to process ... + // + TLV::TLVReader reportIBsReader; + reportIBsReader.Init(data); + + ReturnErrorOnFailure(reportIBsReader.Next()); + if (reportIBsReader.GetType() != TLV::TLVType::kTLVType_Structure) + { + return CHIP_ERROR_INVALID_ARGUMENT; + } + TLV::TLVType outer1; + reportIBsReader.EnterContainer(outer1); + + ReturnErrorOnFailure(reportIBsReader.Next()); + if (reportIBsReader.GetType() != TLV::TLVType::kTLVType_Array) + { + return CHIP_ERROR_INVALID_ARGUMENT; + } + + TLV::TLVType outer2; + reportIBsReader.EnterContainer(outer2); + + CHIP_ERROR err = CHIP_NO_ERROR; + while (CHIP_NO_ERROR == (err = reportIBsReader.Next())) + { + TLV::TLVReader attributeReportReader = reportIBsReader; + AttributeReportIB::Parser attributeReportParser; + ReturnErrorOnFailure(attributeReportParser.Init(attributeReportReader)); + + AttributeDataIB::Parser dataParser; + // NOTE: to also grab statuses, use GetAttributeStatus and check for CHIP_END_OF_TLV + ReturnErrorOnFailure(attributeReportParser.GetAttributeData(&dataParser)); + + DecodedAttributeData decoded; + ReturnErrorOnFailure(decoded.DecodeFrom(dataParser)); + decoded_items.push_back(decoded); + } + + if ((CHIP_END_OF_TLV != err) && (err != CHIP_NO_ERROR)) + { + return CHIP_NO_ERROR; + } + + ReturnErrorOnFailure(reportIBsReader.ExitContainer(outer2)); + ReturnErrorOnFailure(reportIBsReader.ExitContainer(outer1)); + + err = reportIBsReader.Next(); + + if (CHIP_ERROR_END_OF_TLV == err) + { + return CHIP_NO_ERROR; + } + if (CHIP_NO_ERROR == err) + { + // This is NOT ok ... we have multiple things in our buffer? + return CHIP_ERROR_INVALID_ARGUMENT; + } + + return err; +} + +CHIP_ERROR EncodedReportIBs::StartEncoding(app::AttributeReportIBs::Builder & builder) +{ + mEncodeWriter.Init(mTlvDataBuffer); + ReturnErrorOnFailure(mEncodeWriter.StartContainer(TLV::AnonymousTag(), TLV::kTLVType_Structure, mOuterStructureType)); + return builder.Init(&mEncodeWriter, to_underlying(ReportDataMessage::Tag::kAttributeReportIBs)); +} + +CHIP_ERROR EncodedReportIBs::FinishEncoding(app::AttributeReportIBs::Builder & builder) +{ + builder.EndOfContainer(); + ReturnErrorOnFailure(mEncodeWriter.EndContainer(mOuterStructureType)); + ReturnErrorOnFailure(mEncodeWriter.Finalize()); + + mDecodeSpan = ByteSpan(mTlvDataBuffer, mEncodeWriter.GetLengthWritten()); + return CHIP_NO_ERROR; +} + +CHIP_ERROR EncodedReportIBs::Decode(std::vector & decoded_items) +{ + return DecodeAttributeReportIBs(mDecodeSpan, decoded_items); +} + +} // namespace Test +} // namespace chip diff --git a/src/app/codegen-data-model/tests/AttributeReportIBEncodeDecode.h b/src/app/codegen-data-model/tests/AttributeReportIBEncodeDecode.h new file mode 100644 index 00000000000000..83c079f810ccb8 --- /dev/null +++ b/src/app/codegen-data-model/tests/AttributeReportIBEncodeDecode.h @@ -0,0 +1,68 @@ +/* + * 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 +#include +#include +#include +#include +#include +#include + +#include + +namespace chip { +namespace Test { + +struct DecodedAttributeData +{ + chip::DataVersion dataVersion; + chip::app::ConcreteDataAttributePath attributePath; + chip::TLV::TLVReader dataReader; + + CHIP_ERROR DecodeFrom(const chip::app::AttributeDataIB::Parser & parser); +}; + +CHIP_ERROR DecodeAttributeReportIBs(ByteSpan data, std::vector & decoded_items); + +/// Maintains an internal TLV buffer for data encoding and +/// decoding for ReportIBs. +/// +/// Main use case is that explicit TLV layouts (structure and container starting) need to be +/// prepared to have a proper AttributeReportIBs::Builder/parser to exist. +class EncodedReportIBs +{ +public: + /// Initialize the report structures required to encode a + CHIP_ERROR StartEncoding(app::AttributeReportIBs::Builder & builder); + CHIP_ERROR FinishEncoding(app::AttributeReportIBs::Builder & builder); + + /// Decode the embedded attribute report IBs. + /// The TLVReaders inside data have a lifetime tied to the current object (its readers point + /// inside the current object) + CHIP_ERROR Decode(std::vector & decoded_items); + +private: + uint8_t mTlvDataBuffer[1024]; + TLV::TLVType mOuterStructureType; + TLV::TLVWriter mEncodeWriter; + ByteSpan mDecodeSpan; +}; + +} // namespace Test +} // namespace chip diff --git a/src/app/codegen-data-model/tests/BUILD.gn b/src/app/codegen-data-model/tests/BUILD.gn index a3857184c25b74..3d265a96a66b29 100644 --- a/src/app/codegen-data-model/tests/BUILD.gn +++ b/src/app/codegen-data-model/tests/BUILD.gn @@ -15,17 +15,41 @@ import("//build_overrides/chip.gni") import("${chip_root}/build/chip/chip_test_suite.gni") import("${chip_root}/src/app/codegen-data-model/model.gni") +source_set("ember_extra_files") { + sources = [ + # This IS TERRIBLE, however we want to pretend AAI exists for global + # items and we need a shared IO storage to reduce overhead between + # data-model access and ember-compatibility (we share the same buffer) + "${chip_root}/src/app/util/ember-global-attribute-access-interface.cpp", + "${chip_root}/src/app/util/ember-io-storage.cpp", + "AttributeReportIBEncodeDecode.cpp", + "AttributeReportIBEncodeDecode.h", + "EmberReadWriteOverride.cpp", + "EmberReadWriteOverride.h", + "InteractionModelTemporaryOverrides.cpp", + ] + + public_deps = [ + "${chip_root}/src/app/util/mock:mock_ember", + "${chip_root}/src/protocols", + ] +} + source_set("mock_model") { - sources = codegen_interaction_model_SOURCES + sources = codegen_data_model_SOURCES - public_deps = codegen_interaction_model_PUBLIC_DEPS + public_deps = codegen_data_model_PUBLIC_DEPS # this ties in the codegen model to an actual ember implementation - public_deps += [ "${chip_root}/src/app/util/mock:mock_ember" ] + public_deps += [ + ":ember_extra_files", + "${chip_root}/src/app/util/mock:mock_ember", + "${chip_root}/src/lib/core:string-builder-adapters", + ] } chip_test_suite("tests") { - output_name = "libCodegenInteractionModelTests" + output_name = "libCodegenDataModelTests" test_sources = [ "TestCodegenModelViaMocks.cpp" ] diff --git a/src/app/codegen-data-model/tests/EmberReadWriteOverride.cpp b/src/app/codegen-data-model/tests/EmberReadWriteOverride.cpp new file mode 100644 index 00000000000000..8c65ee29b05556 --- /dev/null +++ b/src/app/codegen-data-model/tests/EmberReadWriteOverride.cpp @@ -0,0 +1,87 @@ +/* + * 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 "EmberReadWriteOverride.h" + +#include + +using chip::Protocols::InteractionModel::Status; + +namespace { + +constexpr size_t kMaxTestIoSize = 128; + +uint8_t gEmberIoBuffer[kMaxTestIoSize]; +size_t gEmberIoBufferFill; +Status gEmberStatusCode = Status::InvalidAction; + +} // namespace + +namespace chip { +namespace Test { + +void SetEmberReadOutput(std::variant what) +{ + if (const chip::ByteSpan * span = std::get_if(&what)) + { + gEmberStatusCode = Status::Success; + + if (span->size() > sizeof(gEmberIoBuffer)) + { + ChipLogError(Test, "UNEXPECTED STATE: Too much data set for ember read output"); + gEmberStatusCode = Status::ResourceExhausted; + + return; + } + + memcpy(gEmberIoBuffer, span->data(), span->size()); + gEmberIoBufferFill = span->size(); + return; + } + + if (const Status * status = std::get_if(&what)) + { + gEmberIoBufferFill = 0; + gEmberStatusCode = *status; + return; + } + + ChipLogError(Test, "UNEXPECTED STATE: invalid ember read output setting"); + gEmberStatusCode = Status::InvalidAction; +} + +} // namespace Test +} // namespace chip + +/// TODO: this SHOULD be part of attribute-storage mocks and allow proper I/O control +/// with helpers for "ember encoding" +Status emAfReadOrWriteAttribute(const EmberAfAttributeSearchRecord * attRecord, const EmberAfAttributeMetadata ** metadata, + uint8_t * buffer, uint16_t readLength, bool write) +{ + if (gEmberStatusCode != Status::Success) + { + return gEmberStatusCode; + } + + if (gEmberIoBufferFill > readLength) + { + ChipLogError(Test, "Internal TEST error: insufficient output buffer space."); + return Status::ResourceExhausted; + } + + memcpy(buffer, gEmberIoBuffer, gEmberIoBufferFill); + return Status::Success; +} diff --git a/src/app/codegen-data-model/tests/EmberReadWriteOverride.h b/src/app/codegen-data-model/tests/EmberReadWriteOverride.h new file mode 100644 index 00000000000000..527a6cfd0d18c7 --- /dev/null +++ b/src/app/codegen-data-model/tests/EmberReadWriteOverride.h @@ -0,0 +1,33 @@ +/* + * 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 +#include + +#include + +namespace chip { +namespace Test { + +/// specify what the next `emAfReadOrWriteAttribute` will contain +/// +/// It may return a value with success or some error. The byte span WILL BE COPIED. +void SetEmberReadOutput(std::variant what); + +} // namespace Test +} // namespace chip diff --git a/src/app/codegen-data-model/tests/InteractionModelTemporaryOverrides.cpp b/src/app/codegen-data-model/tests/InteractionModelTemporaryOverrides.cpp new file mode 100644 index 00000000000000..868a26880d3ce7 --- /dev/null +++ b/src/app/codegen-data-model/tests/InteractionModelTemporaryOverrides.cpp @@ -0,0 +1,84 @@ +/* + * 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 +#include +#include +#include +#include +#include +#include + +using chip::Protocols::InteractionModel::Status; + +// TODO: most of the functions here are part of EmberCompatibilityFunctions and is NOT decoupled +// from IM current, but it SHOULD be +// Open issue https://github.com/project-chip/connectedhomeip/issues/34137 for this work. +namespace chip { +namespace app { + +bool ConcreteAttributePathExists(const ConcreteAttributePath & aPath) +{ + // TODO: this is just a noop which may be potentially invalid + return true; +} + +bool IsClusterDataVersionEqual(const ConcreteClusterPath & aConcreteClusterPath, DataVersion aRequiredVersion) +{ + // TODO: this is just a noop which may be potentially invalid + return true; +} + +const EmberAfAttributeMetadata * GetAttributeMetadata(const ConcreteAttributePath & aPath) +{ + return emberAfLocateAttributeMetadata(aPath.mEndpointId, aPath.mClusterId, aPath.mAttributeId); +} + +Status ServerClusterCommandExists(const ConcreteCommandPath & aCommandPath) +{ + // TODO: this is just a noop which may be potentially invalid + return Status::Success; +} + +Status CheckEventSupportStatus(const ConcreteEventPath & aPath) +{ + // TODO: this is just a noop which may be potentially invalid + return Status::Success; +} + +CHIP_ERROR WriteSingleClusterData(const Access::SubjectDescriptor & aSubjectDescriptor, const ConcreteDataAttributePath & aPath, + TLV::TLVReader & aReader, WriteHandler * apWriteHandler) +{ + // this is just to get things to compile. eventually this method should NOT be used + return CHIP_ERROR_NOT_IMPLEMENTED; +} + +CHIP_ERROR ReadSingleClusterData(const Access::SubjectDescriptor & aSubjectDescriptor, bool aIsFabricFiltered, + const ConcreteReadAttributePath & aPath, AttributeReportIBs::Builder & aAttributeReports, + AttributeEncodeState * apEncoderState) +{ + // this is just to get things to compile. eventually this method should NOT be used + return CHIP_ERROR_NOT_IMPLEMENTED; +} + +void DispatchSingleClusterCommand(const ConcreteCommandPath & aRequestCommandPath, chip::TLV::TLVReader & aReader, + CommandHandler * apCommandObj) +{ + // TODO: total hardcoded noop +} + +} // namespace app +} // namespace chip diff --git a/src/app/codegen-data-model/tests/TestCodegenModelViaMocks.cpp b/src/app/codegen-data-model/tests/TestCodegenModelViaMocks.cpp index 4403303c70cbb3..11264bbd33a36e 100644 --- a/src/app/codegen-data-model/tests/TestCodegenModelViaMocks.cpp +++ b/src/app/codegen-data-model/tests/TestCodegenModelViaMocks.cpp @@ -1,5 +1,4 @@ /* - * * Copyright (c) 2024 Project CHIP Authors * All rights reserved. * @@ -17,12 +16,35 @@ */ #include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include #include +#include +#include #include +#include +#include +#include +#include #include +#include using namespace chip; using namespace chip::Test; @@ -32,6 +54,9 @@ using namespace chip::app::Clusters::Globals::Attributes; namespace { +constexpr FabricIndex kTestFabrixIndex = kMinValidFabricIndex; +constexpr NodeId kTestNodeId = 0xFFFF'1234'ABCD'4321; + constexpr EndpointId kEndpointIdThatIsMissing = kMockEndpointMin - 1; static_assert(kEndpointIdThatIsMissing != kInvalidEndpointId); @@ -39,6 +64,85 @@ static_assert(kEndpointIdThatIsMissing != kMockEndpoint1); static_assert(kEndpointIdThatIsMissing != kMockEndpoint2); static_assert(kEndpointIdThatIsMissing != kMockEndpoint3); +constexpr Access::SubjectDescriptor kAdminSubjectDescriptor{ + .fabricIndex = kTestFabrixIndex, + .authMode = Access::AuthMode::kCase, + .subject = kTestNodeId, +}; +constexpr Access::SubjectDescriptor kViewSubjectDescriptor{ + .fabricIndex = kTestFabrixIndex + 1, + .authMode = Access::AuthMode::kCase, + .subject = kTestNodeId, +}; + +constexpr Access::SubjectDescriptor kDenySubjectDescriptor{ + .fabricIndex = kTestFabrixIndex + 2, + .authMode = Access::AuthMode::kCase, + .subject = kTestNodeId, +}; + +bool operator==(const Access::SubjectDescriptor & a, const Access::SubjectDescriptor & b) +{ + if (a.fabricIndex != b.fabricIndex) + { + return false; + } + if (a.authMode != b.authMode) + { + return false; + } + if (a.subject != b.subject) + { + return false; + } + for (unsigned i = 0; i < a.cats.values.size(); i++) + { + if (a.cats.values[i] != b.cats.values[i]) + { + return false; + } + } + return true; +} + +class MockAccessControl : public Access::AccessControl::Delegate, public Access::AccessControl::DeviceTypeResolver +{ +public: + CHIP_ERROR Check(const Access::SubjectDescriptor & subjectDescriptor, const Access::RequestPath & requestPath, + Access::Privilege requestPrivilege) override + { + if (subjectDescriptor == kAdminSubjectDescriptor) + { + return CHIP_NO_ERROR; + } + if ((subjectDescriptor == kViewSubjectDescriptor) && (requestPrivilege == Access::Privilege::kView)) + { + return CHIP_NO_ERROR; + } + return CHIP_ERROR_ACCESS_DENIED; + } + + bool IsDeviceTypeOnEndpoint(DeviceTypeId deviceType, EndpointId endpoint) override { return true; } +}; + +class ScopedMockAccessControl +{ +public: + ScopedMockAccessControl() { Access::GetAccessControl().Init(&mMock, mMock); } + ~ScopedMockAccessControl() { Access::GetAccessControl().Finish(); } + +private: + MockAccessControl mMock; +}; + +#define MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(zcl_type) MockAttributeId(zcl_type + 0x1000) +#define MOCK_ATTRIBUTE_CONFIG_NULLABLE(zcl_type) \ + MockAttributeConfig(MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(zcl_type), zcl_type, ATTRIBUTE_MASK_WRITABLE | ATTRIBUTE_MASK_NULLABLE) + +#define MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(zcl_type) MockAttributeId(zcl_type + 0x2000) +#define MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(zcl_type) \ + MockAttributeConfig(MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(zcl_type), zcl_type, ATTRIBUTE_MASK_WRITABLE) + // clang-format off const MockNodeConfig gTestNodeConfig({ MockEndpointConfig(kMockEndpoint1, { @@ -88,7 +192,155 @@ const MockNodeConfig gTestNodeConfig({ ClusterRevision::Id, FeatureMap::Id, }), MockClusterConfig(MockClusterId(4), { - ClusterRevision::Id, FeatureMap::Id, + ClusterRevision::Id, + FeatureMap::Id, + // several attributes of varying data types for testing. + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_BOOLEAN_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_BITMAP8_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_BITMAP16_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_BITMAP32_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_BITMAP64_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_INT8U_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_INT16U_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_INT24U_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_INT32U_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_INT40U_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_INT48U_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_INT56U_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_INT64U_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_INT8S_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_INT16S_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_INT24S_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_INT32S_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_INT40S_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_INT48S_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_INT56S_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_INT64S_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_ENUM8_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_ENUM16_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_PRIORITY_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_STATUS_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_SINGLE_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_DOUBLE_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_OCTET_STRING_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_CHAR_STRING_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_LONG_OCTET_STRING_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_LONG_CHAR_STRING_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_ARRAY_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_STRUCT_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_GROUP_ID_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_ENDPOINT_NO_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_VENDOR_ID_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_DEVTYPE_ID_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_FABRIC_ID_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_FABRIC_IDX_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_ENTRY_IDX_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_DATA_VER_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_EVENT_NO_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_SEMTAG_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_NAMESPACE_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_TAG_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_SYSTIME_US_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_SYSTIME_MS_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_ELAPSED_S_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_TEMPERATURE_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_POWER_MW_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_AMPERAGE_MA_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_VOLTAGE_MV_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_ENERGY_MWH_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_TOD_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_DATE_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_EPOCH_US_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_EPOCH_S_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_POSIX_MS_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_PERCENT_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_PERCENT100THS_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_CLUSTER_ID_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_ATTRIB_ID_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_FIELD_ID_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_EVENT_ID_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_COMMAND_ID_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_ACTION_ID_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_TRANS_ID_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_NODE_ID_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_IPADR_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_IPV4ADR_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_IPV6ADR_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_IPV6PRE_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_HWADR_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_BOOLEAN_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_BITMAP8_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_BITMAP16_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_BITMAP32_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_BITMAP64_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_INT8U_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_INT16U_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_INT24U_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_INT32U_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_INT40U_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_INT48U_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_INT56U_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_INT64U_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_INT8S_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_INT16S_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_INT24S_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_INT32S_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_INT40S_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_INT48S_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_INT56S_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_INT64S_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_ENUM8_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_ENUM16_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_PRIORITY_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_STATUS_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_SINGLE_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_DOUBLE_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_OCTET_STRING_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_CHAR_STRING_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_LONG_OCTET_STRING_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_LONG_CHAR_STRING_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_ARRAY_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_STRUCT_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_GROUP_ID_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_ENDPOINT_NO_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_VENDOR_ID_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_DEVTYPE_ID_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_FABRIC_ID_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_FABRIC_IDX_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_ENTRY_IDX_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_DATA_VER_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_EVENT_NO_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_SEMTAG_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_NAMESPACE_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_TAG_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_SYSTIME_US_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_SYSTIME_MS_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_ELAPSED_S_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_TEMPERATURE_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_POWER_MW_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_AMPERAGE_MA_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_VOLTAGE_MV_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_ENERGY_MWH_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_TOD_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_DATE_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_EPOCH_US_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_EPOCH_S_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_POSIX_MS_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_PERCENT_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_PERCENT100THS_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_CLUSTER_ID_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_ATTRIB_ID_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_FIELD_ID_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_EVENT_ID_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_COMMAND_ID_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_ACTION_ID_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_TRANS_ID_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_NODE_ID_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_IPADR_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_IPV4ADR_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_IPV6ADR_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_IPV6PRE_ATTRIBUTE_TYPE), + MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_HWADR_ATTRIBUTE_TYPE), }), }), }); @@ -100,6 +352,253 @@ struct UseMockNodeConfig ~UseMockNodeConfig() { ResetMockNodeConfig(); } }; +template +CHIP_ERROR DecodeList(TLV::TLVReader & reader, std::vector & out) +{ + TLV::TLVType outer; + ReturnErrorOnFailure(reader.EnterContainer(outer)); + while (true) + { + CHIP_ERROR err = reader.Next(); + + if (err == CHIP_END_OF_TLV) + { + return CHIP_NO_ERROR; + } + ReturnErrorOnFailure(err); + + T value; + ReturnErrorOnFailure(chip::app::DataModel::Decode(reader, value)); + out.emplace_back(std::move(value)); + } +} + +class UnsupportedReadAccessInterface : public AttributeAccessInterface +{ +public: + UnsupportedReadAccessInterface(ConcreteAttributePath path) : + AttributeAccessInterface(MakeOptional(path.mEndpointId), path.mClusterId), mPath(path) + {} + ~UnsupportedReadAccessInterface() = default; + + CHIP_ERROR Read(const ConcreteReadAttributePath & path, AttributeValueEncoder & encoder) override + { + if (static_cast(path) != mPath) + { + // returning without trying to handle means "I do not handle this" + return CHIP_NO_ERROR; + } + + return CHIP_IM_GLOBAL_STATUS(UnsupportedRead); + } + +private: + ConcreteAttributePath mPath; +}; + +class StructAttributeAccessInterface : public AttributeAccessInterface +{ +public: + StructAttributeAccessInterface(ConcreteAttributePath path) : + AttributeAccessInterface(MakeOptional(path.mEndpointId), path.mClusterId), mPath(path) + {} + ~StructAttributeAccessInterface() = default; + + CHIP_ERROR Read(const ConcreteReadAttributePath & path, AttributeValueEncoder & encoder) override + { + if (static_cast(path) != mPath) + { + // returning without trying to handle means "I do not handle this" + return CHIP_NO_ERROR; + } + + return encoder.Encode(mData); + } + + void SetReturnedData(const Clusters::UnitTesting::Structs::SimpleStruct::Type & data) { mData = data; } + Clusters::UnitTesting::Structs::SimpleStruct::Type simpleStruct; + +private: + ConcreteAttributePath mPath; + Clusters::UnitTesting::Structs::SimpleStruct::Type mData; +}; + +class ListAttributeAcessInterface : public AttributeAccessInterface +{ +public: + ListAttributeAcessInterface(ConcreteAttributePath path) : + AttributeAccessInterface(MakeOptional(path.mEndpointId), path.mClusterId), mPath(path) + {} + ~ListAttributeAcessInterface() = default; + + CHIP_ERROR Read(const ConcreteReadAttributePath & path, AttributeValueEncoder & encoder) override + { + if (static_cast(path) != mPath) + { + // returning without trying to handle means "I do not handle this" + return CHIP_NO_ERROR; + } + + return encoder.EncodeList([this](const auto & listEncoder) { + for (unsigned i = 0; i < mCount; i++) + { + mData.a = static_cast(i % 0xFF); + ReturnErrorOnFailure(listEncoder.Encode(mData)); + } + return CHIP_NO_ERROR; + }); + } + + void SetReturnedData(const Clusters::UnitTesting::Structs::SimpleStruct::Type & data) { mData = data; } + void SetReturnedDataCount(unsigned count) { mCount = count; } + Clusters::UnitTesting::Structs::SimpleStruct::Type simpleStruct; + +private: + ConcreteAttributePath mPath; + Clusters::UnitTesting::Structs::SimpleStruct::Type mData; + unsigned mCount = 0; +}; + +/// RAII registration of an attribute access interface +template +class RegisteredAttributeAccessInterface +{ +public: + template + RegisteredAttributeAccessInterface(Args &&... args) : mData(std::forward(args)...) + { + VerifyOrDie(registerAttributeAccessOverride(&mData)); + } + ~RegisteredAttributeAccessInterface() { unregisterAttributeAccessOverride(&mData); } + + T * operator->() { return &mData; } + T & operator*() { return mData; } + +private: + T mData; +}; + +/// Contains a `ReadAttributeRequest` as well as classes to convert this into a AttributeReportIBs +/// and later decode it +/// +/// It wraps boilerplate code to obtain a `AttributeValueEncoder` as well as later decoding +/// the underlying encoded data for verification. +struct TestReadRequest +{ + ReadAttributeRequest request; + + // encoded-used classes + EncodedReportIBs encodedIBs; + AttributeReportIBs::Builder reportBuilder; + std::unique_ptr encoder; + + TestReadRequest(const Access::SubjectDescriptor & subject, const ConcreteAttributePath & path) + { + // operationFlags is 0 i.e. not internal + // readFlags is 0 i.e. not fabric filtered + // dataVersion is missing (no data version filtering) + request.subjectDescriptor = subject; + request.path = path; + } + + std::unique_ptr StartEncoding(chip::app::InteractionModel::DataModel * model, + AttributeEncodeState state = AttributeEncodeState()) + { + std::optional info = model->GetClusterInfo(request.path); + if (!info.has_value()) + { + ChipLogError(Test, "Missing cluster information - no data version"); + return nullptr; + } + + DataVersion dataVersion = info->dataVersion; // NOLINT(bugprone-unchecked-optional-access) + + CHIP_ERROR err = encodedIBs.StartEncoding(reportBuilder); + if (err != CHIP_NO_ERROR) + { + ChipLogError(Test, "FAILURE starting encoding %" CHIP_ERROR_FORMAT, err.Format()); + return nullptr; + } + + // TODO: could we test isFabricFiltered and EncodeState? + + // request.subjectDescriptor is known non-null because it is set in the constructor + // NOLINTNEXTLINE(bugprone-unchecked-optional-access) + return std::make_unique(reportBuilder, *request.subjectDescriptor, request.path, dataVersion, + false /* aIsFabricFiltered */, state); + } + + CHIP_ERROR FinishEncoding() { return encodedIBs.FinishEncoding(reportBuilder); } +}; + +template +void TestEmberScalarTypeRead(typename NumericAttributeTraits::WorkingType value) +{ + UseMockNodeConfig config(gTestNodeConfig); + chip::app::CodegenDataModel model; + ScopedMockAccessControl accessControl; + + TestReadRequest testRequest( + kAdminSubjectDescriptor, + ConcreteAttributePath(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZclType))); + + // Ember encoding for integers is IDENTICAL to the in-memory representation for them + typename NumericAttributeTraits::StorageType storage; + NumericAttributeTraits::WorkingToStorage(value, storage); + chip::Test::SetEmberReadOutput(ByteSpan(reinterpret_cast(&storage), sizeof(storage))); + + // Data read via the encoder + std::unique_ptr encoder = testRequest.StartEncoding(&model); + ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), CHIP_NO_ERROR); + ASSERT_EQ(testRequest.FinishEncoding(), CHIP_NO_ERROR); + + // Validate after read + std::vector attribute_data; + ASSERT_EQ(testRequest.encodedIBs.Decode(attribute_data), CHIP_NO_ERROR); + ASSERT_EQ(attribute_data.size(), 1u); + + DecodedAttributeData & encodedData = attribute_data[0]; + ASSERT_EQ(encodedData.attributePath, testRequest.request.path); + + typename NumericAttributeTraits::WorkingType actual; + ASSERT_EQ(chip::app::DataModel::Decode::WorkingType>(encodedData.dataReader, actual), + CHIP_NO_ERROR); + ASSERT_EQ(actual, value); +} + +template +void TestEmberScalarNullRead() +{ + UseMockNodeConfig config(gTestNodeConfig); + chip::app::CodegenDataModel model; + ScopedMockAccessControl accessControl; + + TestReadRequest testRequest( + kAdminSubjectDescriptor, + ConcreteAttributePath(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZclType))); + + // Ember encoding for integers is IDENTICAL to the in-memory representation for them + typename NumericAttributeTraits::StorageType nullValue; + NumericAttributeTraits::SetNull(nullValue); + chip::Test::SetEmberReadOutput(ByteSpan(reinterpret_cast(&nullValue), sizeof(nullValue))); + + // Data read via the encoder + std::unique_ptr encoder = testRequest.StartEncoding(&model); + ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), CHIP_NO_ERROR); + ASSERT_EQ(testRequest.FinishEncoding(), CHIP_NO_ERROR); + + // Validate after read + std::vector attribute_data; + ASSERT_EQ(testRequest.encodedIBs.Decode(attribute_data), CHIP_NO_ERROR); + ASSERT_EQ(attribute_data.size(), 1u); + + DecodedAttributeData & encodedData = attribute_data[0]; + ASSERT_EQ(encodedData.attributePath, testRequest.request.path); + chip::app::DataModel::Nullable::WorkingType> actual; + ASSERT_EQ(chip::app::DataModel::Decode(encodedData.dataReader, actual), CHIP_NO_ERROR); + ASSERT_TRUE(actual.IsNull()); +} + } // namespace TEST(TestCodegenModelViaMocks, IterateOverEndpoints) @@ -486,3 +985,685 @@ TEST(TestCodegenModelViaMocks, IterateOverGeneratedCommands) EXPECT_FALSE(path.HasValidIds()); } } + +TEST(TestCodegenModelViaMocks, EmberAttributeReadAclDeny) +{ + UseMockNodeConfig config(gTestNodeConfig); + chip::app::CodegenDataModel model; + ScopedMockAccessControl accessControl; + + TestReadRequest testRequest(kDenySubjectDescriptor, + ConcreteAttributePath(kMockEndpoint1, MockClusterId(1), MockAttributeId(10))); + std::unique_ptr encoder = testRequest.StartEncoding(&model); + + ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), CHIP_ERROR_ACCESS_DENIED); +} + +TEST(TestCodegenModelViaMocks, ReadForInvalidGlobalAttributePath) +{ + UseMockNodeConfig config(gTestNodeConfig); + chip::app::CodegenDataModel model; + ScopedMockAccessControl accessControl; + + { + TestReadRequest testRequest(kAdminSubjectDescriptor, + ConcreteAttributePath(kEndpointIdThatIsMissing, MockClusterId(1), AttributeList::Id)); + std::unique_ptr encoder = testRequest.StartEncoding(&model); + ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), CHIP_IM_GLOBAL_STATUS(UnsupportedEndpoint)); + } + + { + TestReadRequest testRequest(kAdminSubjectDescriptor, + ConcreteAttributePath(kMockEndpoint1, kInvalidClusterId, AttributeList::Id)); + std::unique_ptr encoder = testRequest.StartEncoding(&model); + ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), CHIP_IM_GLOBAL_STATUS(UnsupportedCluster)); + } +} + +TEST(TestCodegenModelViaMocks, EmberAttributeInvalidRead) +{ + UseMockNodeConfig config(gTestNodeConfig); + chip::app::CodegenDataModel model; + ScopedMockAccessControl accessControl; + + // Invalid attribute + { + TestReadRequest testRequest(kAdminSubjectDescriptor, + ConcreteAttributePath(kMockEndpoint1, MockClusterId(1), MockAttributeId(10))); + std::unique_ptr encoder = testRequest.StartEncoding(&model); + + ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), CHIP_IM_GLOBAL_STATUS(UnsupportedAttribute)); + } + + // Invalid cluster + { + TestReadRequest testRequest(kAdminSubjectDescriptor, + ConcreteAttributePath(kMockEndpoint1, MockClusterId(100), MockAttributeId(1))); + std::unique_ptr encoder = testRequest.StartEncoding(&model); + + ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), CHIP_IM_GLOBAL_STATUS(UnsupportedCluster)); + } + + // Invalid endpoint + { + TestReadRequest testRequest(kAdminSubjectDescriptor, + ConcreteAttributePath(kEndpointIdThatIsMissing, MockClusterId(1), MockAttributeId(1))); + std::unique_ptr encoder = testRequest.StartEncoding(&model); + + ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), CHIP_IM_GLOBAL_STATUS(UnsupportedEndpoint)); + } +} + +TEST(TestCodegenModelViaMocks, EmberAttributePathExpansionAccessDeniedRead) +{ + UseMockNodeConfig config(gTestNodeConfig); + chip::app::CodegenDataModel model; + ScopedMockAccessControl accessControl; + + TestReadRequest testRequest(kDenySubjectDescriptor, + ConcreteAttributePath(kMockEndpoint1, MockClusterId(1), MockAttributeId(10))); + std::unique_ptr encoder = testRequest.StartEncoding(&model); + + testRequest.request.path.mExpanded = true; + + // For expanded paths, access control failures succeed without encoding anything + // This is temporary until ACL checks are moved inside the IM/ReportEngine + ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), CHIP_NO_ERROR); + ASSERT_FALSE(encoder->TriedEncode()); +} + +TEST(TestCodegenModelViaMocks, AccessInterfaceUnsupportedRead) +{ + UseMockNodeConfig config(gTestNodeConfig); + chip::app::CodegenDataModel model; + ScopedMockAccessControl accessControl; + + const ConcreteAttributePath kTestPath(kMockEndpoint3, MockClusterId(4), + MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_STRUCT_ATTRIBUTE_TYPE)); + + TestReadRequest testRequest(kAdminSubjectDescriptor, kTestPath); + RegisteredAttributeAccessInterface aai(kTestPath); + + testRequest.request.path.mExpanded = true; + + // For expanded paths, unsupported read from AAI (i.e. reading write-only data) + // succeed without attempting to encode. + // This is temporary until ACL checks are moved inside the IM/ReportEngine + std::unique_ptr encoder = testRequest.StartEncoding(&model); + ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), CHIP_NO_ERROR); + ASSERT_FALSE(encoder->TriedEncode()); +} + +TEST(TestCodegenModelViaMocks, EmberAttributeReadInt32S) +{ + TestEmberScalarTypeRead(-1234); +} + +TEST(TestCodegenModelViaMocks, EmberAttributeReadEnum16) +{ + TestEmberScalarTypeRead(0x1234); +} + +TEST(TestCodegenModelViaMocks, EmberAttributeReadFloat) +{ + TestEmberScalarTypeRead(0.625); +} + +TEST(TestCodegenModelViaMocks, EmberAttributeReadDouble) +{ + TestEmberScalarTypeRead(0.625); +} + +TEST(TestCodegenModelViaMocks, EmberAttributeReadInt24U) +{ + TestEmberScalarTypeRead, ZCL_INT24U_ATTRIBUTE_TYPE>(0x1234AB); +} + +TEST(TestCodegenModelViaMocks, EmberAttributeReadInt32U) +{ + TestEmberScalarTypeRead(0x1234ABCD); +} + +TEST(TestCodegenModelViaMocks, EmberAttributeReadInt40U) +{ + TestEmberScalarTypeRead, ZCL_INT40U_ATTRIBUTE_TYPE>(0x1122334455); +} + +TEST(TestCodegenModelViaMocks, EmberAttributeReadInt48U) +{ + TestEmberScalarTypeRead, ZCL_INT48U_ATTRIBUTE_TYPE>(0xAABB11223344); +} + +TEST(TestCodegenModelViaMocks, EmberAttributeReadInt56U) +{ + TestEmberScalarTypeRead, ZCL_INT56U_ATTRIBUTE_TYPE>(0xAABB11223344); +} + +TEST(TestCodegenModelViaMocks, EmberAttributeReadBool) +{ + TestEmberScalarTypeRead(true); + TestEmberScalarTypeRead(false); +} + +TEST(TestCodegenModelViaMocks, EmberAttributeReadInt8U) +{ + TestEmberScalarTypeRead(0x12); +} + +TEST(TestCodegenModelViaMocks, EmberAttributeReadNulls) +{ + TestEmberScalarNullRead(); + TestEmberScalarNullRead(); + TestEmberScalarNullRead, ZCL_INT24U_ATTRIBUTE_TYPE>(); + TestEmberScalarNullRead(); + TestEmberScalarNullRead, ZCL_INT40U_ATTRIBUTE_TYPE>(); + TestEmberScalarNullRead, ZCL_INT48U_ATTRIBUTE_TYPE>(); + TestEmberScalarNullRead, ZCL_INT56U_ATTRIBUTE_TYPE>(); + TestEmberScalarNullRead(); + + TestEmberScalarNullRead(); + TestEmberScalarNullRead(); + TestEmberScalarNullRead, ZCL_INT24S_ATTRIBUTE_TYPE>(); + TestEmberScalarNullRead(); + TestEmberScalarNullRead, ZCL_INT40S_ATTRIBUTE_TYPE>(); + TestEmberScalarNullRead, ZCL_INT48S_ATTRIBUTE_TYPE>(); + TestEmberScalarNullRead, ZCL_INT56S_ATTRIBUTE_TYPE>(); + TestEmberScalarNullRead(); + + TestEmberScalarNullRead(); + + TestEmberScalarNullRead(); + TestEmberScalarNullRead(); +} + +TEST(TestCodegenModelViaMocks, EmberAttributeReadErrorReading) +{ + UseMockNodeConfig config(gTestNodeConfig); + chip::app::CodegenDataModel model; + ScopedMockAccessControl accessControl; + + { + TestReadRequest testRequest( + kAdminSubjectDescriptor, + ConcreteAttributePath(kMockEndpoint3, MockClusterId(4), + MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZCL_LONG_OCTET_STRING_ATTRIBUTE_TYPE))); + + chip::Test::SetEmberReadOutput(Protocols::InteractionModel::Status::Failure); + + // Actual read via an encoder + std::unique_ptr encoder = testRequest.StartEncoding(&model); + ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), CHIP_IM_GLOBAL_STATUS(Failure)); + } + + { + TestReadRequest testRequest( + kAdminSubjectDescriptor, + ConcreteAttributePath(kMockEndpoint3, MockClusterId(4), + MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZCL_LONG_OCTET_STRING_ATTRIBUTE_TYPE))); + + chip::Test::SetEmberReadOutput(Protocols::InteractionModel::Status::Busy); + + // Actual read via an encoder + std::unique_ptr encoder = testRequest.StartEncoding(&model); + ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), CHIP_IM_GLOBAL_STATUS(Busy)); + } +} + +TEST(TestCodegenModelViaMocks, EmberAttributeReadNullOctetString) +{ + UseMockNodeConfig config(gTestNodeConfig); + chip::app::CodegenDataModel model; + ScopedMockAccessControl accessControl; + + TestReadRequest testRequest(kAdminSubjectDescriptor, + ConcreteAttributePath(kMockEndpoint3, MockClusterId(4), + MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZCL_LONG_OCTET_STRING_ATTRIBUTE_TYPE))); + + // NOTE: This is a pascal string of size 0xFFFF which for null strings is a null marker + char data[] = "\xFF\xFFInvalid length string is null"; + chip::Test::SetEmberReadOutput(ByteSpan(reinterpret_cast(data), sizeof(data))); + + // Actual read via an encoder + std::unique_ptr encoder = testRequest.StartEncoding(&model); + ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), CHIP_NO_ERROR); + ASSERT_EQ(testRequest.FinishEncoding(), CHIP_NO_ERROR); + + // Validate after read + std::vector attribute_data; + ASSERT_EQ(testRequest.encodedIBs.Decode(attribute_data), CHIP_NO_ERROR); + ASSERT_EQ(attribute_data.size(), 1u); + + DecodedAttributeData & encodedData = attribute_data[0]; + ASSERT_EQ(encodedData.attributePath, testRequest.request.path); + + // data element should be null for the given 0xFFFF length + ASSERT_EQ(encodedData.dataReader.GetType(), TLV::kTLVType_Null); + + chip::app::DataModel::Nullable actual; + ASSERT_EQ(chip::app::DataModel::Decode(encodedData.dataReader, actual), CHIP_NO_ERROR); + ASSERT_TRUE(actual.IsNull()); +} + +TEST(TestCodegenModelViaMocks, EmberAttributeReadOctetString) +{ + UseMockNodeConfig config(gTestNodeConfig); + chip::app::CodegenDataModel model; + ScopedMockAccessControl accessControl; + + TestReadRequest testRequest( + kAdminSubjectDescriptor, + ConcreteAttributePath(kMockEndpoint3, MockClusterId(4), + MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_LONG_OCTET_STRING_ATTRIBUTE_TYPE))); + + // NOTE: This is a pascal string, so actual data is "test" + // the longer encoding is to make it clear we do not encode the overflow + char data[] = "\0\0testing here with overflow"; + uint16_t len = 4; + memcpy(data, &len, sizeof(uint16_t)); + chip::Test::SetEmberReadOutput(ByteSpan(reinterpret_cast(data), sizeof(data))); + + // Actual read via an encoder + std::unique_ptr encoder = testRequest.StartEncoding(&model); + ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), CHIP_NO_ERROR); + ASSERT_EQ(testRequest.FinishEncoding(), CHIP_NO_ERROR); + + // Validate after read + std::vector attribute_data; + ASSERT_EQ(testRequest.encodedIBs.Decode(attribute_data), CHIP_NO_ERROR); + ASSERT_EQ(attribute_data.size(), 1u); + + const DecodedAttributeData & encodedData = attribute_data[0]; + ASSERT_EQ(encodedData.attributePath, testRequest.request.path); + + // data element should be a encoded byte string as this is what the attribute type is + ASSERT_EQ(encodedData.dataReader.GetType(), TLV::kTLVType_ByteString); + ByteSpan actual; + ASSERT_EQ(encodedData.dataReader.Get(actual), CHIP_NO_ERROR); + + ByteSpan expected(reinterpret_cast(data + 2), 4); + ASSERT_TRUE(actual.data_equal(expected)); +} + +TEST(TestCodegenModelViaMocks, EmberAttributeReadLongOctetString) +{ + UseMockNodeConfig config(gTestNodeConfig); + chip::app::CodegenDataModel model; + ScopedMockAccessControl accessControl; + + TestReadRequest testRequest(kAdminSubjectDescriptor, + ConcreteAttributePath(kMockEndpoint3, MockClusterId(4), + MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_OCTET_STRING_ATTRIBUTE_TYPE))); + + // NOTE: This is a pascal string, so actual data is "test" + // the longer encoding is to make it clear we do not encode the overflow + const char data[] = "\x04testing here with overflow"; + chip::Test::SetEmberReadOutput(ByteSpan(reinterpret_cast(data), sizeof(data))); + + // Actual read via an encoder + std::unique_ptr encoder = testRequest.StartEncoding(&model); + ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), CHIP_NO_ERROR); + ASSERT_EQ(testRequest.FinishEncoding(), CHIP_NO_ERROR); + + // Validate after read + std::vector attribute_data; + ASSERT_EQ(testRequest.encodedIBs.Decode(attribute_data), CHIP_NO_ERROR); + ASSERT_EQ(attribute_data.size(), 1u); + + const DecodedAttributeData & encodedData = attribute_data[0]; + ASSERT_EQ(encodedData.attributePath, testRequest.request.path); + + // data element should be a encoded byte string as this is what the attribute type is + ASSERT_EQ(encodedData.dataReader.GetType(), TLV::kTLVType_ByteString); + ByteSpan actual; + ASSERT_EQ(encodedData.dataReader.Get(actual), CHIP_NO_ERROR); + + ByteSpan expected(reinterpret_cast(data + 1), 4); + ASSERT_TRUE(actual.data_equal(expected)); +} + +TEST(TestCodegenModelViaMocks, EmberAttributeReadShortString) +{ + UseMockNodeConfig config(gTestNodeConfig); + chip::app::CodegenDataModel model; + ScopedMockAccessControl accessControl; + + TestReadRequest testRequest(kAdminSubjectDescriptor, + ConcreteAttributePath(kMockEndpoint3, MockClusterId(4), + MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_CHAR_STRING_ATTRIBUTE_TYPE))); + + // NOTE: This is a pascal string, so actual data is "abcde" + // the longer encoding is to make it clear we do not encode the overflow + char data[] = "\0abcdef...this is the alphabet"; + uint16_t len = 5; + memcpy(data, &len, sizeof(uint8_t)); + chip::Test::SetEmberReadOutput(ByteSpan(reinterpret_cast(data), sizeof(data))); + + // Actual read via an encoder + std::unique_ptr encoder = testRequest.StartEncoding(&model); + ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), CHIP_NO_ERROR); + ASSERT_EQ(testRequest.FinishEncoding(), CHIP_NO_ERROR); + + // Validate after reading + std::vector attribute_data; + ASSERT_EQ(testRequest.encodedIBs.Decode(attribute_data), CHIP_NO_ERROR); + ASSERT_EQ(attribute_data.size(), 1u); + + const DecodedAttributeData & encodedData = attribute_data[0]; + ASSERT_EQ(encodedData.attributePath, testRequest.request.path); + + // data element should be a encoded byte string as this is what the attribute type is + ASSERT_EQ(encodedData.dataReader.GetType(), TLV::kTLVType_UTF8String); + CharSpan actual; + ASSERT_EQ(encodedData.dataReader.Get(actual), CHIP_NO_ERROR); + ASSERT_TRUE(actual.data_equal("abcde"_span)); +} + +TEST(TestCodegenModelViaMocks, EmberAttributeReadLongString) +{ + UseMockNodeConfig config(gTestNodeConfig); + chip::app::CodegenDataModel model; + ScopedMockAccessControl accessControl; + + TestReadRequest testRequest( + kAdminSubjectDescriptor, + ConcreteAttributePath(kMockEndpoint3, MockClusterId(4), + MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_LONG_CHAR_STRING_ATTRIBUTE_TYPE))); + + // NOTE: This is a pascal string, so actual data is "abcde" + // the longer encoding is to make it clear we do not encode the overflow + char data[] = "\0\0abcdef...this is the alphabet"; + uint16_t len = 5; + memcpy(data, &len, sizeof(uint16_t)); + chip::Test::SetEmberReadOutput(ByteSpan(reinterpret_cast(data), sizeof(data))); + + // Actual read via an encoder + std::unique_ptr encoder = testRequest.StartEncoding(&model); + ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), CHIP_NO_ERROR); + ASSERT_EQ(testRequest.FinishEncoding(), CHIP_NO_ERROR); + + // Validate after reading + std::vector attribute_data; + ASSERT_EQ(testRequest.encodedIBs.Decode(attribute_data), CHIP_NO_ERROR); + ASSERT_EQ(attribute_data.size(), 1u); + + const DecodedAttributeData & encodedData = attribute_data[0]; + ASSERT_EQ(encodedData.attributePath, testRequest.request.path); + + // data element should be a encoded byte string as this is what the attribute type is + ASSERT_EQ(encodedData.dataReader.GetType(), TLV::kTLVType_UTF8String); + CharSpan actual; + ASSERT_EQ(encodedData.dataReader.Get(actual), CHIP_NO_ERROR); + ASSERT_TRUE(actual.data_equal("abcde"_span)); +} + +TEST(TestCodegenModelViaMocks, AttributeAccessInterfaceStructRead) +{ + UseMockNodeConfig config(gTestNodeConfig); + chip::app::CodegenDataModel model; + ScopedMockAccessControl accessControl; + + const ConcreteAttributePath kStructPath(kMockEndpoint3, MockClusterId(4), + MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_STRUCT_ATTRIBUTE_TYPE)); + + TestReadRequest testRequest(kAdminSubjectDescriptor, kStructPath); + RegisteredAttributeAccessInterface aai(kStructPath); + + aai->SetReturnedData(Clusters::UnitTesting::Structs::SimpleStruct::Type{ + .a = 123, + .b = true, + .e = "foo"_span, + .g = 0.5, + .h = 0.125, + }); + + std::unique_ptr encoder = testRequest.StartEncoding(&model); + ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), CHIP_NO_ERROR); + ASSERT_EQ(testRequest.FinishEncoding(), CHIP_NO_ERROR); + + // Validate after read + std::vector attribute_data; + ASSERT_EQ(testRequest.encodedIBs.Decode(attribute_data), CHIP_NO_ERROR); + ASSERT_EQ(attribute_data.size(), 1u); + + DecodedAttributeData & encodedData = attribute_data[0]; + ASSERT_EQ(encodedData.attributePath, testRequest.request.path); + + ASSERT_EQ(encodedData.dataReader.GetType(), TLV::kTLVType_Structure); + Clusters::UnitTesting::Structs::SimpleStruct::DecodableType actual; + ASSERT_EQ(chip::app::DataModel::Decode(encodedData.dataReader, actual), CHIP_NO_ERROR); + + ASSERT_EQ(actual.a, 123); + ASSERT_EQ(actual.b, true); + ASSERT_EQ(actual.g, 0.5); + ASSERT_EQ(actual.h, 0.125); + ASSERT_TRUE(actual.e.data_equal("foo"_span)); +} + +TEST(TestCodegenModelViaMocks, AttributeAccessInterfaceListRead) +{ + UseMockNodeConfig config(gTestNodeConfig); + chip::app::CodegenDataModel model; + ScopedMockAccessControl accessControl; + + const ConcreteAttributePath kStructPath(kMockEndpoint3, MockClusterId(4), + MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_ARRAY_ATTRIBUTE_TYPE)); + + TestReadRequest testRequest(kAdminSubjectDescriptor, kStructPath); + RegisteredAttributeAccessInterface aai(kStructPath); + + constexpr unsigned kDataCount = 5; + aai->SetReturnedData(Clusters::UnitTesting::Structs::SimpleStruct::Type{ + .b = true, + .e = "xyz"_span, + .g = 0.25, + .h = 0.5, + }); + aai->SetReturnedDataCount(kDataCount); + + std::unique_ptr encoder = testRequest.StartEncoding(&model); + ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), CHIP_NO_ERROR); + ASSERT_EQ(testRequest.FinishEncoding(), CHIP_NO_ERROR); + + // Validate after read + std::vector attribute_data; + ASSERT_EQ(testRequest.encodedIBs.Decode(attribute_data), CHIP_NO_ERROR); + ASSERT_EQ(attribute_data.size(), 1u); + + DecodedAttributeData & encodedData = attribute_data[0]; + ASSERT_EQ(encodedData.attributePath, testRequest.request.path); + + ASSERT_EQ(encodedData.dataReader.GetType(), TLV::kTLVType_Array); + + std::vector items; + ASSERT_EQ(DecodeList(encodedData.dataReader, items), CHIP_NO_ERROR); + + ASSERT_EQ(items.size(), kDataCount); + + for (unsigned i = 0; i < kDataCount; i++) + { + Clusters::UnitTesting::Structs::SimpleStruct::DecodableType & actual = items[i]; + + ASSERT_EQ(actual.a, static_cast(i & 0xFF)); + ASSERT_EQ(actual.b, true); + ASSERT_EQ(actual.g, 0.25); + ASSERT_EQ(actual.h, 0.5); + ASSERT_TRUE(actual.e.data_equal("xyz"_span)); + } +} + +TEST(TestCodegenModelViaMocks, AttributeAccessInterfaceListOverflowRead) +{ + UseMockNodeConfig config(gTestNodeConfig); + chip::app::CodegenDataModel model; + ScopedMockAccessControl accessControl; + + const ConcreteAttributePath kStructPath(kMockEndpoint3, MockClusterId(4), + MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_ARRAY_ATTRIBUTE_TYPE)); + + TestReadRequest testRequest(kAdminSubjectDescriptor, kStructPath); + RegisteredAttributeAccessInterface aai(kStructPath); + + constexpr unsigned kDataCount = 1024; + aai->SetReturnedData(Clusters::UnitTesting::Structs::SimpleStruct::Type{ + .b = true, + .e = "thisislongertofillupfaster"_span, + .g = 0.25, + .h = 0.5, + }); + aai->SetReturnedDataCount(kDataCount); + + std::unique_ptr encoder = testRequest.StartEncoding(&model); + // NOTE: overflow, however data should be valid. Technically both NO_MEMORY and BUFFER_TOO_SMALL + // should be ok here, however we know buffer-too-small is the error in this case hence + // the compare (easier to write the test and read the output) + ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), CHIP_ERROR_BUFFER_TOO_SMALL); + ASSERT_EQ(testRequest.FinishEncoding(), CHIP_NO_ERROR); + + // Validate after read + std::vector attribute_data; + ASSERT_EQ(testRequest.encodedIBs.Decode(attribute_data), CHIP_NO_ERROR); + ASSERT_EQ(attribute_data.size(), 1u); + + DecodedAttributeData & encodedData = attribute_data[0]; + ASSERT_EQ(encodedData.attributePath, testRequest.request.path); + + ASSERT_EQ(encodedData.dataReader.GetType(), TLV::kTLVType_Array); + + std::vector items; + ASSERT_EQ(DecodeList(encodedData.dataReader, items), CHIP_NO_ERROR); + + // On last check, 16 items can be encoded. Set some non-zero range to be enforced here that + // SOME list items are actually encoded. Actual lower bound here IS ARBITRARY and was picked + // to just ensure non-zero item count for checks. + ASSERT_GT(items.size(), 5u); + ASSERT_LT(items.size(), kDataCount); + + for (unsigned i = 0; i < items.size(); i++) + { + Clusters::UnitTesting::Structs::SimpleStruct::DecodableType & actual = items[i]; + + ASSERT_EQ(actual.a, static_cast(i & 0xFF)); + ASSERT_EQ(actual.b, true); + ASSERT_EQ(actual.g, 0.25); + ASSERT_EQ(actual.h, 0.5); + ASSERT_TRUE(actual.e.data_equal("thisislongertofillupfaster"_span)); + } +} + +TEST(TestCodegenModelViaMocks, AttributeAccessInterfaceListIncrementalRead) +{ + UseMockNodeConfig config(gTestNodeConfig); + chip::app::CodegenDataModel model; + ScopedMockAccessControl accessControl; + + const ConcreteAttributePath kStructPath(kMockEndpoint3, MockClusterId(4), + MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_ARRAY_ATTRIBUTE_TYPE)); + + TestReadRequest testRequest(kAdminSubjectDescriptor, kStructPath); + RegisteredAttributeAccessInterface aai(kStructPath); + + constexpr unsigned kDataCount = 1024; + constexpr unsigned kEncodeIndexStart = 101; + aai->SetReturnedData(Clusters::UnitTesting::Structs::SimpleStruct::Type{ + .b = true, + .e = "thisislongertofillupfaster"_span, + .g = 0.25, + .h = 0.5, + }); + aai->SetReturnedDataCount(kDataCount); + + AttributeEncodeState encodeState; + encodeState.SetCurrentEncodingListIndex(kEncodeIndexStart); + + std::unique_ptr encoder = testRequest.StartEncoding(&model, encodeState); + // NOTE: overflow, however data should be valid. Technically both NO_MEMORY and BUFFER_TOO_SMALL + // should be ok here, however we know buffer-too-small is the error in this case hence + // the compare (easier to write the test and read the output) + ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), CHIP_ERROR_BUFFER_TOO_SMALL); + ASSERT_EQ(testRequest.FinishEncoding(), CHIP_NO_ERROR); + + // Validate after read + std::vector attribute_data; + ASSERT_EQ(testRequest.encodedIBs.Decode(attribute_data), CHIP_NO_ERROR); + + // Incremental encodes are separate list items, repeated + // actual size IS ARBITRARY (current test sets it at 11) + ASSERT_GT(attribute_data.size(), 3u); + + for (unsigned i = 0; i < attribute_data.size(); i++) + { + DecodedAttributeData & encodedData = attribute_data[i]; + ASSERT_EQ(encodedData.attributePath.mEndpointId, testRequest.request.path.mEndpointId); + ASSERT_EQ(encodedData.attributePath.mClusterId, testRequest.request.path.mClusterId); + ASSERT_EQ(encodedData.attributePath.mAttributeId, testRequest.request.path.mAttributeId); + ASSERT_EQ(encodedData.attributePath.mListOp, ConcreteDataAttributePath::ListOperation::AppendItem); + + // individual structures encoded in each item + ASSERT_EQ(encodedData.dataReader.GetType(), TLV::kTLVType_Structure); + + Clusters::UnitTesting::Structs::SimpleStruct::DecodableType actual; + ASSERT_EQ(chip::app::DataModel::Decode(encodedData.dataReader, actual), CHIP_NO_ERROR); + + ASSERT_EQ(actual.a, static_cast((i + kEncodeIndexStart) & 0xFF)); + ASSERT_EQ(actual.b, true); + ASSERT_EQ(actual.g, 0.25); + ASSERT_EQ(actual.h, 0.5); + ASSERT_TRUE(actual.e.data_equal("thisislongertofillupfaster"_span)); + } +} + +TEST(TestCodegenModelViaMocks, ReadGlobalAttributeAttributeList) +{ + UseMockNodeConfig config(gTestNodeConfig); + chip::app::CodegenDataModel model; + ScopedMockAccessControl accessControl; + + TestReadRequest testRequest(kAdminSubjectDescriptor, + ConcreteAttributePath(kMockEndpoint2, MockClusterId(3), AttributeList::Id)); + + // Data read via the encoder + std::unique_ptr encoder = testRequest.StartEncoding(&model); + ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), CHIP_NO_ERROR); + ASSERT_EQ(testRequest.FinishEncoding(), CHIP_NO_ERROR); + + // Validate after read + std::vector attribute_data; + ASSERT_EQ(testRequest.encodedIBs.Decode(attribute_data), CHIP_NO_ERROR); + ASSERT_EQ(attribute_data.size(), 1u); + + DecodedAttributeData & encodedData = attribute_data[0]; + ASSERT_EQ(encodedData.attributePath, testRequest.request.path); + + ASSERT_EQ(encodedData.dataReader.GetType(), TLV::kTLVType_Array); + + std::vector items; + ASSERT_EQ(DecodeList(encodedData.dataReader, items), CHIP_NO_ERROR); + + // Mock data contains ClusterRevision and FeatureMap. + // After this, Global attributes are auto-added + std::vector expected; + + // Encoding in global-attribute-access-interface has a logic of: + // - Append global attributes in front of the first specified + // large number global attribute. + // Since ClusterRevision and FeatureMap are + // global attributes, the order here is reversed for them + for (AttributeId id : GlobalAttributesNotInMetadata) + { + expected.push_back(id); + } + expected.push_back(ClusterRevision::Id); + expected.push_back(FeatureMap::Id); + expected.push_back(MockAttributeId(1)); + expected.push_back(MockAttributeId(2)); + expected.push_back(MockAttributeId(3)); + + ASSERT_EQ(items.size(), expected.size()); + + // Since we have no std::vector formatter, comparing element by element is somewhat + // more readable in case of failure. + for (unsigned i = 0; i < items.size(); i++) + { + EXPECT_EQ(items[i], expected[i]); + } +} diff --git a/src/app/data-model-interface/DataModel.h b/src/app/data-model-interface/DataModel.h index 0ecfd6fb624532..04911fd75cccc3 100644 --- a/src/app/data-model-interface/DataModel.h +++ b/src/app/data-model-interface/DataModel.h @@ -55,22 +55,29 @@ class DataModel : public DataModelMetadataTree // event emitting, path marking and other operations virtual InteractionModelContext CurrentContext() const { return mContext; } - /// List reading has specific handling logic: - /// `state` contains in/out data about the current list reading. MUST start with kInvalidListIndex on first call + /// TEMPORARY/TRANSITIONAL requirement for transitioning from ember-specific code + /// ReadAttribute is REQUIRED to perform: + /// - ACL validation (see notes on OperationFlags::kInternal) + /// - Validation of readability/writability + /// - use request.path.mExpanded to skip encoding replies for data according + /// to 8.4.3.2 of the spec: + /// > If the path indicates attribute data that is not readable, then the path SHALL + /// be discarded. + /// > Else if reading from the attribute in the path requires a privilege that is not + /// granted to access the cluster in the path, then the path SHALL be discarded. /// /// Return codes: - /// CHIP_ERROR_MORE_LIST_DATA_AVAILABLE (NOTE: new error defined for this purpose) - /// - partial data written to the destination - /// - destination will contain AT LEAST one valid list entry fully serialized - /// - destination will be fully valid (it will be rolled back on partial list writes) + /// CHIP_ERROR_NO_MEMORY or CHIP_ERROR_BUFFER_TOO_SMALL: + /// - Indicates that list encoding had insufficient buffer space to encode elements. + /// - encoder::GetState().AllowPartialData() determines if these errors are permanent (no partial + /// data allowed) or further encoding can be retried (AllowPartialData true for list encoding) /// CHIP_IM_GLOBAL_STATUS(code): /// - error codes that are translatable in IM status codes (otherwise we expect Failure to be reported) - /// - In particular, some handlers rely on special handling for: - /// - `UnsupportedAccess` - for ACL checks (e.g. wildcard expansion may choose to skip these) /// - to check for this, CHIP_ERROR provides: /// - ::IsPart(ChipError::SdkPart::kIMGlobalStatus) -> bool /// - ::GetSdkCode() -> uint8_t to translate to the actual code - virtual CHIP_ERROR ReadAttribute(const ReadAttributeRequest & request, ReadState & state, AttributeValueEncoder & encoder) = 0; + /// other internal falures + virtual CHIP_ERROR ReadAttribute(const ReadAttributeRequest & request, AttributeValueEncoder & encoder) = 0; /// Requests a write of an attribute. /// diff --git a/src/app/data-model-interface/OperationTypes.h b/src/app/data-model-interface/OperationTypes.h index feb2e173d91600..d19621db71d26a 100644 --- a/src/app/data-model-interface/OperationTypes.h +++ b/src/app/data-model-interface/OperationTypes.h @@ -31,17 +31,29 @@ namespace InteractionModel { /// Contains common flags among all interaction model operations: read/write/invoke enum class OperationFlags : uint32_t { - kInternal = 0x0001, // Internal request for data changes (can bypass checks/ACL etc.) + // NOTE: temporary flag. This flag exists to faciliate transition from ember-compatibilty-functions + // implementation to DataModel Interface functionality. Specifically currently the + // model is expected to perform ACL and readability/writability checks. + // + // In the future, this flag will be removed and InteractionModelEngine/ReportingEngine + // will perform the required validation. + // + // Currently the flag FORCES a bypass of: + // - ACL validation (will allow any read/write) + // - Access validation (will allow reading write-only data for example) + kInternal = 0x0001, }; /// This information is available for ALL interactions: read/write/invoke struct OperationRequest { - OperationFlags operationFlags; + BitFlags operationFlags; /// Current authentication data EXCEPT for internal requests. /// - Non-internal requests MUST have this set. /// - operationFlags.Has(OperationFlags::kInternal) MUST NOT have this set + /// + /// NOTE: once kInternal flag is removed, this will become non-optional std::optional subjectDescriptor; }; @@ -57,13 +69,6 @@ struct ReadAttributeRequest : OperationRequest BitFlags readFlags; }; -struct ReadState -{ - // When reading lists, reading will start at this index. - // As list data is read, this index is incremented - ListIndex listEncodeStart = kInvalidListIndex; -}; - enum class WriteFlags : uint32_t { kTimed = 0x0001, // Write is a timed write (i.e. a Timed Request Action preceeded it) diff --git a/src/lib/core/CHIPError.h b/src/lib/core/CHIPError.h index aebf7804aefb10..b49088247e126c 100644 --- a/src/lib/core/CHIPError.h +++ b/src/lib/core/CHIPError.h @@ -439,6 +439,15 @@ using CHIP_ERROR = ::chip::ChipError; CHIP_SDK_ERROR(::chip::ChipError::SdkPart::kIMGlobalStatus, \ ::chip::to_underlying(::chip::Protocols::InteractionModel::Status::type)) +// Defines a runtime-value for a chip-error that contains a global IM Status. +#if CHIP_CONFIG_ERROR_SOURCE +#define CHIP_ERROR_IM_GLOBAL_STATUS_VALUE(status_value) \ + ::chip::ChipError(::chip::ChipError::SdkPart::kIMGlobalStatus, ::chip::to_underlying(status_value), __FILE__, __LINE__) +#else +#define CHIP_ERROR_IM_GLOBAL_STATUS_VALUE(status_value) \ + ::chip::ChipError(::chip::ChipError::SdkPart::kIMGlobalStatus, ::chip::to_underlying(status_value)) +#endif // CHIP_CONFIG_ERROR_SOURCE + // // type must be a compile-time constant as mandated by CHIP_SDK_ERROR. //