blob: 1e1f0c41fd6e7d47561e90c25c3a956aedb66c6f [file] [log] [blame]
/*
*
* Copyright (c) 2021 Project CHIP Authors
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#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/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::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(TestCluster::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(TestCluster::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(TestCluster::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(TestCluster::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);
}
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 = TestCluster::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(), TestCluster::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 == TestCluster::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(&ctx.GetExchangeManager());
// Register our fake dynamic endpoint.
DataVersion dataVersionStorage[ArraySize(testEndpointClusters)];
emberAfSetDynamicEndpoint(0, kTestEndpointId, &testEndpoint, Span<DataVersion>(dataVersionStorage));
app::AttributePathParams attributePath(kTestEndpointId, app::Clusters::TestCluster::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 +
// AttributeList + AcceptedCommandList +
// GeneratedCommandList = 9).
//
NL_TEST_ASSERT(apSuite, readCallback.mAttributeCount == 9);
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(&ctx.GetExchangeManager());
// Register our fake dynamic endpoint.
DataVersion dataVersionStorage[ArraySize(testEndpoint3Clusters)];
emberAfSetDynamicEndpoint(0, kTestEndpointId3, &testEndpoint3, Span<DataVersion>(dataVersionStorage));
app::AttributePathParams attributePath(kTestEndpointId3, app::Clusters::TestCluster::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(&ctx.GetExchangeManager());
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::TestCluster::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(&ctx.GetExchangeManager());
// 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.
// AcceptedCommandList / GeneratedCommandList / AttributeList attribute are not included in
// testClusterAttrsOnEndpoint4.
NL_TEST_ASSERT(apSuite, readCallback.mAttributeCount == ArraySize(testClusterAttrsOnEndpoint4) + 3);
// 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.
// AcceptedCommandList / GeneratedCommandList / AttributeList attribute are not include in
// testClusterAttrsOnEndpoint4.
NL_TEST_ASSERT(apSuite, readCallback.mAttributeCount == ArraySize(testClusterAttrsOnEndpoint4) + 3);
// 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 = TestCluster::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(&ctx.GetExchangeManager());
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, TestCluster::Id, Attr1);
attributePath[1] = app::AttributePathParams(kTestEndpointId5, TestCluster::Id, Attr2);
attributePath[2] = app::AttributePathParams(kTestEndpointId5, TestCluster::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)