| /* |
| * |
| * Copyright (c) 2021-2023 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 "app-common/zap-generated/ids/Attributes.h" |
| #include "app-common/zap-generated/ids/Clusters.h" |
| #include "app/ConcreteAttributePath.h" |
| #include "protocols/interaction_model/Constants.h" |
| #include <app-common/zap-generated/cluster-objects.h> |
| #include <app/AppConfig.h> |
| #include <app/AttributeAccessInterface.h> |
| #include <app/BufferedReadCallback.h> |
| #include <app/CommandHandlerInterface.h> |
| #include <app/GlobalAttributes.h> |
| #include <app/InteractionModelEngine.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 <functional> |
| #include <lib/support/ErrorStr.h> |
| #include <lib/support/TimeUtils.h> |
| #include <lib/support/UnitTestContext.h> |
| #include <lib/support/UnitTestRegistration.h> |
| #include <lib/support/UnitTestUtils.h> |
| #include <lib/support/logging/CHIPLogging.h> |
| #include <map> |
| #include <messaging/tests/MessagingContext.h> |
| #include <nlunit-test.h> |
| #include <utility> |
| |
| using TestContext = chip::Test::AppContext; |
| using namespace chip; |
| using namespace chip::app; |
| using namespace chip::app::Clusters; |
| |
| namespace { |
| |
| uint32_t gIterationCount = 0; |
| nlTestSuite * gSuite = nullptr; |
| TestContext * gCtx = nullptr; |
| |
| // |
| // 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 = 2; |
| // Another endpoint, with a list attribute only. |
| constexpr EndpointId kTestEndpointId3 = 3; |
| // Another endpoint, for adding / enabling during running. |
| constexpr EndpointId kTestEndpointId4 = 4; |
| constexpr EndpointId kTestEndpointId5 = 5; |
| constexpr AttributeId kTestListAttribute = 6; |
| constexpr AttributeId kTestBadAttribute = |
| 7; // Reading this attribute will return CHIP_ERROR_NO_MEMORY but nothing is actually encoded. |
| |
| class TestReadChunking |
| { |
| public: |
| TestReadChunking() {} |
| static void TestChunking(nlTestSuite * apSuite, void * apContext); |
| static void TestListChunking(nlTestSuite * apSuite, void * apContext); |
| static void TestBadChunking(nlTestSuite * apSuite, void * apContext); |
| static void TestDynamicEndpoint(nlTestSuite * apSuite, void * apContext); |
| static void TestSetDirtyBetweenChunks(nlTestSuite * apSuite, void * apContext); |
| |
| private: |
| }; |
| |
| //clang-format off |
| DECLARE_DYNAMIC_ATTRIBUTE_LIST_BEGIN(testClusterAttrs) |
| DECLARE_DYNAMIC_ATTRIBUTE(0x00000001, INT8U, 1, 0), DECLARE_DYNAMIC_ATTRIBUTE(0x00000002, INT8U, 1, 0), |
| DECLARE_DYNAMIC_ATTRIBUTE(0x00000003, INT8U, 1, 0), DECLARE_DYNAMIC_ATTRIBUTE(0x00000004, INT8U, 1, 0), |
| DECLARE_DYNAMIC_ATTRIBUTE(0x00000005, INT8U, 1, 0), DECLARE_DYNAMIC_ATTRIBUTE_LIST_END(); |
| |
| DECLARE_DYNAMIC_CLUSTER_LIST_BEGIN(testEndpointClusters) |
| DECLARE_DYNAMIC_CLUSTER(Clusters::UnitTesting::Id, testClusterAttrs, nullptr, nullptr), DECLARE_DYNAMIC_CLUSTER_LIST_END; |
| |
| DECLARE_DYNAMIC_ENDPOINT(testEndpoint, testEndpointClusters); |
| |
| DECLARE_DYNAMIC_ATTRIBUTE_LIST_BEGIN(testClusterAttrsOnEndpoint3) |
| DECLARE_DYNAMIC_ATTRIBUTE(kTestListAttribute, ARRAY, 1, 0), DECLARE_DYNAMIC_ATTRIBUTE(kTestBadAttribute, ARRAY, 1, 0), |
| DECLARE_DYNAMIC_ATTRIBUTE_LIST_END(); |
| |
| DECLARE_DYNAMIC_CLUSTER_LIST_BEGIN(testEndpoint3Clusters) |
| DECLARE_DYNAMIC_CLUSTER(Clusters::UnitTesting::Id, testClusterAttrsOnEndpoint3, nullptr, nullptr), DECLARE_DYNAMIC_CLUSTER_LIST_END; |
| |
| DECLARE_DYNAMIC_ENDPOINT(testEndpoint3, testEndpoint3Clusters); |
| |
| DECLARE_DYNAMIC_ATTRIBUTE_LIST_BEGIN(testClusterAttrsOnEndpoint4) |
| DECLARE_DYNAMIC_ATTRIBUTE(0x00000001, INT8U, 1, 0), DECLARE_DYNAMIC_ATTRIBUTE_LIST_END(); |
| |
| DECLARE_DYNAMIC_CLUSTER_LIST_BEGIN(testEndpoint4Clusters) |
| DECLARE_DYNAMIC_CLUSTER(Clusters::UnitTesting::Id, testClusterAttrsOnEndpoint4, nullptr, nullptr), DECLARE_DYNAMIC_CLUSTER_LIST_END; |
| |
| DECLARE_DYNAMIC_ENDPOINT(testEndpoint4, testEndpoint4Clusters); |
| |
| // Unlike endpoint 1, we can modify the values for values in endpoint 5 |
| DECLARE_DYNAMIC_ATTRIBUTE_LIST_BEGIN(testClusterAttrsOnEndpoint5) |
| DECLARE_DYNAMIC_ATTRIBUTE(0x00000001, INT8U, 1, 0), DECLARE_DYNAMIC_ATTRIBUTE(0x00000002, INT8U, 1, 0), |
| DECLARE_DYNAMIC_ATTRIBUTE(0x00000003, INT8U, 1, 0), DECLARE_DYNAMIC_ATTRIBUTE_LIST_END(); |
| |
| DECLARE_DYNAMIC_CLUSTER_LIST_BEGIN(testEndpoint5Clusters) |
| DECLARE_DYNAMIC_CLUSTER(Clusters::UnitTesting::Id, testClusterAttrsOnEndpoint5, nullptr, nullptr), DECLARE_DYNAMIC_CLUSTER_LIST_END; |
| |
| DECLARE_DYNAMIC_ENDPOINT(testEndpoint5, testEndpoint5Clusters); |
| |
| //clang-format on |
| |
| uint8_t sAnStringThatCanNeverFitIntoTheMTU[4096] = { 0 }; |
| |
| class TestReadCallback : public app::ReadClient::Callback |
| { |
| public: |
| TestReadCallback() : mBufferedCallback(*this) {} |
| void OnAttributeData(const app::ConcreteDataAttributePath & aPath, TLV::TLVReader * apData, |
| const app::StatusIB & aStatus) override; |
| |
| void OnDone(app::ReadClient * apReadClient) override; |
| |
| void OnReportEnd() override { mOnReportEnd = true; } |
| |
| void OnSubscriptionEstablished(SubscriptionId aSubscriptionId) override { mOnSubscriptionEstablished = true; } |
| |
| uint32_t mAttributeCount = 0; |
| bool mOnReportEnd = false; |
| bool mOnSubscriptionEstablished = false; |
| app::BufferedReadCallback mBufferedCallback; |
| }; |
| |
| void TestReadCallback::OnAttributeData(const app::ConcreteDataAttributePath & aPath, TLV::TLVReader * apData, |
| const app::StatusIB & aStatus) |
| { |
| if (aPath.mAttributeId == Globals::Attributes::GeneratedCommandList::Id) |
| { |
| app::DataModel::DecodableList<CommandId> v; |
| NL_TEST_ASSERT(gSuite, app::DataModel::Decode(*apData, v) == CHIP_NO_ERROR); |
| auto it = v.begin(); |
| size_t arraySize = 0; |
| while (it.Next()) |
| { |
| NL_TEST_ASSERT(gSuite, false); |
| } |
| NL_TEST_ASSERT(gSuite, it.GetStatus() == CHIP_NO_ERROR); |
| NL_TEST_ASSERT(gSuite, v.ComputeSize(&arraySize) == CHIP_NO_ERROR); |
| NL_TEST_ASSERT(gSuite, arraySize == 0); |
| } |
| else if (aPath.mAttributeId == Globals::Attributes::AcceptedCommandList::Id) |
| { |
| app::DataModel::DecodableList<CommandId> v; |
| NL_TEST_ASSERT(gSuite, app::DataModel::Decode(*apData, v) == CHIP_NO_ERROR); |
| auto it = v.begin(); |
| size_t arraySize = 0; |
| while (it.Next()) |
| { |
| NL_TEST_ASSERT(gSuite, false); |
| } |
| NL_TEST_ASSERT(gSuite, it.GetStatus() == CHIP_NO_ERROR); |
| NL_TEST_ASSERT(gSuite, v.ComputeSize(&arraySize) == CHIP_NO_ERROR); |
| NL_TEST_ASSERT(gSuite, arraySize == 0); |
| } |
| #if CHIP_CONFIG_ENABLE_EVENTLIST_ATTRIBUTE |
| else if (aPath.mAttributeId == Globals::Attributes::EventList::Id) |
| { |
| // Nothing to check for this one; depends on the endpoint. |
| } |
| #endif // CHIP_CONFIG_ENABLE_EVENTLIST_ATTRIBUTE |
| else if (aPath.mAttributeId == Globals::Attributes::AttributeList::Id) |
| { |
| // Nothing to check for this one; depends on the endpoint. |
| } |
| else if (aPath.mAttributeId != kTestListAttribute) |
| { |
| uint8_t v; |
| NL_TEST_ASSERT(gSuite, app::DataModel::Decode(*apData, v) == CHIP_NO_ERROR); |
| NL_TEST_ASSERT(gSuite, v == (uint8_t) gIterationCount); |
| } |
| else |
| { |
| app::DataModel::DecodableList<uint8_t> v; |
| NL_TEST_ASSERT(gSuite, app::DataModel::Decode(*apData, v) == CHIP_NO_ERROR); |
| auto it = v.begin(); |
| size_t arraySize = 0; |
| while (it.Next()) |
| { |
| NL_TEST_ASSERT(gSuite, it.GetValue() == static_cast<uint8_t>(gIterationCount)); |
| } |
| NL_TEST_ASSERT(gSuite, it.GetStatus() == CHIP_NO_ERROR); |
| NL_TEST_ASSERT(gSuite, v.ComputeSize(&arraySize) == CHIP_NO_ERROR); |
| NL_TEST_ASSERT(gSuite, arraySize == 5); |
| } |
| mAttributeCount++; |
| } |
| |
| void TestReadCallback::OnDone(app::ReadClient *) {} |
| |
| class TestMutableAttrAccess |
| { |
| public: |
| CHIP_ERROR Read(const app::ConcreteReadAttributePath & aPath, app::AttributeValueEncoder & aEncoder); |
| |
| void SetDirty(AttributeId attr) |
| { |
| app::AttributePathParams path; |
| path.mEndpointId = kTestEndpointId5; |
| path.mClusterId = Clusters::UnitTesting::Id; |
| path.mAttributeId = attr; |
| app::InteractionModelEngine::GetInstance()->GetReportingEngine().SetDirty(path); |
| } |
| |
| // These setters |
| void SetVal(uint8_t attribute, uint8_t newVal) |
| { |
| uint8_t index = static_cast<uint8_t>(attribute - 1); |
| if (index < ArraySize(val) && val[index] != newVal) |
| { |
| val[index] = newVal; |
| SetDirty(attribute); |
| } |
| } |
| |
| void Reset() { val[0] = val[1] = val[2] = 0; } |
| |
| uint8_t val[3] = { 0, 0, 0 }; |
| }; |
| |
| CHIP_ERROR TestMutableAttrAccess::Read(const app::ConcreteReadAttributePath & aPath, app::AttributeValueEncoder & aEncoder) |
| { |
| uint8_t index = static_cast<uint8_t>(aPath.mAttributeId - 1); |
| VerifyOrReturnError(aPath.mEndpointId == kTestEndpointId5 && index < ArraySize(val), CHIP_ERROR_NOT_FOUND); |
| return aEncoder.Encode(val[index]); |
| } |
| |
| TestMutableAttrAccess gMutableAttrAccess; |
| |
| class TestAttrAccess : public app::AttributeAccessInterface |
| { |
| public: |
| // Register for the Test Cluster cluster on all endpoints. |
| TestAttrAccess() : AttributeAccessInterface(Optional<EndpointId>::Missing(), Clusters::UnitTesting::Id) |
| { |
| registerAttributeAccessOverride(this); |
| } |
| |
| CHIP_ERROR Read(const app::ConcreteReadAttributePath & aPath, app::AttributeValueEncoder & aEncoder) override; |
| CHIP_ERROR Write(const app::ConcreteDataAttributePath & aPath, app::AttributeValueDecoder & aDecoder) override; |
| }; |
| |
| TestAttrAccess gAttrAccess; |
| |
| CHIP_ERROR TestAttrAccess::Read(const app::ConcreteReadAttributePath & aPath, app::AttributeValueEncoder & aEncoder) |
| { |
| CHIP_ERROR err = gMutableAttrAccess.Read(aPath, aEncoder); |
| if (err != CHIP_ERROR_NOT_FOUND) |
| { |
| return err; |
| } |
| |
| switch (aPath.mAttributeId) |
| { |
| case kTestListAttribute: |
| return aEncoder.EncodeList([](const auto & encoder) { |
| for (int i = 0; i < 5; i++) |
| { |
| ReturnErrorOnFailure(encoder.Encode((uint8_t) gIterationCount)); |
| } |
| return CHIP_NO_ERROR; |
| }); |
| case kTestBadAttribute: |
| // The "BadAttribute" is implemented by encoding a very large octet string, then the encode will always return |
| // CHIP_ERROR_NO_MEMORY. |
| return aEncoder.EncodeList([](const auto & encoder) { |
| return encoder.Encode(ByteSpan(sAnStringThatCanNeverFitIntoTheMTU, sizeof(sAnStringThatCanNeverFitIntoTheMTU))); |
| }); |
| default: |
| return aEncoder.Encode((uint8_t) gIterationCount); |
| } |
| } |
| |
| CHIP_ERROR TestAttrAccess::Write(const app::ConcreteDataAttributePath & aPath, app::AttributeValueDecoder & aDecoder) |
| { |
| return CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE; |
| } |
| |
| class TestMutableReadCallback : public app::ReadClient::Callback |
| { |
| public: |
| TestMutableReadCallback() : mBufferedCallback(*this) {} |
| void OnAttributeData(const app::ConcreteDataAttributePath & aPath, TLV::TLVReader * apData, |
| const app::StatusIB & aStatus) override; |
| |
| void OnDone(app::ReadClient *) override {} |
| |
| void OnReportBegin() override { mAttributeCount = 0; } |
| |
| void OnReportEnd() override { mOnReportEnd = true; } |
| |
| void OnSubscriptionEstablished(SubscriptionId aSubscriptionId) override { mOnSubscriptionEstablished = true; } |
| |
| uint32_t mAttributeCount = 0; |
| // We record every dataversion field from every attribute IB. |
| std::map<std::pair<EndpointId, AttributeId>, DataVersion> mDataVersions; |
| std::map<std::pair<EndpointId, AttributeId>, uint8_t> mValues; |
| std::map<std::pair<EndpointId, AttributeId>, std::function<void()>> mActionOn; |
| bool mOnReportEnd = false; |
| bool mOnSubscriptionEstablished = false; |
| app::BufferedReadCallback mBufferedCallback; |
| }; |
| |
| void TestMutableReadCallback::OnAttributeData(const app::ConcreteDataAttributePath & aPath, TLV::TLVReader * apData, |
| const app::StatusIB & aStatus) |
| { |
| VerifyOrReturn(apData != nullptr); |
| NL_TEST_ASSERT(gSuite, aPath.mClusterId == Clusters::UnitTesting::Id); |
| |
| mAttributeCount++; |
| if (aPath.mAttributeId <= 5) |
| { |
| uint8_t v; |
| NL_TEST_ASSERT(gSuite, app::DataModel::Decode(*apData, v) == CHIP_NO_ERROR); |
| mValues[std::make_pair(aPath.mEndpointId, aPath.mAttributeId)] = v; |
| |
| auto action = mActionOn.find(std::make_pair(aPath.mEndpointId, aPath.mAttributeId)); |
| if (action != mActionOn.end() && action->second) |
| { |
| action->second(); |
| } |
| } |
| |
| if (aPath.mDataVersion.HasValue()) |
| { |
| mDataVersions[std::make_pair(aPath.mEndpointId, aPath.mAttributeId)] = aPath.mDataVersion.Value(); |
| } |
| // Ignore all other attributes, we don't care above the global attributes. |
| } |
| |
| /* |
| * 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 report generation logic. This 1-byte incremental approach sweeps through from a base scenario of |
| * N attributes fitting in a report, to eventually resulting in N+1 attributes fitting in a report. |
| |
| * This will cause all the various corner cases encountered of closing out the various containers within |
| * the report and thoroughly and definitely validate those edge cases. |
| * |
| * Importantly, this test tries to re-use *as much as possible* the actual IM constructs used by real |
| * server-side applications. Consequently, this is why it registers a dynamic endpoint + fake attribute access |
| * interface to simulate faithfully a real application. This ensures validation of as much production logic pathways |
| * as we can possibly cover. |
| * |
| */ |
| void TestReadChunking::TestChunking(nlTestSuite * apSuite, void * apContext) |
| { |
| TestContext & ctx = *static_cast<TestContext *>(apContext); |
| auto sessionHandle = ctx.GetSessionBobToAlice(); |
| app::InteractionModelEngine * engine = app::InteractionModelEngine::GetInstance(); |
| |
| // Initialize the ember side server logic |
| InitDataModelHandler(); |
| |
| // Register our fake dynamic endpoint. |
| DataVersion dataVersionStorage[ArraySize(testEndpointClusters)]; |
| emberAfSetDynamicEndpoint(0, kTestEndpointId, &testEndpoint, Span<DataVersion>(dataVersionStorage)); |
| |
| app::AttributePathParams attributePath(kTestEndpointId, app::Clusters::UnitTesting::Id); |
| app::ReadPrepareParams readParams(sessionHandle); |
| |
| readParams.mpAttributePathParamsList = &attributePath; |
| readParams.mAttributePathParamsListSize = 1; |
| |
| // |
| // We've empirically determined that by reserving 950 bytes in the packet buffer, we can fit 2 |
| // AttributeDataIBs into the packet. ~30-40 bytes covers a single AttributeDataIB, but let's 2-3x that |
| // to ensure we'll sweep from fitting 2 IBs to 3-4 IBs. |
| // |
| for (int i = 100; i > 0; i--) |
| { |
| TestReadCallback readCallback; |
| |
| ChipLogDetail(DataManagement, "Running iteration %d\n", i); |
| |
| gIterationCount = (uint32_t) i; |
| |
| app::InteractionModelEngine::GetInstance()->GetReportingEngine().SetWriterReserved(static_cast<uint32_t>(850 + i)); |
| |
| app::ReadClient readClient(engine, &ctx.GetExchangeManager(), readCallback.mBufferedCallback, |
| app::ReadClient::InteractionType::Read); |
| |
| NL_TEST_ASSERT(apSuite, readClient.SendRequest(readParams) == CHIP_NO_ERROR); |
| |
| ctx.DrainAndServiceIO(); |
| NL_TEST_ASSERT(apSuite, readCallback.mOnReportEnd); |
| |
| // |
| // Always returns the same number of attributes read (5 + revision + GlobalAttributesNotInMetadata). |
| // |
| NL_TEST_ASSERT(apSuite, readCallback.mAttributeCount == 6 + ArraySize(GlobalAttributesNotInMetadata)); |
| readCallback.mAttributeCount = 0; |
| |
| NL_TEST_ASSERT(apSuite, ctx.GetExchangeManager().GetNumActiveExchanges() == 0); |
| |
| // |
| // Stop the test if we detected an error. Otherwise, it'll be difficult to read the logs. |
| // |
| if (apSuite->flagError) |
| { |
| break; |
| } |
| } |
| |
| emberAfClearDynamicEndpoint(0); |
| } |
| |
| // Similar to the test above, but for the list chunking feature. |
| void TestReadChunking::TestListChunking(nlTestSuite * apSuite, void * apContext) |
| { |
| TestContext & ctx = *static_cast<TestContext *>(apContext); |
| auto sessionHandle = ctx.GetSessionBobToAlice(); |
| app::InteractionModelEngine * engine = app::InteractionModelEngine::GetInstance(); |
| |
| // Initialize the ember side server logic |
| InitDataModelHandler(); |
| |
| // Register our fake dynamic endpoint. |
| DataVersion dataVersionStorage[ArraySize(testEndpoint3Clusters)]; |
| emberAfSetDynamicEndpoint(0, kTestEndpointId3, &testEndpoint3, Span<DataVersion>(dataVersionStorage)); |
| |
| app::AttributePathParams attributePath(kTestEndpointId3, app::Clusters::UnitTesting::Id, kTestListAttribute); |
| app::ReadPrepareParams readParams(sessionHandle); |
| |
| readParams.mpAttributePathParamsList = &attributePath; |
| readParams.mAttributePathParamsListSize = 1; |
| |
| // |
| // We've empirically determined that by reserving 950 bytes in the packet buffer, we can fit 2 |
| // AttributeDataIBs into the packet. ~30-40 bytes covers a single AttributeDataIB, but let's 2-3x that |
| // to ensure we'll sweep from fitting 2 IBs to 3-4 IBs. |
| // |
| for (int i = 100; i > 0; i--) |
| { |
| TestReadCallback readCallback; |
| |
| ChipLogDetail(DataManagement, "Running iteration %d\n", i); |
| |
| gIterationCount = (uint32_t) i; |
| |
| app::InteractionModelEngine::GetInstance()->GetReportingEngine().SetWriterReserved(static_cast<uint32_t>(850 + i)); |
| |
| app::ReadClient readClient(engine, &ctx.GetExchangeManager(), readCallback.mBufferedCallback, |
| app::ReadClient::InteractionType::Read); |
| |
| NL_TEST_ASSERT(apSuite, readClient.SendRequest(readParams) == CHIP_NO_ERROR); |
| |
| ctx.DrainAndServiceIO(); |
| NL_TEST_ASSERT(apSuite, readCallback.mOnReportEnd); |
| |
| // |
| // Always returns the same number of attributes read (merged by buffered read callback). The content is checked in |
| // TestReadCallback::OnAttributeData |
| // |
| NL_TEST_ASSERT(apSuite, readCallback.mAttributeCount == 1); |
| readCallback.mAttributeCount = 0; |
| |
| NL_TEST_ASSERT(apSuite, ctx.GetExchangeManager().GetNumActiveExchanges() == 0); |
| |
| // |
| // Stop the test if we detected an error. Otherwise, it'll be difficult to read the logs. |
| // |
| if (apSuite->flagError) |
| { |
| break; |
| } |
| } |
| |
| emberAfClearDynamicEndpoint(0); |
| } |
| |
| // Read an attribute that can never fit into the buffer. Result in an empty report, server should shutdown the transaction. |
| void TestReadChunking::TestBadChunking(nlTestSuite * apSuite, void * apContext) |
| { |
| TestContext & ctx = *static_cast<TestContext *>(apContext); |
| auto sessionHandle = ctx.GetSessionBobToAlice(); |
| app::InteractionModelEngine * engine = app::InteractionModelEngine::GetInstance(); |
| |
| // Initialize the ember side server logic |
| InitDataModelHandler(); |
| |
| app::InteractionModelEngine::GetInstance()->GetReportingEngine().SetWriterReserved(0); |
| |
| // Register our fake dynamic endpoint. |
| DataVersion dataVersionStorage[ArraySize(testEndpoint3Clusters)]; |
| emberAfSetDynamicEndpoint(0, kTestEndpointId3, &testEndpoint3, Span<DataVersion>(dataVersionStorage)); |
| |
| app::AttributePathParams attributePath(kTestEndpointId3, app::Clusters::UnitTesting::Id, kTestBadAttribute); |
| app::ReadPrepareParams readParams(sessionHandle); |
| |
| readParams.mpAttributePathParamsList = &attributePath; |
| readParams.mAttributePathParamsListSize = 1; |
| |
| TestReadCallback readCallback; |
| |
| { |
| app::ReadClient readClient(engine, &ctx.GetExchangeManager(), readCallback.mBufferedCallback, |
| app::ReadClient::InteractionType::Read); |
| |
| NL_TEST_ASSERT(apSuite, readClient.SendRequest(readParams) == CHIP_NO_ERROR); |
| |
| ctx.DrainAndServiceIO(); |
| |
| // The server should return an empty list as attribute data for the first report (for list chunking), and encodes nothing |
| // (then shuts down the read handler) for the second report. |
| // |
| |
| // Nothing is actually encoded. buffered callback does not handle the message to us. |
| NL_TEST_ASSERT(apSuite, readCallback.mAttributeCount == 0); |
| NL_TEST_ASSERT(apSuite, !readCallback.mOnReportEnd); |
| |
| // The server should shutted down, while the client is still alive (pending for the attribute data.) |
| NL_TEST_ASSERT(apSuite, ctx.GetExchangeManager().GetNumActiveExchanges() == 0); |
| } |
| |
| // Sanity check |
| NL_TEST_ASSERT(apSuite, ctx.GetExchangeManager().GetNumActiveExchanges() == 0); |
| |
| emberAfClearDynamicEndpoint(0); |
| } |
| |
| /* |
| * This test contains two parts, one is to enable a new endpoint on the fly, another is to disable it and re-enable it. |
| */ |
| void TestReadChunking::TestDynamicEndpoint(nlTestSuite * apSuite, void * apContext) |
| { |
| TestContext & ctx = *static_cast<TestContext *>(apContext); |
| auto sessionHandle = ctx.GetSessionBobToAlice(); |
| app::InteractionModelEngine * engine = app::InteractionModelEngine::GetInstance(); |
| |
| // Initialize the ember side server logic |
| InitDataModelHandler(); |
| |
| // Register our fake dynamic endpoint. |
| DataVersion dataVersionStorage[ArraySize(testEndpoint4Clusters)]; |
| |
| app::AttributePathParams attributePath; |
| app::ReadPrepareParams readParams(sessionHandle); |
| |
| readParams.mpAttributePathParamsList = &attributePath; |
| readParams.mAttributePathParamsListSize = 1; |
| readParams.mMaxIntervalCeilingSeconds = 1; |
| |
| TestReadCallback readCallback; |
| |
| { |
| |
| app::ReadClient readClient(engine, &ctx.GetExchangeManager(), readCallback.mBufferedCallback, |
| app::ReadClient::InteractionType::Subscribe); |
| // Enable the new endpoint |
| emberAfSetDynamicEndpoint(0, kTestEndpointId, &testEndpoint, Span<DataVersion>(dataVersionStorage)); |
| |
| NL_TEST_ASSERT(apSuite, readClient.SendRequest(readParams) == CHIP_NO_ERROR); |
| |
| ctx.DrainAndServiceIO(); |
| |
| NL_TEST_ASSERT(apSuite, readCallback.mOnSubscriptionEstablished); |
| readCallback.mAttributeCount = 0; |
| |
| emberAfSetDynamicEndpoint(0, kTestEndpointId4, &testEndpoint4, Span<DataVersion>(dataVersionStorage)); |
| |
| ctx.DrainAndServiceIO(); |
| |
| // Ensure we have received the report, we do not care about the initial report here. |
| // GlobalAttributesNotInMetadata attributes are not included in testClusterAttrsOnEndpoint4. |
| NL_TEST_ASSERT(apSuite, |
| readCallback.mAttributeCount == |
| ArraySize(testClusterAttrsOnEndpoint4) + ArraySize(GlobalAttributesNotInMetadata)); |
| |
| // We have received all report data. |
| NL_TEST_ASSERT(apSuite, readCallback.mOnReportEnd); |
| |
| readCallback.mAttributeCount = 0; |
| readCallback.mOnReportEnd = false; |
| |
| // Disable the new endpoint |
| emberAfEndpointEnableDisable(kTestEndpointId4, false); |
| |
| ctx.DrainAndServiceIO(); |
| |
| // We may receive some attribute reports for descriptor cluster, but we do not care about it for now. |
| |
| // Enable the new endpoint |
| |
| readCallback.mAttributeCount = 0; |
| readCallback.mOnReportEnd = false; |
| |
| emberAfEndpointEnableDisable(kTestEndpointId4, true); |
| ctx.DrainAndServiceIO(); |
| |
| // Ensure we have received the report, we do not care about the initial report here. |
| // GlobalAttributesNotInMetadata attributes are not included in testClusterAttrsOnEndpoint4. |
| NL_TEST_ASSERT(apSuite, |
| readCallback.mAttributeCount == |
| ArraySize(testClusterAttrsOnEndpoint4) + ArraySize(GlobalAttributesNotInMetadata)); |
| |
| // We have received all report data. |
| NL_TEST_ASSERT(apSuite, readCallback.mOnReportEnd); |
| } |
| |
| chip::test_utils::SleepMillis(secondsToMilliseconds(2)); |
| |
| // Destroying the read client will terminate the subscription transaction. |
| ctx.DrainAndServiceIO(); |
| |
| NL_TEST_ASSERT(apSuite, ctx.GetExchangeManager().GetNumActiveExchanges() == 0); |
| |
| emberAfClearDynamicEndpoint(0); |
| } |
| /* |
| * The tests below are for testing deatiled bwhavior when the attributes are modified between two chunks. In this test, we only care |
| * above whether we will receive correct attribute values in reasonable messages with reduced reporting traffic. |
| */ |
| |
| namespace TestSetDirtyBetweenChunksUtil { |
| |
| using AttributeIdWithEndpointId = std::pair<EndpointId, AttributeId>; |
| |
| template <AttributeId id> |
| constexpr AttributeIdWithEndpointId AttrOnEp1 = AttributeIdWithEndpointId(kTestEndpointId, id); |
| |
| template <AttributeId id> |
| constexpr AttributeIdWithEndpointId AttrOnEp5 = AttributeIdWithEndpointId(kTestEndpointId5, id); |
| |
| auto WriteAttrOp(AttributeIdWithEndpointId attr, uint8_t val) |
| { |
| return [=]() { gMutableAttrAccess.SetVal(static_cast<uint8_t>(attr.second), val); }; |
| } |
| |
| auto TouchAttrOp(AttributeIdWithEndpointId attr) |
| { |
| return [=]() { |
| app::AttributePathParams path; |
| path.mEndpointId = attr.first; |
| path.mClusterId = Clusters::UnitTesting::Id; |
| path.mAttributeId = attr.second; |
| gIterationCount++; |
| app::InteractionModelEngine::GetInstance()->GetReportingEngine().SetDirty(path); |
| }; |
| } |
| |
| enum AttrIds |
| { |
| Attr1 = 1, |
| Attr2 = 2, |
| Attr3 = 3, |
| }; |
| |
| using AttributeWithValue = std::pair<AttributeIdWithEndpointId, uint8_t>; |
| using AttributesList = std::vector<AttributeIdWithEndpointId>; |
| |
| struct Instruction |
| { |
| // The maximum number of attributes should be iterated in a single report chunk. |
| uint32_t chunksize; |
| // A list of functions that will be executed before driving the main loop. |
| std::vector<std::function<void()>> preworks; |
| // A list of pair for attributes and their expected values in the report. |
| std::vector<AttributeWithValue> expectedValues; |
| // A list of list of various attributes which should have the same data version in the report. |
| std::vector<AttributesList> attributesWithSameDataVersion; |
| }; |
| |
| void DriveIOUntilSubscriptionEstablished(TestMutableReadCallback * callback) |
| { |
| callback->mOnReportEnd = false; |
| gCtx->GetIOContext().DriveIOUntil(System::Clock::Seconds16(5), [&]() { return callback->mOnSubscriptionEstablished; }); |
| NL_TEST_ASSERT(gSuite, callback->mOnReportEnd); |
| NL_TEST_ASSERT(gSuite, callback->mOnSubscriptionEstablished); |
| callback->mActionOn.clear(); |
| } |
| |
| void DriveIOUntilEndOfReport(TestMutableReadCallback * callback) |
| { |
| callback->mOnReportEnd = false; |
| gCtx->GetIOContext().DriveIOUntil(System::Clock::Seconds16(5), [&]() { return callback->mOnReportEnd; }); |
| NL_TEST_ASSERT(gSuite, callback->mOnReportEnd); |
| callback->mActionOn.clear(); |
| } |
| |
| void CheckValues(TestMutableReadCallback * callback, std::vector<AttributeWithValue> expectedValues = {}) |
| { |
| for (const auto & vals : expectedValues) |
| { |
| NL_TEST_ASSERT(gSuite, callback->mValues[vals.first] == vals.second); |
| } |
| } |
| |
| void ExpectSameDataVersions(TestMutableReadCallback * callback, AttributesList attrList) |
| { |
| if (attrList.size() == 0) |
| { |
| return; |
| } |
| DataVersion expectedVersion = callback->mDataVersions[attrList[0]]; |
| for (const auto & attr : attrList) |
| { |
| NL_TEST_ASSERT(gSuite, callback->mDataVersions[attr] == expectedVersion); |
| } |
| } |
| |
| void DoTest(TestMutableReadCallback * callback, Instruction instruction) |
| { |
| app::InteractionModelEngine::GetInstance()->GetReportingEngine().SetMaxAttributesPerChunk(instruction.chunksize); |
| |
| for (const auto & act : instruction.preworks) |
| { |
| act(); |
| } |
| |
| DriveIOUntilEndOfReport(callback); |
| |
| CheckValues(callback, instruction.expectedValues); |
| |
| for (const auto & attrList : instruction.attributesWithSameDataVersion) |
| { |
| ExpectSameDataVersions(callback, attrList); |
| } |
| } |
| |
| }; // namespace TestSetDirtyBetweenChunksUtil |
| |
| void TestReadChunking::TestSetDirtyBetweenChunks(nlTestSuite * apSuite, void * apContext) |
| { |
| using namespace TestSetDirtyBetweenChunksUtil; |
| TestContext & ctx = *static_cast<TestContext *>(apContext); |
| auto sessionHandle = ctx.GetSessionBobToAlice(); |
| app::InteractionModelEngine * engine = app::InteractionModelEngine::GetInstance(); |
| |
| gCtx = &ctx; |
| gSuite = apSuite; |
| |
| // Initialize the ember side server logic |
| InitDataModelHandler(); |
| |
| app::InteractionModelEngine::GetInstance()->GetReportingEngine().SetWriterReserved(0); |
| app::InteractionModelEngine::GetInstance()->GetReportingEngine().SetMaxAttributesPerChunk(2); |
| |
| DataVersion dataVersionStorage1[ArraySize(testEndpointClusters)]; |
| DataVersion dataVersionStorage5[ArraySize(testEndpoint5Clusters)]; |
| |
| gMutableAttrAccess.Reset(); |
| |
| // Register our fake dynamic endpoint. |
| emberAfSetDynamicEndpoint(0, kTestEndpointId, &testEndpoint, Span<DataVersion>(dataVersionStorage1)); |
| emberAfSetDynamicEndpoint(1, kTestEndpointId5, &testEndpoint5, Span<DataVersion>(dataVersionStorage5)); |
| |
| { |
| app::AttributePathParams attributePath; |
| app::ReadPrepareParams readParams(sessionHandle); |
| |
| readParams.mpAttributePathParamsList = &attributePath; |
| readParams.mAttributePathParamsListSize = 1; |
| readParams.mMinIntervalFloorSeconds = 0; |
| readParams.mMaxIntervalCeilingSeconds = 2; |
| |
| // TEST 1 -- Read using wildcard paths |
| ChipLogProgress(DataManagement, "Test 1: Read using wildcard paths."); |
| { |
| TestMutableReadCallback readCallback; |
| |
| gIterationCount = 1; |
| |
| app::ReadClient readClient(engine, &ctx.GetExchangeManager(), readCallback.mBufferedCallback, |
| app::ReadClient::InteractionType::Subscribe); |
| |
| NL_TEST_ASSERT(apSuite, readClient.SendRequest(readParams) == CHIP_NO_ERROR); |
| |
| // CASE 1 -- Touch an attribute during priming report, then verify it is included in first report after priming report. |
| { |
| // When the report engine starts to report attributes in endpoint 5, mark cluster 1 as dirty. |
| // The report engine should NOT include it in initial report to reduce traffic. |
| // We are expected to miss attributes on kTestEndpointId during initial reports. |
| ChipLogProgress(DataManagement, "Case 1-1: Set dirty during priming report."); |
| readCallback.mActionOn[AttrOnEp5<Attr1>] = TouchAttrOp(AttrOnEp1<Attr1>); |
| DriveIOUntilSubscriptionEstablished(&readCallback); |
| CheckValues(&readCallback, { { AttrOnEp1<Attr1>, 1 } }); |
| |
| ChipLogProgress(DataManagement, "Case 1-2: Check for attributes missed last report."); |
| DoTest(&readCallback, Instruction{ .chunksize = 2, .expectedValues = { { AttrOnEp1<Attr1>, 2 } } }); |
| } |
| |
| // CASE 2 -- Set dirty during chunked report, the attribute is already dirty. |
| { |
| ChipLogProgress(DataManagement, "Case 2: Set dirty during chunked report by wildcard path."); |
| readCallback.mActionOn[AttrOnEp5<Attr2>] = WriteAttrOp(AttrOnEp5<Attr3>, 3); |
| DoTest( |
| &readCallback, |
| Instruction{ .chunksize = 2, |
| .preworks = { WriteAttrOp(AttrOnEp5<Attr1>, 2), WriteAttrOp(AttrOnEp5<Attr2>, 2), |
| WriteAttrOp(AttrOnEp5<Attr3>, 2) }, |
| .expectedValues = { { AttrOnEp5<Attr1>, 2 }, { AttrOnEp5<Attr2>, 2 }, { AttrOnEp5<Attr3>, 3 } }, |
| .attributesWithSameDataVersion = { { AttrOnEp5<Attr1>, AttrOnEp5<Attr2>, AttrOnEp5<Attr3> } } }); |
| } |
| |
| // CASE 3 -- Set dirty during chunked report, the attribute is not dirty, and it may catch / missed the current report. |
| { |
| ChipLogProgress(DataManagement, |
| "Case 3-1: Set dirty during chunked report by wildcard path -- new dirty attribute."); |
| readCallback.mActionOn[AttrOnEp5<Attr2>] = WriteAttrOp(AttrOnEp5<Attr3>, 4); |
| DoTest( |
| &readCallback, |
| Instruction{ .chunksize = 1, |
| .preworks = { WriteAttrOp(AttrOnEp5<Attr1>, 4), WriteAttrOp(AttrOnEp5<Attr2>, 4) }, |
| .expectedValues = { { AttrOnEp5<Attr1>, 4 }, { AttrOnEp5<Attr2>, 4 }, { AttrOnEp5<Attr3>, 4 } }, |
| .attributesWithSameDataVersion = { { AttrOnEp5<Attr1>, AttrOnEp5<Attr2>, AttrOnEp5<Attr3> } } }); |
| |
| ChipLogProgress(DataManagement, |
| "Case 3-2: Set dirty during chunked report by wildcard path -- new dirty attribute."); |
| app::InteractionModelEngine::GetInstance()->GetReportingEngine().SetMaxAttributesPerChunk(1); |
| readCallback.mActionOn[AttrOnEp5<Attr2>] = WriteAttrOp(AttrOnEp5<Attr1>, 5); |
| DoTest( |
| &readCallback, |
| Instruction{ .chunksize = 1, |
| .preworks = { WriteAttrOp(AttrOnEp5<Attr2>, 5), WriteAttrOp(AttrOnEp5<Attr3>, 5) }, |
| .expectedValues = { { AttrOnEp5<Attr1>, 5 }, { AttrOnEp5<Attr2>, 5 }, { AttrOnEp5<Attr3>, 5 } }, |
| .attributesWithSameDataVersion = { { AttrOnEp5<Attr1>, AttrOnEp5<Attr2>, AttrOnEp5<Attr3> } } }); |
| } |
| } |
| } |
| // The read client is destructed, server will shutdown the corresponding subscription later. |
| |
| // TEST 2 -- Read using concrete paths. |
| ChipLogProgress(DataManagement, "Test 2: Read using concrete paths."); |
| { |
| app::AttributePathParams attributePath[3]; |
| app::ReadPrepareParams readParams(sessionHandle); |
| |
| attributePath[0] = app::AttributePathParams(kTestEndpointId5, Clusters::UnitTesting::Id, Attr1); |
| attributePath[1] = app::AttributePathParams(kTestEndpointId5, Clusters::UnitTesting::Id, Attr2); |
| attributePath[2] = app::AttributePathParams(kTestEndpointId5, Clusters::UnitTesting::Id, Attr3); |
| |
| readParams.mpAttributePathParamsList = attributePath; |
| readParams.mAttributePathParamsListSize = 3; |
| readParams.mMinIntervalFloorSeconds = 0; |
| readParams.mMaxIntervalCeilingSeconds = 2; |
| gMutableAttrAccess.Reset(); |
| |
| // CASE 1 -- Touch an attribute during priming report, then verify it is included in first report after priming report. |
| { |
| TestMutableReadCallback readCallback; |
| |
| app::ReadClient readClient(engine, &ctx.GetExchangeManager(), readCallback.mBufferedCallback, |
| app::ReadClient::InteractionType::Subscribe); |
| |
| NL_TEST_ASSERT(apSuite, readClient.SendRequest(readParams) == CHIP_NO_ERROR); |
| |
| DriveIOUntilSubscriptionEstablished(&readCallback); |
| |
| // Note, although the two attributes comes from the same cluster, they are generated by different interested paths. |
| // In this case, we won't reset the path iterator. |
| ChipLogProgress(DataManagement, "Case 1-1: Test set dirty during reports generated by concrete paths."); |
| readCallback.mActionOn[AttrOnEp5<Attr2>] = WriteAttrOp(AttrOnEp5<Attr3>, 4); |
| DoTest(&readCallback, |
| Instruction{ .chunksize = 1, |
| .preworks = { WriteAttrOp(AttrOnEp5<Attr1>, 3), WriteAttrOp(AttrOnEp5<Attr2>, 3), |
| WriteAttrOp(AttrOnEp5<Attr3>, 3) }, |
| .expectedValues = { { AttrOnEp5<Attr1>, 3 }, { AttrOnEp5<Attr2>, 3 }, { AttrOnEp5<Attr3>, 3 } } }); |
| |
| // The attribute failed to catch last report will be picked by this report. |
| ChipLogProgress(DataManagement, "Case 1-2: Check for attributes missed last report."); |
| DoTest(&readCallback, { .chunksize = 1, .expectedValues = { { AttrOnEp5<Attr3>, 4 } } }); |
| } |
| } |
| |
| chip::test_utils::SleepMillis(secondsToMilliseconds(3)); |
| |
| // Destroying the read client will terminate the subscription transaction. |
| ctx.DrainAndServiceIO(); |
| |
| NL_TEST_ASSERT(apSuite, ctx.GetExchangeManager().GetNumActiveExchanges() == 0); |
| |
| emberAfClearDynamicEndpoint(1); |
| emberAfClearDynamicEndpoint(0); |
| app::InteractionModelEngine::GetInstance()->GetReportingEngine().SetMaxAttributesPerChunk(UINT32_MAX); |
| } |
| |
| // clang-format off |
| const nlTest sTests[] = |
| { |
| NL_TEST_DEF("TestChunking", TestReadChunking::TestChunking), |
| NL_TEST_DEF("TestListChunking", TestReadChunking::TestListChunking), |
| NL_TEST_DEF("TestBadChunking", TestReadChunking::TestBadChunking), |
| NL_TEST_DEF("TestDynamicEndpoint", TestReadChunking::TestDynamicEndpoint), |
| NL_TEST_DEF("TestSetDirtyBetweenChunks", TestReadChunking::TestSetDirtyBetweenChunks), |
| NL_TEST_SENTINEL() |
| }; |
| |
| // clang-format on |
| |
| // clang-format off |
| nlTestSuite sSuite = |
| { |
| "TestReadChunking", |
| &sTests[0], |
| TestContext::Initialize, |
| TestContext::Finalize |
| }; |
| // clang-format on |
| |
| } // namespace |
| |
| int TestReadChunkingTests() |
| { |
| gSuite = &sSuite; |
| return chip::ExecuteTestsWithContext<TestContext>(&sSuite); |
| } |
| |
| CHIP_REGISTER_TEST_SUITE(TestReadChunkingTests) |