/* * * Copyright (c) 2022-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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace chip; using namespace chip::app; using namespace chip::app::Clusters; namespace { static uint8_t gDebugEventBuffer[4096]; static uint8_t gInfoEventBuffer[4096]; static uint8_t gCritEventBuffer[4096]; static chip::app::CircularEventBuffer gCircularEventBuffer[3]; 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 = 2; constexpr AttributeId kTestListLargeAttribute = 8; // This attribute will be larger than the event size we used in this test. // The size of the attribute which is a bit larger than the size of event used in the test. constexpr size_t kSizeOfLargeAttribute = 60; class TestEventChunking : public chip::Test::AppContext { protected: // Performs setup for each test in the suite void SetUp() { const chip::app::LogStorageResources logStorageResources[] = { { &gDebugEventBuffer[0], sizeof(gDebugEventBuffer), chip::app::PriorityLevel::Debug }, { &gInfoEventBuffer[0], sizeof(gInfoEventBuffer), chip::app::PriorityLevel::Info }, { &gCritEventBuffer[0], sizeof(gCritEventBuffer), chip::app::PriorityLevel::Critical }, }; AppContext::SetUp(); CHIP_ERROR err = CHIP_NO_ERROR; // TODO: use ASSERT_EQ, once transition to pw_unit_test is complete VerifyOrDieWithMsg((err = mEventCounter.Init(0)) == CHIP_NO_ERROR, AppServer, "Init EventCounter failed: %" CHIP_ERROR_FORMAT, err.Format()); chip::app::EventManagement::CreateEventManagement(&GetExchangeManager(), ArraySize(logStorageResources), gCircularEventBuffer, logStorageResources, &mEventCounter); } // Performs teardown for each test in the suite void TearDown() { chip::app::EventManagement::DestroyEventManagement(); AppContext::TearDown(); } private: MonotonicallyIncreasingCounter mEventCounter; }; //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, ZAP_CLUSTER_MASK(SERVER), nullptr, nullptr), DECLARE_DYNAMIC_CLUSTER_LIST_END; DECLARE_DYNAMIC_ENDPOINT(testEndpoint, testEndpointClusters); DECLARE_DYNAMIC_ATTRIBUTE_LIST_BEGIN(testClusterAttrsOnEndpoint4) DECLARE_DYNAMIC_ATTRIBUTE(kTestListLargeAttribute, ARRAY, 1, 0), DECLARE_DYNAMIC_ATTRIBUTE_LIST_END(); DECLARE_DYNAMIC_CLUSTER_LIST_BEGIN(testEndpoint4Clusters) DECLARE_DYNAMIC_CLUSTER(Clusters::UnitTesting::Id, testClusterAttrsOnEndpoint4, ZAP_CLUSTER_MASK(SERVER), nullptr, nullptr), DECLARE_DYNAMIC_CLUSTER_LIST_END; DECLARE_DYNAMIC_ENDPOINT(testEndpoint4, testEndpoint4Clusters); //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 OnEventData(const app::EventHeader & aEventHeader, TLV::TLVReader * apData, const app::StatusIB * apStatus) override; void OnDone(app::ReadClient * apReadClient) override; void OnReportEnd() override { mOnReportEnd = true; } void OnSubscriptionEstablished(SubscriptionId aSubscriptionId) override { mOnSubscriptionEstablished = true; } uint32_t mAttributeCount = 0; uint32_t mEventCount = 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 v; EXPECT_EQ(app::DataModel::Decode(*apData, v), CHIP_NO_ERROR); auto it = v.begin(); size_t arraySize = 0; while (it.Next()) { FAIL(); } EXPECT_EQ(it.GetStatus(), CHIP_NO_ERROR); EXPECT_EQ(v.ComputeSize(&arraySize), CHIP_NO_ERROR); EXPECT_EQ(arraySize, 0u); } else if (aPath.mAttributeId == Globals::Attributes::AcceptedCommandList::Id) { app::DataModel::DecodableList v; EXPECT_EQ(app::DataModel::Decode(*apData, v), CHIP_NO_ERROR); auto it = v.begin(); size_t arraySize = 0; while (it.Next()) { FAIL(); } EXPECT_EQ(it.GetStatus(), CHIP_NO_ERROR); EXPECT_EQ(v.ComputeSize(&arraySize), CHIP_NO_ERROR); EXPECT_EQ(arraySize, 0u); } else if (aPath.mAttributeId == Globals::Attributes::AttributeList::Id) { // Nothing to check for this one; depends on the endpoint. } else if (aPath.mAttributeId == kTestListLargeAttribute) { app::DataModel::DecodableList v; EXPECT_EQ(app::DataModel::Decode(*apData, v), CHIP_NO_ERROR); auto it = v.begin(); size_t arraySize = 0; EXPECT_EQ(v.ComputeSize(&arraySize), CHIP_NO_ERROR); EXPECT_EQ(arraySize, 4u); } else { uint8_t v; EXPECT_EQ(app::DataModel::Decode(*apData, v), CHIP_NO_ERROR); EXPECT_EQ(v, (uint8_t) gIterationCount); } mAttributeCount++; } void TestReadCallback::OnEventData(const app::EventHeader & aEventHeader, TLV::TLVReader * apData, const app::StatusIB * aStatus) { VerifyOrReturn(apData != nullptr); mEventCount++; } void TestReadCallback::OnDone(app::ReadClient *) {} class TestAttrAccess : public app::AttributeAccessInterface { public: // Register for the Test Cluster cluster on all endpoints. TestAttrAccess() : AttributeAccessInterface(Optional::Missing(), Clusters::UnitTesting::Id) { AttributeAccessInterfaceRegistry::Instance().Register(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) { switch (aPath.mAttributeId) { case kTestListLargeAttribute: return aEncoder.EncodeList([](const auto & encoder) { for (int i = 0; i < 4; i++) { // When putting even numbers of list entries, there is a point (a range of iterations) that we can put an event // between two list items in the same chunk. ReturnErrorOnFailure(encoder.Encode(ByteSpan(sAnStringThatCanNeverFitIntoTheMTU, kSizeOfLargeAttribute))); } return CHIP_NO_ERROR; }); default: return aEncoder.Encode((uint8_t) gIterationCount); } } CHIP_ERROR TestAttrAccess::Write(const app::ConcreteDataAttributePath & aPath, app::AttributeValueDecoder & aDecoder) { return CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE; } void GenerateEvents(chip::EventNumber & firstEventNumber, chip::EventNumber & lastEventNumber) { CHIP_ERROR err = CHIP_NO_ERROR; Clusters::UnitTesting::Events::TestEvent::Type content; content.arg1 = static_cast(gIterationCount); for (int i = 0; i < 5; i++) { EXPECT_EQ((err = app::LogEvent(content, kTestEndpointId, lastEventNumber)), CHIP_NO_ERROR); if (i == 0) { firstEventNumber = lastEventNumber; } } } /* * This validates all the various corner cases encountered during chunking by * artificially reducing the size of a packet buffer used to encode attribute & event data * to force chunking to happen over multiple packets even with a small number of attributes or events * 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 or events 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 + fake event * generation interface to simulate faithfully a real application. This ensures validation of as much production logic pathways * as we can possibly cover. * */ TEST_F(TestEventChunking, TestEventChunking) { auto sessionHandle = GetSessionBobToAlice(); app::InteractionModelEngine * engine = app::InteractionModelEngine::GetInstance(); // Initialize the ember side server logic CodegenDataModelProviderInstance()->Shutdown(); InitDataModelHandler(); // Register our fake dynamic endpoint. DataVersion dataVersionStorage[ArraySize(testEndpointClusters)]; emberAfSetDynamicEndpoint(0, kTestEndpointId, &testEndpoint, Span(dataVersionStorage)); chip::EventNumber firstEventNumber; chip::EventNumber lastEventNumber; GenerateEvents(firstEventNumber, lastEventNumber); app::EventPathParams eventPath; eventPath.mEndpointId = kTestEndpointId; eventPath.mClusterId = app::Clusters::UnitTesting::Id; app::ReadPrepareParams readParams(sessionHandle); readParams.mpEventPathParamsList = &eventPath; readParams.mEventPathParamsListSize = 1; readParams.mEventNumber.SetValue(firstEventNumber); // Since we will always read from the first event, we only generate event once. // // 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 EventDataIB, 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(800 + i)); app::ReadClient readClient(engine, &GetExchangeManager(), readCallback.mBufferedCallback, app::ReadClient::InteractionType::Read); EXPECT_EQ(readClient.SendRequest(readParams), CHIP_NO_ERROR); DrainAndServiceIO(); EXPECT_EQ(readCallback.mEventCount, static_cast((lastEventNumber - firstEventNumber) + 1)); 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); } // Similar to the tests above, but it will read attributes AND events TEST_F(TestEventChunking, TestMixedEventsAndAttributesChunking) { auto sessionHandle = GetSessionBobToAlice(); app::InteractionModelEngine * engine = app::InteractionModelEngine::GetInstance(); // Initialize the ember side server logic CodegenDataModelProviderInstance()->Shutdown(); InitDataModelHandler(); // Register our fake dynamic endpoint. DataVersion dataVersionStorage[ArraySize(testEndpointClusters)]; emberAfSetDynamicEndpoint(0, kTestEndpointId, &testEndpoint, Span(dataVersionStorage)); chip::EventNumber firstEventNumber; chip::EventNumber lastEventNumber; // We will always read from the first event, so it is enough to only generate events once. GenerateEvents(firstEventNumber, lastEventNumber); app::EventPathParams eventPath; app::AttributePathParams attributePath(kTestEndpointId, app::Clusters::UnitTesting::Id); eventPath.mEndpointId = kTestEndpointId; eventPath.mClusterId = app::Clusters::UnitTesting::Id; app::ReadPrepareParams readParams(sessionHandle); readParams.mpAttributePathParamsList = &attributePath; readParams.mAttributePathParamsListSize = 1; readParams.mpEventPathParamsList = &eventPath; readParams.mEventPathParamsListSize = 1; readParams.mEventNumber.SetValue(firstEventNumber); // // 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 EventDataIB, 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(800 + i)); app::ReadClient readClient(engine, &GetExchangeManager(), readCallback.mBufferedCallback, app::ReadClient::InteractionType::Read); EXPECT_EQ(readClient.SendRequest(readParams), CHIP_NO_ERROR); DrainAndServiceIO(); // // Always returns the same number of attributes read (5 + revision + GlobalAttributesNotInMetadata). // EXPECT_TRUE(readCallback.mOnReportEnd); EXPECT_EQ(readCallback.mAttributeCount, 6 + ArraySize(GlobalAttributesNotInMetadata)); EXPECT_EQ(readCallback.mEventCount, static_cast(lastEventNumber - firstEventNumber + 1)); 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); } // Similar to the tests above, however, there is one another case -- the event payload is very large usually, so when it is failed // to encode an attribute, it is usually impossible to encode a event data, so we cannot verify the case when events and attributes // can be encoded in to one chunk in the tests above. This test will force it by reading only one attribtue and read many events. TEST_F(TestEventChunking, TestMixedEventsAndLargeAttributesChunking) { auto sessionHandle = GetSessionBobToAlice(); app::InteractionModelEngine * engine = app::InteractionModelEngine::GetInstance(); // Initialize the ember side server logic CodegenDataModelProviderInstance()->Shutdown(); InitDataModelHandler(); // Register our fake dynamic endpoint. DataVersion dataVersionStorage[ArraySize(testEndpointClusters)]; emberAfSetDynamicEndpoint(0, kTestEndpointId, &testEndpoint4, Span(dataVersionStorage)); chip::EventNumber firstEventNumber; chip::EventNumber lastEventNumber; // We will always read from the first event, so it is enough to only generate events once. GenerateEvents(firstEventNumber, lastEventNumber); app::EventPathParams eventPath; app::AttributePathParams attributePath(kTestEndpointId, app::Clusters::UnitTesting::Id, kTestListLargeAttribute); eventPath.mEndpointId = kTestEndpointId; eventPath.mClusterId = app::Clusters::UnitTesting::Id; app::ReadPrepareParams readParams(sessionHandle); readParams.mpAttributePathParamsList = &attributePath; readParams.mAttributePathParamsListSize = 1; readParams.mpEventPathParamsList = &eventPath; readParams.mEventPathParamsListSize = 1; readParams.mEventNumber.SetValue(firstEventNumber); // // 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 EventDataIB, 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(800 + i)); app::ReadClient readClient(engine, &GetExchangeManager(), readCallback.mBufferedCallback, app::ReadClient::InteractionType::Read); EXPECT_EQ(readClient.SendRequest(readParams), CHIP_NO_ERROR); DrainAndServiceIO(); EXPECT_TRUE(readCallback.mOnReportEnd); EXPECT_EQ(readCallback.mAttributeCount, 1u); EXPECT_EQ(readCallback.mEventCount, static_cast(lastEventNumber - firstEventNumber + 1)); 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); } } // namespace