| /* |
| * Copyright (c) 2021 Project CHIP Authors |
| * Copyright (c) 2021 SmartThings |
| * 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 <lib/support/StateMachine.h> |
| #include <lib/support/UnitTestRegistration.h> |
| #include <lib/support/Variant.h> |
| #include <nlunit-test.h> |
| |
| namespace { |
| |
| struct Event1 |
| { |
| }; |
| struct Event2 |
| { |
| }; |
| struct Event3 |
| { |
| }; |
| struct Event4 |
| { |
| }; |
| struct Event5 |
| { |
| }; |
| |
| using Event = chip::Variant<Event1, Event2, Event3, Event4, Event5>; |
| using Context = chip::StateMachine::Context<Event>; |
| |
| struct MockState |
| { |
| unsigned mEntered; |
| unsigned mExited; |
| unsigned mLogged; |
| const char * mPrevious; |
| |
| void Enter() { ++mEntered; } |
| void Exit() { ++mExited; } |
| void LogTransition(const char * previous) |
| { |
| ++mLogged; |
| mPrevious = previous; |
| } |
| }; |
| |
| struct BaseState |
| { |
| void Enter() { mMock.Enter(); } |
| void Exit() { mMock.Exit(); } |
| void LogTransition(const char * previous) { mMock.LogTransition(previous); } |
| const char * GetName() { return mName; } |
| |
| Context * mCtx; |
| const char * mName; |
| MockState & mMock; |
| }; |
| |
| struct State1 : public BaseState |
| { |
| State1(Context * ctx, MockState & mock) : BaseState{ ctx, "State1", mock } {} |
| }; |
| |
| struct State2 : public BaseState |
| { |
| State2(Context * ctx, MockState & mock) : BaseState{ ctx, "State2", mock } {} |
| }; |
| |
| struct State3 : public BaseState |
| { |
| State3(Context * ctx, MockState & mock) : BaseState{ ctx, "State3", mock } {} |
| void Enter() |
| { |
| BaseState::Enter(); |
| if (this->mCtx) |
| { |
| this->mCtx->Dispatch(Event::Create<Event5>()); |
| } |
| } |
| }; |
| |
| // Place State3 first in the variant. This can evoke the behavior that |
| // TestNestedDispatch is looking for. |
| using State = chip::StateMachine::VariantState<State3, State2, State1>; |
| |
| struct StateFactory |
| { |
| Context * mCtx; |
| MockState ms1{ 0, 0, 0, nullptr }; |
| MockState ms2{ 0, 0, 0, nullptr }; |
| MockState ms3{ 0, 0, 0, nullptr }; |
| |
| StateFactory(Context * ctx) : mCtx(ctx) {} |
| |
| auto CreateState1() { return State::Create<State1>(mCtx, ms1); } |
| auto CreateState2() { return State::Create<State2>(mCtx, ms2); } |
| auto CreateState3() { return State::Create<State3>(mCtx, ms3); } |
| }; |
| |
| struct Transitions |
| { |
| Context * mCtx; |
| StateFactory mFactory; |
| Transitions(Context * ctx) : mCtx(ctx), mFactory(ctx) {} |
| |
| using OptState = chip::StateMachine::Optional<State>; |
| State GetInitState() { return mFactory.CreateState1(); } |
| OptState operator()(const State & state, const Event & event) |
| { |
| if (state.Is<State1>() && event.Is<Event2>()) |
| { |
| return mFactory.CreateState2(); |
| } |
| if (state.Is<State2>() && event.Is<Event1>()) |
| { |
| return mFactory.CreateState1(); |
| } |
| if (state.Is<State1>() && event.Is<Event4>()) |
| { |
| // legal - Dispatches event without transition |
| if (mCtx) |
| { |
| mCtx->Dispatch(Event::Create<Event2>()); |
| } |
| return {}; |
| } |
| if (state.Is<State2>() && event.Is<Event4>()) |
| { |
| // mCtx.Dispatch(Event::Create<Event2>()); // dispatching an event and returning a transition would be illegal |
| return mFactory.CreateState1(); |
| } |
| if (state.Is<State1>() && event.Is<Event5>()) |
| { |
| return mFactory.CreateState3(); |
| } |
| if (state.Is<State3>() && event.Is<Event5>()) |
| { |
| return mFactory.CreateState2(); |
| } |
| |
| return {}; |
| } |
| }; |
| |
| class SimpleStateMachine |
| { |
| public: |
| Transitions mTransitions; |
| chip::StateMachine::StateMachine<State, Event, Transitions> mStateMachine; |
| |
| SimpleStateMachine() : mTransitions(&mStateMachine), mStateMachine(mTransitions) {} |
| ~SimpleStateMachine() {} |
| }; |
| |
| void TestInit(nlTestSuite * inSuite, void * inContext) |
| { |
| // state machine initializes to State1 |
| SimpleStateMachine fsm; |
| NL_TEST_ASSERT(inSuite, fsm.mStateMachine.GetState().Is<State1>()); |
| } |
| |
| void TestIgnoredEvents(nlTestSuite * inSuite, void * inContext) |
| { |
| // in State1 - ignore Event1 and Event3 |
| SimpleStateMachine fsm; |
| fsm.mStateMachine.Dispatch(Event::Create<Event1>()); |
| NL_TEST_ASSERT(inSuite, fsm.mStateMachine.GetState().Is<State1>()); |
| fsm.mStateMachine.Dispatch(Event::Create<Event3>()); |
| NL_TEST_ASSERT(inSuite, fsm.mStateMachine.GetState().Is<State1>()); |
| // transition to State2 |
| fsm.mStateMachine.Dispatch(Event::Create<Event2>()); |
| NL_TEST_ASSERT(inSuite, fsm.mStateMachine.GetState().Is<State2>()); |
| // in State2 - ignore Event2 and Event3 |
| fsm.mStateMachine.Dispatch(Event::Create<Event2>()); |
| NL_TEST_ASSERT(inSuite, fsm.mStateMachine.GetState().Is<State2>()); |
| fsm.mStateMachine.Dispatch(Event::Create<Event3>()); |
| NL_TEST_ASSERT(inSuite, fsm.mStateMachine.GetState().Is<State2>()); |
| } |
| |
| void TestTransitions(nlTestSuite * inSuite, void * inContext) |
| { |
| // in State1 |
| SimpleStateMachine fsm; |
| // dispatch Event2 to transition to State2 |
| fsm.mStateMachine.Dispatch(Event::Create<Event2>()); |
| NL_TEST_ASSERT(inSuite, fsm.mStateMachine.GetState().Is<State2>()); |
| // dispatch Event1 to transition back to State1 |
| fsm.mStateMachine.Dispatch(Event::Create<Event1>()); |
| NL_TEST_ASSERT(inSuite, fsm.mStateMachine.GetState().Is<State1>()); |
| // dispatch Event2 to transition to State2 |
| fsm.mStateMachine.Dispatch(Event::Create<Event2>()); |
| NL_TEST_ASSERT(inSuite, fsm.mStateMachine.GetState().Is<State2>()); |
| // dispatch Event4 to transitions to State1. |
| fsm.mStateMachine.Dispatch(Event::Create<Event4>()); |
| NL_TEST_ASSERT(inSuite, fsm.mStateMachine.GetState().Is<State1>()); |
| } |
| |
| void TestTransitionsDispatch(nlTestSuite * inSuite, void * inContext) |
| { |
| // in State1 |
| SimpleStateMachine fsm; |
| // Dispatch Event4, which in turn dispatches Event2 from the transitions |
| // table and ultimately places us in State2. |
| fsm.mStateMachine.Dispatch(Event::Create<Event4>()); |
| NL_TEST_ASSERT(inSuite, fsm.mStateMachine.GetState().Is<State2>()); |
| } |
| |
| void TestNestedDispatch(nlTestSuite * inSuite, void * inContext) |
| { |
| // in State1 |
| SimpleStateMachine fsm; |
| // Dispatch Event5, which places us into State3, which will dispatch |
| // Event5 again from its Enter method to place us into State2. |
| fsm.mStateMachine.Dispatch(Event::Create<Event5>()); |
| NL_TEST_ASSERT(inSuite, fsm.mStateMachine.GetState().Is<State2>()); |
| // Make sure that Enter methods execute the correct number of times. |
| // This helps verify that pattern matching is working correctly. |
| // Specifically, we need to verify this case: State3 creates State2 |
| // by dispatching Event5 from its Enter method. This means that the |
| // Dispatch call from State3 also destructs State3. If the State3 |
| // Enter method pattern matching triggers Enter more than once, this |
| // is use-after-destruction. What can appear to happen is that the |
| // State2 Enter method will execute twice, as State2 will already have |
| // been constructed when the State3 Enter method executes a second |
| // time. The state machine pattern matching has code to explicitly |
| // prevent this double-execution. This is testing that. |
| NL_TEST_ASSERT(inSuite, fsm.mTransitions.mFactory.ms1.mEntered == 0); |
| NL_TEST_ASSERT(inSuite, fsm.mTransitions.mFactory.ms1.mExited == 1); |
| NL_TEST_ASSERT(inSuite, fsm.mTransitions.mFactory.ms1.mLogged == 0); |
| NL_TEST_ASSERT(inSuite, fsm.mTransitions.mFactory.ms2.mEntered == 1); |
| NL_TEST_ASSERT(inSuite, fsm.mTransitions.mFactory.ms2.mExited == 0); |
| NL_TEST_ASSERT(inSuite, fsm.mTransitions.mFactory.ms2.mLogged == 1); |
| NL_TEST_ASSERT(inSuite, fsm.mTransitions.mFactory.ms3.mEntered == 1); |
| NL_TEST_ASSERT(inSuite, fsm.mTransitions.mFactory.ms3.mExited == 1); |
| NL_TEST_ASSERT(inSuite, fsm.mTransitions.mFactory.ms3.mLogged == 1); |
| } |
| |
| void TestMethodExec(nlTestSuite * inSuite, void * inContext) |
| { |
| // in State1 |
| SimpleStateMachine fsm; |
| // transition to State2 |
| fsm.mStateMachine.Dispatch(Event::Create<Event2>()); |
| NL_TEST_ASSERT(inSuite, fsm.mStateMachine.GetState().Is<State2>()); |
| // verify expected method calls |
| NL_TEST_ASSERT(inSuite, fsm.mTransitions.mFactory.ms1.mEntered == 0); |
| NL_TEST_ASSERT(inSuite, fsm.mTransitions.mFactory.ms1.mExited == 1); |
| NL_TEST_ASSERT(inSuite, fsm.mTransitions.mFactory.ms1.mLogged == 0); |
| NL_TEST_ASSERT(inSuite, fsm.mTransitions.mFactory.ms1.mPrevious == nullptr); |
| NL_TEST_ASSERT(inSuite, fsm.mTransitions.mFactory.ms2.mEntered == 1); |
| NL_TEST_ASSERT(inSuite, fsm.mTransitions.mFactory.ms2.mExited == 0); |
| NL_TEST_ASSERT(inSuite, fsm.mTransitions.mFactory.ms2.mLogged == 1); |
| NL_TEST_ASSERT(inSuite, strcmp(fsm.mTransitions.mFactory.ms2.mPrevious, "State1") == 0); |
| // transition back to State1 |
| fsm.mStateMachine.Dispatch(Event::Create<Event1>()); |
| NL_TEST_ASSERT(inSuite, fsm.mStateMachine.GetState().Is<State1>()); |
| // verify expected method calls |
| NL_TEST_ASSERT(inSuite, fsm.mTransitions.mFactory.ms1.mEntered == 1); |
| NL_TEST_ASSERT(inSuite, fsm.mTransitions.mFactory.ms1.mExited == 1); |
| NL_TEST_ASSERT(inSuite, fsm.mTransitions.mFactory.ms1.mLogged == 1); |
| NL_TEST_ASSERT(inSuite, strcmp(fsm.mTransitions.mFactory.ms1.mPrevious, "State2") == 0); |
| NL_TEST_ASSERT(inSuite, fsm.mTransitions.mFactory.ms2.mEntered == 1); |
| NL_TEST_ASSERT(inSuite, fsm.mTransitions.mFactory.ms2.mExited == 1); |
| NL_TEST_ASSERT(inSuite, fsm.mTransitions.mFactory.ms2.mLogged == 1); |
| NL_TEST_ASSERT(inSuite, strcmp(fsm.mTransitions.mFactory.ms2.mPrevious, "State1") == 0); |
| } |
| |
| int Setup(void * inContext) |
| { |
| return SUCCESS; |
| } |
| |
| int Teardown(void * inContext) |
| { |
| return SUCCESS; |
| } |
| |
| } // namespace |
| |
| static const nlTest sTests[] = { |
| NL_TEST_DEF("TestInit", TestInit), |
| NL_TEST_DEF("TestIgnoredEvents", TestIgnoredEvents), |
| NL_TEST_DEF("TestTransitions", TestTransitions), |
| NL_TEST_DEF("TestTransitionsDispatch", TestTransitionsDispatch), |
| NL_TEST_DEF("TestNestedDispatch", TestNestedDispatch), |
| NL_TEST_DEF("TestMethodExec", TestMethodExec), |
| NL_TEST_SENTINEL(), |
| }; |
| |
| int StateMachineTestSuite() |
| { |
| nlTestSuite suite = { "CHIP State Machine tests", &sTests[0], Setup, Teardown }; |
| nlTestRunner(&suite, nullptr); |
| return nlTestRunnerStats(&suite); |
| } |
| |
| CHIP_REGISTER_TEST_SUITE(StateMachineTestSuite); |