From 844f126e8f187239ac3b98af0369436c554514b1 Mon Sep 17 00:00:00 2001 From: p-groarke Date: Sat, 25 Apr 2020 15:43:05 -0400 Subject: [PATCH] fsm : Builder, nicer argument template, finish state, better errors, bug fixes. * fsm : Add finish state, refactor to support member functions, cleaner api, fsm_builder. * fsm : Fix finished. * fsm : Add checks for invalid states. * fsm : Better checks. * Bump version to 1.1, fix assert and add nothrow tests. * Fixup nothrow tests. Fix a leaked throw. --- CMakeLists.txt | 2 +- include/fea_state_machines/fsm.hpp | 272 +++++++++++++++++------ tests/fsm.cpp | 79 ++++--- tests/fsm_nothrow.cpp | 346 +++++++++++++++++++++++++++++ 4 files changed, 592 insertions(+), 107 deletions(-) create mode 100644 tests/fsm_nothrow.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index d207622..f17de14 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ include(${CMAKE_CURRENT_SOURCE_DIR}/CMakeLists.conan.txt) cmake_minimum_required (VERSION 3.10) -project(fea_state_machines VERSION 1.0.0 LANGUAGES CXX) +project(fea_state_machines VERSION 1.1.0 LANGUAGES CXX) include(GNUInstallDirs) include(CMakePackageConfigHelpers) diff --git a/include/fea_state_machines/fsm.hpp b/include/fea_state_machines/fsm.hpp index bc911c0..2778eae 100644 --- a/include/fea_state_machines/fsm.hpp +++ b/include/fea_state_machines/fsm.hpp @@ -32,6 +32,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #pragma once #include #include +#include #include #include #include @@ -44,10 +45,11 @@ A small, fast and simple stack based fsm. Features : - OnEnter, OnUpdate, OnExit. - OnEnterFrom, OnExitTo. - Overrides event behavior when coming from/going to specified states. + Overrides event behavior when coming from/going to specified states or + transitions. - Supports user arguments in the callbacks (explained below). - - DelayedTrigger. - Trigger will happen next time you call fsm::update. + //- DelayedTrigger. // Removed till bug fix + // Trigger will happen next time you call fsm::update. - Define FEA_FSM_NOTHROW to assert instead of throw. - Does NOT provide a "get_current_state" function. Checking the current state of an fsm is a major smell and usually points @@ -55,17 +57,25 @@ Features : fsm. Do not do that, rethink your states and transitions instead. Callbacks : - - The first argument of your callback is always a ref to the fsm itself. + - The last argument of your callback is always a ref to the fsm itself. This is useful for retriggering and when you store fsms in containers. You can use auto& to simplify your callback signature. - [](auto& mymachine){} + [](your_args..., auto& mymachine){} - - Pass your own types at the end of the fsm and fsm_state template. + - Pass your event signature at the end of the fsm and fsm_state template. These will be passed on to your callbacks when you call update or trigger. - For example : fsm; + For example : + fsm; Callback signature is: - [](auto& machine, int, bool&, const void*){} + [](int, bool&, const void*, auto& machine){} + - The state machine is passed at the end to allow you to call member + functions directly. + For example : + fsm + Call your triggers passing the object pointer as the first argument, and + the member functions will be called. + machine.trigger(&obj, 42); Notes : @@ -92,26 +102,31 @@ enum class fsm_event : uint8_t { count, }; -template +template struct fsm; -template -struct fsm_state { - using fsm_t = fsm; - using fsm_func_t = std::function; +template +struct fsm_state; + +template +struct fsm_state { + using fsm_t = fsm; + using fsm_func_t = std::function; fsm_state() { std::fill(_transitions.begin(), _transitions.end(), StateEnum::count); } // Add your event implementation. - template + template void add_event(fsm_func_t&& func) { - if constexpr (Event == fsm_event::on_enter_from) { - std::get(_on_enter_from_funcs) = std::move(func); - } else if constexpr (Event == fsm_event::on_exit_to) { - std::get(_on_exit_to_funcs) = std::move(func); - } else if constexpr (Event == fsm_event::on_enter) { + static_assert(Event == fsm_event::on_enter + || Event == fsm_event::on_exit + || Event == fsm_event::on_update, + "add_event : wrong template resolution called"); + + if constexpr (Event == fsm_event::on_enter) { _on_enter_func = std::move(func); } else if constexpr (Event == fsm_event::on_update) { _on_update_func = std::move(func); @@ -120,6 +135,37 @@ struct fsm_state { } } + template + void add_event(fsm_func_t&& func) { + static_assert(Event == fsm_event::on_enter_from + || Event == fsm_event::on_exit_to, + "add_event : must use on_enter_from or on_exit_to when " + "custumizing on transition"); + + if constexpr (Event == fsm_event::on_enter_from) { + std::get(_on_enter_from_state_funcs) + = std::move(func); + } else if constexpr (Event == fsm_event::on_exit_to) { + std::get(_on_exit_to_state_funcs) = std::move(func); + } + } + + template + void add_event(fsm_func_t&& func) { + static_assert(Event == fsm_event::on_enter_from + || Event == fsm_event::on_exit_to, + "add_event : must use on_enter_from or on_exit_to when " + "custumizing on transition"); + + if constexpr (Event == fsm_event::on_enter_from) { + std::get(_on_enter_from_transition_funcs) + = std::move(func); + } else if constexpr (Event == fsm_event::on_exit_to) { + std::get(_on_exit_to_transition_funcs) + = std::move(func); + } + } + // Handle transition to a specified state. template void add_transition() { @@ -133,9 +179,10 @@ struct fsm_state { // transition. template StateEnum transition_target() const { -#if defined(FEA_FSM_NOTHROW) - assert(std::get(_transitions) == StateEnum::count); -#else + assert(std::get(_transitions) != StateEnum::count + && "fsm_state : unhandled transition"); + +#if !defined(FEA_FSM_NOTHROW) if (std::get(_transitions) == StateEnum::count) { throw std::invalid_argument{ "fsm_state : unhandled transition" }; } @@ -146,7 +193,8 @@ struct fsm_state { // Used internally, executes a specific event. template - void execute_event([[maybe_unused]] StateEnum to_from_state, fsm_t& machine, + auto execute_event([[maybe_unused]] StateEnum to_from_state, + [[maybe_unused]] TransitionEnum to_from_transition, fsm_t& machine, FuncArgs... func_args) { static_assert(Event != fsm_event::on_enter_from, "state : do not execute on_enter_from, use on_enter instead " @@ -161,28 +209,44 @@ struct fsm_state { // Check the event, call the appropriate user functions if it is stored. if constexpr (Event == fsm_event::on_enter) { if (to_from_state != StateEnum::count - && _on_enter_from_funcs[size_t(to_from_state)]) { - // has enter_from - std::invoke(_on_enter_from_funcs[size_t(to_from_state)], - machine, func_args...); + && _on_enter_from_state_funcs[size_t(to_from_state)]) { + // has enter_from state + std::invoke(_on_enter_from_state_funcs[size_t(to_from_state)], + func_args..., machine); + + } else if (to_from_transition != TransitionEnum::count + && _on_enter_from_transition_funcs[size_t( + to_from_transition)]) { + // has enter_from transition + std::invoke(_on_enter_from_transition_funcs[size_t( + to_from_transition)], + func_args..., machine); } else if (_on_enter_func) { - std::invoke(_on_enter_func, machine, func_args...); + std::invoke(_on_enter_func, func_args..., machine); } } else if constexpr (Event == fsm_event::on_update) { if (_on_update_func) { - std::invoke(_on_update_func, machine, func_args...); + return std::invoke(_on_update_func, func_args..., machine); } } else if constexpr (Event == fsm_event::on_exit) { if (to_from_state != StateEnum::count - && _on_exit_to_funcs[size_t(to_from_state)]) { + && _on_exit_to_state_funcs[size_t(to_from_state)]) { + // has exit_to + std::invoke(_on_exit_to_state_funcs[size_t(to_from_state)], + func_args..., machine); + + } else if (to_from_transition != TransitionEnum::count + && _on_exit_to_transition_funcs[size_t( + to_from_transition)]) { // has exit_to - std::invoke(_on_exit_to_funcs[size_t(to_from_state)], machine, - func_args...); + std::invoke(_on_exit_to_transition_funcs[size_t( + to_from_transition)], + func_args..., machine); } else if (_on_exit_func) { - std::invoke(_on_exit_func, machine, func_args...); + std::invoke(_on_exit_func, func_args..., machine); } } } @@ -193,17 +257,22 @@ struct fsm_state { fsm_func_t _on_update_func; fsm_func_t _on_exit_func; - std::array _on_enter_from_funcs; - std::array _on_exit_to_funcs; + std::array _on_enter_from_state_funcs; + std::array _on_exit_to_state_funcs; + + std::array + _on_enter_from_transition_funcs; + std::array + _on_exit_to_transition_funcs; // TBD, makes it heavy but helps debuggability // const char* _name; }; -template -struct fsm { - // using fsm_t = fsm; - using state_t = fsm_state; +template +struct fsm { + using state_t = fsm_state; using fsm_func_t = typename state_t::fsm_func_t; // Here, we use move semantics not for performance (it doesn't do anything). @@ -214,6 +283,7 @@ struct fsm { static_assert(State != StateEnum::count, "fsm : bad state"); std::get(_states) = std::move(state); + _state_valid[size_t(State)] = true; if (_default_state == StateEnum::count) { _default_state = State; @@ -223,27 +293,45 @@ struct fsm { // Set starting state. // By default, the first added state is used. template - void set_default_state() { + void set_start_state() { static_assert(State != StateEnum::count, "fsm : bad state"); _default_state = State; } - // First come first served. - // Trigger will be called next update(...). - // Calling this prevents subsequent triggers to be executed. - // Allows more relaxed trigger argument requirements. - template - void delayed_trigger() { - if (_has_delayed_trigger) - return; + template + void set_finish_state() { + static_assert(State != StateEnum::count, "fsm : bad state"); + _finish_state = State; + } + + bool finished() const { + if (_finish_state != StateEnum::count) { + return _finish_state == _current_state; + } + return false; + } - _has_delayed_trigger = true; - _delayed_trigger_func = [](fsm& f, FuncArgs... func_args) { - f._has_delayed_trigger = false; - f.trigger(func_args...); - }; + void reset() { + _current_state = StateEnum::count; } + // TODO : Fix retrigger on_exit. + //// First come first served. + //// Trigger will be called next update(...). + //// Calling this prevents subsequent triggers to be executed. + //// Allows more relaxed trigger argument requirements. + // template + // void delayed_trigger() { + // if (_has_delayed_trigger) + // return; + + // _has_delayed_trigger = true; + // _delayed_trigger_func = [](fsm& f, FuncArgs... func_args) { + // f._has_delayed_trigger = false; + // f.trigger(func_args...); + // }; + //} + // Trigger a transition. // Throws on bad transition (or asserts, if you defined FEA_FSM_NOTHROW). // If you had previously called delayed_trigger, this @@ -255,18 +343,20 @@ struct fsm { maybe_init(func_args...); - StateEnum from_state = _current_state; - StateEnum to_state = _states[size_t(_current_state)] - .template transition_target(); + StateEnum from_state_e = _current_state; + state_t& from_state = get_state(_current_state); + + StateEnum to_state_e + = from_state.template transition_target(); + state_t& to_state = get_state(to_state_e); // Only execute on_exit if we aren't in a trigger from on_exit. if (!_in_on_exit) { _in_on_exit = true; // Can recursively call trigger. We must handle that. - _states[size_t(from_state)] - .template execute_event( - to_state, *this, func_args...); + from_state.template execute_event( + to_state_e, Transition, *this, func_args...); if (_in_on_exit == false) { // Exit has triggered transition. Abort. @@ -275,26 +365,26 @@ struct fsm { } _in_on_exit = false; - _current_state = to_state; + _current_state = to_state_e; // Always execute on_enter. - _states[size_t(to_state)].template execute_event( - from_state, *this, func_args...); + to_state.template execute_event( + from_state_e, Transition, *this, func_args...); } // Update the fsm. // Calls on_update on the current state. // Processes delay_trigger if that was called. - void update(FuncArgs... func_args) { + FuncRet update(FuncArgs... func_args) { while (_has_delayed_trigger) { - std::invoke(_delayed_trigger_func, *this, func_args...); + std::invoke(_delayed_trigger_func, func_args..., *this); } maybe_init(func_args...); - _states[size_t(_current_state)] - .template execute_event( - StateEnum::count, *this, func_args...); + return get_state(_current_state) + .template execute_event(StateEnum::count, + TransitionEnum::count, *this, func_args...); } // Get the specified state. @@ -314,13 +404,42 @@ struct fsm { _current_state = _default_state; _states[size_t(_current_state)] - .template execute_event( - StateEnum::count, *this, func_args...); + .template execute_event(StateEnum::count, + TransitionEnum::count, *this, func_args...); + } + + const state_t& get_state(StateEnum s) const { + assert(s != StateEnum::count && "fsm : Accessing invalid state."); +#if !defined(FEA_FSM_NOTHROW) + if (s == StateEnum::count) { + throw std::runtime_error{ "fsm : Accessing invalid state." }; + } +#endif + + assert(_state_valid[size_t(s)] + && "fsm : Accessing invalid state, did you forget to add a " + "state?"); + +#if !defined(FEA_FSM_NOTHROW) + if (!_state_valid[size_t(s)]) { + throw std::runtime_error{ + "fsm : Accessing invalid state, did you forget to add a state?" + }; + } +#endif + + return _states[size_t(s)]; + } + state_t& get_state(StateEnum s) { + return const_cast( + static_cast(this)->get_state(s)); } std::array _states; + std::bitset _state_valid; StateEnum _current_state = StateEnum::count; StateEnum _default_state = StateEnum::count; + StateEnum _finish_state = StateEnum::count; bool _in_on_exit = false; @@ -328,4 +447,19 @@ struct fsm { bool _has_delayed_trigger = false; }; +template +struct fsm_builder; + +template +struct fsm_builder { + static constexpr auto make_state() { + return fsm_state{}; + } + + static constexpr fsm + make_machine() { + return fsm{}; + } +}; } // namespace fea diff --git a/tests/fsm.cpp b/tests/fsm.cpp index 6c02ead..bf31ef7 100644 --- a/tests/fsm.cpp +++ b/tests/fsm.cpp @@ -22,27 +22,28 @@ TEST(fsm, example) { enum class transition { do_walk, do_run, do_jump, count }; // Used for callbacks - using machine_t = fea::fsm; + using machine_t = fea::fsm; // Create your state machine. - fea::fsm machine; + fea::fsm_builder builder; + auto machine = builder.make_machine(); // Create your states. // Walk { - fea::fsm_state walk_state; + auto walk_state = builder.make_state(); // Add allowed transitions. walk_state.add_transition(); // Add state events. walk_state.add_event( - [](machine_t& /*machine*/, test_data& t) { + [](test_data& t, machine_t& /*machine*/) { t.walk_enter = true; ++t.num_onenter_calls; }); walk_state.add_event( - [](machine_t& /*machine*/, test_data& t) { + [](test_data& t, machine_t& /*machine*/) { t.walk_update = true; ++t.num_onupdate_calls; }); @@ -52,19 +53,19 @@ TEST(fsm, example) { // Run { - fea::fsm_state run_state; + auto run_state = builder.make_state(); run_state.add_transition(); run_state.add_transition(); run_state.add_event( - [](machine_t& machine, test_data& t) { + [](test_data& t, machine_t& machine) { ++t.num_onenterfrom_calls; // This is OK. machine.trigger(t); }); run_state.add_event( - [](machine_t&, test_data& t) { ++t.num_onupdate_calls; }); + [](test_data& t, machine_t&) { ++t.num_onupdate_calls; }); run_state.add_event( - [](machine_t& machine, test_data& t) { + [](test_data& t, machine_t& machine) { ++t.num_onexit_calls; // This is also OK, though probably not recommended from a @@ -76,15 +77,15 @@ TEST(fsm, example) { // Jump { - fea::fsm_state jump_state; + auto jump_state = builder.make_state(); jump_state.add_transition(); jump_state.add_transition(); jump_state.add_event( - [](machine_t&, test_data& t) { ++t.num_onenterfrom_calls; }); + [](test_data& t, machine_t&) { ++t.num_onenterfrom_calls; }); jump_state.add_event( - [](machine_t&, test_data& t) { ++t.num_onexitto_calls; }); + [](test_data& t, machine_t&) { ++t.num_onexitto_calls; }); machine.add_state(std::move(jump_state)); } @@ -101,8 +102,12 @@ TEST(fsm, example) { EXPECT_EQ(mtest_data.num_onexitto_calls, 0u); // Currently doesn't handle walk to jump transition. +#if !defined(NDEBUG) + EXPECT_DEATH(machine.trigger(mtest_data), ""); +#else EXPECT_THROW(machine.trigger(mtest_data), std::invalid_argument); +#endif // Go to jump. machine.state() @@ -182,53 +187,53 @@ TEST(fsm, basics) { size_t on_exits = 0; bool inpute = false; - fea::fsm machine; + fea::fsm machine; - fea::fsm_state walk_state; - walk_state.add_event([&](auto&, bool& b) { + fea::fsm_state walk_state; + walk_state.add_event([&](bool& b, auto&) { b = true; ++on_enters; }); - walk_state.add_event([&](auto&, bool& b) { + walk_state.add_event([&](bool& b, auto&) { b = true; ++on_updates; machine.trigger(b); }); - walk_state.add_event([&](auto&, bool& b) { + walk_state.add_event([&](bool& b, auto&) { b = true; ++on_exits; }); walk_state.add_transition(); machine.add_state(std::move(walk_state)); - fea::fsm_state run_state; - run_state.add_event([&](auto&, bool& b) { + fea::fsm_state run_state; + run_state.add_event([&](bool& b, auto&) { b = true; ++on_enters; }); - run_state.add_event([&](auto&, bool& b) { + run_state.add_event([&](bool& b, auto&) { b = true; ++on_updates; machine.trigger(b); }); - run_state.add_event([&](auto&, bool& b) { + run_state.add_event([&](bool& b, auto&) { b = true; ++on_exits; }); run_state.add_transition(); machine.add_state(std::move(run_state)); - fea::fsm_state jump_state; - jump_state.add_event([&](auto&, bool& b) { + fea::fsm_state jump_state; + jump_state.add_event([&](bool& b, auto&) { b = true; ++on_enters; }); - jump_state.add_event([&](auto&, bool& b) { + jump_state.add_event([&](bool& b, auto&) { b = true; ++on_updates; machine.trigger(b); }); - jump_state.add_event([&](auto&, bool& b) { + jump_state.add_event([&](bool& b, auto&) { b = true; ++on_exits; }); @@ -260,15 +265,15 @@ TEST(fsm, event_triggering) { enum class transition { do_walk, do_run, do_jump, count }; // Used for callbacks - using machine_t = fea::fsm; + using machine_t = fea::fsm; // Create your state machine. - fea::fsm machine; + fea::fsm machine; // Create your states. // Walk { - fea::fsm_state walk_state; + fea::fsm_state walk_state; // Add allowed transitions. walk_state.add_transition(); @@ -276,18 +281,18 @@ TEST(fsm, event_triggering) { // Add state events. walk_state.add_event( - [](machine_t& machine, test_data& t) { + [](test_data& t, machine_t& machine) { ++t.num_onenter_calls; machine.trigger(t); }); walk_state.add_event( - [](machine_t&, test_data& t) { + [](test_data& t, machine_t&) { ++t.num_onenterfrom_calls; // Should finish here. // machine.trigger(t); }); walk_state.add_event( - [](machine_t& machine, test_data& t) { + [](test_data& t, machine_t& machine) { ++t.num_onexitto_calls; machine.trigger(t); }); @@ -296,16 +301,16 @@ TEST(fsm, event_triggering) { // Run { - fea::fsm_state run_state; + fea::fsm_state run_state; run_state.add_transition(); run_state.add_transition(); run_state.add_event( - [](machine_t& machine, test_data& t) { + [](test_data& t, machine_t& machine) { ++t.num_onenterfrom_calls; machine.trigger(t); }); run_state.add_event( - [](machine_t& machine, test_data& t) { + [](test_data& t, machine_t& machine) { ++t.num_onexitto_calls; machine.trigger(t); }); @@ -314,18 +319,18 @@ TEST(fsm, event_triggering) { // Jump { - fea::fsm_state jump_state; + fea::fsm_state jump_state; jump_state.add_transition(); jump_state.add_transition(); jump_state.add_event( - [](machine_t& m, test_data& t) { + [](test_data& t, machine_t& m) { ++t.num_onenterfrom_calls; m.trigger(t); }); jump_state.add_event( - [](machine_t& m, test_data& t) { + [](test_data& t, machine_t& m) { ++t.num_onexitto_calls; m.trigger(t); }); diff --git a/tests/fsm_nothrow.cpp b/tests/fsm_nothrow.cpp new file mode 100644 index 0000000..22f9701 --- /dev/null +++ b/tests/fsm_nothrow.cpp @@ -0,0 +1,346 @@ +#define FEA_FSM_NOTHROW +#include +#include + + +namespace { + +TEST(fsm_nothrow, example) { + struct test_data { + bool walk_enter = false; + bool walk_update = false; + + size_t num_onenterfrom_calls = 0; + size_t num_onenter_calls = 0; + size_t num_onupdate_calls = 0; + size_t num_onexit_calls = 0; + size_t num_onexitto_calls = 0; + }; + test_data mtest_data; + + // Create your enums. They MUST end with 'count'. + enum class state { walk, run, jump, count }; + enum class transition { do_walk, do_run, do_jump, count }; + + // Used for callbacks + using machine_t = fea::fsm; + + // Create your state machine. + fea::fsm_builder builder; + auto machine = builder.make_machine(); + + // Create your states. + // Walk + { + auto walk_state = builder.make_state(); + + // Add allowed transitions. + walk_state.add_transition(); + + // Add state events. + walk_state.add_event( + [](test_data& t, machine_t& /*machine*/) { + t.walk_enter = true; + ++t.num_onenter_calls; + }); + walk_state.add_event( + [](test_data& t, machine_t& /*machine*/) { + t.walk_update = true; + ++t.num_onupdate_calls; + }); + + machine.add_state(std::move(walk_state)); + } + + // Run + { + auto run_state = builder.make_state(); + run_state.add_transition(); + run_state.add_transition(); + run_state.add_event( + [](test_data& t, machine_t& machine) { + ++t.num_onenterfrom_calls; + // This is OK. + machine.trigger(t); + }); + run_state.add_event( + [](test_data& t, machine_t&) { ++t.num_onupdate_calls; }); + run_state.add_event( + [](test_data& t, machine_t& machine) { + ++t.num_onexit_calls; + + // This is also OK, though probably not recommended from a + // "design" standpoint. + machine.trigger(t); + }); + machine.add_state(std::move(run_state)); + } + + // Jump + { + auto jump_state = builder.make_state(); + jump_state.add_transition(); + jump_state.add_transition(); + + jump_state.add_event( + [](test_data& t, machine_t&) { ++t.num_onenterfrom_calls; }); + + jump_state.add_event( + [](test_data& t, machine_t&) { ++t.num_onexitto_calls; }); + + machine.add_state(std::move(jump_state)); + } + + + // Init and update default state (walk). + machine.update(mtest_data); + EXPECT_TRUE(mtest_data.walk_enter); + EXPECT_TRUE(mtest_data.walk_update); + EXPECT_EQ(mtest_data.num_onenterfrom_calls, 0u); + EXPECT_EQ(mtest_data.num_onenter_calls, 1u); + EXPECT_EQ(mtest_data.num_onupdate_calls, 1u); + EXPECT_EQ(mtest_data.num_onexit_calls, 0u); + EXPECT_EQ(mtest_data.num_onexitto_calls, 0u); + + // Currently doesn't handle walk to jump transition. +#if !defined(NDEBUG) + EXPECT_DEATH(machine.trigger(mtest_data), ""); +#endif + + // Go to jump. + machine.state() + .add_transition(); + EXPECT_NO_THROW(machine.trigger(mtest_data)); + + // Nothing should have changed. + EXPECT_EQ(mtest_data.num_onenterfrom_calls, 0u); + EXPECT_EQ(mtest_data.num_onenter_calls, 1u); + EXPECT_EQ(mtest_data.num_onupdate_calls, 1u); + EXPECT_EQ(mtest_data.num_onexit_calls, 0u); + EXPECT_EQ(mtest_data.num_onexitto_calls, 0u); + + // Go back to walk. + machine.trigger(mtest_data); + + // Should get on exit to walk + on enter walk. + EXPECT_EQ(mtest_data.num_onenterfrom_calls, 0u); + EXPECT_EQ(mtest_data.num_onenter_calls, 2u); + EXPECT_EQ(mtest_data.num_onupdate_calls, 1u); + EXPECT_EQ(mtest_data.num_onexit_calls, 0u); + EXPECT_EQ(mtest_data.num_onexitto_calls, 1u); + + // Update walk. + machine.update(mtest_data); + EXPECT_EQ(mtest_data.num_onenterfrom_calls, 0u); + EXPECT_EQ(mtest_data.num_onenter_calls, 2u); + EXPECT_EQ(mtest_data.num_onupdate_calls, 2u); + EXPECT_EQ(mtest_data.num_onexit_calls, 0u); + EXPECT_EQ(mtest_data.num_onexitto_calls, 1u); + + // Test retrigger in on_enter and in on_exit. + machine.trigger(mtest_data); + // run on_enter_from -> run on_exit -> jump on_enter_from + EXPECT_EQ(mtest_data.num_onenterfrom_calls, 2u); + EXPECT_EQ(mtest_data.num_onenter_calls, 2u); + EXPECT_EQ(mtest_data.num_onupdate_calls, 2u); + EXPECT_EQ(mtest_data.num_onexit_calls, 1u); + EXPECT_EQ(mtest_data.num_onexitto_calls, 1u); + + // Does nothing, no jump update. + machine.update(mtest_data); + machine.update(mtest_data); + machine.update(mtest_data); + EXPECT_EQ(mtest_data.num_onenterfrom_calls, 2u); + EXPECT_EQ(mtest_data.num_onenter_calls, 2u); + EXPECT_EQ(mtest_data.num_onupdate_calls, 2u); + EXPECT_EQ(mtest_data.num_onexit_calls, 1u); + EXPECT_EQ(mtest_data.num_onexitto_calls, 1u); + + // And back to walk. + machine.trigger(mtest_data); + EXPECT_EQ(mtest_data.num_onenterfrom_calls, 2u); + EXPECT_EQ(mtest_data.num_onenter_calls, 3u); + EXPECT_EQ(mtest_data.num_onupdate_calls, 2u); + EXPECT_EQ(mtest_data.num_onexit_calls, 1u); + EXPECT_EQ(mtest_data.num_onexitto_calls, 2u); +} + +TEST(fsm_nothrow, basics) { + enum class state { + walk, + run, + jump, + count, + }; + + enum class transition { + do_walk, + do_run, + do_jump, + count, + }; + + size_t on_enters = 0; + size_t on_updates = 0; + size_t on_exits = 0; + bool inpute = false; + + fea::fsm machine; + + fea::fsm_state walk_state; + walk_state.add_event([&](bool& b, auto&) { + b = true; + ++on_enters; + }); + walk_state.add_event([&](bool& b, auto&) { + b = true; + ++on_updates; + machine.trigger(b); + }); + walk_state.add_event([&](bool& b, auto&) { + b = true; + ++on_exits; + }); + walk_state.add_transition(); + machine.add_state(std::move(walk_state)); + + fea::fsm_state run_state; + run_state.add_event([&](bool& b, auto&) { + b = true; + ++on_enters; + }); + run_state.add_event([&](bool& b, auto&) { + b = true; + ++on_updates; + machine.trigger(b); + }); + run_state.add_event([&](bool& b, auto&) { + b = true; + ++on_exits; + }); + run_state.add_transition(); + machine.add_state(std::move(run_state)); + + fea::fsm_state jump_state; + jump_state.add_event([&](bool& b, auto&) { + b = true; + ++on_enters; + }); + jump_state.add_event([&](bool& b, auto&) { + b = true; + ++on_updates; + machine.trigger(b); + }); + jump_state.add_event([&](bool& b, auto&) { + b = true; + ++on_exits; + }); + jump_state.add_transition(); + machine.add_state(std::move(jump_state)); + + machine.update(inpute); + machine.update(inpute); + machine.update(inpute); + + EXPECT_TRUE(inpute); + EXPECT_EQ(on_enters, 4u); + EXPECT_EQ(on_updates, 3u); + EXPECT_EQ(on_exits, 3u); +} + +TEST(fsm_nothrow, event_triggering) { + struct test_data { + size_t num_onenterfrom_calls = 0; + size_t num_onenter_calls = 0; + size_t num_onupdate_calls = 0; + size_t num_onexit_calls = 0; + size_t num_onexitto_calls = 0; + }; + test_data mtest_data; + + // Create your enums. They MUST end with 'count'. + enum class state { walk, run, jump, count }; + enum class transition { do_walk, do_run, do_jump, count }; + + // Used for callbacks + using machine_t = fea::fsm; + + // Create your state machine. + fea::fsm machine; + + // Create your states. + // Walk + { + fea::fsm_state walk_state; + + // Add allowed transitions. + walk_state.add_transition(); + walk_state.add_transition(); + + // Add state events. + walk_state.add_event( + [](test_data& t, machine_t& machine) { + ++t.num_onenter_calls; + machine.trigger(t); + }); + walk_state.add_event( + [](test_data& t, machine_t&) { + ++t.num_onenterfrom_calls; + // Should finish here. + // machine.trigger(t); + }); + walk_state.add_event( + [](test_data& t, machine_t& machine) { + ++t.num_onexitto_calls; + machine.trigger(t); + }); + machine.add_state(std::move(walk_state)); + } + + // Run + { + fea::fsm_state run_state; + run_state.add_transition(); + run_state.add_transition(); + run_state.add_event( + [](test_data& t, machine_t& machine) { + ++t.num_onenterfrom_calls; + machine.trigger(t); + }); + run_state.add_event( + [](test_data& t, machine_t& machine) { + ++t.num_onexitto_calls; + machine.trigger(t); + }); + machine.add_state(std::move(run_state)); + } + + // Jump + { + fea::fsm_state jump_state; + jump_state.add_transition(); + jump_state.add_transition(); + + jump_state.add_event( + [](test_data& t, machine_t& m) { + ++t.num_onenterfrom_calls; + m.trigger(t); + }); + + jump_state.add_event( + [](test_data& t, machine_t& m) { + ++t.num_onexitto_calls; + m.trigger(t); + }); + + machine.add_state(std::move(jump_state)); + } + + machine.update(mtest_data); + EXPECT_EQ(mtest_data.num_onenterfrom_calls, 3u); + EXPECT_EQ(mtest_data.num_onenter_calls, 1u); + EXPECT_EQ(mtest_data.num_onupdate_calls, 0u); + EXPECT_EQ(mtest_data.num_onexit_calls, 0u); + EXPECT_EQ(mtest_data.num_onexitto_calls, 3u); +} +} // namespace