diff --git a/src/app/icd/ICDManager.cpp b/src/app/icd/ICDManager.cpp index e3787f2ddc747a..287d0dd11a385a 100644 --- a/src/app/icd/ICDManager.cpp +++ b/src/app/icd/ICDManager.cpp @@ -75,9 +75,12 @@ void ICDManager::Shutdown() bool ICDManager::SupportsCheckInProtocol() { - bool success; - uint32_t featureMap; + bool success = false; + uint32_t featureMap = 0; + // Can't use attribute accessors/Attributes::FeatureMap::Get in unit tests +#ifndef CONFIG_BUILD_FOR_HOST_UNIT_TEST success = (Attributes::FeatureMap::Get(kRootEndpointId, &featureMap) == EMBER_ZCL_STATUS_SUCCESS); +#endif return success ? ((featureMap & to_underlying(Feature::kCheckInProtocolSupport)) != 0) : false; } diff --git a/src/app/icd/ICDManager.h b/src/app/icd/ICDManager.h index e5406f14615837..6e9890240c5eee 100644 --- a/src/app/icd/ICDManager.h +++ b/src/app/icd/ICDManager.h @@ -26,6 +26,10 @@ namespace chip { namespace app { +// Forward declaration of TestICDManager to allow it to be friend with ICDManager +// Used in unit tests +class TestICDManager; + /** * @brief ICD Manager is responsible of processing the events and triggering the correct action for an ICD */ @@ -67,6 +71,8 @@ class ICDManager static System::Clock::Milliseconds32 GetFastPollingInterval() { return kFastPollingInterval; } protected: + friend class TestICDManager; + static void OnIdleModeDone(System::Layer * aLayer, void * appState); static void OnActiveModeDone(System::Layer * aLayer, void * appState); diff --git a/src/app/tests/TestICDManager.cpp b/src/app/tests/TestICDManager.cpp index 6fdaeedf45a73f..81739aea43ceb4 100644 --- a/src/app/tests/TestICDManager.cpp +++ b/src/app/tests/TestICDManager.cpp @@ -15,17 +15,199 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +#include +#include +#include +#include #include #include +#include -int TestICDManager() +#include +#include +#include + +using namespace chip; +using namespace chip::app; +using namespace chip::System; + +namespace { + +class TestICDStateObserver : public app::ICDStateObserver +{ +public: + void OnEnterActiveMode() {} +}; + +TestICDStateObserver mICDStateObserver; +static Clock::Internal::MockClock gMockClock; +static Clock::ClockBase * gRealClock; + +class TestContext : public Test::AppContext +{ +public: + static int Initialize(void * context) + { + if (AppContext::Initialize(context) != SUCCESS) + return FAILURE; + + auto * ctx = static_cast(context); + DeviceLayer::SetSystemLayerForTesting(&ctx->GetSystemLayer()); + + gRealClock = &SystemClock(); + Clock::Internal::SetSystemClockForTesting(&gMockClock); + + if (ctx->mEventCounter.Init(0) != CHIP_NO_ERROR) + { + return FAILURE; + } + + ctx->mICDManager.Init(&ctx->testStorage, &ctx->GetFabricTable(), &mICDStateObserver); + return SUCCESS; + } + + static int Finalize(void * context) + { + auto * ctx = static_cast(context); + ctx->mICDManager.Shutdown(); + app::EventManagement::DestroyEventManagement(); + System::Clock::Internal::SetSystemClockForTesting(gRealClock); + DeviceLayer::SetSystemLayerForTesting(nullptr); + + if (AppContext::Finalize(context) != SUCCESS) + return FAILURE; + + return SUCCESS; + } + + app::ICDManager mICDManager; + +private: + TestPersistentStorageDelegate testStorage; + MonotonicallyIncreasingCounter mEventCounter; +}; + +} // namespace + +namespace chip { +namespace app { +class TestICDManager { - static nlTest sTests[] = { NL_TEST_SENTINEL() }; +public: + /* + * Advance the test Mock clock time by the amout passed in argument + * and then force the SystemLayer Timer event loop. It will check for any expired timer, + * and invoke their callbacks if there are any. + * + * @param time_ms: Value in milliseconds. + */ + static void AdvanceClockAndRunEventLoop(TestContext * ctx, uint32_t time_ms) + { + gMockClock.AdvanceMonotonic(System::Clock::Timeout(time_ms)); + ctx->GetIOContext().DriveIO(); + } - nlTestSuite cmSuite = { "TestICDManager", &sTests[0], nullptr, nullptr }; + static void TestICDModeIntervals(nlTestSuite * aSuite, void * aContext) + { + TestContext * ctx = static_cast(aContext); - nlTestRunner(&cmSuite, nullptr); - return (nlTestRunnerStats(&cmSuite)); + // After the init we should be in active mode + NL_TEST_ASSERT(aSuite, ctx->mICDManager.mOperationalState == ICDManager::OperationalState::ActiveMode); + AdvanceClockAndRunEventLoop(ctx, IcdManagementServer::GetInstance().GetActiveModeInterval() + 1); + // Active mode interval expired, ICDManager transitioned to the IdleMode. + NL_TEST_ASSERT(aSuite, ctx->mICDManager.mOperationalState == ICDManager::OperationalState::IdleMode); + AdvanceClockAndRunEventLoop(ctx, IcdManagementServer::GetInstance().GetIdleModeInterval() + 1); + // Idle mode interval expired, ICDManager transitioned to the ActiveMode. + NL_TEST_ASSERT(aSuite, ctx->mICDManager.mOperationalState == ICDManager::OperationalState::ActiveMode); + + // Events updating the Operation to Active mode can extend the current active mode time by 1 Active mode threshold. + // Kick an active Threshold just before the end of the Active interval and validate that the active mode is extended. + AdvanceClockAndRunEventLoop(ctx, IcdManagementServer::GetInstance().GetActiveModeInterval() - 1); + ctx->mICDManager.UpdateOperationState(ICDManager::OperationalState::ActiveMode); + AdvanceClockAndRunEventLoop(ctx, IcdManagementServer::GetInstance().GetActiveModeThreshold() / 2); + NL_TEST_ASSERT(aSuite, ctx->mICDManager.mOperationalState == ICDManager::OperationalState::ActiveMode); + AdvanceClockAndRunEventLoop(ctx, IcdManagementServer::GetInstance().GetActiveModeThreshold()); + NL_TEST_ASSERT(aSuite, ctx->mICDManager.mOperationalState == ICDManager::OperationalState::IdleMode); + } + + static void TestKeepActivemodeRequests(nlTestSuite * aSuite, void * aContext) + { + TestContext * ctx = static_cast(aContext); + + // Setting a requirement will transition the ICD to active mode. + ctx->mICDManager.SetKeepActiveModeRequirements(ICDManager::KeepActiveFlags::kCommissioningWindowOpen, true); + NL_TEST_ASSERT(aSuite, ctx->mICDManager.mOperationalState == ICDManager::OperationalState::ActiveMode); + // Advance time so active mode interval expires. + AdvanceClockAndRunEventLoop(ctx, IcdManagementServer::GetInstance().GetActiveModeInterval() + 1); + // Requirement flag still set. We stay in active mode + NL_TEST_ASSERT(aSuite, ctx->mICDManager.mOperationalState == ICDManager::OperationalState::ActiveMode); + + // Remove requirement. we should directly transition to idle mode. + ctx->mICDManager.SetKeepActiveModeRequirements(ICDManager::KeepActiveFlags::kCommissioningWindowOpen, false); + NL_TEST_ASSERT(aSuite, ctx->mICDManager.mOperationalState == ICDManager::OperationalState::IdleMode); + + ctx->mICDManager.SetKeepActiveModeRequirements(ICDManager::KeepActiveFlags::kFailSafeArmed, true); + // Requirement will transition us to active mode. + NL_TEST_ASSERT(aSuite, ctx->mICDManager.mOperationalState == ICDManager::OperationalState::ActiveMode); + + // Advance time, but by less than the active mode interval and remove the requirement. + // We should stay in active mode. + AdvanceClockAndRunEventLoop(ctx, IcdManagementServer::GetInstance().GetActiveModeInterval() / 2); + ctx->mICDManager.SetKeepActiveModeRequirements(ICDManager::KeepActiveFlags::kFailSafeArmed, false); + NL_TEST_ASSERT(aSuite, ctx->mICDManager.mOperationalState == ICDManager::OperationalState::ActiveMode); + + // Advance time again, The activemode interval is completed. + AdvanceClockAndRunEventLoop(ctx, IcdManagementServer::GetInstance().GetActiveModeInterval() + 1); + NL_TEST_ASSERT(aSuite, ctx->mICDManager.mOperationalState == ICDManager::OperationalState::IdleMode); + + // Set two requirements + ctx->mICDManager.SetKeepActiveModeRequirements(ICDManager::KeepActiveFlags::kExpectingMsgResponse, true); + ctx->mICDManager.SetKeepActiveModeRequirements(ICDManager::KeepActiveFlags::kAwaitingMsgAck, true); + NL_TEST_ASSERT(aSuite, ctx->mICDManager.mOperationalState == ICDManager::OperationalState::ActiveMode); + // advance time so the active mode interval expires. + AdvanceClockAndRunEventLoop(ctx, IcdManagementServer::GetInstance().GetActiveModeInterval() + 1); + // A requirement flag is still set. We stay in active mode. + NL_TEST_ASSERT(aSuite, ctx->mICDManager.mOperationalState == ICDManager::OperationalState::ActiveMode); + + // remove 1 requirement. Active mode is maintained + ctx->mICDManager.SetKeepActiveModeRequirements(ICDManager::KeepActiveFlags::kExpectingMsgResponse, false); + NL_TEST_ASSERT(aSuite, ctx->mICDManager.mOperationalState == ICDManager::OperationalState::ActiveMode); + // remove the last requirement + ctx->mICDManager.SetKeepActiveModeRequirements(ICDManager::KeepActiveFlags::kAwaitingMsgAck, false); + NL_TEST_ASSERT(aSuite, ctx->mICDManager.mOperationalState == ICDManager::OperationalState::IdleMode); + } +}; + +} // namespace app +} // namespace chip + +namespace { +/** + * Test Suite. It lists all the test functions. + */ +// clang-format off +static const nlTest sTests[] = +{ + NL_TEST_DEF("TestICDModeIntervals", TestICDManager::TestICDModeIntervals), + NL_TEST_DEF("TestKeepActivemodeRequests", TestICDManager::TestKeepActivemodeRequests), + NL_TEST_SENTINEL() +}; +// clang-format on + +// clang-format off +nlTestSuite cmSuite = +{ + "TestICDManager", + &sTests[0], + TestContext::Initialize, + TestContext::Finalize +}; +// clang-format on +} // namespace + +int TestSuiteICDManager() +{ + return ExecuteTestsWithContext(&cmSuite); } -CHIP_REGISTER_TEST_SUITE(TestICDManager) +CHIP_REGISTER_TEST_SUITE(TestSuiteICDManager)