blob: a24422010c47c9a13a10885268d8d415aaeae644 [file] [log] [blame]
/*
*
* Copyright (c) 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/InteractionModelEngine.h>
#include <app/reporting/ReportSchedulerImpl.h>
#include <app/tests/AppTestContext.h>
#include <lib/support/UnitTestContext.h>
#include <lib/support/UnitTestRegistration.h>
#include <lib/support/logging/CHIPLogging.h>
#include <nlunit-test.h>
namespace {
class TestContext : public chip::Test::AppContext
{
public:
static int Initialize(void * context)
{
if (AppContext::Initialize(context) != SUCCESS)
return FAILURE;
auto * ctx = static_cast<TestContext *>(context);
if (ctx->mEventCounter.Init(0) != CHIP_NO_ERROR)
{
return FAILURE;
}
return SUCCESS;
}
static int Finalize(void * context)
{
chip::app::EventManagement::DestroyEventManagement();
if (AppContext::Finalize(context) != SUCCESS)
return FAILURE;
return SUCCESS;
}
private:
chip::MonotonicallyIncreasingCounter<chip::EventNumber> mEventCounter;
};
class NullReadHandlerCallback : public chip::app::ReadHandler::ManagementCallback
{
public:
void OnDone(chip::app::ReadHandler & apReadHandlerObj) override {}
chip::app::ReadHandler::ApplicationCallback * GetAppCallback() override { return nullptr; }
};
} // namespace
namespace chip {
namespace app {
namespace reporting {
using InteractionModelEngine = InteractionModelEngine;
using ReportScheduler = reporting::ReportScheduler;
using ReportSchedulerImpl = reporting::ReportSchedulerImpl;
using ReadHandlerNode = reporting::ReportScheduler::ReadHandlerNode;
using Milliseconds64 = System::Clock::Milliseconds64;
static const size_t kNumMaxReadHandlers = 16;
class TestTimerDelegate : public ReportScheduler::TimerDelegate
{
public:
struct NodeTimeoutPair
{
ReadHandlerNode * node;
System::Clock::Timeout timeout;
};
NodeTimeoutPair mPairArray[kNumMaxReadHandlers];
size_t mPairArraySize = 0;
System::Clock::Timestamp mMockSystemTimestamp = System::Clock::Milliseconds64(0);
NodeTimeoutPair * FindPair(ReadHandlerNode * node, size_t & position)
{
for (size_t i = 0; i < mPairArraySize; i++)
{
if (mPairArray[i].node == node)
{
position = i;
return &mPairArray[i];
}
}
return nullptr;
}
CHIP_ERROR insertPair(ReadHandlerNode * node, System::Clock::Timeout timeout)
{
VerifyOrReturnError(mPairArraySize < kNumMaxReadHandlers, CHIP_ERROR_NO_MEMORY);
mPairArray[mPairArraySize].node = node;
mPairArray[mPairArraySize].timeout = timeout;
mPairArraySize++;
return CHIP_NO_ERROR;
}
void removePair(ReadHandlerNode * node)
{
size_t position;
NodeTimeoutPair * pair = FindPair(node, position);
VerifyOrReturn(pair != nullptr);
size_t nextPos = static_cast<size_t>(position + 1);
size_t moveNum = static_cast<size_t>(mPairArraySize - nextPos);
// Compress array after removal, if the removed position is not the last
if (moveNum)
{
memmove(&mPairArray[position], &mPairArray[nextPos], sizeof(NodeTimeoutPair) * moveNum);
}
mPairArraySize--;
}
static void TimerCallbackInterface(System::Layer * aLayer, void * aAppState)
{
// Normaly we would call the callback here, thus scheduling an engine run, but we don't need it for this test as we simulate
// all the callbacks related to report emissions. The actual callback should look like this:
//
// ReadHandlerNode * node = static_cast<ReadHandlerNode *>(aAppState);
// node->RunCallback();
ChipLogProgress(DataManagement, "Simluating engine run for Handler: %p", aAppState);
}
virtual CHIP_ERROR StartTimer(void * context, System::Clock::Timeout aTimeout) override
{
return insertPair(static_cast<ReadHandlerNode *>(context), aTimeout + mMockSystemTimestamp);
}
virtual void CancelTimer(void * context) override { removePair(static_cast<ReadHandlerNode *>(context)); }
virtual bool IsTimerActive(void * context) override
{
size_t position;
NodeTimeoutPair * pair = FindPair(static_cast<ReadHandlerNode *>(context), position);
VerifyOrReturnValue(pair != nullptr, false);
return pair->timeout > mMockSystemTimestamp;
}
virtual System::Clock::Timestamp GetCurrentMonotonicTimestamp() override { return mMockSystemTimestamp; }
void SetMockSystemTimestamp(System::Clock::Timestamp aMockTimestamp) { mMockSystemTimestamp = aMockTimestamp; }
// Increment the mock timestamp one milisecond at a time for a total of aTime miliseconds. Checks if
void IncrementMockTimestamp(System::Clock::Milliseconds64 aTime)
{
mMockSystemTimestamp = mMockSystemTimestamp + aTime;
for (size_t i = 0; i < mPairArraySize; i++)
{
if (mPairArray[i].timeout <= mMockSystemTimestamp)
{
TimerCallbackInterface(nullptr, mPairArray[i].node);
}
}
}
};
TestTimerDelegate sTestTimerDelegate;
ReportSchedulerImpl sScheduler(&sTestTimerDelegate);
class TestReportScheduler
{
public:
static void TestReadHandlerList(nlTestSuite * aSuite, void * aContext)
{
TestContext & ctx = *static_cast<TestContext *>(aContext);
NullReadHandlerCallback nullCallback;
// exchange context
Messaging::ExchangeContext * exchangeCtx = ctx.NewExchangeToAlice(nullptr, false);
// Read handler pool
ObjectPool<ReadHandler, kNumMaxReadHandlers> readHandlerPool;
// Initialize mock timestamp
sTestTimerDelegate.SetMockSystemTimestamp(Milliseconds64(0));
for (size_t i = 0; i < kNumMaxReadHandlers; i++)
{
ReadHandler * readHandler =
readHandlerPool.CreateObject(nullCallback, exchangeCtx, ReadHandler::InteractionType::Subscribe);
NL_TEST_ASSERT(aSuite, nullptr != readHandler);
VerifyOrReturn(nullptr != readHandler);
NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == sScheduler.RegisterReadHandler(readHandler));
NL_TEST_ASSERT(aSuite, nullptr != sScheduler.FindReadHandlerNode(readHandler));
}
NL_TEST_ASSERT(aSuite, readHandlerPool.Allocated() == kNumMaxReadHandlers);
NL_TEST_ASSERT(aSuite, sScheduler.GetNumReadHandlers() == kNumMaxReadHandlers);
NL_TEST_ASSERT(aSuite, ctx.GetExchangeManager().GetNumActiveExchanges() == 1);
// Test unregister first ReadHandler
ReadHandler * firstReadHandler = sScheduler.mReadHandlerList.begin()->GetReadHandler();
sScheduler.UnregisterReadHandler(firstReadHandler);
NL_TEST_ASSERT(aSuite, sScheduler.GetNumReadHandlers() == kNumMaxReadHandlers - 1);
NL_TEST_ASSERT(aSuite, nullptr == sScheduler.FindReadHandlerNode(firstReadHandler));
// Test unregister middle ReadHandler
auto iter = sScheduler.mReadHandlerList.begin();
for (size_t i = 0; i < static_cast<size_t>(kNumMaxReadHandlers / 2); i++)
{
iter++;
}
ReadHandler * middleReadHandler = iter->GetReadHandler();
sScheduler.UnregisterReadHandler(middleReadHandler);
NL_TEST_ASSERT(aSuite, sScheduler.GetNumReadHandlers() == kNumMaxReadHandlers - 2);
NL_TEST_ASSERT(aSuite, nullptr == sScheduler.FindReadHandlerNode(middleReadHandler));
// Test unregister last ReadHandler
iter = sScheduler.mReadHandlerList.end();
iter--;
ReadHandler * lastReadHandler = iter->GetReadHandler();
sScheduler.UnregisterReadHandler(lastReadHandler);
NL_TEST_ASSERT(aSuite, sScheduler.GetNumReadHandlers() == kNumMaxReadHandlers - 3);
NL_TEST_ASSERT(aSuite, nullptr == sScheduler.FindReadHandlerNode(lastReadHandler));
sScheduler.UnregisterAllHandlers();
// Confirm all ReadHandlers are unregistered from the scheduler
NL_TEST_ASSERT(aSuite, sScheduler.GetNumReadHandlers() == 0);
readHandlerPool.ForEachActiveObject([&](ReadHandler * handler) {
NL_TEST_ASSERT(aSuite, nullptr == sScheduler.FindReadHandlerNode(handler));
return Loop::Continue;
});
readHandlerPool.ReleaseAll();
exchangeCtx->Close();
NL_TEST_ASSERT(aSuite, ctx.GetExchangeManager().GetNumActiveExchanges() == 0);
}
static void TestReportTiming(nlTestSuite * aSuite, void * aContext)
{
TestContext & ctx = *static_cast<TestContext *>(aContext);
NullReadHandlerCallback nullCallback;
// exchange context
Messaging::ExchangeContext * exchangeCtx = ctx.NewExchangeToAlice(nullptr, false);
// Read handler pool
ObjectPool<ReadHandler, kNumMaxReadHandlers> readHandlerPool;
// Initialize mock timestamp
sTestTimerDelegate.SetMockSystemTimestamp(Milliseconds64(0));
// Dirty read handler, will be triggered at min interval
ReadHandler * readHandler1 =
readHandlerPool.CreateObject(nullCallback, exchangeCtx, ReadHandler::InteractionType::Subscribe);
NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == readHandler1->SetMaxReportingInterval(2));
NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == readHandler1->SetMinReportingIntervalForTests(1));
// Do those manually to avoid scheduling an engine run
readHandler1->mFlags.Set(ReadHandler::ReadHandlerFlags::ForceDirty, true);
readHandler1->mState = ReadHandler::HandlerState::GeneratingReports;
// Clean read handler, will be triggered at max interval
ReadHandler * readHandler2 =
readHandlerPool.CreateObject(nullCallback, exchangeCtx, ReadHandler::InteractionType::Subscribe);
NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == readHandler2->SetMaxReportingInterval(3));
NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == readHandler2->SetMinReportingIntervalForTests(0));
// Do those manually to avoid scheduling an engine run
readHandler2->mState = ReadHandler::HandlerState::GeneratingReports;
// Clean read handler, will be triggered at max interval, but will be cancelled before
ReadHandler * readHandler3 =
readHandlerPool.CreateObject(nullCallback, exchangeCtx, ReadHandler::InteractionType::Subscribe);
NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == readHandler3->SetMaxReportingInterval(3));
NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == readHandler3->SetMinReportingIntervalForTests(0));
// Do those manually to avoid scheduling an engine run
readHandler3->mState = ReadHandler::HandlerState::GeneratingReports;
NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == sScheduler.RegisterReadHandler(readHandler1));
NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == sScheduler.RegisterReadHandler(readHandler2));
NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == sScheduler.RegisterReadHandler(readHandler3));
// Confirms that none of the ReadHandlers are currently reportable
NL_TEST_ASSERT(aSuite, !sScheduler.IsReportableNow(readHandler1));
NL_TEST_ASSERT(aSuite, !sScheduler.IsReportableNow(readHandler2));
NL_TEST_ASSERT(aSuite, !sScheduler.IsReportableNow(readHandler3));
// Simulate system clock increment
sTestTimerDelegate.IncrementMockTimestamp(Milliseconds64(1100));
// Checks that the first ReadHandler is reportable after 1 second since it is dirty and min interval has expired
NL_TEST_ASSERT(aSuite, sScheduler.IsReportableNow(readHandler1));
NL_TEST_ASSERT(aSuite, !sScheduler.IsReportableNow(readHandler2));
NL_TEST_ASSERT(aSuite, !sScheduler.IsReportableNow(readHandler3));
NL_TEST_ASSERT(aSuite, sScheduler.IsReportScheduled(readHandler3));
sScheduler.CancelReport(readHandler3);
NL_TEST_ASSERT(aSuite, !sScheduler.IsReportScheduled(readHandler3));
// Simulate system clock increment
sTestTimerDelegate.IncrementMockTimestamp(Milliseconds64(2000));
// Checks that all ReadHandlers are reportable
NL_TEST_ASSERT(aSuite, sScheduler.IsReportableNow(readHandler1));
NL_TEST_ASSERT(aSuite, sScheduler.IsReportableNow(readHandler2));
// Even if its timer got cancelled, readHandler3 should still be considered reportable as the max interval has expired
// and it is in generating report state
NL_TEST_ASSERT(aSuite, sScheduler.IsReportableNow(readHandler3));
sScheduler.UnregisterAllHandlers();
readHandlerPool.ReleaseAll();
exchangeCtx->Close();
NL_TEST_ASSERT(aSuite, ctx.GetExchangeManager().GetNumActiveExchanges() == 0);
}
static void TestObserverCallbacks(nlTestSuite * aSuite, void * aContext)
{
TestContext & ctx = *static_cast<TestContext *>(aContext);
NullReadHandlerCallback nullCallback;
// exchange context
Messaging::ExchangeContext * exchangeCtx = ctx.NewExchangeToAlice(nullptr, false);
// Read handler pool
ObjectPool<ReadHandler, kNumMaxReadHandlers> readHandlerPool;
// Initialize mock timestamp
sTestTimerDelegate.SetMockSystemTimestamp(Milliseconds64(0));
ReadHandler * readHandler =
readHandlerPool.CreateObject(nullCallback, exchangeCtx, ReadHandler::InteractionType::Subscribe);
NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == readHandler->SetMaxReportingInterval(2));
NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == readHandler->SetMinReportingIntervalForTests(1));
// Do those manually to avoid scheduling an engine run
readHandler->mState = ReadHandler::HandlerState::GeneratingReports;
readHandler->SetObserver(&sScheduler);
// Test OnReadHandlerCreated
readHandler->mObserver->OnReadHandlerCreated(readHandler);
// Should have registered the read handler in the scheduler and scheduled a report
NL_TEST_ASSERT(aSuite, sScheduler.GetNumReadHandlers() == 1);
NL_TEST_ASSERT(aSuite, sScheduler.IsReportScheduled(readHandler));
ReadHandlerNode * node = sScheduler.FindReadHandlerNode(readHandler);
NL_TEST_ASSERT(aSuite, nullptr != node);
VerifyOrReturn(nullptr != node);
NL_TEST_ASSERT(aSuite, node->GetReadHandler() == readHandler);
// Test OnBecameReportable
readHandler->mFlags.Set(ReadHandler::ReadHandlerFlags::ForceDirty, true);
readHandler->mObserver->OnBecameReportable(readHandler);
// Should have changed the scheduled timeout to the handler's min interval, to check, we wait for the min interval to
// expire
// Simulate system clock increment
sTestTimerDelegate.IncrementMockTimestamp(Milliseconds64(1100));
// Check that no report is scheduled since the min interval has expired, the timer should now be stopped
NL_TEST_ASSERT(aSuite, !sScheduler.IsReportScheduled(readHandler));
// Test OnSubscriptionAction
readHandler->mFlags.Set(ReadHandler::ReadHandlerFlags::ForceDirty, false);
readHandler->mObserver->OnSubscriptionAction(readHandler);
// Should have changed the scheduled timeout to the handlers max interval, to check, we wait for the min interval to
// confirm it is not expired yet so the report should still be scheduled
NL_TEST_ASSERT(aSuite, sScheduler.IsReportScheduled(readHandler));
// Simulate system clock increment
sTestTimerDelegate.IncrementMockTimestamp(Milliseconds64(1100));
// Check that the report is still scheduled as the max interval has not expired yet and the dirty flag was cleared
NL_TEST_ASSERT(aSuite, sScheduler.IsReportScheduled(readHandler));
// Simulate system clock increment
sTestTimerDelegate.IncrementMockTimestamp(Milliseconds64(2100));
// Check that no report is scheduled since the max interval should have expired, the timer should now be stopped
NL_TEST_ASSERT(aSuite, !sScheduler.IsReportScheduled(readHandler));
// Test OnReadHandlerDestroyed
readHandler->mObserver->OnReadHandlerDestroyed(readHandler);
// Should have unregistered the read handler in the scheduler and cancelled the report
NL_TEST_ASSERT(aSuite, !sScheduler.IsReportScheduled(readHandler));
NL_TEST_ASSERT(aSuite, sScheduler.GetNumReadHandlers() == 0);
NL_TEST_ASSERT(aSuite, nullptr == sScheduler.FindReadHandlerNode(readHandler));
sScheduler.UnregisterReadHandler(readHandler);
readHandlerPool.ReleaseAll();
exchangeCtx->Close();
NL_TEST_ASSERT(aSuite, ctx.GetExchangeManager().GetNumActiveExchanges() == 0);
}
};
} // namespace reporting
} // namespace app
} // namespace chip
namespace {
/**
* Test Suite. It lists all the test functions.
*/
static nlTest sTests[] = {
NL_TEST_DEF("TestReadHandlerList", chip::app::reporting::TestReportScheduler::TestReadHandlerList),
NL_TEST_DEF("TestReportTiming", chip::app::reporting::TestReportScheduler::TestReportTiming),
NL_TEST_DEF("TestObserverCallbacks", chip::app::reporting::TestReportScheduler::TestObserverCallbacks),
NL_TEST_SENTINEL(),
};
nlTestSuite sSuite = { "TestReportScheduler", &sTests[0], TestContext::Initialize, TestContext::Finalize };
} // namespace
int TestReportScheduler()
{
return chip::ExecuteTestsWithContext<TestContext>(&sSuite);
}
CHIP_REGISTER_TEST_SUITE(TestReportScheduler);