diff --git a/src/app/AttributeAccessInterface.cpp b/src/app/AttributeAccessInterface.cpp index e9ca4325b1f3d2..0c5a85c2fe92f8 100644 --- a/src/app/AttributeAccessInterface.cpp +++ b/src/app/AttributeAccessInterface.cpp @@ -55,39 +55,82 @@ CHIP_ERROR AttributeReportBuilder::FinishAttribute(AttributeReportIBs::Builder & return aAttributeReportIBsBuilder.GetAttributeReport().EndOfAttributeReportIB().GetError(); } +namespace { + +constexpr uint32_t kEndOfListByteCount = 1; +// 2 bytes: one to end the AttributeDataIB and one to end the AttributeReportIB. +constexpr uint32_t kEndOfAttributeReportIBByteCount = 2; +constexpr TLV::TLVType kAttributeDataIBType = TLV::kTLVType_Structure; + +} // anonymous namespace + CHIP_ERROR AttributeValueEncoder::EnsureListStarted() { - if (mCurrentEncodingListIndex == kInvalidListIndex) + VerifyOrDie(mCurrentEncodingListIndex == kInvalidListIndex); + + mEncodingInitialList = (mEncodeState.mCurrentEncodingListIndex == kInvalidListIndex); + if (mEncodingInitialList) { - if (mEncodeState.mCurrentEncodingListIndex == kInvalidListIndex) - { - // Clear mAllowPartialData flag here since this encode procedure is not atomic. - // The most common error in this function is CHIP_ERROR_NO_MEMORY / CHIP_ERROR_BUFFER_TOO_SMALL, just revert and try - // next time is ok. - mEncodeState.mAllowPartialData = false; - // Spec 10.5.4.3.1, 10.5.4.6 (Replace a list w/ Multiple IBs) - // Put an empty array before encoding the first array element for list chunking. - AttributeReportBuilder builder; - - mPath.mListOp = ConcreteDataAttributePath::ListOperation::ReplaceAll; - ReturnErrorOnFailure(builder.PrepareAttribute(mAttributeReportIBsBuilder, mPath, mDataVersion)); - ReturnErrorOnFailure(builder.EncodeValue(mAttributeReportIBsBuilder, DataModel::List())); - - ReturnErrorOnFailure(builder.FinishAttribute(mAttributeReportIBsBuilder)); - mEncodeState.mCurrentEncodingListIndex = 0; - } - mCurrentEncodingListIndex = 0; + // Clear mAllowPartialData flag here since this encode procedure is not atomic. + // The most common error in this function is CHIP_ERROR_NO_MEMORY / CHIP_ERROR_BUFFER_TOO_SMALL, just revert and try + // next time is ok. + mEncodeState.mAllowPartialData = false; + + AttributeReportBuilder builder; + + mPath.mListOp = ConcreteDataAttributePath::ListOperation::ReplaceAll; + ReturnErrorOnFailure(builder.PrepareAttribute(mAttributeReportIBsBuilder, mPath, mDataVersion)); + + auto * attributeDataWriter = mAttributeReportIBsBuilder.GetAttributeReport().GetAttributeData().GetWriter(); + TLV::TLVType outerType; + ReturnErrorOnFailure( + attributeDataWriter->StartContainer(TLV::ContextTag(AttributeDataIB::Tag::kData), TLV::kTLVType_Array, outerType)); + VerifyOrDie(outerType == kAttributeDataIBType); + + // Instead of reserving hardcoded amounts, we could checkpoint the + // writer, encode array end and FinishAttribute, check that this fits, + // measure how much the writer advanced, then restore the checkpoint, + // reserve the measured value, and save it. But that's probably more + // cycles than just reserving this known constant. + ReturnErrorOnFailure( + mAttributeReportIBsBuilder.GetWriter()->ReserveBuffer(kEndOfAttributeReportIBByteCount + kEndOfListByteCount)); + + mEncodeState.mCurrentEncodingListIndex = 0; } + else + { + // For all elements in the list, a report with append operation will be generated. This will not be changed during encoding + // of each report since the users cannot access mPath. + mPath.mListOp = ConcreteDataAttributePath::ListOperation::AppendItem; + } + + mCurrentEncodingListIndex = 0; - // After encoding the empty list, the remaining items are atomically encoded into the buffer. Tell report engine to not + // After encoding the initial list start, the remaining items are atomically encoded into the buffer. Tell report engine to not // revert partial data. mEncodeState.mAllowPartialData = true; - // For all elements in the list, a report with append operation will be generated. This will not be changed during encoding - // of each report since the users cannot access mPath. - mPath.mListOp = ConcreteDataAttributePath::ListOperation::AppendItem; return CHIP_NO_ERROR; } +void AttributeValueEncoder::EnsureListEnded() +{ + if (!mEncodingInitialList) + { + // Nothing to do. + return; + } + + // Unreserve the space we reserved just for this. Crash if anything here + // fails, because that would mean that we've corrupted our data, and since + // mEncodeState.mAllowPartialData is true nothing will clean up for us here. + auto * attributeDataWriter = mAttributeReportIBsBuilder.GetAttributeReport().GetAttributeData().GetWriter(); + VerifyOrDie(attributeDataWriter->UnreserveBuffer(kEndOfListByteCount + kEndOfAttributeReportIBByteCount) == CHIP_NO_ERROR); + VerifyOrDie(attributeDataWriter->EndContainer(kAttributeDataIBType) == CHIP_NO_ERROR); + + AttributeReportBuilder builder; + VerifyOrDie(builder.FinishAttribute(mAttributeReportIBsBuilder) == CHIP_NO_ERROR); +} + } // namespace app } // namespace chip diff --git a/src/app/AttributeAccessInterface.h b/src/app/AttributeAccessInterface.h index e61c97195187d3..2b16392e107fb1 100644 --- a/src/app/AttributeAccessInterface.h +++ b/src/app/AttributeAccessInterface.h @@ -77,18 +77,18 @@ class AttributeReportBuilder * EncodeValue encodes the value field of the report, it should be called exactly once. */ template ::value, bool> = true, typename... Ts> - CHIP_ERROR EncodeValue(AttributeReportIBs::Builder & aAttributeReportIBs, T && item, Ts &&... aArgs) + CHIP_ERROR EncodeValue(AttributeReportIBs::Builder & aAttributeReportIBs, TLV::Tag tag, T && item, Ts &&... aArgs) { - return DataModel::Encode(*(aAttributeReportIBs.GetAttributeReport().GetAttributeData().GetWriter()), - TLV::ContextTag(AttributeDataIB::Tag::kData), item, std::forward(aArgs)...); + return DataModel::Encode(*(aAttributeReportIBs.GetAttributeReport().GetAttributeData().GetWriter()), tag, item, + std::forward(aArgs)...); } template ::value, bool> = true, typename... Ts> - CHIP_ERROR EncodeValue(AttributeReportIBs::Builder & aAttributeReportIBs, FabricIndex accessingFabricIndex, T && item, - Ts &&... aArgs) + CHIP_ERROR EncodeValue(AttributeReportIBs::Builder & aAttributeReportIBs, TLV::Tag tag, FabricIndex accessingFabricIndex, + T && item, Ts &&... aArgs) { - return DataModel::EncodeForRead(*(aAttributeReportIBs.GetAttributeReport().GetAttributeData().GetWriter()), - TLV::ContextTag(AttributeDataIB::Tag::kData), accessingFabricIndex, item); + return DataModel::EncodeForRead(*(aAttributeReportIBs.GetAttributeReport().GetAttributeData().GetWriter()), tag, + accessingFabricIndex, item, std::forward(aArgs)...); } }; @@ -224,10 +224,19 @@ class AttributeValueEncoder // An empty list is encoded iff both mCurrentEncodingListIndex and mEncodeState.mCurrentEncodingListIndex are invalid // values. After encoding the empty list, mEncodeState.mCurrentEncodingListIndex and mCurrentEncodingListIndex are set to 0. ReturnErrorOnFailure(EnsureListStarted()); - ReturnErrorOnFailure(aCallback(ListEncodeHelper(*this))); - // The Encode procedure finished without any error, clear the state. - mEncodeState = AttributeEncodeState(); - return CHIP_NO_ERROR; + CHIP_ERROR err = aCallback(ListEncodeHelper(*this)); + + // Even if encoding list items failed, make sure we EnsureListEnded(). + // Since we encode list items atomically, in the case when we just + // didn't fit the next item we want to make sure our list is properly + // ended before the reporting engine starts chunking. + EnsureListEnded(); + if (err == CHIP_NO_ERROR) + { + // The Encode procedure finished without any error, clear the state. + mEncodeState = AttributeEncodeState(); + } + return err; } bool TriedEncode() const { return mTriedEncode; } @@ -261,7 +270,17 @@ class AttributeValueEncoder TLV::TLVWriter backup; mAttributeReportIBsBuilder.Checkpoint(backup); - CHIP_ERROR err = EncodeAttributeReportIB(std::forward(aArgs)...); + CHIP_ERROR err; + if (mEncodingInitialList) + { + // Just encode a single item, with an anonymous tag. + AttributeReportBuilder builder; + err = builder.EncodeValue(mAttributeReportIBsBuilder, TLV::AnonymousTag(), std::forward(aArgs)...); + } + else + { + err = EncodeAttributeReportIB(std::forward(aArgs)...); + } if (err != CHIP_NO_ERROR) { // For list chunking, ReportEngine should not rollback the buffer when CHIP_ERROR_NO_MEMORY or similar error occurred. @@ -288,32 +307,39 @@ class AttributeValueEncoder CHIP_ERROR EncodeAttributeReportIB(Ts &&... aArgs) { AttributeReportBuilder builder; - ReturnErrorOnFailure(builder.PrepareAttribute(mAttributeReportIBsBuilder, mPath, mDataVersion)); - ReturnErrorOnFailure(builder.EncodeValue(mAttributeReportIBsBuilder, std::forward(aArgs)...)); + ReturnErrorOnFailure(builder.EncodeValue(mAttributeReportIBsBuilder, TLV::ContextTag(AttributeDataIB::Tag::kData), + std::forward(aArgs)...)); return builder.FinishAttribute(mAttributeReportIBsBuilder); } /** - * EnsureListStarted encodes the first item of one report with lists (an - * empty list), as needed. + * EnsureListStarted sets our mCurrentEncodingListIndex to 0, and: * - * If internal state indicates we have already encoded the empty list, this function will encode nothing, set - * mCurrentEncodingListIndex to 0 and return CHIP_NO_ERROR. + * * If we are just starting the list, gets us ready to encode list items. * - * In all cases this function guarantees that mPath.mListOp is AppendItem - * after it returns, because at that point we will be encoding the list - * items. + * * If we are continuing a chunked list, guarantees that mPath.mListOp is + * AppendItem after it returns. */ CHIP_ERROR EnsureListStarted(); + /** + * EnsureListEnded writes out the end of the list and our attribute data IB, + * if we were encoding our initial list + */ + void EnsureListEnded(); + bool mTriedEncode = false; AttributeReportIBs::Builder & mAttributeReportIBsBuilder; const FabricIndex mAccessingFabricIndex; ConcreteDataAttributePath mPath; DataVersion mDataVersion; bool mIsFabricFiltered = false; + // mEncodingInitialList is true if we're encoding a list and we have not + // started chunking it yet, so we're encoding a single attribute report IB + // for the whole list, not one per item. + bool mEncodingInitialList = false; AttributeEncodeState mEncodeState; ListIndex mCurrentEncodingListIndex = kInvalidListIndex; }; diff --git a/src/app/tests/TestAttributeValueEncoder.cpp b/src/app/tests/TestAttributeValueEncoder.cpp index 8c044929ad21d6..991f0ab0169307 100644 --- a/src/app/tests/TestAttributeValueEncoder.cpp +++ b/src/app/tests/TestAttributeValueEncoder.cpp @@ -148,9 +148,9 @@ void TestEncodeListOfBools1(nlTestSuite * aSuite, void * aContext) 0x36, 0x02, // Start 1 byte tag array + Tag (02) (Attribute Value) 0x09, // True 0x08, // False - 0x18, - 0x18, // End of container - 0x18, // End of container + 0x18, // End of array + 0x18, // End of attribute data structure + 0x18, // End of attribute structure // clang-format on }; VERIFY_BUFFER_STATE(aSuite, test, expected); @@ -179,38 +179,12 @@ void TestEncodeListOfBools2(nlTestSuite * aSuite, void * aContext) 0x24, 0x03, 0xaa, // Tag (03) Value (1 byte uint) 0xaa 0x24, 0x04, 0xcc, // Tag (04) Value (1 byte uint) 0xcc 0x18, // End of container - // Intended empty array 0x36, 0x02, // Start 1 byte tag array + Tag (02) (Attribute Value) - 0x18, // End of container - 0x18, // End of container - 0x18, // End of container - - 0x15, // Start anonymous struct - 0x35, 0x01, // Start 1 byte tag struct + Tag (01) - 0x24, 0x00, 0x99, // Tag (00) Value (1 byte uint) 0x99 (Attribute Version) - 0x37, 0x01, // Start 1 byte tag list + Tag (01) (Attribute Path) - 0x24, 0x02, 0x55, // Tag (02) Value (1 byte uint) 0x55 - 0x24, 0x03, 0xaa, // Tag (03) Value (1 byte uint) 0xaa - 0x24, 0x04, 0xcc, // Tag (04) Value (1 byte uint) 0xcc - 0x34, 0x05, // Tag (05) Null - 0x18, // End of container - 0x29, 0x02, // Tag (02) Value True (Attribute Value) - 0x18, // End of container - 0x18, // End of container - - 0x15, // Start anonymous struct - 0x35, 0x01, // Start 1 byte tag struct + Tag (01) - 0x24, 0x00, 0x99, // Tag (00) Value (1 byte uint) 0x99 (Attribute Version) - 0x37, 0x01, // Start 1 byte tag list + Tag (01) (Attribute Path) - 0x24, 0x02, 0x55, // Tag (02) Value (1 byte uint) 0x55 - 0x24, 0x03, 0xaa, // Tag (03) Value (1 byte uint) 0xaa - 0x24, 0x04, 0xcc, // Tag (04) Value (1 byte uint) 0xcc - 0x34, 0x05, // Tag (05) Null - 0x18, // End of container - 0x28, 0x02, // Tag (02) Value False (Attribute Value) - 0x18, // End of container - 0x18, // End of container - + 0x09, // True + 0x08, // False + 0x18, // End of array + 0x18, // End of attribute data structure + 0x18, // End of attribute structure // clang-format on }; VERIFY_BUFFER_STATE(aSuite, test, expected); @@ -279,26 +253,14 @@ void TestEncodeFabricScoped(nlTestSuite * aSuite, void * aContext) 0x24, 0x03, 0xaa, // Tag (03) Value (1 byte uint) 0xaa 0x24, 0x04, 0xcc, // Tag (04) Value (1 byte uint) 0xcc 0x18, // End of container - // Intended empty array 0x36, 0x02, // Start 1 byte tag array + Tag (02) (Attribute Value) - 0x18, // End of container - 0x18, // End of container - 0x18, // End of container - 0x15, // Start anonymous struct - 0x35, 0x01, // Start 1 byte tag struct + Tag (01) - 0x24, 0x00, 0x99, // Tag (00) Value (1 byte uint) 0x99 (Attribute Version) - 0x37, 0x01, // Start 1 byte tag list + Tag (01) (Attribute Path) - 0x24, 0x02, 0x55, // Tag (02) Value (1 byte uint) 0x55 - 0x24, 0x03, 0xaa, // Tag (03) Value (1 byte uint) 0xaa - 0x24, 0x04, 0xcc, // Tag (04) Value (1 byte uint) 0xcc - 0x34, 0x05, // Tag (05) Null - 0x18, // End of container (attribute path) - 0x35, 0x02, // Tag 02 (attribute data) - 0x30, 0x01, 0x00, // Tag 1, OCTET_STRING length 0 (data) - 0x24, 0xFE, 0x01, // Tag 0xFE, UINT8 Value 1 (fabric index) - 0x18, - 0x18, - 0x18, + 0x15, // Start anonymous structure + 0x30, 0x01, 0x00, // Tag 1, OCTET_STRING length 0 (data) + 0x24, 0xFE, 0x01, // Tag 0xFE, UINT8 Value 1 (fabric index) + 0x18, // End of array element (structure) + 0x18, // End of array + 0x18, // End of attribute data structure + 0x18, // End of attribute structure // clang-format on }; VERIFY_BUFFER_STATE(aSuite, test, expected); @@ -308,7 +270,7 @@ void TestEncodeListChunking(nlTestSuite * aSuite, void * aContext) { AttributeValueEncoder::AttributeEncodeState state; - bool list[] = { true, false }; + bool list[] = { true, false, false, true, true, false }; auto listEncoder = [&list](const auto & encoder) -> CHIP_ERROR { for (auto & item : list) { @@ -318,8 +280,14 @@ void TestEncodeListChunking(nlTestSuite * aSuite, void * aContext) }; { - // Use 60 bytes buffer to force chunking. The kTestFabricIndex is not effective in this test. - LimitedTestSetup<60> test1(aSuite, kTestFabricIndex); + // Use 30 bytes buffer to force chunking after the first "false". The kTestFabricIndex is + // not effective in this test. + // + // We only encode 28 bytes, because we don't encode our last two "close container" bits + // corresponding to the "test overhead" container starts. But TLVWriter automatically + // reserves space when containers are opened, so we have to have enough space to have + // encoded those last two close containers. + LimitedTestSetup<30> test1(aSuite, kTestFabricIndex); CHIP_ERROR err = test1.encoder.EncodeList(listEncoder); NL_TEST_ASSERT(aSuite, err == CHIP_ERROR_NO_MEMORY || err == CHIP_ERROR_BUFFER_TOO_SMALL); state = test1.encoder.GetState(); @@ -335,12 +303,64 @@ void TestEncodeListChunking(nlTestSuite * aSuite, void * aContext) 0x24, 0x03, 0xaa, // Tag (03) Value (1 byte uint) 0xaa 0x24, 0x04, 0xcc, // Tag (04) Value (1 byte uint) 0xcc 0x18, // End of container - // Intended empty array 0x36, 0x02, // Start 1 byte tag array + Tag (02) (Attribute Value) + 0x09, // True + 0x08, // False + 0x18, // End of array + 0x18, // End of attribute data structure + 0x18, // End of attribute structure + // clang-format on + }; + VERIFY_BUFFER_STATE(aSuite, test1, expected); + } + { + // Use 30 bytes buffer to force chunking after the second "false". The kTestFabricIndex is + // not effective in this test. + LimitedTestSetup<30> test2(aSuite, 0, state); + CHIP_ERROR err = test2.encoder.EncodeList(listEncoder); + NL_TEST_ASSERT(aSuite, err == CHIP_ERROR_NO_MEMORY || err == CHIP_ERROR_BUFFER_TOO_SMALL); + state = test2.encoder.GetState(); + + const uint8_t expected[] = { + // clang-format off + 0x15, 0x36, 0x01, // Test overhead, Start Anonymous struct + Start 1 byte Tag Array + Tag (01) + 0x15, // Start anonymous struct + 0x35, 0x01, // Start 1 byte tag struct + Tag (01) + 0x24, 0x00, 0x99, // Tag (00) Value (1 byte uint) 0x99 (Attribute Version) + 0x37, 0x01, // Start 1 byte tag list + Tag (01) (Attribute Path) + 0x24, 0x02, 0x55, // Tag (02) Value (1 byte uint) 0x55 + 0x24, 0x03, 0xaa, // Tag (03) Value (1 byte uint) 0xaa + 0x24, 0x04, 0xcc, // Tag (04) Value (1 byte uint) 0xcc + 0x34, 0x05, // Tag (05) Null 0x18, // End of container + 0x28, 0x02, // Tag (02) Value False (Attribute Value) 0x18, // End of container 0x18, // End of container + // clang-format on + }; + VERIFY_BUFFER_STATE(aSuite, test2, expected); + } + { + // Allow encoding everything else. The kTestFabricIndex is not effective in this test. + TestSetup test3(aSuite, 0, state); + CHIP_ERROR err = test3.encoder.EncodeList(listEncoder); + NL_TEST_ASSERT(aSuite, err == CHIP_NO_ERROR); + const uint8_t expected[] = { + // clang-format off + 0x15, 0x36, 0x01, // Test overhead, Start Anonymous struct + Start 1 byte Tag Array + Tag (01) + 0x15, // Start anonymous struct + 0x35, 0x01, // Start 1 byte tag struct + Tag (01) + 0x24, 0x00, 0x99, // Tag (00) Value (1 byte uint) 0x99 (Attribute Version) + 0x37, 0x01, // Start 1 byte tag list + Tag (01) (Attribute Path) + 0x24, 0x02, 0x55, // Tag (02) Value (1 byte uint) 0x55 + 0x24, 0x03, 0xaa, // Tag (03) Value (1 byte uint) 0xaa + 0x24, 0x04, 0xcc, // Tag (04) Value (1 byte uint) 0xcc + 0x34, 0x05, // Tag (05) Null + 0x18, // End of container + 0x29, 0x02, // Tag (02) Value True (Attribute Value) + 0x18, // End of container + 0x18, // End of container 0x15, // Start anonymous struct 0x35, 0x01, // Start 1 byte tag struct + Tag (01) 0x24, 0x00, 0x99, // Tag (00) Value (1 byte uint) 0x99 (Attribute Version) @@ -353,19 +373,168 @@ void TestEncodeListChunking(nlTestSuite * aSuite, void * aContext) 0x29, 0x02, // Tag (02) Value True (Attribute Value) 0x18, // End of container 0x18, // End of container + 0x15, // Start anonymous struct + 0x35, 0x01, // Start 1 byte tag struct + Tag (01) + 0x24, 0x00, 0x99, // Tag (00) Value (1 byte uint) 0x99 (Attribute Version) + 0x37, 0x01, // Start 1 byte tag list + Tag (01) (Attribute Path) + 0x24, 0x02, 0x55, // Tag (02) Value (1 byte uint) 0x55 + 0x24, 0x03, 0xaa, // Tag (03) Value (1 byte uint) 0xaa + 0x24, 0x04, 0xcc, // Tag (04) Value (1 byte uint) 0xcc + 0x34, 0x05, // Tag (05) Null + 0x18, // End of container + 0x28, 0x02, // Tag (02) Value False (Attribute Value) + 0x18, // End of container + 0x18, // End of container + // clang-format on + }; + VERIFY_BUFFER_STATE(aSuite, test3, expected); + } +} + +void TestEncodeListChunking2(nlTestSuite * aSuite, void * aContext) +{ + AttributeValueEncoder::AttributeEncodeState state; + + bool list[] = { true, false, false, true, true, false }; + auto listEncoder = [&list](const auto & encoder) -> CHIP_ERROR { + for (auto & item : list) + { + ReturnErrorOnFailure(encoder.Encode(item)); + } + return CHIP_NO_ERROR; + }; + + { + // Use 28 bytes buffer to force chunking right after we start the list. kTestFabricIndex is + // not effective in this test. + // + // We only encode 26 bytes, because we don't encode our last two "close container" bits + // corresponding to the "test overhead" container starts. But TLVWriter automatically + // reserves space when containers are opened, so we have to have enough space to have + // encoded those last two close containers. + LimitedTestSetup<28> test1(aSuite, kTestFabricIndex); + CHIP_ERROR err = test1.encoder.EncodeList(listEncoder); + NL_TEST_ASSERT(aSuite, err == CHIP_ERROR_NO_MEMORY || err == CHIP_ERROR_BUFFER_TOO_SMALL); + state = test1.encoder.GetState(); + + const uint8_t expected[] = { + // clang-format off + 0x15, 0x36, 0x01, // Test overhead, Start Anonymous struct + Start 1 byte Tag Array + Tag (01) + 0x15, // Start anonymous struct + 0x35, 0x01, // Start 1 byte tag struct + Tag (01) + 0x24, 0x00, 0x99, // Tag (00) Value (1 byte uint) 0x99 (Attribute Version) + 0x37, 0x01, // Start 1 byte tag list + Tag (01) (Attribute Path) + 0x24, 0x02, 0x55, // Tag (02) Value (1 byte uint) 0x55 + 0x24, 0x03, 0xaa, // Tag (03) Value (1 byte uint) 0xaa + 0x24, 0x04, 0xcc, // Tag (04) Value (1 byte uint) 0xcc + 0x18, // End of container + 0x36, 0x02, // Start 1 byte tag array + Tag (02) (Attribute Value) + 0x18, // End of array + 0x18, // End of attribute data structure + 0x18, // End of attribute structure // clang-format on }; VERIFY_BUFFER_STATE(aSuite, test1, expected); } { - // Use 60 bytes buffer to force chunking. The kTestFabricIndex is not effective in this test. - LimitedTestSetup<60> test2(aSuite, 0, state); + // Use 30 bytes buffer to force chunking after the first "true". The kTestFabricIndex is not + // effective in this test. + LimitedTestSetup<30> test2(aSuite, 0, state); CHIP_ERROR err = test2.encoder.EncodeList(listEncoder); + NL_TEST_ASSERT(aSuite, err == CHIP_ERROR_NO_MEMORY || err == CHIP_ERROR_BUFFER_TOO_SMALL); + state = test2.encoder.GetState(); + + const uint8_t expected[] = { + // clang-format off + 0x15, 0x36, 0x01, // Test overhead, Start Anonymous struct + Start 1 byte Tag Array + Tag (01) + 0x15, // Start anonymous struct + 0x35, 0x01, // Start 1 byte tag struct + Tag (01) + 0x24, 0x00, 0x99, // Tag (00) Value (1 byte uint) 0x99 (Attribute Version) + 0x37, 0x01, // Start 1 byte tag list + Tag (01) (Attribute Path) + 0x24, 0x02, 0x55, // Tag (02) Value (1 byte uint) 0x55 + 0x24, 0x03, 0xaa, // Tag (03) Value (1 byte uint) 0xaa + 0x24, 0x04, 0xcc, // Tag (04) Value (1 byte uint) 0xcc + 0x34, 0x05, // Tag (05) Null + 0x18, // End of container + 0x29, 0x02, // Tag (02) Value True (Attribute Value) + 0x18, // End of container + 0x18, // End of container + // clang-format on + }; + VERIFY_BUFFER_STATE(aSuite, test2, expected); + } + { + // Use 60 bytes buffer to force chunking after the second "false". The kTestFabricIndex is not + // effective in this test. + LimitedTestSetup<60> test3(aSuite, 0, state); + CHIP_ERROR err = test3.encoder.EncodeList(listEncoder); + NL_TEST_ASSERT(aSuite, err == CHIP_ERROR_NO_MEMORY || err == CHIP_ERROR_BUFFER_TOO_SMALL); + state = test3.encoder.GetState(); + + const uint8_t expected[] = { + // clang-format off + 0x15, 0x36, 0x01, // Test overhead, Start Anonymous struct + Start 1 byte Tag Array + Tag (01) + 0x15, // Start anonymous struct + 0x35, 0x01, // Start 1 byte tag struct + Tag (01) + 0x24, 0x00, 0x99, // Tag (00) Value (1 byte uint) 0x99 (Attribute Version) + 0x37, 0x01, // Start 1 byte tag list + Tag (01) (Attribute Path) + 0x24, 0x02, 0x55, // Tag (02) Value (1 byte uint) 0x55 + 0x24, 0x03, 0xaa, // Tag (03) Value (1 byte uint) 0xaa + 0x24, 0x04, 0xcc, // Tag (04) Value (1 byte uint) 0xcc + 0x34, 0x05, // Tag (05) Null + 0x18, // End of container + 0x28, 0x02, // Tag (02) Value False (Attribute Value) + 0x18, // End of container + 0x18, // End of container + 0x15, // Start anonymous struct + 0x35, 0x01, // Start 1 byte tag struct + Tag (01) + 0x24, 0x00, 0x99, // Tag (00) Value (1 byte uint) 0x99 (Attribute Version) + 0x37, 0x01, // Start 1 byte tag list + Tag (01) (Attribute Path) + 0x24, 0x02, 0x55, // Tag (02) Value (1 byte uint) 0x55 + 0x24, 0x03, 0xaa, // Tag (03) Value (1 byte uint) 0xaa + 0x24, 0x04, 0xcc, // Tag (04) Value (1 byte uint) 0xcc + 0x34, 0x05, // Tag (05) Null + 0x18, // End of container + 0x28, 0x02, // Tag (02) Value False (Attribute Value) + 0x18, // End of container + 0x18, // End of container + // clang-format on + }; + VERIFY_BUFFER_STATE(aSuite, test3, expected); + } + { + // Allow encoding everything else. The kTestFabricIndex is not effective in this test. + TestSetup test4(aSuite, 0, state); + CHIP_ERROR err = test4.encoder.EncodeList(listEncoder); NL_TEST_ASSERT(aSuite, err == CHIP_NO_ERROR); const uint8_t expected[] = { // clang-format off 0x15, 0x36, 0x01, // Test overhead, Start Anonymous struct + Start 1 byte Tag Array + Tag (01) + 0x15, // Start anonymous struct + 0x35, 0x01, // Start 1 byte tag struct + Tag (01) + 0x24, 0x00, 0x99, // Tag (00) Value (1 byte uint) 0x99 (Attribute Version) + 0x37, 0x01, // Start 1 byte tag list + Tag (01) (Attribute Path) + 0x24, 0x02, 0x55, // Tag (02) Value (1 byte uint) 0x55 + 0x24, 0x03, 0xaa, // Tag (03) Value (1 byte uint) 0xaa + 0x24, 0x04, 0xcc, // Tag (04) Value (1 byte uint) 0xcc + 0x34, 0x05, // Tag (05) Null + 0x18, // End of container + 0x29, 0x02, // Tag (02) Value True (Attribute Value) + 0x18, // End of container + 0x18, // End of container + 0x15, // Start anonymous struct + 0x35, 0x01, // Start 1 byte tag struct + Tag (01) + 0x24, 0x00, 0x99, // Tag (00) Value (1 byte uint) 0x99 (Attribute Version) + 0x37, 0x01, // Start 1 byte tag list + Tag (01) (Attribute Path) + 0x24, 0x02, 0x55, // Tag (02) Value (1 byte uint) 0x55 + 0x24, 0x03, 0xaa, // Tag (03) Value (1 byte uint) 0xaa + 0x24, 0x04, 0xcc, // Tag (04) Value (1 byte uint) 0xcc + 0x34, 0x05, // Tag (05) Null + 0x18, // End of container + 0x29, 0x02, // Tag (02) Value True (Attribute Value) + 0x18, // End of container + 0x18, // End of container 0x15, // Start anonymous struct 0x35, 0x01, // Start 1 byte tag struct + Tag (01) 0x24, 0x00, 0x99, // Tag (00) Value (1 byte uint) 0x99 (Attribute Version) @@ -380,7 +549,7 @@ void TestEncodeListChunking(nlTestSuite * aSuite, void * aContext) 0x18, // End of container // clang-format on }; - VERIFY_BUFFER_STATE(aSuite, test2, expected); + VERIFY_BUFFER_STATE(aSuite, test4, expected); } } @@ -389,15 +558,20 @@ void TestEncodeListChunking(nlTestSuite * aSuite, void * aContext) } // anonymous namespace namespace { -const nlTest sTests[] = { NL_TEST_DEF("TestEncodeNothing", TestEncodeNothing), - NL_TEST_DEF("TestEncodeBool", TestEncodeBool), - NL_TEST_DEF("TestEncodeEmptyList1", TestEncodeEmptyList1), - NL_TEST_DEF("TestEncodeEmptyList2", TestEncodeEmptyList2), - NL_TEST_DEF("TestEncodeListOfBools1", TestEncodeListOfBools1), - NL_TEST_DEF("TestEncodeListOfBools2", TestEncodeListOfBools2), - NL_TEST_DEF("TestEncodeListChunking", TestEncodeListChunking), - NL_TEST_DEF("TestEncodeFabricScoped", TestEncodeFabricScoped), - NL_TEST_SENTINEL() }; +const nlTest sTests[] = { + // clang-format off + NL_TEST_DEF("TestEncodeNothing", TestEncodeNothing), + NL_TEST_DEF("TestEncodeBool", TestEncodeBool), + NL_TEST_DEF("TestEncodeEmptyList1", TestEncodeEmptyList1), + NL_TEST_DEF("TestEncodeEmptyList2", TestEncodeEmptyList2), + NL_TEST_DEF("TestEncodeListOfBools1", TestEncodeListOfBools1), + NL_TEST_DEF("TestEncodeListOfBools2", TestEncodeListOfBools2), + NL_TEST_DEF("TestEncodeListChunking", TestEncodeListChunking), + NL_TEST_DEF("TestEncodeListChunking2", TestEncodeListChunking2), + NL_TEST_DEF("TestEncodeFabricScoped", TestEncodeFabricScoped), + NL_TEST_SENTINEL() + // clang-format on +}; } int TestAttributeValueEncoder() diff --git a/src/app/tests/TestReadInteraction.cpp b/src/app/tests/TestReadInteraction.cpp index b7f0d2b3df0e0e..d49a4fa61a5be7 100644 --- a/src/app/tests/TestReadInteraction.cpp +++ b/src/app/tests/TestReadInteraction.cpp @@ -173,6 +173,24 @@ class MockInteractionModelApp : public chip::app::ReadClient::Callback mReceivedAttributePaths.push_back(aPath); mNumAttributeResponse++; mGotReport = true; + + if (aPath.IsListItemOperation()) + { + mNumArrayItems++; + } + else if (aPath.IsListOperation()) + { + // This is an entire list of things; count up how many. + chip::TLV::TLVType containerType; + if (apData->EnterContainer(containerType) == CHIP_NO_ERROR) + { + size_t count = 0; + if (chip::TLV::Utilities::Count(*apData, count, /* aRecurse = */ false) == CHIP_NO_ERROR) + { + mNumArrayItems += static_cast(count); + } + } + } } mLastStatusReceived = status; } @@ -207,6 +225,7 @@ class MockInteractionModelApp : public chip::app::ReadClient::Callback bool mGotEventResponse = false; int mNumReadEventFailureStatusReceived = 0; int mNumAttributeResponse = 0; + int mNumArrayItems = 0; bool mGotReport = false; bool mReadError = false; chip::app::ReadHandler * mpReadHandler = nullptr; @@ -1192,7 +1211,9 @@ void TestReadInteraction::TestReadChunking(nlTestSuite * apSuite, void * apConte ctx.DrainAndServiceIO(); - NL_TEST_ASSERT(apSuite, delegate.mNumAttributeResponse == 7); // One empty string, with 6 array evelemtns. + // We get one chunk with 3 array elements, and then one chunk per element. + NL_TEST_ASSERT(apSuite, delegate.mNumAttributeResponse == 4); + NL_TEST_ASSERT(apSuite, delegate.mNumArrayItems == 6); NL_TEST_ASSERT(apSuite, delegate.mGotReport); NL_TEST_ASSERT(apSuite, !delegate.mReadError); // By now we should have closed all exchanges and sent all pending acks, so @@ -1236,12 +1257,16 @@ void TestReadInteraction::TestSetDirtyBetweenChunks(nlTestSuite * apSuite, void { int currentAttributeResponsesWhenSetDirty = 0; + int currentArrayItemsWhenSetDirty = 0; class DirtyingMockDelegate : public MockInteractionModelApp { public: - DirtyingMockDelegate(AttributePathParams (&aReadPaths)[2], int & aNumAttributeResponsesWhenSetDirty) : - mReadPaths(aReadPaths), mNumAttributeResponsesWhenSetDirty(aNumAttributeResponsesWhenSetDirty) + DirtyingMockDelegate(AttributePathParams (&aReadPaths)[2], int & aNumAttributeResponsesWhenSetDirty, + int & aNumArrayItemsWhenSetDirty) : + mReadPaths(aReadPaths), + mNumAttributeResponsesWhenSetDirty(aNumAttributeResponsesWhenSetDirty), + mNumArrayItemsWhenSetDirty(aNumArrayItemsWhenSetDirty) {} private: @@ -1285,6 +1310,7 @@ void TestReadInteraction::TestSetDirtyBetweenChunks(nlTestSuite * apSuite, void // We're finishing out the message where we decided to // SetDirty. ++mNumAttributeResponsesWhenSetDirty; + ++mNumArrayItemsWhenSetDirty; } } @@ -1302,6 +1328,7 @@ void TestReadInteraction::TestSetDirtyBetweenChunks(nlTestSuite * apSuite, void { // At this time, we are in the middle of report for second item. mNumAttributeResponsesWhenSetDirty = mNumAttributeResponse; + mNumArrayItemsWhenSetDirty = mNumArrayItems; InteractionModelEngine::GetInstance()->GetReportingEngine().SetDirty(dirtyPath); } } @@ -1316,9 +1343,10 @@ void TestReadInteraction::TestSetDirtyBetweenChunks(nlTestSuite * apSuite, void bool mDidSetDirty = false; AttributePathParams (&mReadPaths)[2]; int & mNumAttributeResponsesWhenSetDirty; + int & mNumArrayItemsWhenSetDirty; }; - DirtyingMockDelegate delegate(attributePathParams, currentAttributeResponsesWhenSetDirty); + DirtyingMockDelegate delegate(attributePathParams, currentAttributeResponsesWhenSetDirty, currentArrayItemsWhenSetDirty); NL_TEST_ASSERT(apSuite, !delegate.mGotEventResponse); app::ReadClient readClient(chip::app::InteractionModelEngine::GetInstance(), &ctx.GetExchangeManager(), delegate, @@ -1329,11 +1357,13 @@ void TestReadInteraction::TestSetDirtyBetweenChunks(nlTestSuite * apSuite, void ctx.DrainAndServiceIO(); - // We should receive another (6 + 1) = 7 attribute reports since the underlying path iterator should be reset to the - // beginning of the cluster it is currently iterating. + // We should receive another (3 + 1) = 4 attribute reports represeting 6 + // array items, since the underlying path iterator should be reset to + // the beginning of the cluster it is currently iterating. ChipLogError(DataManagement, "OLD: %d\n", currentAttributeResponsesWhenSetDirty); ChipLogError(DataManagement, "NEW: %d\n", delegate.mNumAttributeResponse); - NL_TEST_ASSERT(apSuite, delegate.mNumAttributeResponse == currentAttributeResponsesWhenSetDirty + 7); + NL_TEST_ASSERT(apSuite, delegate.mNumAttributeResponse == currentAttributeResponsesWhenSetDirty + 4); + NL_TEST_ASSERT(apSuite, delegate.mNumArrayItems == currentArrayItemsWhenSetDirty + 6); NL_TEST_ASSERT(apSuite, delegate.mGotReport); NL_TEST_ASSERT(apSuite, !delegate.mReadError); // By now we should have closed all exchanges and sent all pending acks, so @@ -1868,9 +1898,14 @@ void TestReadInteraction::TestSubscribeWildcard(nlTestSuite * apSuite, void * ap NL_TEST_ASSERT(apSuite, delegate.mGotReport); // We have 29 attributes in our mock attribute storage. And we subscribed twice. - // And attribute 3/2/4 is a list with 6 elements and list chunking is applied to it, thus we should receive ( 29 + 6 ) * 2 = - // 70 attribute data in total. - NL_TEST_ASSERT(apSuite, delegate.mNumAttributeResponse == 70); + // And attribute 3/2/4 is a list with 6 elements and list chunking is + // applied to it, but the way the packet boundaries fall we get two of + // its items as a single list, followed by 4 more single items for one + // of our subscriptions, but every item as a separate IB for the other. + // + // Thus we should receive 29*2 + 4 + 6 = 68 attribute data in total. + NL_TEST_ASSERT(apSuite, delegate.mNumAttributeResponse == 68); + NL_TEST_ASSERT(apSuite, delegate.mNumArrayItems == 12); NL_TEST_ASSERT(apSuite, engine->GetNumActiveReadHandlers(ReadHandler::InteractionType::Subscribe) == 1); NL_TEST_ASSERT(apSuite, engine->ActiveHandlerAt(0) != nullptr); delegate.mpReadHandler = engine->ActiveHandlerAt(0); @@ -1901,6 +1936,7 @@ void TestReadInteraction::TestSubscribeWildcard(nlTestSuite * apSuite, void * ap delegate.mpReadHandler->SetStateFlag(ReadHandler::ReadHandlerFlags::HoldReport, false); delegate.mGotReport = false; delegate.mNumAttributeResponse = 0; + delegate.mNumArrayItems = 0; AttributePathParams dirtyPath; dirtyPath.mEndpointId = Test::kMockEndpoint3; @@ -1921,10 +1957,16 @@ void TestReadInteraction::TestSubscribeWildcard(nlTestSuite * apSuite, void * ap NL_TEST_ASSERT(apSuite, delegate.mGotReport); // Mock endpoint3 has 13 attributes in total, and we subscribed twice. - // And attribute 3/2/4 is a list with 6 elements and list chunking is applied to it, thus we should receive ( 13 + 6 ) * - // 2 = 28 attribute data in total. + // And attribute 3/2/4 is a list with 6 elements and list chunking + // is applied to it, but the way the packet boundaries fall we get two of + // its items as a single list, followed by 4 more items for one + // of our subscriptions, and 3 items as a single list followed by 3 + // more items for the other. + // + // Thus we should receive 13*2 + 4 + 3 = 33 attribute data in total. ChipLogError(DataManagement, "RESPO: %d\n", delegate.mNumAttributeResponse); - NL_TEST_ASSERT(apSuite, delegate.mNumAttributeResponse == 38); + NL_TEST_ASSERT(apSuite, delegate.mNumAttributeResponse == 33); + NL_TEST_ASSERT(apSuite, delegate.mNumArrayItems == 12); } } @@ -2925,6 +2967,7 @@ void TestReadInteraction::TestPostSubscribeRoundtripChunkReport(nlTestSuite * ap err = engine->GetReportingEngine().SetDirty(dirtyPath1); delegate.mGotReport = false; delegate.mNumAttributeResponse = 0; + delegate.mNumArrayItems = 0; // wait for min interval 2 seconds(in test, we use 1.9second considering the time variation), expect no event is received, // then wait for 0.5 seconds, then all chunked dirty reports are sent out, which would not honor minInterval @@ -2949,8 +2992,10 @@ void TestReadInteraction::TestPostSubscribeRoundtripChunkReport(nlTestSuite * ap } ctx.DrainAndServiceIO(); } - // Two chunked reports carry 7 attributeDataIB - NL_TEST_ASSERT(apSuite, delegate.mNumAttributeResponse == 7); + // Two chunked reports carry 4 attributeDataIB: 1 with a list of 3 items, + // and then one per remaining item. + NL_TEST_ASSERT(apSuite, delegate.mNumAttributeResponse == 4); + NL_TEST_ASSERT(apSuite, delegate.mNumArrayItems == 6); NL_TEST_ASSERT(apSuite, engine->GetNumActiveReadClients() == 0); engine->Shutdown();