From 4513748024d2d3800dc3b3cc84fbf5f68b5b4160 Mon Sep 17 00:00:00 2001 From: Boris Zbarsky Date: Thu, 7 Oct 2021 02:40:27 -0400 Subject: [PATCH] Add a helper for encoding lists via AttributeValueEncoder. (#10265) * Add a helper for encoding lists via AttributeValueEncoder. The idea is to not need the whole list in memory at once. * Addressing review comments --- src/app/AttributeAccessInterface.h | 31 ++++- src/app/data-model/TagBoundEncoder.h | 76 +++++++++++ src/app/tests/BUILD.gn | 1 + src/app/tests/TestAttributeValueEncoder.cpp | 138 ++++++++++++++++++++ 4 files changed, 242 insertions(+), 4 deletions(-) create mode 100644 src/app/data-model/TagBoundEncoder.h create mode 100644 src/app/tests/TestAttributeValueEncoder.cpp diff --git a/src/app/AttributeAccessInterface.h b/src/app/AttributeAccessInterface.h index 11608b51d31140..568b8ba0bb0c6d 100644 --- a/src/app/AttributeAccessInterface.h +++ b/src/app/AttributeAccessInterface.h @@ -21,6 +21,8 @@ #include #include #include +#include // So we can encode lists +#include #include #include #include @@ -38,10 +40,11 @@ namespace chip { namespace app { -class AttributeValueEncoder +class AttributeValueEncoder : protected TagBoundEncoder { public: - AttributeValueEncoder(TLV::TLVWriter * aWriter) : mWriter(aWriter) {} + AttributeValueEncoder(TLV::TLVWriter * aWriter) : TagBoundEncoder(aWriter, TLV::ContextTag(AttributeDataElement::kCsTag_Data)) + {} template CHIP_ERROR Encode(Ts... aArgs) @@ -51,7 +54,28 @@ class AttributeValueEncoder { return CHIP_NO_ERROR; } - return DataModel::Encode(*mWriter, TLV::ContextTag(AttributeDataElement::kCsTag_Data), std::forward(aArgs)...); + return TagBoundEncoder::Encode(std::forward(aArgs)...); + } + + /** + * aCallback is expected to take a const TagBoundEncoder& argument and + * Encode() on it as many times as needed to encode all the list elements + * one by one. If any of those Encode() calls returns failure, aCallback + * must stop encoding and return failure. When all items are encoded + * aCallback is expected to return success. + * + * aCallback may not be called. Consumers must not assume it will be + * called. + */ + template + CHIP_ERROR EncodeList(ListGenerator aCallback) + { + mTriedEncode = true; + if (mWriter == nullptr) + { + return CHIP_NO_ERROR; + } + return TagBoundEncoder::EncodeList(aCallback); } bool TriedEncode() const { return mTriedEncode; } @@ -66,7 +90,6 @@ class AttributeValueEncoder } private: - TLV::TLVWriter * mWriter; bool mTriedEncode = false; }; diff --git a/src/app/data-model/TagBoundEncoder.h b/src/app/data-model/TagBoundEncoder.h new file mode 100644 index 00000000000000..aa0d89455517ec --- /dev/null +++ b/src/app/data-model/TagBoundEncoder.h @@ -0,0 +1,76 @@ +/* + * + * Copyright (c) 2021 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include // So we can encode lists +#include +#include + +/** + * Class that encapsulates a TLVWriter and tag that can be provided to a + * consumer so that the consumer can just call Encode and have the tagging + * handled for them. + */ +namespace chip { +namespace app { + +class TagBoundEncoder +{ +public: + // Initialization with a null TLVWriter is allowed, but attempts to Encode() + // will fail. + TagBoundEncoder(TLV::TLVWriter * aWriter, TLV::Tag aTag) : mWriter(aWriter), mTag(aTag) {} + + template + CHIP_ERROR Encode(Ts... aArgs) const + { + VerifyOrReturnError(mWriter != nullptr, CHIP_ERROR_INCORRECT_STATE); + return DataModel::Encode(*mWriter, mTag, std::forward(aArgs)...); + } + + /** + * aCallback is expected to take a const TagBoundEncoder& argument and + * Encode() on it as many times as needed to encode all the list elements + * one by one. If any of those Encode() calls returns failure, aCallback + * must stop encoding and return failure. When all items are encoded + * aCallback is expected to return success. + * + * aCallback may not be called. Consumers must not assume it will be + * called. + */ + template + CHIP_ERROR EncodeList(ListGenerator aCallback) + { + VerifyOrReturnError(mWriter != nullptr, CHIP_ERROR_INCORRECT_STATE); + TLV::TLVType outerType; + ReturnErrorOnFailure(mWriter->StartContainer(mTag, TLV::kTLVType_Array, outerType)); + ReturnErrorOnFailure(aCallback(TagBoundEncoder(mWriter, TLV::AnonymousTag))); + return mWriter->EndContainer(outerType); + } + +protected: + TLV::TLVWriter * const mWriter; + +private: + const TLV::Tag mTag; +}; + +} // namespace app +} // namespace chip diff --git a/src/app/tests/BUILD.gn b/src/app/tests/BUILD.gn index a132f91ca8ec6d..12ad872bbe47c7 100644 --- a/src/app/tests/BUILD.gn +++ b/src/app/tests/BUILD.gn @@ -23,6 +23,7 @@ chip_test_suite("tests") { output_name = "libAppTests" test_sources = [ + "TestAttributeValueEncoder.cpp", "TestCHIPDeviceCallbacksMgr.cpp", "TestClusterInfo.cpp", "TestCommandPathParams.cpp", diff --git a/src/app/tests/TestAttributeValueEncoder.cpp b/src/app/tests/TestAttributeValueEncoder.cpp new file mode 100644 index 00000000000000..4a781de9870b2b --- /dev/null +++ b/src/app/tests/TestAttributeValueEncoder.cpp @@ -0,0 +1,138 @@ +/* + * + * Copyright (c) 2021 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @file + * This file implements unit tests for CommandPathParams + * + */ + +#include +#include +#include +#include +#include + +using namespace chip; +using namespace chip::app; +using namespace chip::TLV; + +namespace { + +struct TestSetup +{ + TestSetup(nlTestSuite * aSuite) : encoder(&writer) + { + writer.Init(buf); + TLVType ignored; + CHIP_ERROR err = writer.StartContainer(AnonymousTag, kTLVType_Structure, ignored); + NL_TEST_ASSERT(aSuite, err == CHIP_NO_ERROR); + } + + AttributeValueEncoder encoder; + uint8_t buf[1024]; + TLVWriter writer; +}; + +// Macro so we get better error reporting in terms of which test failed, because +// the reporting uses __LINE__. +#define VERIFY_BUFFER_STATE(aSuite, aSetup, aExpected) \ + do \ + { \ + NL_TEST_ASSERT(aSuite, aSetup.writer.GetLengthWritten() == sizeof(aExpected)); \ + NL_TEST_ASSERT(aSuite, memcmp(aSetup.buf, aExpected, sizeof(aExpected)) == 0); \ + } while (0) + +void TestEncodeNothing(nlTestSuite * aSuite, void * aContext) +{ + TestSetup test(aSuite); + // Just have an anonymous struct marker. + const uint8_t expected[] = { 0x15 }; + VERIFY_BUFFER_STATE(aSuite, test, expected); +} + +void TestEncodeBool(nlTestSuite * aSuite, void * aContext) +{ + TestSetup test(aSuite); + CHIP_ERROR err = test.encoder.Encode(true); + NL_TEST_ASSERT(aSuite, err == CHIP_NO_ERROR); + // Anonymous tagged struct. + // Control tag for boolean true with context tag. + // Context tag with value AttributeDataElement::kCsTag_Data. + const uint8_t expected[] = { 0x15, 0x29, 0x02 }; + VERIFY_BUFFER_STATE(aSuite, test, expected); +} + +void TestEncodeListOfBools1(nlTestSuite * aSuite, void * aContext) +{ + TestSetup test(aSuite); + bool list[] = { true, false }; + CHIP_ERROR err = test.encoder.Encode(DataModel::List(list)); + NL_TEST_ASSERT(aSuite, err == CHIP_NO_ERROR); + // Anonymous tagged struct. + // Control tag for array with context tag. + // Context tag with value AttributeDataElement::kCsTag_Data. + // Control tag for boolean true with anonymous tag. + // Control tag for boolean false with anonymous tag. + // End of list marker. + const uint8_t expected[] = { 0x15, 0x36, 0x02, 0x09, 0x08, 0x18 }; + VERIFY_BUFFER_STATE(aSuite, test, expected); +} + +void TestEncodeListOfBools2(nlTestSuite * aSuite, void * aContext) +{ + TestSetup test(aSuite); + bool list[] = { true, false }; + CHIP_ERROR err = test.encoder.EncodeList([&list](const TagBoundEncoder & encoder) -> CHIP_ERROR { + for (auto & item : list) + { + ReturnErrorOnFailure(encoder.Encode(item)); + } + return CHIP_NO_ERROR; + }); + NL_TEST_ASSERT(aSuite, err == CHIP_NO_ERROR); + // Anonymous tagged struct. + // Control tag for array with context tag. + // Context tag with value AttributeDataElement::kCsTag_Data. + // Control tag for boolean true with anonymous tag. + // Control tag for boolean false with anonymous tag. + // End of list marker. + const uint8_t expected[] = { 0x15, 0x36, 0x02, 0x09, 0x08, 0x18 }; + VERIFY_BUFFER_STATE(aSuite, test, expected); +} + +#undef VERIFY_STATE + +} // anonymous namespace + +namespace { +const nlTest sTests[] = { NL_TEST_DEF("TestEncodeNothing", TestEncodeNothing), NL_TEST_DEF("TestEncodeBool", TestEncodeBool), + NL_TEST_DEF("TestEncodeListOfBools1", TestEncodeListOfBools1), + NL_TEST_DEF("TestEncodeListOfBools2", TestEncodeListOfBools2), NL_TEST_SENTINEL() }; +} + +int TestAttributeValueEncoder() +{ + nlTestSuite theSuite = { "AttributeValueEncoder", &sTests[0], nullptr, nullptr }; + + nlTestRunner(&theSuite, nullptr); + + return (nlTestRunnerStats(&theSuite)); +} + +CHIP_REGISTER_TEST_SUITE(TestAttributeValueEncoder)