diff --git a/Core/include/Acts/Utilities/FiniteStateMachine.hpp b/Core/include/Acts/Utilities/FiniteStateMachine.hpp new file mode 100644 index 0000000000000000000000000000000000000000..91c857860fed8dcfd30fdafd9c77042b814a4c40 --- /dev/null +++ b/Core/include/Acts/Utilities/FiniteStateMachine.hpp @@ -0,0 +1,234 @@ +// This file is part of the Acts project. +// +// Copyright (C) 2019 CERN for the benefit of the Acts project +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#pragma once + +#include <optional> +#include <string_view> +#include <variant> + +#include "Acts/Utilities/TypeTraits.hpp" + +namespace Acts { + +/// Implementation of a finite state machine engine +/// +/// Allows setting up a system of states and transitions between them. States +/// are definedd as empty structs (footprint: 1 byte). Tranitions call functions +/// using overload resolution. This works by subclassing this class, providing +/// the deriving type as the first template argument (CRTP) and providing +/// methods like +/// +/// ```cpp +/// event_return on_event(const S&, const E&); +/// ``` +/// +/// The arguments are the state `S` and the triggered event `E`. Their values +/// can be discarded (you can attach values to events of course, if you like) +/// The return type of these functions is effectively `std::optional<State>`, so +/// you can either return `std::nullopt` to remain in the same state, or an +/// instance of another state. That state will then become active. +/// +/// You can also define a method template, which will serve as a catch-all +/// handler (due to the fact that it will match any state/event combination): +/// +/// ```cpp +/// template <typename State, typename Event> +/// event_return on_event(const State&, const Event&) const { +/// return Terminated{}; +/// } +/// ``` +/// +/// If for a given state and event no suitable overload of `on_event` (and you +/// also haven't defined a catch-all as described above), a transition to +/// `Terminated` will be triggered. This is essentially equivalent to the method +/// template above. +/// +/// If this triggers, it will switch to the `Terminated` state (which is always +/// included in the FSM). +/// +/// Additionally, the FSM will attempt to call functions like +/// ```cpp +/// void on_enter(const State&); +/// void on_exit(const State&); +/// ``` +/// when entering/exiting a state. This can be used to +/// perform actions regardless of the source or destination state in a +/// transition to a given state. This is also fired in case a transition to +/// `Terminated` occurs. +/// +/// The base class also calls +/// ```cpp +/// void on_process(const Event&); +/// void on_process(const State&, const Event&); +/// void on_process(const State1& const Event&, const State2&); +/// ``` +/// during event processing, and allow for things like event and +/// transition logging. +/// +/// The `on_event`, `on_enter`, `on_exit` and `on_process` methods need to be +/// implemented exhaustively, i.e. for all state/event combinations. This might +/// require you to add catch-all no-op functions like +/// ```cpp +/// template <typename...Args> +/// event_return on_event(Args&&...args) {} // noop +/// ``` +/// and so on. +/// +/// The public interface for the user of the FSM are the +/// ```cpp +/// template <typename... Args> +/// void setState(StateVariant state, Args&&... args); +/// +/// template <typename Event, typename... Args> +/// void dispatch(Event&& event, Args&&... args) { +/// ``` +/// +/// `setState` triggers a transition to a given state, `dispatch` triggers +/// processing on an event from the given state. Both will call the appropriate +/// `on_exit` and `on_enter` overloads. Both also accept an arbitrary number of +/// additional arguments that are passed to the `on_event`, `on_exit` and +/// `on_enter` overloads. +/// +/// @tparam Derived Class deriving from the FSM +/// @tparam States Argument pack with the state types that the FSM can be +/// handled. +template <typename Derived, typename... States> +class FiniteStateMachine { + public: + /// Contractual termination state. Is transitioned to if State+Event do not + /// have a transition defined. + struct Terminated { + /// Name of this state (useful for logging) + constexpr static std::string_view name = "Terminated"; + }; + + /// Variant type allowing tagged type erased storage of the current state of + /// the FSM. + using StateVariant = std::variant<Terminated, States...>; + + protected: + using fsm_base = FiniteStateMachine<Derived, States...>; + + using event_return = std::optional<StateVariant>; + + public: + /// Default constructor. The default state is taken to be the first in the + /// `States` template arguments + FiniteStateMachine() + : m_state( + typename std::tuple_element<0, std::tuple<States...>>::type{}){}; + + /// Constructor from an explicit state. The FSM is initialized to this state. + /// @param state Initial state for the FSM. + FiniteStateMachine(StateVariant state) : m_state(std::move(state)){}; + + /// Get the current state of of the FSM (as a variant). + /// @return StateVariant The current state of the FSM. + const StateVariant& getState() const noexcept { return m_state; } + + public: + /// Sets the state to a given one. Triggers `on_exit` and `on_enter` for the + /// given states. + /// @tparam State Type of the target state + /// @tparam Args Additional arguments passed through callback overloads. + /// @param state Instance of the target state + /// @param args The additional arguments + template <typename State, typename... Args> + void setState(State state, Args&&... args) { + Derived& child = static_cast<Derived&>(*this); + + // call on exit function + std::visit([&](auto& s) { child.on_exit(s, std::forward<Args>(args)...); }, + m_state); + + m_state = std::move(state); + + // call on enter function, the type is known from the template argument. + child.on_enter(std::get<State>(m_state), std::forward<Args>(args)...); + } + + /// Returns whether the FSM is in the specified state + /// @tparam State type to check against + /// @param state State instance to check against + /// @return Whether the FSM is in the given state. + template <typename S> + bool is(const S& /*state*/) const noexcept { + return is<S>(); + } + + /// Returns whether the FSM is in the specified state. Alternative version + /// directly taking only the template argument. + /// @tparam State type to check against + /// @return Whether the FSM is in the given state. + template <typename S> + bool is() const noexcept { + if (std::get_if<S>(&m_state)) { + return true; + } + return false; + } + + /// Returns whether the FSM is in the terminated state. + /// @return Whether the FSM is in the terminated state. + bool terminated() const noexcept { return is<Terminated>(); } + + protected: + /// Handles processing of an event. + /// @note This should only be called from inside the class Deriving from FSM. + /// @tparam Event Type of the event being processed + /// @tparam Args Arguments being passed to the overload handlers. + /// @param event Instance of the event + /// @param args Additional arguments + /// @return Variant state type, signifying if a transition is supposed to + /// happen. + template <typename Event, typename... Args> + event_return process_event(Event&& event, Args&&... args) { + Derived& child = static_cast<Derived&>(*this); + + child.on_process(event); + + auto new_state = std::visit( + [&](auto& s) -> std::optional<StateVariant> { + auto s2 = child.on_event(s, std::forward<Event>(event), + std::forward<Args>(args)...); + + if (s2) { + std::visit([&](auto& s2_) { child.on_process(s, event, s2_); }, + *s2); + } else { + child.on_process(s, event); + } + return std::move(s2); + }, + m_state); + return std::move(new_state); + } + + public: + /// Public interface to handle an event. Will call the appropriate event + /// handlers and perform any required transitions. + /// @tparam Event Type of the event being triggered + /// @tparam Args Additional arguments being passed to overload handlers. + /// @param event Instance of the event being triggere + /// @param args Additional arguments + template <typename Event, typename... Args> + void dispatch(Event&& event, Args&&... args) { + auto new_state = process_event(std::forward<Event>(event), args...); + if (new_state) { + std::visit( + [&](auto& s) { setState(std::move(s), std::forward<Args>(args)...); }, + *new_state); + } + } + + private: + StateVariant m_state; +}; + +} // namespace Acts diff --git a/Tests/Core/Utilities/CMakeLists.txt b/Tests/Core/Utilities/CMakeLists.txt index e28b06f13b7b61e0a5360b87b36a161c22fc234d..93c74c243bd0f8e53f1839249b0a4b73dc1f129f 100644 --- a/Tests/Core/Utilities/CMakeLists.txt +++ b/Tests/Core/Utilities/CMakeLists.txt @@ -97,3 +97,7 @@ target_include_directories (AnnealingUtilityTests PRIVATE ${Boost_INCLUDE_DIRS}) target_link_libraries (AnnealingUtilityTests PRIVATE ActsCore ActsTestsCommonHelpers ${Boost_UNIT_TEST_FRAMEWORK_LIBRARY}) add_test (NAME VertexAnnealingToolUnitTests COMMAND AnnealingUtilityTests) acts_add_test_to_cdash_project (PROJECT ACore TEST VertexAnnealingToolUnitTests TARGETS AnnealingUtilityTests) + +add_executable (FiniteStateMachineTests FiniteStateMachineTests.cpp) +target_link_libraries (FiniteStateMachineTests PRIVATE ActsCore ActsTestsCommonHelpers ${Boost_UNIT_TEST_FRAMEWORK_LIBRARY}) +add_test (NAME FiniteStateMachineTests COMMAND FiniteStateMachineTests) diff --git a/Tests/Core/Utilities/FiniteStateMachineTests.cpp b/Tests/Core/Utilities/FiniteStateMachineTests.cpp new file mode 100644 index 0000000000000000000000000000000000000000..2b68b072487181a679b447f362b56ce546ad5172 --- /dev/null +++ b/Tests/Core/Utilities/FiniteStateMachineTests.cpp @@ -0,0 +1,291 @@ +// This file is part of the Acts project. +// +// Copyright (C) 2019 CERN for the benefit of the Acts project +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// clang-format off +#define BOOST_TEST_MODULE FSM Tests +#define BOOST_TEST_DYN_LINK +#include <boost/test/unit_test.hpp> +// clang-format on + +#include "Acts/Utilities/FiniteStateMachine.hpp" + +#include <iostream> + +namespace tt = boost::test_tools; + +namespace Acts { + +namespace Test { + +namespace states { +struct Disconnected {}; + +struct Connecting {}; +struct Pinging {}; +struct Connected {}; +} // namespace states + +namespace events { +struct Connect {}; +struct Established {}; +struct Timeout {}; +struct Ping {}; +struct Pong {}; +struct Disconnect {}; +} // namespace events + +struct fsm : FiniteStateMachine<fsm, states::Disconnected, states::Connecting, + states::Pinging, states::Connected> { + fsm() : fsm_base(states::Disconnected{}){}; + + event_return on_event(const states::Disconnected&, const events::Connect&) { + return states::Connecting{}; + } + + event_return on_event(const states::Connecting&, const events::Established&) { + return states::Connected{}; + } + + event_return on_event(const states::Connected&, const events::Ping&) { + std::cout << "ping!" << std::endl; + setState(states::Pinging{}); + return process_event(events::Pong{}); + } + + event_return on_event(const states::Pinging&, const events::Pong&) { + std::cout << "pong!" << std::endl; + return states::Connected{}; + } + + event_return on_event(const states::Connected&, const events::Timeout&) { + return states::Connecting{}; + } + + event_return on_event(const states::Connected&, const events::Disconnect&) { + return states::Disconnected{}; + } + + template <typename State, typename Event> + event_return on_event(const State&, const Event&) const { + return Terminated{}; + } + + template <typename State, typename... Args> + void on_enter(const State&, Args&&...) {} + + template <typename State, typename... Args> + void on_exit(const State&, Args&&...) {} + + template <typename... Args> + void on_process(Args&&...) {} +}; + +BOOST_AUTO_TEST_SUITE(Utilities) + +BOOST_AUTO_TEST_CASE(Transitions) { + fsm sm{}; + BOOST_CHECK(sm.is(states::Disconnected{})); + sm.dispatch(events::Connect{}); + BOOST_CHECK(sm.is(states::Connecting{})); + sm.dispatch(events::Established{}); + BOOST_CHECK(sm.is(states::Connected{})); + sm.dispatch(events::Ping{}); + sm.dispatch(events::Ping{}); + sm.dispatch(events::Ping{}); + sm.dispatch(events::Ping{}); + BOOST_CHECK(sm.is(states::Connected{})); + sm.dispatch(events::Timeout{}); + BOOST_CHECK(sm.is(states::Connecting{})); + sm.dispatch(events::Established{}); + BOOST_CHECK(sm.is(states::Connected{})); + sm.dispatch(events::Disconnect{}); + BOOST_CHECK(sm.is(states::Disconnected{})); +} + +BOOST_AUTO_TEST_CASE(Terminted) { + fsm sm{}; + BOOST_CHECK(sm.is(states::Disconnected{})); + + sm.dispatch(events::Disconnect{}); + BOOST_CHECK(sm.terminated()); +} + +struct fsm2 + : FiniteStateMachine<fsm2, states::Disconnected, states::Connected> { + fsm2() : fsm_base(states::Disconnected{}){}; + + event_return on_event(const states::Disconnected&, const events::Connect&, + double f) { + std::cout << "f: " << f << std::endl; + return states::Connected{}; + } + + event_return on_event(const states::Connected&, const events::Disconnect&) { + std::cout << "disconnect!" << std::endl; + return states::Disconnected{}; + } + + template <typename State, typename Event, typename... Args> + event_return on_event(const State&, const Event&, Args&&...) const { + return Terminated{}; + } + + template <typename... Args> + void on_enter(const Terminated&, Args&&...) { + throw std::runtime_error("FSM terminated!"); + } + + template <typename State, typename... Args> + void on_enter(const State&, Args&&...) {} + + template <typename State, typename... Args> + void on_exit(const State&, Args&&...) {} + template <typename... Args> + void on_process(Args&&...) {} +}; + +BOOST_AUTO_TEST_CASE(Arguments) { + fsm2 sm{}; + BOOST_CHECK(sm.is(states::Disconnected{})); + + sm.dispatch(events::Connect{}, 42.); + BOOST_CHECK(sm.is(states::Connected{})); + sm.dispatch(events::Disconnect{}); + BOOST_CHECK(sm.is(states::Disconnected{})); + sm.dispatch(events::Connect{}, -1.); + + // call disconnect, but disconnect does not accept this call signature + BOOST_REQUIRE_THROW(sm.dispatch(events::Disconnect{}, 9), std::runtime_error); + BOOST_CHECK(sm.terminated()); + + // cannot dispatch on terminated (in this specific configuration, in + // general terminated is just another state). + BOOST_REQUIRE_THROW(sm.dispatch(events::Connect{}), std::runtime_error); + // still in terminated + BOOST_CHECK(sm.terminated()); + + // we can reset the state though! + sm.setState(states::Disconnected{}); + BOOST_CHECK(sm.is(states::Disconnected{})); + sm.dispatch(events::Connect{}, -1.); + BOOST_CHECK(sm.is(states::Connected{})); +} + +struct S1 {}; +struct S2 {}; +struct S3 {}; + +struct E1 {}; +struct E2 {}; +struct E3 {}; + +struct fsm3 : FiniteStateMachine<fsm3, S1, S2, S3> { + bool on_exit_called = false; + bool on_enter_called = false; + bool on_process_called = false; + void reset() { + on_exit_called = false; + on_enter_called = false; + on_process_called = false; + } + + // S1 + E1 = S2 + event_return on_event(const S1&, const E1&) { return S2{}; } + + // S2 + E1 = S2 + // external transition to self + event_return on_event(const S2&, const E1&) { return S2{}; } + + // S2 + E2 + // internal transition + event_return on_event(const S2&, const E2&) { + return std::nullopt; + // return S2{}; + } + + // S2 + E3 = S3 + // external transition + event_return on_event(const S2&, const E3&) { return S3{}; } + + // catchers + + template <typename State, typename Event, typename... Args> + event_return on_event(const State&, const Event&, Args&&...) const { + return Terminated{}; + } + + template <typename State, typename... Args> + void on_enter(const State&, Args&&...) { + on_enter_called = true; + } + + template <typename State, typename... Args> + void on_exit(const State&, Args&&...) { + on_exit_called = true; + } + + template <typename... Args> + void on_process(Args&&...) { + on_process_called = true; + } +}; + +BOOST_AUTO_TEST_CASE(InternalTransitions) { + fsm3 sm; + BOOST_CHECK(sm.is(S1{})); + + sm.dispatch(E1{}); + BOOST_CHECK(sm.is(S2{})); + BOOST_CHECK(sm.on_exit_called); + BOOST_CHECK(sm.on_enter_called); + BOOST_CHECK(sm.on_process_called); + + sm.reset(); + + sm.dispatch(E1{}); + // still in S2 + BOOST_CHECK(sm.is(S2{})); + // on_enter / exit should have been called + BOOST_CHECK(sm.on_exit_called); + BOOST_CHECK(sm.on_enter_called); + BOOST_CHECK(sm.on_process_called); + sm.reset(); + + sm.dispatch(E2{}); + // still in S2 + BOOST_CHECK(sm.is(S2{})); + // on_enter / exit should NOT have been called + BOOST_CHECK(!sm.on_exit_called); + BOOST_CHECK(!sm.on_enter_called); + BOOST_CHECK(sm.on_process_called); + sm.reset(); + + sm.dispatch(E3{}); + BOOST_CHECK(sm.is(S3{})); + // on_enter / exit should have been called + BOOST_CHECK(sm.on_exit_called); + BOOST_CHECK(sm.on_enter_called); + BOOST_CHECK(sm.on_process_called); + + sm.setState(S1{}); + sm.reset(); + BOOST_CHECK(sm.is(S1{})); + // dispatch invalid event + sm.dispatch(E3{}); + // should be terminated now + BOOST_CHECK(sm.terminated()); + // hooks should have fired + BOOST_CHECK(sm.on_exit_called); + BOOST_CHECK(sm.on_enter_called); + BOOST_CHECK(sm.on_process_called); +} + +BOOST_AUTO_TEST_SUITE_END() + +} // namespace Test +} // namespace Acts