| /* |
| * |
| * Copyright (c) 2022 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 <memory> |
| #include <utility> |
| |
| #include <pw_unit_test/framework.h> |
| |
| #include <app-common/zap-generated/cluster-objects.h> |
| #include <app-common/zap-generated/ids/Attributes.h> |
| #include <app-common/zap-generated/ids/Clusters.h> |
| #include <app/AttributeAccessInterface.h> |
| #include <app/AttributeAccessInterfaceRegistry.h> |
| #include <app/CommandHandlerInterface.h> |
| #include <app/ConcreteAttributePath.h> |
| #include <app/InteractionModelEngine.h> |
| #include <app/WriteClient.h> |
| #include <app/data-model/Decode.h> |
| #include <app/tests/AppTestContext.h> |
| #include <app/util/DataModelHandler.h> |
| #include <app/util/attribute-storage.h> |
| #include <controller/InvokeInteraction.h> |
| #include <data-model-providers/codegen/Instance.h> |
| #include <lib/core/ErrorStr.h> |
| #include <lib/core/StringBuilderAdapters.h> |
| #include <lib/support/logging/CHIPLogging.h> |
| #include <protocols/interaction_model/Constants.h> |
| |
| using namespace chip; |
| using namespace chip::app; |
| using namespace chip::app::Clusters; |
| |
| namespace { |
| |
| uint32_t gIterationCount = 0; |
| |
| // |
| // The generated endpoint_config for the controller app has Endpoint 1 |
| // already used in the fixed endpoint set of size 1. Consequently, let's use the next |
| // number higher than that for our dynamic test endpoint. |
| // |
| constexpr EndpointId kTestEndpointId = 0; |
| constexpr AttributeId kTestListAttribute = 6; |
| constexpr AttributeId kTestListAttribute2 = 7; |
| constexpr uint32_t kTestListLength = 5; |
| |
| // We don't really care about the content, we just need a buffer. |
| uint8_t sByteSpanData[app::kMaxSecureSduLengthBytes]; |
| |
| class TestWriteChunking : public Test::AppContext |
| { |
| private: |
| using PathStatus = std::pair<app::ConcreteAttributePath, bool>; |
| |
| protected: |
| enum class Operations : uint8_t |
| { |
| kNoop, |
| kShutdownWriteClient, |
| }; |
| |
| enum class ListData : uint8_t |
| { |
| kNull, |
| kList, |
| kBadValue, |
| }; |
| |
| enum class EncodingMethod |
| { |
| Standard, // Encoding using WriteClient::EncodeAttribute() |
| PreencodedTLV, // Encoding using WriteClient::PutPreencodedAttribute() |
| }; |
| |
| struct Instructions |
| { |
| // The paths used in write request |
| std::vector<ConcreteAttributePath> paths; |
| // The type of content of the list, it should be an empty vector or its size should equals to the list of paths. |
| std::vector<ListData> data; |
| // operations on OnListWriteBegin and OnListWriteEnd on the server side. |
| std::function<Operations(const app::ConcreteAttributePath & path)> onListWriteBeginActions; |
| // The expected status when OnListWriteEnd is called. In the same order as paths |
| std::vector<bool> expectedStatus; |
| }; |
| |
| void RunTest(Instructions instructions, EncodingMethod encodingMethod); |
| void RunTest_NonEmptyReplaceAll(Instructions instructions, EncodingMethod encodingMethod); |
| |
| template <class T> |
| void EncodeAttributeListIntoTLV(const DataModel::List<T> & aListAttribute, TLV::ScopedBufferTLVReader & outTlvReader); |
| }; |
| |
| // Encodes an attribute of List Data Type into a TLV Reader object for testing WriteClient::PutPreencodedAttribute |
| // Warning: This method only encodes uint8_t or ByteSpans whose length fits in one octet |
| template <class T> |
| void TestWriteChunking::EncodeAttributeListIntoTLV(const DataModel::List<T> & aListAttribute, |
| TLV::ScopedBufferTLVReader & outEncodedListTlvReader) |
| { |
| static_assert(std::is_same<T, chip::ByteSpan>::value || std::is_same<T, uint8_t>::value, |
| "This method only encodes uint8_t or ByteSpans whose length fits in one octet"); |
| |
| size_t estimatedSize = 0; |
| for (size_t i = 0; i < aListAttribute.size(); i++) |
| { |
| if constexpr (std::is_same<T, uint8_t>::value) |
| { |
| // Control Octet (1) + size of uint8_t |
| estimatedSize += 1 + sizeof(uint8_t); |
| } |
| else if constexpr (std::is_same<T, chip::ByteSpan>::value) |
| { |
| ASSERT_LE(aListAttribute[i].size(), static_cast<size_t>(UINT8_MAX)); |
| |
| // Control Octet (1) + Length Octet (1) + size of a single ByteSpan |
| estimatedSize += 2 + aListAttribute[i].size(); |
| } |
| } |
| |
| // Encode AttributeData into a TLV Array |
| chip::Platform::ScopedMemoryBufferWithSize<uint8_t> buffer; |
| buffer.Alloc(TLV::EstimateStructOverhead(estimatedSize)); |
| |
| TLV::TLVWriter writer; |
| writer.Init(buffer.Get(), buffer.AllocatedSize()); |
| TLV::TLVType outerContainer; |
| |
| EXPECT_EQ(CHIP_NO_ERROR, writer.StartContainer(TLV::AnonymousTag(), TLV::kTLVType_Array, outerContainer)); |
| for (auto & item : aListAttribute) |
| { |
| EXPECT_EQ(CHIP_NO_ERROR, writer.Put(TLV::AnonymousTag(), item)); |
| } |
| EXPECT_EQ(CHIP_NO_ERROR, writer.EndContainer(outerContainer)); |
| |
| // Move Encoded TLV Array into TLVReader Object |
| outEncodedListTlvReader.Init(std::move(buffer), writer.GetLengthWritten()); |
| outEncodedListTlvReader.Next(); |
| } |
| |
| //clang-format off |
| |
| DECLARE_DYNAMIC_ATTRIBUTE_LIST_BEGIN(testClusterAttrsOnEndpoint) |
| DECLARE_DYNAMIC_ATTRIBUTE(kTestListAttribute, ARRAY, 1, MATTER_ATTRIBUTE_FLAG_WRITABLE), |
| DECLARE_DYNAMIC_ATTRIBUTE(kTestListAttribute2, ARRAY, 1, MATTER_ATTRIBUTE_FLAG_WRITABLE), DECLARE_DYNAMIC_ATTRIBUTE_LIST_END(); |
| |
| DECLARE_DYNAMIC_CLUSTER_LIST_BEGIN(testEndpointClusters) |
| DECLARE_DYNAMIC_CLUSTER(Clusters::UnitTesting::Id, testClusterAttrsOnEndpoint, ZAP_CLUSTER_MASK(SERVER), nullptr, nullptr), |
| DECLARE_DYNAMIC_CLUSTER_LIST_END; |
| |
| DECLARE_DYNAMIC_ENDPOINT(testEndpoint, testEndpointClusters); |
| |
| // Second Endpoint to Test Chunking when we send non-empty initial ReplaceAll List, which is used for ACL, see |
| // WriteClient::EncodeAttribute() |
| |
| DECLARE_DYNAMIC_ATTRIBUTE_LIST_BEGIN(testClusterAttrsOnEndpoint2) |
| DECLARE_DYNAMIC_ATTRIBUTE(Clusters::AccessControl::Attributes::Acl::Id, ARRAY, 8, MATTER_ATTRIBUTE_FLAG_WRITABLE), |
| DECLARE_DYNAMIC_ATTRIBUTE(Clusters::AccessControl::Attributes::Extension::Id, ARRAY, 1, MATTER_ATTRIBUTE_FLAG_WRITABLE), |
| DECLARE_DYNAMIC_ATTRIBUTE_LIST_END(); |
| |
| DECLARE_DYNAMIC_CLUSTER_LIST_BEGIN(testEndpointClusters2) |
| DECLARE_DYNAMIC_CLUSTER(Clusters::AccessControl::Id, testClusterAttrsOnEndpoint2, ZAP_CLUSTER_MASK(SERVER), nullptr, nullptr), |
| DECLARE_DYNAMIC_CLUSTER_LIST_END; |
| |
| DECLARE_DYNAMIC_ENDPOINT(testEndpointAcl, testEndpointClusters2); |
| |
| DataVersion dataVersionStorage[MATTER_ARRAY_SIZE(testEndpointClusters)]; |
| DataVersion dataVersionStorageAcl[MATTER_ARRAY_SIZE(testEndpointClusters2)]; |
| |
| //clang-format on |
| |
| class TestWriteCallback : public app::WriteClient::Callback |
| { |
| public: |
| void OnResponse(const app::WriteClient * apWriteClient, const app::ConcreteDataAttributePath & aPath, |
| app::StatusIB status) override |
| { |
| if (status.mStatus == Protocols::InteractionModel::Status::Success) |
| { |
| mSuccessCount++; |
| } |
| else |
| { |
| mLastErrorReason = status; |
| mErrorCount++; |
| } |
| } |
| |
| void OnError(const app::WriteClient * apWriteClient, CHIP_ERROR aError) override |
| { |
| mLastErrorReason = app::StatusIB(aError); |
| mErrorCount++; |
| } |
| |
| void OnDone(app::WriteClient * apWriteClient) override { mOnDoneCount++; } |
| |
| uint32_t mSuccessCount = 0; |
| uint32_t mErrorCount = 0; |
| uint32_t mOnDoneCount = 0; |
| app::StatusIB mLastErrorReason; |
| }; |
| |
| class TestAttrAccess : public app::AttributeAccessInterface |
| { |
| public: |
| // Register for the Test Cluster cluster on all endpoints. |
| TestAttrAccess() : AttributeAccessInterface(Optional<EndpointId>::Missing(), Clusters::UnitTesting::Id) {} |
| |
| CHIP_ERROR Read(const app::ConcreteReadAttributePath & aPath, app::AttributeValueEncoder & aEncoder) override; |
| CHIP_ERROR Write(const app::ConcreteDataAttributePath & aPath, app::AttributeValueDecoder & aDecoder) override; |
| |
| void OnListWriteBegin(const app::ConcreteAttributePath & aPath) override |
| { |
| if (mOnListWriteBegin) |
| { |
| mOnListWriteBegin(aPath); |
| } |
| } |
| |
| void OnListWriteEnd(const app::ConcreteAttributePath & aPath, bool aWriteWasSuccessful) override |
| { |
| if (mOnListWriteEnd) |
| { |
| mOnListWriteEnd(aPath, aWriteWasSuccessful); |
| } |
| } |
| |
| std::function<void(const app::ConcreteAttributePath & path)> mOnListWriteBegin; |
| std::function<void(const app::ConcreteAttributePath & path, bool wasSuccessful)> mOnListWriteEnd; |
| } testServer; |
| |
| CHIP_ERROR TestAttrAccess::Read(const app::ConcreteReadAttributePath & aPath, app::AttributeValueEncoder & aEncoder) |
| { |
| return CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE; |
| } |
| |
| CHIP_ERROR TestAttrAccess::Write(const app::ConcreteDataAttributePath & aPath, app::AttributeValueDecoder & aDecoder) |
| { |
| // We only care about the number of attribute data. |
| if (!aPath.IsListItemOperation()) |
| { |
| app::DataModel::Nullable<app::DataModel::DecodableList<ByteSpan>> list; |
| CHIP_ERROR err = aDecoder.Decode(list); |
| ChipLogError(Zcl, "NotList/ReplaceAll: Decode result: %s", err.AsString()); |
| return err; |
| } |
| if (aPath.mListOp == app::ConcreteDataAttributePath::ListOperation::AppendItem) |
| { |
| ByteSpan listItem; |
| CHIP_ERROR err = aDecoder.Decode(listItem); |
| ChipLogError(Zcl, "AppendItem: Decode result: %s", err.AsString()); |
| return err; |
| } |
| |
| return CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE; |
| } |
| |
| class TestAttrAccessAcl : public app::AttributeAccessInterface |
| { |
| public: |
| // Register for the Test Cluster cluster on all endpoints. |
| TestAttrAccessAcl() : AttributeAccessInterface(Optional<EndpointId>::Missing(), AccessControl::Id) {} |
| |
| CHIP_ERROR Read(const app::ConcreteReadAttributePath & aPath, app::AttributeValueEncoder & aEncoder) override; |
| CHIP_ERROR Write(const app::ConcreteDataAttributePath & aPath, app::AttributeValueDecoder & aDecoder) override; |
| |
| void OnListWriteBegin(const app::ConcreteAttributePath & aPath) override |
| { |
| if (mOnListWriteBegin) |
| { |
| mOnListWriteBegin(aPath); |
| } |
| } |
| |
| void OnListWriteEnd(const app::ConcreteAttributePath & aPath, bool aWriteWasSuccessful) override |
| { |
| if (mOnListWriteEnd) |
| { |
| mOnListWriteEnd(aPath, aWriteWasSuccessful); |
| } |
| } |
| |
| std::function<void(const app::ConcreteAttributePath & path)> mOnListWriteBegin; |
| std::function<void(const app::ConcreteAttributePath & path, bool wasSuccessful)> mOnListWriteEnd; |
| } testServerAcl; |
| |
| CHIP_ERROR TestAttrAccessAcl::Read(const app::ConcreteReadAttributePath & aPath, app::AttributeValueEncoder & aEncoder) |
| { |
| return CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE; |
| } |
| |
| CHIP_ERROR TestAttrAccessAcl::Write(const app::ConcreteDataAttributePath & aPath, app::AttributeValueDecoder & aDecoder) |
| { |
| // We only care about the number of attribute data. |
| if (!aPath.IsListItemOperation()) |
| { |
| app::DataModel::Nullable<app::DataModel::DecodableList<ByteSpan>> list; |
| CHIP_ERROR err = aDecoder.Decode(list); |
| ChipLogError(Zcl, "NotList/ReplaceAll: Decode result: %s", err.AsString()); |
| return err; |
| } |
| if (aPath.mListOp == app::ConcreteDataAttributePath::ListOperation::AppendItem) |
| { |
| ByteSpan listItem; |
| CHIP_ERROR err = aDecoder.Decode(listItem); |
| ChipLogError(Zcl, "AppendItem: Decode result: %s", err.AsString()); |
| return err; |
| } |
| |
| return CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE; |
| } |
| |
| /* |
| * This validates all the various corner cases encountered during chunking by artificially reducing the size of a packet buffer used |
| * to encode attribute data to force chunking to happen over multiple packets even with a small number of attributes and then slowly |
| * increasing the available size by 1 byte in each test iteration and re-running the write request generation logic. This 1-byte |
| * incremental approach sweeps through from a base scenario of N attributes fitting in a write request chunk, to eventually |
| * resulting in N+1 attributes fitting in a write request chunk. |
| * |
| * This will cause all the various corner cases encountered of closing out the various containers within the write request and |
| * thoroughly and definitely validate those edge cases. |
| */ |
| TEST_F(TestWriteChunking, TestListChunking) |
| { |
| auto sessionHandle = GetSessionBobToAlice(); |
| |
| // Initialize the ember side server logic |
| app::InteractionModelEngine::GetInstance()->SetDataModelProvider(CodegenDataModelProviderInstance(nullptr /* delegate */)); |
| InitDataModelHandler(); |
| |
| // Register our fake dynamic endpoint. |
| emberAfSetDynamicEndpoint(0, kTestEndpointId, &testEndpoint, Span<DataVersion>(dataVersionStorage)); |
| |
| // Register our fake attribute access interface. |
| AttributeAccessInterfaceRegistry::Instance().Register(&testServer); |
| |
| app::AttributePathParams attributePath(kTestEndpointId, app::Clusters::UnitTesting::Id, kTestListAttribute); |
| // |
| // We've empirically determined that by reserving all but 75 bytes in the packet buffer, we can fit 2 |
| // AttributeDataIBs into the packet. ~30-40 bytes covers a single write chunk, but let's 2-3x that |
| // to ensure we'll sweep from fitting 2 chunks to 3-4 chunks. |
| // |
| constexpr size_t minReservationSize = kMaxSecureSduLengthBytes - 75 - 100; |
| |
| for (EncodingMethod encodingMethod : { EncodingMethod::Standard, EncodingMethod::PreencodedTLV }) |
| { |
| |
| for (uint32_t i = 100; i > 0; i--) |
| { |
| CHIP_ERROR err = CHIP_NO_ERROR; |
| TestWriteCallback writeCallback; |
| |
| ChipLogDetail(DataManagement, "Running iteration %d\n", static_cast<int>(i)); |
| |
| gIterationCount = i; |
| |
| app::WriteClient writeClient(&GetExchangeManager(), &writeCallback, Optional<uint16_t>::Missing(), |
| static_cast<uint16_t>(minReservationSize + i) /* reserved buffer size */); |
| |
| ByteSpan list[kTestListLength]; |
| |
| if (encodingMethod == EncodingMethod::Standard) |
| { |
| err = writeClient.EncodeAttribute(attributePath, app::DataModel::List<ByteSpan>(list, kTestListLength)); |
| EXPECT_EQ(err, CHIP_NO_ERROR); |
| } |
| else if (encodingMethod == EncodingMethod::PreencodedTLV) |
| { |
| TLV::ScopedBufferTLVReader encodedListTLV; |
| EncodeAttributeListIntoTLV(DataModel::List<ByteSpan>(list, kTestListLength), encodedListTLV); |
| |
| ConcreteDataAttributePath path = |
| ConcreteDataAttributePath(attributePath.mEndpointId, attributePath.mClusterId, attributePath.mAttributeId); |
| EXPECT_EQ(writeClient.PutPreencodedAttribute(path, encodedListTLV), CHIP_NO_ERROR); |
| } |
| |
| // |
| |
| err = writeClient.SendWriteRequest(sessionHandle); |
| EXPECT_EQ(err, CHIP_NO_ERROR); |
| |
| // |
| // Service the IO + Engine till we get a ReportEnd callback on the client. |
| // Since bugs can happen, we don't want this test to never stop, so create a ceiling for how many |
| // times this can run without seeing expected results. |
| // |
| for (int j = 0; j < 10 && writeCallback.mOnDoneCount == 0; j++) |
| { |
| DrainAndServiceIO(); |
| } |
| |
| EXPECT_EQ(writeCallback.mSuccessCount, kTestListLength + 1 /* an extra item for the empty list at the beginning */); |
| EXPECT_EQ(writeCallback.mErrorCount, 0u); |
| EXPECT_EQ(writeCallback.mOnDoneCount, 1u); |
| |
| EXPECT_EQ(GetExchangeManager().GetNumActiveExchanges(), 0u); |
| |
| // |
| // Stop the test if we detected an error. Otherwise, it'll be difficult to read the logs. |
| // |
| if (HasFailure()) |
| { |
| break; |
| } |
| } |
| } |
| emberAfClearDynamicEndpoint(0); |
| } |
| |
| /* |
| * A Variant of TestListChunking above, that tests the Code Path where we encode a Non-Replace All List in WriteRequests, this |
| * happens with the ACL Cluster (this would be generalised to all relevant Attributes after issue #38270 is resolved) |
| */ |
| TEST_F(TestWriteChunking, TestListChunking_NonEmptyReplaceAllList) |
| { |
| auto sessionHandle = GetSessionBobToAlice(); |
| |
| // Initialize the ember side server logic |
| app::InteractionModelEngine::GetInstance()->SetDataModelProvider(CodegenDataModelProviderInstance(nullptr /* delegate */)); |
| InitDataModelHandler(); |
| |
| // Register our fake dynamic endpoint. |
| emberAfSetDynamicEndpoint(0, kTestEndpointId, &testEndpointAcl, Span<DataVersion>(dataVersionStorageAcl)); |
| |
| // Register our fake attribute access interface. |
| AttributeAccessInterfaceRegistry::Instance().Register(&testServerAcl); |
| |
| app::AttributePathParams attributePath(kTestEndpointId, AccessControl::Id, AccessControl::Attributes::Acl::Id); |
| |
| // We've empirically determined that by reserving all but 65 bytes in the packet buffer, we can fit a Single AttributeDataIB |
| // into the packet with a List of 8/9 ByteSpans with empty items. So if we have a ByteSpan List of 20 items, our initial |
| // ReplaceAll list will contain 8/9 items, and the remaining items are chunked as appropriate. |
| constexpr size_t maxReservationSize = kMaxSecureSduLengthBytes - 65; |
| |
| constexpr uint8_t kTestListLength2 = 20; |
| |
| // Start with a high reservation (maxReservationSize) to force chunking, then decrease the reservation in 1-byte steps. |
| // This increases the buffer space available for encoding, gradually reducing the need for chunking, until chunking would not |
| // occur anymore. This helps validate various edge cases. |
| |
| for (EncodingMethod encodingMethod : { EncodingMethod::Standard, EncodingMethod::PreencodedTLV }) |
| { |
| for (uint32_t reservationReduction = 0; reservationReduction < 40; reservationReduction++) |
| { |
| CHIP_ERROR err = CHIP_NO_ERROR; |
| TestWriteCallback writeCallback; |
| |
| ChipLogDetail(DataManagement, "Running iteration %d\n", static_cast<int>(reservationReduction)); |
| |
| app::WriteClient writeClient( |
| &GetExchangeManager(), &writeCallback, NullOptional, |
| static_cast<uint16_t>(maxReservationSize - reservationReduction) /* reserved buffer size */); |
| |
| ByteSpan list[kTestListLength2]; |
| |
| if (encodingMethod == EncodingMethod::Standard) |
| { |
| err = writeClient.EncodeAttribute(attributePath, app::DataModel::List<ByteSpan>(list, kTestListLength2)); |
| EXPECT_EQ(err, CHIP_NO_ERROR); |
| } |
| else if (encodingMethod == EncodingMethod::PreencodedTLV) |
| { |
| TLV::ScopedBufferTLVReader encodedListTLV; |
| EncodeAttributeListIntoTLV(DataModel::List<ByteSpan>(list, kTestListLength2), encodedListTLV); |
| |
| ConcreteDataAttributePath path = |
| ConcreteDataAttributePath(attributePath.mEndpointId, attributePath.mClusterId, attributePath.mAttributeId); |
| EXPECT_EQ(writeClient.PutPreencodedAttribute(path, encodedListTLV), CHIP_NO_ERROR); |
| } |
| |
| // Ensure that chunking actually occurred in the first iteration. We will iteratively chunk less and less, until |
| // chunking would not occur anymore. Thus, this check is only needed at start. |
| if (reservationReduction == 0) |
| { |
| ASSERT_TRUE(writeClient.IsWriteRequestChunked()); |
| } |
| |
| err = writeClient.SendWriteRequest(sessionHandle); |
| EXPECT_EQ(err, CHIP_NO_ERROR); |
| |
| // |
| // Service the IO + Engine till we get a ReportEnd callback on the client. |
| // Since bugs can happen, we don't want this test to never stop, so create a ceiling for how many |
| // times this can run without seeing expected results. |
| // |
| for (int j = 0; j < 10 && writeCallback.mOnDoneCount == 0; j++) |
| { |
| DrainAndServiceIO(); |
| } |
| |
| // Due to Write Chunking being done dynamically (fitting as many items as possible into an initial ReplaceAll List, |
| // before starting to chunk), it is fragile to try to predict mSuccessCount. It all depends on how much was packed into |
| // the initial ReplaceAll List. However, we know for sure that writeCallback should NEVER fail. |
| EXPECT_EQ(writeCallback.mErrorCount, 0u); |
| EXPECT_EQ(writeCallback.mOnDoneCount, 1u); |
| |
| EXPECT_EQ(GetExchangeManager().GetNumActiveExchanges(), 0u); |
| |
| // |
| // Stop the test if we detected an error. Otherwise, it'll be difficult to read the logs. |
| // |
| if (HasFailure()) |
| { |
| break; |
| } |
| } |
| } |
| |
| emberAfClearDynamicEndpoint(0); |
| } |
| |
| // We encode a pretty large write payload to test the corner cases related to message layer and secure session overheads. |
| // The test should gurantee that if encode returns no error, the send should also success. |
| // As the actual overhead may change, we will test over a few possible payload lengths, from 850 to MTU used in write clients. |
| TEST_F(TestWriteChunking, TestBadChunking) |
| { |
| auto sessionHandle = GetSessionBobToAlice(); |
| |
| bool atLeastOneRequestSent = false; |
| bool atLeastOneRequestFailed = false; |
| |
| // Initialize the ember side server logic |
| app::InteractionModelEngine::GetInstance()->SetDataModelProvider(CodegenDataModelProviderInstance(nullptr /* delegate */)); |
| InitDataModelHandler(); |
| |
| // Register our fake dynamic endpoint. |
| emberAfSetDynamicEndpoint(0, kTestEndpointId, &testEndpoint, Span<DataVersion>(dataVersionStorage)); |
| |
| // Register our fake attribute access interface. |
| AttributeAccessInterfaceRegistry::Instance().Register(&testServer); |
| |
| app::AttributePathParams attributePath(kTestEndpointId, app::Clusters::UnitTesting::Id, kTestListAttribute); |
| |
| for (int i = 850; i < static_cast<int>(chip::app::kMaxSecureSduLengthBytes); i++) |
| { |
| CHIP_ERROR err = CHIP_NO_ERROR; |
| TestWriteCallback writeCallback; |
| |
| ChipLogDetail(DataManagement, "Running iteration with OCTET_STRING length = %d\n", i); |
| |
| gIterationCount = (uint32_t) i; |
| |
| app::WriteClient writeClient(&GetExchangeManager(), &writeCallback, Optional<uint16_t>::Missing()); |
| |
| ByteSpan list[kTestListLength]; |
| for (auto & item : list) |
| { |
| item = ByteSpan(sByteSpanData, static_cast<uint32_t>(i)); |
| } |
| |
| err = writeClient.EncodeAttribute(attributePath, app::DataModel::List<ByteSpan>(list, kTestListLength)); |
| if (err == CHIP_ERROR_NO_MEMORY || err == CHIP_ERROR_BUFFER_TOO_SMALL) |
| { |
| // This kind of error is expected. |
| atLeastOneRequestFailed = true; |
| continue; |
| } |
| |
| atLeastOneRequestSent = true; |
| |
| // If we successfully encoded the attribute, then we must be able to send the message. |
| err = writeClient.SendWriteRequest(sessionHandle); |
| EXPECT_EQ(err, CHIP_NO_ERROR); |
| |
| // |
| // Service the IO + Engine till we get a ReportEnd callback on the client. |
| // Since bugs can happen, we don't want this test to never stop, so create a ceiling for how many |
| // times this can run without seeing expected results. |
| // |
| for (int j = 0; j < 10 && writeCallback.mOnDoneCount == 0; j++) |
| { |
| DrainAndServiceIO(); |
| } |
| |
| EXPECT_EQ(writeCallback.mSuccessCount, kTestListLength + 1 /* an extra item for the empty list at the beginning */); |
| EXPECT_EQ(writeCallback.mErrorCount, 0u); |
| EXPECT_EQ(writeCallback.mOnDoneCount, 1u); |
| |
| EXPECT_EQ(GetExchangeManager().GetNumActiveExchanges(), 0u); |
| |
| // |
| // Stop the test if we detected an error. Otherwise, it'll be difficult to read the logs. |
| // |
| if (HasFailure()) |
| { |
| break; |
| } |
| } |
| EXPECT_EQ(GetExchangeManager().GetNumActiveExchanges(), 0u); |
| EXPECT_TRUE(atLeastOneRequestSent && atLeastOneRequestFailed); |
| emberAfClearDynamicEndpoint(0); |
| } |
| |
| /* |
| * A Variant of TestBadChunking above, that tests the Code Path where we encode a Non-Replace All List in WriteRequests, this |
| * happens with the ACL Cluster. |
| */ |
| TEST_F(TestWriteChunking, TestBadChunking_NonEmptyReplaceAllList) |
| { |
| auto sessionHandle = GetSessionBobToAlice(); |
| |
| bool atLeastOneRequestSent = false; |
| bool atLeastOneRequestFailed = false; |
| |
| // Initialize the ember side server logic |
| app::InteractionModelEngine::GetInstance()->SetDataModelProvider(CodegenDataModelProviderInstance(nullptr /* delegate */)); |
| InitDataModelHandler(); |
| |
| // Register our fake dynamic endpoint. |
| emberAfSetDynamicEndpoint(0, kTestEndpointId, &testEndpointAcl, Span<DataVersion>(dataVersionStorageAcl)); |
| |
| // Register our fake attribute access interface. |
| AttributeAccessInterfaceRegistry::Instance().Register(&testServerAcl); |
| |
| app::AttributePathParams attributePath(kTestEndpointId, AccessControl::Id, AccessControl::Attributes::Acl::Id); |
| |
| constexpr uint8_t kTestListLengthBadChunking = 5; |
| |
| for (int bufferSize = 850; bufferSize < static_cast<int>(chip::app::kMaxSecureSduLengthBytes); bufferSize++) |
| { |
| CHIP_ERROR err = CHIP_NO_ERROR; |
| TestWriteCallback writeCallback; |
| |
| ChipLogDetail(DataManagement, "Running iteration with OCTET_STRING length = %d\n", bufferSize); |
| |
| app::WriteClient writeClient(&GetExchangeManager(), &writeCallback, NullOptional); |
| |
| ByteSpan list[kTestListLengthBadChunking]; |
| for (auto & item : list) |
| { |
| item = ByteSpan(sByteSpanData, static_cast<uint32_t>(bufferSize)); |
| } |
| |
| err = writeClient.EncodeAttribute(attributePath, app::DataModel::List<ByteSpan>(list, kTestListLengthBadChunking)); |
| if (err == CHIP_ERROR_NO_MEMORY || err == CHIP_ERROR_BUFFER_TOO_SMALL) |
| { |
| // This kind of error is expected. |
| atLeastOneRequestFailed = true; |
| continue; |
| } |
| |
| atLeastOneRequestSent = true; |
| |
| // Ensure that chunking actually occurred. Since chunking is dynamic, it's easy to unintentionally avoid it. |
| // This check guarantees that the test is validating chunked behavior as intended. |
| EXPECT_TRUE(writeClient.IsWriteRequestChunked()); |
| |
| // If we successfully encoded the attribute, then we must be able to send the message. |
| err = writeClient.SendWriteRequest(sessionHandle); |
| EXPECT_EQ(err, CHIP_NO_ERROR); |
| |
| // |
| // Service the IO + Engine till we get a ReportEnd callback on the client. |
| // Since bugs can happen, we don't want this test to never stop, so create a ceiling for how many |
| // times this can run without seeing expected results. |
| // |
| for (int j = 0; j < 10 && writeCallback.mOnDoneCount == 0; j++) |
| { |
| DrainAndServiceIO(); |
| } |
| |
| // Due to the way Write Chunking is done, it is difficult to predict mSuccessCount. It all depends on how much was |
| // packed into the initial ReplaceAll List. |
| EXPECT_EQ(writeCallback.mErrorCount, 0u); |
| EXPECT_EQ(writeCallback.mOnDoneCount, 1u); |
| |
| EXPECT_EQ(GetExchangeManager().GetNumActiveExchanges(), 0u); |
| |
| // |
| // Stop the test if we detected an error. Otherwise, it'll be difficult to read the logs. |
| // |
| if (HasFailure()) |
| { |
| break; |
| } |
| } |
| EXPECT_EQ(GetExchangeManager().GetNumActiveExchanges(), 0u); |
| EXPECT_TRUE(atLeastOneRequestSent && atLeastOneRequestFailed); |
| emberAfClearDynamicEndpoint(0); |
| } |
| |
| /* |
| * When chunked write is enabled, it is dangerous to handle multiple write requests at the same time. In this case, we will |
| * reject the latter write requests to the same attribute. |
| */ |
| TEST_F(TestWriteChunking, TestConflictWrite) |
| { |
| auto sessionHandle = GetSessionBobToAlice(); |
| |
| // Initialize the ember side server logic |
| app::InteractionModelEngine::GetInstance()->SetDataModelProvider(CodegenDataModelProviderInstance(nullptr /* delegate */)); |
| InitDataModelHandler(); |
| |
| // Register our fake dynamic endpoint. |
| emberAfSetDynamicEndpoint(0, kTestEndpointId, &testEndpoint, Span<DataVersion>(dataVersionStorage)); |
| |
| // Register our fake attribute access interface. |
| AttributeAccessInterfaceRegistry::Instance().Register(&testServer); |
| |
| app::AttributePathParams attributePath(kTestEndpointId, app::Clusters::UnitTesting::Id, kTestListAttribute); |
| |
| /* use a smaller chunk (128 bytes) so we only need a few attributes in the write request. */ |
| constexpr size_t kReserveSize = kMaxSecureSduLengthBytes - 128; |
| |
| TestWriteCallback writeCallback1; |
| app::WriteClient writeClient1(&GetExchangeManager(), &writeCallback1, Optional<uint16_t>::Missing(), |
| static_cast<uint16_t>(kReserveSize)); |
| |
| TestWriteCallback writeCallback2; |
| app::WriteClient writeClient2(&GetExchangeManager(), &writeCallback2, Optional<uint16_t>::Missing(), |
| static_cast<uint16_t>(kReserveSize)); |
| |
| ByteSpan list[kTestListLength]; |
| |
| CHIP_ERROR err = CHIP_NO_ERROR; |
| |
| err = writeClient1.EncodeAttribute(attributePath, app::DataModel::List<ByteSpan>(list, kTestListLength)); |
| EXPECT_EQ(err, CHIP_NO_ERROR); |
| err = writeClient2.EncodeAttribute(attributePath, app::DataModel::List<ByteSpan>(list, kTestListLength)); |
| EXPECT_EQ(err, CHIP_NO_ERROR); |
| |
| err = writeClient1.SendWriteRequest(sessionHandle); |
| EXPECT_EQ(err, CHIP_NO_ERROR); |
| |
| err = writeClient2.SendWriteRequest(sessionHandle); |
| EXPECT_EQ(err, CHIP_NO_ERROR); |
| |
| DrainAndServiceIO(); |
| |
| { |
| const TestWriteCallback * writeCallbackRef1 = &writeCallback1; |
| const TestWriteCallback * writeCallbackRef2 = &writeCallback2; |
| |
| // Exactly one of WriteClient1 and WriteClient2 should success, not both. |
| |
| if (writeCallback1.mSuccessCount == 0) |
| { |
| writeCallbackRef2 = &writeCallback1; |
| writeCallbackRef1 = &writeCallback2; |
| } |
| |
| EXPECT_EQ(writeCallbackRef1->mSuccessCount, kTestListLength + 1 /* an extra item for the empty list at the beginning */); |
| EXPECT_EQ(writeCallbackRef1->mErrorCount, 0u); |
| EXPECT_EQ(writeCallbackRef2->mSuccessCount, 0u); |
| EXPECT_EQ(writeCallbackRef2->mErrorCount, kTestListLength + 1); |
| EXPECT_EQ(writeCallbackRef2->mLastErrorReason.mStatus, Protocols::InteractionModel::Status::Busy); |
| |
| EXPECT_EQ(writeCallbackRef1->mOnDoneCount, 1u); |
| EXPECT_EQ(writeCallbackRef2->mOnDoneCount, 1u); |
| } |
| |
| EXPECT_EQ(GetExchangeManager().GetNumActiveExchanges(), 0u); |
| |
| emberAfClearDynamicEndpoint(0); |
| } |
| |
| /* |
| * A Variant of TestConflictWrite above, that tests the Code Path where we encode a Non-Replace All List in WriteRequests, this |
| * happens with the ACL Cluster. |
| */ |
| TEST_F(TestWriteChunking, TestConflictWrite_NonEmptyReplaceAllList) |
| { |
| auto sessionHandle = GetSessionBobToAlice(); |
| |
| // Initialize the ember side server logic |
| app::InteractionModelEngine::GetInstance()->SetDataModelProvider(CodegenDataModelProviderInstance(nullptr /* delegate */)); |
| InitDataModelHandler(); |
| |
| // Register our fake dynamic endpoint. |
| emberAfSetDynamicEndpoint(0, kTestEndpointId, &testEndpointAcl, Span<DataVersion>(dataVersionStorageAcl)); |
| |
| // Register our fake attribute access interface. |
| AttributeAccessInterfaceRegistry::Instance().Register(&testServerAcl); |
| |
| app::AttributePathParams attributePath(kTestEndpointId, AccessControl::Id, AccessControl::Attributes::Acl::Id); |
| |
| // To ensure that chunking is triggered: We've empirically determined that by reserving all but 60 bytes in the packet |
| // buffer, we can fit 1 AttributeDataIB into the packet with a List of 5/6 ByteSpans with empty items. So if we have a |
| // ByteSpan List of 10 items, our initial ReplaceAll list will contain 5/6 items, and the remaining items are chunked. |
| constexpr size_t kReserveSize = kMaxSecureSduLengthBytes - 60; |
| constexpr uint8_t kTestListLength2 = 10; |
| |
| ByteSpan list[kTestListLength2]; |
| |
| TestWriteCallback writeCallback1; |
| app::WriteClient writeClient1(&GetExchangeManager(), &writeCallback1, NullOptional, static_cast<uint16_t>(kReserveSize)); |
| |
| TestWriteCallback writeCallback2; |
| app::WriteClient writeClient2(&GetExchangeManager(), &writeCallback2, NullOptional, static_cast<uint16_t>(kReserveSize)); |
| |
| CHIP_ERROR err = CHIP_NO_ERROR; |
| |
| err = writeClient1.EncodeAttribute(attributePath, app::DataModel::List<ByteSpan>(list, kTestListLength2)); |
| EXPECT_EQ(err, CHIP_NO_ERROR); |
| err = writeClient2.EncodeAttribute(attributePath, app::DataModel::List<ByteSpan>(list, kTestListLength2)); |
| EXPECT_EQ(err, CHIP_NO_ERROR); |
| |
| // Ensure that chunking actually occurred. Since chunking is dynamic, it's easy to unintentionally avoid it. |
| // This check guarantees that the test is validating chunked behavior as intended. |
| EXPECT_TRUE(writeClient1.IsWriteRequestChunked()); |
| EXPECT_TRUE(writeClient2.IsWriteRequestChunked()); |
| |
| err = writeClient1.SendWriteRequest(sessionHandle); |
| EXPECT_EQ(err, CHIP_NO_ERROR); |
| |
| err = writeClient2.SendWriteRequest(sessionHandle); |
| EXPECT_EQ(err, CHIP_NO_ERROR); |
| |
| DrainAndServiceIO(); |
| |
| { |
| const TestWriteCallback * writeCallbackRef1 = &writeCallback1; |
| const TestWriteCallback * writeCallbackRef2 = &writeCallback2; |
| |
| // Exactly one of WriteClient1 and WriteClient2 should success, not both. |
| |
| if (writeCallback1.mSuccessCount == 0) |
| { |
| writeCallbackRef2 = &writeCallback1; |
| writeCallbackRef1 = &writeCallback2; |
| } |
| |
| // Due to Write Chunking being done dynamically (fitting as many items as possible into an initial ReplaceAll List, |
| // before starting to chunk), it is fragile to try to predict mSuccessCount. It all depends on how much was packed into |
| // the initial ReplaceAll List. However, we know for sure that writeCallbackRef1 should NEVER fail, and that |
| // writeCallbackRef2 should NEVER Succeed. |
| EXPECT_EQ(writeCallbackRef1->mErrorCount, 0u); |
| EXPECT_EQ(writeCallbackRef2->mSuccessCount, 0u); |
| |
| EXPECT_EQ(writeCallbackRef2->mLastErrorReason.mStatus, Protocols::InteractionModel::Status::Busy); |
| |
| EXPECT_EQ(writeCallbackRef1->mOnDoneCount, 1u); |
| EXPECT_EQ(writeCallbackRef2->mOnDoneCount, 1u); |
| } |
| |
| EXPECT_EQ(GetExchangeManager().GetNumActiveExchanges(), 0u); |
| |
| emberAfClearDynamicEndpoint(0); |
| } |
| |
| /* |
| * When chunked write is enabled, it is dangerous to handle multiple write requests at the same time. However, we will allow |
| * such change when writing to different attributes in parallel. |
| */ |
| TEST_F(TestWriteChunking, TestNonConflictWrite) |
| { |
| auto sessionHandle = GetSessionBobToAlice(); |
| |
| // Initialize the ember side server logic |
| app::InteractionModelEngine::GetInstance()->SetDataModelProvider(CodegenDataModelProviderInstance(nullptr /* delegate */)); |
| InitDataModelHandler(); |
| |
| // Register our fake dynamic endpoint. |
| emberAfSetDynamicEndpoint(0, kTestEndpointId, &testEndpoint, Span<DataVersion>(dataVersionStorage)); |
| |
| // Register our fake attribute access interface. |
| AttributeAccessInterfaceRegistry::Instance().Register(&testServer); |
| |
| app::AttributePathParams attributePath1(kTestEndpointId, app::Clusters::UnitTesting::Id, kTestListAttribute); |
| app::AttributePathParams attributePath2(kTestEndpointId, app::Clusters::UnitTesting::Id, kTestListAttribute2); |
| |
| /* use a smaller chunk (128 bytes) so we only need a few attributes in the write request. */ |
| constexpr size_t kReserveSize = kMaxSecureSduLengthBytes - 128; |
| |
| TestWriteCallback writeCallback1; |
| app::WriteClient writeClient1(&GetExchangeManager(), &writeCallback1, Optional<uint16_t>::Missing(), |
| static_cast<uint16_t>(kReserveSize)); |
| |
| TestWriteCallback writeCallback2; |
| app::WriteClient writeClient2(&GetExchangeManager(), &writeCallback2, Optional<uint16_t>::Missing(), |
| static_cast<uint16_t>(kReserveSize)); |
| |
| ByteSpan list[kTestListLength]; |
| |
| CHIP_ERROR err = CHIP_NO_ERROR; |
| |
| err = writeClient1.EncodeAttribute(attributePath1, app::DataModel::List<ByteSpan>(list, kTestListLength)); |
| EXPECT_EQ(err, CHIP_NO_ERROR); |
| err = writeClient2.EncodeAttribute(attributePath2, app::DataModel::List<ByteSpan>(list, kTestListLength)); |
| EXPECT_EQ(err, CHIP_NO_ERROR); |
| |
| err = writeClient1.SendWriteRequest(sessionHandle); |
| EXPECT_EQ(err, CHIP_NO_ERROR); |
| |
| err = writeClient2.SendWriteRequest(sessionHandle); |
| EXPECT_EQ(err, CHIP_NO_ERROR); |
| |
| DrainAndServiceIO(); |
| |
| { |
| EXPECT_EQ(writeCallback1.mErrorCount, 0u); |
| EXPECT_EQ(writeCallback1.mSuccessCount, kTestListLength + 1); |
| EXPECT_EQ(writeCallback2.mErrorCount, 0u); |
| EXPECT_EQ(writeCallback2.mSuccessCount, kTestListLength + 1); |
| |
| EXPECT_EQ(writeCallback1.mOnDoneCount, 1u); |
| EXPECT_EQ(writeCallback2.mOnDoneCount, 1u); |
| } |
| |
| EXPECT_EQ(GetExchangeManager().GetNumActiveExchanges(), 0u); |
| |
| emberAfClearDynamicEndpoint(0); |
| } |
| |
| /* |
| * A Variant of TestNonConflictWrite above, that tests the Code Path where we encode a Non-Replace All List in WriteRequests, |
| * this happens with the ACL Cluster. |
| */ |
| TEST_F(TestWriteChunking, TestNonConflictWrite_NonEmptyReplaceAllList) |
| { |
| auto sessionHandle = GetSessionBobToAlice(); |
| |
| // Initialize the ember side server logic |
| app::InteractionModelEngine::GetInstance()->SetDataModelProvider(CodegenDataModelProviderInstance(nullptr /* delegate */)); |
| InitDataModelHandler(); |
| |
| // Register our fake dynamic endpoint. |
| emberAfSetDynamicEndpoint(0, kTestEndpointId, &testEndpointAcl, Span<DataVersion>(dataVersionStorageAcl)); |
| |
| // Register our fake attribute access interface. |
| AttributeAccessInterfaceRegistry::Instance().Register(&testServerAcl); |
| |
| app::AttributePathParams attributePath1(kTestEndpointId, AccessControl::Id, AccessControl::Attributes::Acl::Id); |
| app::AttributePathParams attributePath2(kTestEndpointId, AccessControl::Id, AccessControl::Attributes::Extension::Id); |
| |
| // To ensure that chunking is triggered: We've empirically determined that by reserving all but 60 bytes in the packet |
| // buffer, we can fit 1 AttributeDataIB into the packet with a List of 5/6 ByteSpans with empty items. So if we have a |
| // ByteSpan List of 10 items, our initial ReplaceAll list will contain 5/6 items, and the remaining items are chunked. |
| constexpr size_t kReserveSize = kMaxSecureSduLengthBytes - 65; |
| constexpr uint8_t kTestListLength2 = 10; |
| |
| TestWriteCallback writeCallback1; |
| app::WriteClient writeClient1(&GetExchangeManager(), &writeCallback1, NullOptional, static_cast<uint16_t>(kReserveSize)); |
| |
| TestWriteCallback writeCallback2; |
| app::WriteClient writeClient2(&GetExchangeManager(), &writeCallback2, NullOptional, static_cast<uint16_t>(kReserveSize)); |
| |
| ByteSpan list[kTestListLength2]; |
| |
| CHIP_ERROR err = CHIP_NO_ERROR; |
| |
| err = writeClient1.EncodeAttribute(attributePath1, app::DataModel::List<ByteSpan>(list, kTestListLength2)); |
| EXPECT_EQ(err, CHIP_NO_ERROR); |
| |
| err = writeClient2.EncodeAttribute(attributePath2, app::DataModel::List<ByteSpan>(list, kTestListLength2)); |
| EXPECT_EQ(err, CHIP_NO_ERROR); |
| |
| // Ensure that chunking actually occurred. Since chunking is dynamic, it's easy to unintentionally avoid it. |
| // This check guarantees that the test is validating chunked behavior as intended. |
| EXPECT_TRUE(writeClient1.IsWriteRequestChunked()); |
| EXPECT_TRUE(writeClient2.IsWriteRequestChunked()); |
| |
| err = writeClient1.SendWriteRequest(sessionHandle); |
| EXPECT_EQ(err, CHIP_NO_ERROR); |
| |
| err = writeClient2.SendWriteRequest(sessionHandle); |
| EXPECT_EQ(err, CHIP_NO_ERROR); |
| |
| DrainAndServiceIO(); |
| |
| { |
| |
| // Due to Write Chunking being done dynamically, it is fragile to try to predict mSuccessCount. It all depends on how |
| // much was packed into the initial ReplaceAll List. However, we know for sure that writeCallback1 and writeCallback2 |
| // should NEVER fail. |
| EXPECT_EQ(writeCallback1.mErrorCount, 0u); |
| EXPECT_EQ(writeCallback2.mErrorCount, 0u); |
| |
| EXPECT_EQ(writeCallback1.mOnDoneCount, 1u); |
| EXPECT_EQ(writeCallback2.mOnDoneCount, 1u); |
| } |
| |
| EXPECT_EQ(GetExchangeManager().GetNumActiveExchanges(), 0u); |
| |
| emberAfClearDynamicEndpoint(0); |
| } |
| // for (EncodingMethod encodingMethod : { EncodingMethod::Standard, EncodingMethod::PreencodedTLV }) |
| |
| void TestWriteChunking::RunTest(Instructions instructions, EncodingMethod encodingMethod = EncodingMethod::Standard) |
| { |
| CHIP_ERROR err = CHIP_NO_ERROR; |
| auto sessionHandle = GetSessionBobToAlice(); |
| |
| TestWriteCallback writeCallback; |
| std::unique_ptr<WriteClient> writeClient = std::make_unique<WriteClient>( |
| &GetExchangeManager(), &writeCallback, Optional<uint16_t>::Missing(), |
| static_cast<uint16_t>(kMaxSecureSduLengthBytes - |
| 128) /* use a smaller chunk so we only need a few attributes in the write request. */); |
| |
| ConcreteAttributePath onGoingPath = ConcreteAttributePath(); |
| std::vector<PathStatus> status; |
| |
| testServer.mOnListWriteBegin = [&](const ConcreteAttributePath & aPath) { |
| EXPECT_EQ(onGoingPath, ConcreteAttributePath()); |
| onGoingPath = aPath; |
| ChipLogProgress(Zcl, "OnListWriteBegin endpoint=%u Cluster=" ChipLogFormatMEI " attribute=" ChipLogFormatMEI, |
| aPath.mEndpointId, ChipLogValueMEI(aPath.mClusterId), ChipLogValueMEI(aPath.mAttributeId)); |
| if (instructions.onListWriteBeginActions) |
| { |
| switch (instructions.onListWriteBeginActions(aPath)) |
| { |
| case Operations::kNoop: |
| break; |
| case Operations::kShutdownWriteClient: |
| // By setting writeClient to nullptr, we actually shutdown the write interaction to simulate a timeout. |
| writeClient = nullptr; |
| } |
| } |
| }; |
| testServer.mOnListWriteEnd = [&](const ConcreteAttributePath & aPath, bool aWasSuccessful) { |
| EXPECT_EQ(onGoingPath, aPath); |
| status.push_back(PathStatus(aPath, aWasSuccessful)); |
| onGoingPath = ConcreteAttributePath(); |
| ChipLogProgress(Zcl, "OnListWriteEnd endpoint=%u Cluster=" ChipLogFormatMEI " attribute=" ChipLogFormatMEI, |
| aPath.mEndpointId, ChipLogValueMEI(aPath.mClusterId), ChipLogValueMEI(aPath.mAttributeId)); |
| }; |
| |
| ByteSpan list[kTestListLength]; |
| uint8_t badList[kTestListLength]; |
| |
| if (instructions.data.size() == 0) |
| { |
| instructions.data = std::vector<ListData>(instructions.paths.size(), ListData::kList); |
| } |
| EXPECT_EQ(instructions.paths.size(), instructions.data.size()); |
| |
| for (size_t i = 0; i < instructions.paths.size(); i++) |
| { |
| const auto & p = instructions.paths[i]; |
| switch (instructions.data[i]) |
| { |
| case ListData::kNull: { |
| DataModel::Nullable<uint8_t> null; // The actual type is not important since we will only put a null value. |
| err = writeClient->EncodeAttribute(AttributePathParams(p.mEndpointId, p.mClusterId, p.mAttributeId), null); |
| break; |
| } |
| case ListData::kList: { |
| |
| if (encodingMethod == EncodingMethod::Standard) |
| { |
| err = writeClient->EncodeAttribute(AttributePathParams(p.mEndpointId, p.mClusterId, p.mAttributeId), |
| DataModel::List<ByteSpan>(list, kTestListLength)); |
| } |
| else if (encodingMethod == EncodingMethod::PreencodedTLV) |
| { |
| TLV::ScopedBufferTLVReader encodedListTLV; |
| EncodeAttributeListIntoTLV(DataModel::List<ByteSpan>(list, kTestListLength), encodedListTLV); |
| |
| ConcreteDataAttributePath path = ConcreteDataAttributePath(p.mEndpointId, p.mClusterId, p.mAttributeId); |
| err = writeClient->PutPreencodedAttribute(path, encodedListTLV); |
| } |
| break; |
| } |
| case ListData::kBadValue: { |
| |
| if (encodingMethod == EncodingMethod::Standard) |
| { |
| err = writeClient->EncodeAttribute(AttributePathParams(p.mEndpointId, p.mClusterId, p.mAttributeId), |
| DataModel::List<uint8_t>(badList, kTestListLength)); |
| } |
| else if (encodingMethod == EncodingMethod::PreencodedTLV) |
| { |
| TLV::ScopedBufferTLVReader encodedListTLV; |
| EncodeAttributeListIntoTLV(DataModel::List<uint8_t>(badList, kTestListLength), encodedListTLV); |
| |
| ConcreteDataAttributePath path = ConcreteDataAttributePath(p.mEndpointId, p.mClusterId, p.mAttributeId); |
| err = writeClient->PutPreencodedAttribute(path, encodedListTLV); |
| } |
| |
| break; |
| } |
| } |
| EXPECT_EQ(err, CHIP_NO_ERROR); |
| } |
| |
| err = writeClient->SendWriteRequest(sessionHandle); |
| EXPECT_EQ(err, CHIP_NO_ERROR); |
| |
| GetIOContext().DriveIOUntil( |
| sessionHandle->ComputeRoundTripTimeout(app::kExpectedIMProcessingTime, true /*isFirstMessageOnExchange*/) + |
| System::Clock::Seconds16(1), |
| [&]() { return GetExchangeManager().GetNumActiveExchanges() == 0; }); |
| |
| EXPECT_EQ(onGoingPath, app::ConcreteAttributePath()); |
| EXPECT_EQ(status.size(), instructions.expectedStatus.size()); |
| |
| for (size_t i = 0; i < status.size(); i++) |
| { |
| EXPECT_EQ(status[i], PathStatus(instructions.paths[i], instructions.expectedStatus[i])); |
| } |
| |
| testServer.mOnListWriteBegin = nullptr; |
| testServer.mOnListWriteEnd = nullptr; |
| } |
| |
| TEST_F(TestWriteChunking, TestTransactionalList) |
| { |
| // Initialize the ember side server logic |
| app::InteractionModelEngine::GetInstance()->SetDataModelProvider(CodegenDataModelProviderInstance(nullptr /* delegate */)); |
| InitDataModelHandler(); |
| |
| // Register our fake dynamic endpoint. |
| emberAfSetDynamicEndpoint(0, kTestEndpointId, &testEndpoint, Span<DataVersion>(dataVersionStorage)); |
| |
| // Register our fake attribute access interface. |
| AttributeAccessInterfaceRegistry::Instance().Register(&testServer); |
| |
| for (EncodingMethod encodingMethod : { EncodingMethod::Standard, EncodingMethod::PreencodedTLV }) |
| { |
| // For builds without ChipLogProgress, encodingMethodName will be ununsed and trigger build failures |
| [[maybe_unused]] const char * encodingMethodName = |
| (encodingMethod == EncodingMethod::Standard ? "StandardEncoding" : "PreencodedTLV"); |
| |
| // Test 1: we should receive transaction notifications |
| ChipLogProgress(Zcl, "Test 1 [%s]: we should receive transaction notifications", encodingMethodName); |
| RunTest( |
| Instructions{ |
| .paths = { ConcreteAttributePath(kTestEndpointId, Clusters::UnitTesting::Id, kTestListAttribute) }, |
| .expectedStatus = { true }, |
| }, |
| encodingMethod); |
| |
| ChipLogProgress(Zcl, "Test 2a [%s]: we should receive transaction notifications for incomplete list operations", |
| encodingMethodName); |
| RunTest( |
| Instructions{ |
| .paths = { ConcreteAttributePath(kTestEndpointId, Clusters::UnitTesting::Id, kTestListAttribute) }, |
| .onListWriteBeginActions = |
| [&](const app::ConcreteAttributePath & aPath) { return Operations::kShutdownWriteClient; }, |
| .expectedStatus = { false }, |
| }, |
| encodingMethod); |
| |
| ChipLogProgress(Zcl, "Test 3 [%s]: we should receive transaction notifications for every list in the transaction", |
| encodingMethodName); |
| RunTest( |
| Instructions{ |
| .paths = { ConcreteAttributePath(kTestEndpointId, Clusters::UnitTesting::Id, kTestListAttribute), |
| ConcreteAttributePath(kTestEndpointId, Clusters::UnitTesting::Id, kTestListAttribute2) }, |
| .expectedStatus = { true, true }, |
| }, |
| encodingMethod); |
| |
| ChipLogProgress(Zcl, "Test 4 [%s]: we should receive transaction notifications with the status of each list", |
| encodingMethodName); |
| RunTest( |
| Instructions{ |
| .paths = { ConcreteAttributePath(kTestEndpointId, Clusters::UnitTesting::Id, kTestListAttribute), |
| ConcreteAttributePath(kTestEndpointId, Clusters::UnitTesting::Id, kTestListAttribute2) }, |
| .onListWriteBeginActions = |
| [&](const app::ConcreteAttributePath & aPath) { |
| if (aPath.mAttributeId == kTestListAttribute2) |
| { |
| return Operations::kShutdownWriteClient; |
| } |
| return Operations::kNoop; |
| }, |
| .expectedStatus = { true, false }, |
| }, |
| encodingMethod); |
| |
| ChipLogProgress(Zcl, |
| "Test 5 [%s]: transactional list callbacks will be called for nullable lists, test if it is handled " |
| "correctly for null value before non null values", |
| encodingMethodName); |
| RunTest( |
| Instructions{ |
| .paths = { ConcreteAttributePath(kTestEndpointId, Clusters::UnitTesting::Id, kTestListAttribute), |
| ConcreteAttributePath(kTestEndpointId, Clusters::UnitTesting::Id, kTestListAttribute) }, |
| .data = { ListData::kNull, ListData::kList }, |
| .expectedStatus = { true }, |
| }, |
| encodingMethod); |
| |
| ChipLogProgress(Zcl, |
| "Test 6 [%s]: transactional list callbacks will be called for nullable lists, test if it is handled " |
| "correctly for null value after non null values", |
| encodingMethodName); |
| RunTest( |
| Instructions{ |
| .paths = { ConcreteAttributePath(kTestEndpointId, Clusters::UnitTesting::Id, kTestListAttribute), |
| ConcreteAttributePath(kTestEndpointId, Clusters::UnitTesting::Id, kTestListAttribute) }, |
| .data = { ListData::kList, ListData::kNull }, |
| .expectedStatus = { true }, |
| }, |
| encodingMethod); |
| |
| ChipLogProgress(Zcl, |
| "Test 7 [%s]: transactional list callbacks will be called for nullable lists, test if it is handled " |
| "correctly for null value between non null values", |
| encodingMethodName); |
| RunTest( |
| Instructions{ |
| .paths = { ConcreteAttributePath(kTestEndpointId, Clusters::UnitTesting::Id, kTestListAttribute), |
| ConcreteAttributePath(kTestEndpointId, Clusters::UnitTesting::Id, kTestListAttribute), |
| ConcreteAttributePath(kTestEndpointId, Clusters::UnitTesting::Id, kTestListAttribute) }, |
| .data = { ListData::kList, ListData::kNull, ListData::kList }, |
| .expectedStatus = { true }, |
| }, |
| encodingMethod); |
| |
| ChipLogProgress(Zcl, "Test 8 [%s]: transactional list callbacks will be called for nullable lists", encodingMethodName); |
| RunTest( |
| Instructions{ |
| .paths = { ConcreteAttributePath(kTestEndpointId, Clusters::UnitTesting::Id, kTestListAttribute) }, |
| .data = { ListData::kNull }, |
| .expectedStatus = { true }, |
| }, |
| encodingMethod); |
| |
| ChipLogProgress(Zcl, |
| "Test 9 [%s]: for nullable lists, we should receive notifications for unsuccessful writes when non-fatal " |
| "occurred during processing the requests", |
| encodingMethodName); |
| RunTest( |
| Instructions{ |
| .paths = { ConcreteAttributePath(kTestEndpointId, Clusters::UnitTesting::Id, kTestListAttribute) }, |
| .data = { ListData::kBadValue }, |
| .expectedStatus = { false }, |
| }, |
| encodingMethod); |
| } |
| EXPECT_EQ(GetExchangeManager().GetNumActiveExchanges(), 0u); |
| |
| emberAfClearDynamicEndpoint(0); |
| } |
| |
| /* |
| * A Variant of RunTest above, that tests the Code Path where we encode a Non-Replace All List in WriteRequests, this |
| * happens with the ACL Cluster. |
| */ |
| void TestWriteChunking::RunTest_NonEmptyReplaceAll(Instructions instructions, |
| EncodingMethod encodingMethod = EncodingMethod::Standard) |
| { |
| CHIP_ERROR err = CHIP_NO_ERROR; |
| auto sessionHandle = GetSessionBobToAlice(); |
| |
| TestWriteCallback writeCallback; |
| std::unique_ptr<WriteClient> writeClient = std::make_unique<WriteClient>( |
| &GetExchangeManager(), &writeCallback, NullOptional, |
| static_cast<uint16_t>(kMaxSecureSduLengthBytes - |
| 66) /* use a smaller chunk so we only need a few attributes in the write request. */); |
| |
| ConcreteAttributePath onGoingPath = ConcreteAttributePath(); |
| std::vector<PathStatus> status; |
| |
| testServerAcl.mOnListWriteBegin = [&](const ConcreteAttributePath & aPath) { |
| EXPECT_EQ(onGoingPath, ConcreteAttributePath()); |
| onGoingPath = aPath; |
| ChipLogProgress(Zcl, "OnListWriteBegin endpoint=%u Cluster=" ChipLogFormatMEI " attribute=" ChipLogFormatMEI, |
| aPath.mEndpointId, ChipLogValueMEI(aPath.mClusterId), ChipLogValueMEI(aPath.mAttributeId)); |
| if (instructions.onListWriteBeginActions) |
| { |
| switch (instructions.onListWriteBeginActions(aPath)) |
| { |
| case Operations::kNoop: |
| break; |
| case Operations::kShutdownWriteClient: |
| // By setting writeClient to nullptr, we actually shutdown the write interaction to simulate a timeout. |
| writeClient = nullptr; |
| } |
| } |
| }; |
| testServerAcl.mOnListWriteEnd = [&](const ConcreteAttributePath & aPath, bool aWasSuccessful) { |
| EXPECT_EQ(onGoingPath, aPath); |
| status.push_back(PathStatus(aPath, aWasSuccessful)); |
| onGoingPath = ConcreteAttributePath(); |
| ChipLogProgress(Zcl, "OnListWriteEnd endpoint=%u Cluster=" ChipLogFormatMEI " attribute=" ChipLogFormatMEI, |
| aPath.mEndpointId, ChipLogValueMEI(aPath.mClusterId), ChipLogValueMEI(aPath.mAttributeId)); |
| }; |
| |
| constexpr uint8_t kTestListLength2 = 10; |
| |
| ByteSpan list[kTestListLength2]; |
| uint8_t badList[kTestListLength2]; |
| |
| if (instructions.data.size() == 0) |
| { |
| instructions.data = std::vector<ListData>(instructions.paths.size(), ListData::kList); |
| } |
| EXPECT_EQ(instructions.paths.size(), instructions.data.size()); |
| |
| for (size_t i = 0; i < instructions.paths.size(); i++) |
| { |
| const auto & p = instructions.paths[i]; |
| switch (instructions.data[i]) |
| { |
| case ListData::kNull: { |
| DataModel::Nullable<uint8_t> null; // The actual type is not important since we will only put a null value. |
| err = writeClient->EncodeAttribute(AttributePathParams(p.mEndpointId, p.mClusterId, p.mAttributeId), null); |
| break; |
| } |
| case ListData::kList: { |
| |
| if (encodingMethod == EncodingMethod::Standard) |
| { |
| err = writeClient->EncodeAttribute(AttributePathParams(p.mEndpointId, p.mClusterId, p.mAttributeId), |
| DataModel::List<ByteSpan>(list, kTestListLength2)); |
| } |
| else if (encodingMethod == EncodingMethod::PreencodedTLV) |
| { |
| TLV::ScopedBufferTLVReader encodedListTLV; |
| EncodeAttributeListIntoTLV(DataModel::List<ByteSpan>(list, kTestListLength2), encodedListTLV); |
| |
| ConcreteDataAttributePath path = ConcreteDataAttributePath(p.mEndpointId, p.mClusterId, p.mAttributeId); |
| err = writeClient->PutPreencodedAttribute(path, encodedListTLV); |
| } |
| break; |
| } |
| case ListData::kBadValue: { |
| |
| if (encodingMethod == EncodingMethod::Standard) |
| { |
| err = writeClient->EncodeAttribute(AttributePathParams(p.mEndpointId, p.mClusterId, p.mAttributeId), |
| DataModel::List<uint8_t>(badList, kTestListLength2)); |
| } |
| else if (encodingMethod == EncodingMethod::PreencodedTLV) |
| { |
| TLV::ScopedBufferTLVReader encodedListTLV; |
| EncodeAttributeListIntoTLV(DataModel::List<uint8_t>(badList, kTestListLength2), encodedListTLV); |
| |
| ConcreteDataAttributePath path = ConcreteDataAttributePath(p.mEndpointId, p.mClusterId, p.mAttributeId); |
| err = writeClient->PutPreencodedAttribute(path, encodedListTLV); |
| } |
| |
| break; |
| } |
| } |
| EXPECT_EQ(err, CHIP_NO_ERROR); |
| } |
| |
| err = writeClient->SendWriteRequest(sessionHandle); |
| EXPECT_EQ(err, CHIP_NO_ERROR); |
| |
| GetIOContext().DriveIOUntil( |
| sessionHandle->ComputeRoundTripTimeout(app::kExpectedIMProcessingTime, true /*isFirstMessageOnExchange*/) + |
| System::Clock::Seconds16(1), |
| [&]() { return GetExchangeManager().GetNumActiveExchanges() == 0; }); |
| |
| EXPECT_EQ(onGoingPath, app::ConcreteAttributePath()); |
| EXPECT_EQ(status.size(), instructions.expectedStatus.size()); |
| |
| for (size_t i = 0; i < status.size(); i++) |
| { |
| EXPECT_EQ(status[i], PathStatus(instructions.paths[i], instructions.expectedStatus[i])); |
| } |
| |
| testServerAcl.mOnListWriteBegin = nullptr; |
| testServerAcl.mOnListWriteEnd = nullptr; |
| } |
| |
| TEST_F(TestWriteChunking, TestTransactionalList_NonEmptyReplaceAllList) |
| { |
| // Initialize the ember side server logic. |
| app::InteractionModelEngine::GetInstance()->SetDataModelProvider(CodegenDataModelProviderInstance(nullptr /* delegate */)); |
| InitDataModelHandler(); |
| |
| // Register our fake dynamic endpoint. |
| emberAfSetDynamicEndpoint(0, kTestEndpointId, &testEndpointAcl, Span<DataVersion>(dataVersionStorageAcl)); |
| |
| // Register our fake attribute access interface. |
| AttributeAccessInterfaceRegistry::Instance().Register(&testServerAcl); |
| |
| for (EncodingMethod encodingMethod : { EncodingMethod::Standard, EncodingMethod::PreencodedTLV }) |
| { |
| // For builds without ChipLogProgress, encodingMethodName will be ununsed and trigger build failures |
| [[maybe_unused]] const char * encodingMethodName = |
| (encodingMethod == EncodingMethod::Standard ? "StandardEncoding" : "PreencodedTLV"); |
| |
| // Test 1: we should receive transaction notifications |
| ChipLogProgress(Zcl, "Test 1 [%s]: we should receive transaction notifications", encodingMethodName); |
| RunTest_NonEmptyReplaceAll( |
| Instructions{ |
| .paths = { ConcreteAttributePath(kTestEndpointId, AccessControl::Id, AccessControl::Attributes::Acl::Id) }, |
| .expectedStatus = { true }, |
| }, |
| encodingMethod); |
| |
| ChipLogProgress(Zcl, "Test 2 [%s]: we should receive transaction notifications for incomplete list operations", |
| encodingMethodName); |
| RunTest_NonEmptyReplaceAll( |
| Instructions{ |
| .paths = { ConcreteAttributePath(kTestEndpointId, AccessControl::Id, AccessControl::Attributes::Acl::Id) }, |
| .onListWriteBeginActions = |
| [&](const app::ConcreteAttributePath & aPath) { return Operations::kShutdownWriteClient; }, |
| .expectedStatus = { false }, |
| }, |
| encodingMethod); |
| |
| ChipLogProgress(Zcl, "Test 3 [%s]: we should receive transaction notifications for every list in the transaction", |
| encodingMethodName); |
| RunTest_NonEmptyReplaceAll( |
| Instructions{ |
| .paths = { ConcreteAttributePath(kTestEndpointId, AccessControl::Id, AccessControl::Attributes::Acl::Id), |
| ConcreteAttributePath(kTestEndpointId, AccessControl::Id, AccessControl::Attributes::Extension::Id) }, |
| .expectedStatus = { true, true }, |
| }, |
| encodingMethod); |
| |
| ChipLogProgress(Zcl, "Test 4 [%s]: we should receive transaction notifications with the status of each list", |
| encodingMethodName); |
| RunTest_NonEmptyReplaceAll( |
| Instructions{ |
| .paths = { ConcreteAttributePath(kTestEndpointId, AccessControl::Id, AccessControl::Attributes::Acl::Id), |
| ConcreteAttributePath(kTestEndpointId, AccessControl::Id, AccessControl::Attributes::Extension::Id) }, |
| .onListWriteBeginActions = |
| [&](const app::ConcreteAttributePath & aPath) { |
| if (aPath.mAttributeId == AccessControl::Attributes::Extension::Id) |
| { |
| return Operations::kShutdownWriteClient; |
| } |
| return Operations::kNoop; |
| }, |
| .expectedStatus = { true, false }, |
| }, |
| encodingMethod); |
| |
| ChipLogProgress(Zcl, |
| "Test 5 [%s]: transactional list callbacks will be called for nullable lists, test if it is handled " |
| "correctly for null value before non null values", |
| encodingMethodName); |
| RunTest_NonEmptyReplaceAll( |
| Instructions{ |
| .paths = { ConcreteAttributePath(kTestEndpointId, AccessControl::Id, AccessControl::Attributes::Acl::Id), |
| ConcreteAttributePath(kTestEndpointId, AccessControl::Id, AccessControl::Attributes::Acl::Id) }, |
| .data = { ListData::kNull, ListData::kList }, |
| .expectedStatus = { true }, |
| }, |
| encodingMethod); |
| |
| ChipLogProgress(Zcl, |
| "Test 6 [%s]: transactional list callbacks will be called for nullable lists, test if it is handled " |
| "correctly for null value after non null values", |
| encodingMethodName); |
| RunTest_NonEmptyReplaceAll( |
| Instructions{ |
| .paths = { ConcreteAttributePath(kTestEndpointId, AccessControl::Id, AccessControl::Attributes::Acl::Id), |
| ConcreteAttributePath(kTestEndpointId, AccessControl::Id, AccessControl::Attributes::Acl::Id) }, |
| .data = { ListData::kList, ListData::kNull }, |
| .expectedStatus = { true }, |
| }, |
| encodingMethod); |
| |
| ChipLogProgress(Zcl, |
| "Test 7 [%s]: transactional list callbacks will be called for nullable lists, test if it is handled " |
| "correctly for null value between non null values", |
| encodingMethodName); |
| RunTest_NonEmptyReplaceAll( |
| Instructions{ |
| .paths = { ConcreteAttributePath(kTestEndpointId, AccessControl::Id, AccessControl::Attributes::Acl::Id), |
| ConcreteAttributePath(kTestEndpointId, AccessControl::Id, AccessControl::Attributes::Acl::Id), |
| ConcreteAttributePath(kTestEndpointId, AccessControl::Id, AccessControl::Attributes::Acl::Id) }, |
| .data = { ListData::kList, ListData::kNull, ListData::kList }, |
| .expectedStatus = { true }, |
| }, |
| encodingMethod); |
| |
| ChipLogProgress(Zcl, "Test 8 [%s]: transactional list callbacks will be called for nullable lists", encodingMethodName); |
| RunTest_NonEmptyReplaceAll( |
| Instructions{ |
| .paths = { ConcreteAttributePath(kTestEndpointId, AccessControl::Id, AccessControl::Attributes::Acl::Id) }, |
| .data = { ListData::kNull }, |
| .expectedStatus = { true }, |
| }, |
| encodingMethod); |
| |
| ChipLogProgress(Zcl, |
| "Test 9 [%s]: for nullable lists, we should receive notifications for unsuccessful writes when non-fatal " |
| "occurred during processing the requests", |
| encodingMethodName); |
| RunTest_NonEmptyReplaceAll( |
| Instructions{ |
| .paths = { ConcreteAttributePath(kTestEndpointId, AccessControl::Id, AccessControl::Attributes::Acl::Id) }, |
| .data = { ListData::kBadValue }, |
| .expectedStatus = { false }, |
| }, |
| encodingMethod); |
| |
| // This TestCase tests corner cases when we Encode many attributes into the same WriteRequest, up to 10 Attributes will be |
| // Encoded. |
| |
| for (int nullableListCount = 1; nullableListCount <= 10; nullableListCount++) |
| { |
| ChipLogProgress(Zcl, "Test 10.%d [%s]: Encoding %d nullable list(s) following a single non-nullable list", |
| nullableListCount, encodingMethodName, nullableListCount); |
| |
| Instructions test; |
| |
| // Add the single non-nullable list |
| test.paths.push_back(ConcreteAttributePath(kTestEndpointId, AccessControl::Id, AccessControl::Attributes::Acl::Id)); |
| test.data.push_back(ListData::kList); |
| |
| // Add the nullable lists |
| for (int i = 0; i < nullableListCount; i++) |
| { |
| test.paths.push_back(ConcreteAttributePath(kTestEndpointId, AccessControl::Id, AccessControl::Attributes::Acl::Id)); |
| test.data.push_back(ListData::kNull); |
| } |
| |
| test.expectedStatus = { true }; |
| RunTest_NonEmptyReplaceAll(test, encodingMethod); |
| } |
| } |
| EXPECT_EQ(GetExchangeManager().GetNumActiveExchanges(), 0u); |
| |
| emberAfClearDynamicEndpoint(0); |
| } |
| |
| } // namespace |