Jelajahi Sumber

added process and scheduler

Michele Caini 8 tahun lalu
induk
melakukan
cf6022866d

+ 335 - 0
src/entt/process/process.hpp

@@ -0,0 +1,335 @@
+#ifndef ENTT_PROCESS_PROCESS_HPP
+#define ENTT_PROCESS_PROCESS_HPP
+
+
+#include <type_traits>
+#include <functional>
+#include <utility>
+
+
+namespace entt {
+
+
+namespace {
+
+
+struct BaseProcess {
+    enum class State: unsigned int {
+        UNINITIALIZED = 0,
+        RUNNING,
+        PAUSED,
+        SUCCEEDED,
+        FAILED,
+        ABORTED,
+        FINISHED
+    };
+
+    template<State state>
+    using tag = std::integral_constant<State, state>;
+};
+
+
+}
+
+
+/**
+ * @brief Base class for processes.
+ *
+ * This class stays true to the CRTP idiom. Derived classes must specify what's
+ * the intended type for elapsed times.<br/>
+ * A process should expose publicly the following member functions whether
+ * required:
+ *
+ * * @code{.cpp}
+ *   void update(Delta);
+ *   @endcode
+ *   It's invoked once per tick until a process is explicitly aborted or it
+ *   terminates either with or without errors. Even though it's not mandatory to
+ *   declare this member function, as a rule of thumb each process should at
+ *   least define it to work properly.
+ *
+ * * @code{.cpp}
+ *   void init();
+ *   @endcode
+ *   It's invoked at the first tick, immediately before an update.
+ *
+ * * @code{.cpp}
+ *   void succeeded();
+ *   @endcode
+ *   It's invoked in case of success, immediately after an update and during the
+ *   same tick.
+ *
+ * * @code{.cpp}
+ *   void failed();
+ *   @endcode
+ *   It's invoked in case of errors, immediately after an update and during the
+ *   same tick.
+ *
+ * * @code{.cpp}
+ *   void aborted();
+ *   @endcode
+ *   It's invoked only if a process is explicitly aborted. There is no guarantee
+ *   that it executes in the same tick, this depends solely on whether the
+ *   process is aborted immediately or not.
+ *
+ * Derived classes can change the internal state of a process by invoking the
+ * `succeed` and `fail` protected member functions and even pause or unpause the
+ * process itself.
+ *
+ * @sa Scheduler
+ *
+ * @tparam Derived Actual type of process that extends the class template.
+ * @tparam Delta Type to use to provide elapsed time.
+ */
+template<typename Derived, typename Delta>
+class Process: private BaseProcess {
+    template<typename Target = Derived>
+    auto tick(int, tag<State::UNINITIALIZED>)
+    -> decltype(std::declval<Target>().init()) {
+        static_cast<Target *>(this)->init();
+    }
+
+    template<typename Target = Derived>
+    auto tick(int, tag<State::RUNNING>, Delta delta)
+    -> decltype(std::declval<Target>().update(delta)) {
+        static_cast<Target *>(this)->update(delta);
+    }
+
+    template<typename Target = Derived>
+    auto tick(int, tag<State::SUCCEEDED>)
+    -> decltype(std::declval<Target>().succeeded()) {
+        static_cast<Target *>(this)->succeeded();
+    }
+
+    template<typename Target = Derived>
+    auto tick(int, tag<State::FAILED>)
+    -> decltype(std::declval<Target>().failed()) {
+        static_cast<Target *>(this)->failed();
+    }
+
+    template<typename Target = Derived>
+    auto tick(int, tag<State::ABORTED>)
+    -> decltype(std::declval<Target>().aborted()) {
+        static_cast<Target *>(this)->aborted();
+    }
+
+    template<State S, typename... Args>
+    void tick(char, tag<S>, Args&&...) {}
+
+protected:
+    /**
+     * @brief Terminates a process with success if it's still alive.
+     *
+     * The function is idempotent and it does nothing if the process isn't
+     * alive.
+     */
+    void succeed() noexcept {
+        if(alive()) {
+            current = State::SUCCEEDED;
+        }
+    }
+
+    /**
+     * @brief Terminates a process with errors if it's still alive.
+     *
+     * The function is idempotent and it does nothing if the process isn't
+     * alive.
+     */
+    void fail() noexcept {
+        if(alive()) {
+            current = State::FAILED;
+        }
+    }
+
+    /**
+     * @brief Stops a process if it's in a running state.
+     *
+     * The function is idempotent and it does nothing if the process isn't
+     * running.
+     */
+    void pause() noexcept {
+        if(current == State::RUNNING) {
+            current = State::PAUSED;
+        }
+    }
+
+    /**
+     * @brief Restarts a process if it's paused.
+     *
+     * The function is idempotent and it does nothing if the process isn't
+     * paused.
+     */
+    void unpause() noexcept {
+        if(current  == State::PAUSED) {
+            current  = State::RUNNING;
+        }
+    }
+
+public:
+    /*! @brief Type used to provide elapsed time. */
+    using delta_type = Delta;
+
+    /*! @brief Default destructor. */
+    ~Process() noexcept {
+        static_assert(std::is_base_of<Process, Derived>::value, "!");
+    }
+
+    /**
+     * @brief Aborts a process if it's still alive.
+     *
+     * The function is idempotent and it does nothing if the process isn't
+     * alive.
+     *
+     * @param immediately Requests an immediate operation.
+     */
+    void abort(bool immediately = false) noexcept {
+        if(alive()) {
+            current = State::ABORTED;
+
+            if(immediately) {
+                tick(0);
+            }
+        }
+    }
+
+    /**
+     * @brief Returns true if a process is either running or paused.
+     * @return True if the process is still alive, false otherwise.
+     */
+    bool alive() const noexcept {
+        return current == State::RUNNING || current == State::PAUSED;
+    }
+
+    /**
+     * @brief Returns true if a process is already terminated.
+     * @return True if the process is terminated, false otherwise.
+     */
+    bool dead() const noexcept {
+        return current == State::FINISHED;
+    }
+
+    /**
+     * @brief Returns true if a process is currently paused.
+     * @return True if the process is paused, false otherwise.
+     */
+    bool paused() const noexcept {
+        return current == State::PAUSED;
+    }
+
+    /**
+     * @brief Returns true if a process terminated with errors.
+     * @return True if the process terminated with errors, false otherwise.
+     */
+    bool rejected() const noexcept {
+        return stopped;
+    }
+
+    /**
+     * @brief Updates a process and its internal state if required.
+     * @param delta Elapsed time.
+     */
+    void tick(Delta delta) {
+        switch (current) {
+        case State::UNINITIALIZED:
+            tick(0, tag<State::UNINITIALIZED>{});
+            current = State::RUNNING;
+            // no break on purpose, tasks are executed immediately
+        case State::RUNNING:
+            tick(0, tag<State::RUNNING>{}, delta);
+        default:
+            // suppress warnings
+            break;
+        }
+
+        // if it's dead, it must be notified and removed immediately
+        switch(current) {
+        case State::SUCCEEDED:
+            tick(0, tag<State::SUCCEEDED>{});
+            current = State::FINISHED;
+            break;
+        case State::FAILED:
+            tick(0, tag<State::FAILED>{});
+            current = State::FINISHED;
+            stopped = true;
+            break;
+        case State::ABORTED:
+            tick(0, tag<State::ABORTED>{});
+            current = State::FINISHED;
+            stopped = true;
+            break;
+        default:
+            // suppress warnings
+            break;
+        }
+    }
+
+private:
+    State current{State::UNINITIALIZED};
+    bool stopped{false};
+};
+
+
+/**
+ * @brief Adaptor for lambdas and functors to turn them into processes.
+ *
+ * Lambdas and functors can't be used directly with a scheduler for they are not
+ * properly defined processes with managed life cycles.<br/>
+ * This class helps in filling the gap and turning lambdas and functors into
+ * full featured processes usable by a scheduler.
+ *
+ * The signature of the function call operator should be equivalent to the
+ * following:
+ *
+ * @code{.cpp}
+ * void(Delta delta, auto succeed, auto fail);
+ * @endcode
+ *
+ * Where:
+ *
+ * * `delta` is the elapsed time.
+ * * `succeed` is a function to call when a process terminates with success.
+ * * `fail` is a function to call when a process terminates with errors.
+ *
+ * The signature of the function call operator of both `succeed` and `fail`
+ * is equivalent to the following:
+ *
+ * @code{.cpp}
+ * void();
+ * @endcode
+ *
+ * Usually users shouldn't worry about creating adaptors. A scheduler will
+ * create them internally each and avery time a lambda or a functor is used as
+ * a process.
+ *
+ * @sa Process
+ * @sa Scheduler
+ *
+ * @tparam Func Actual type of process.
+ * @tparam Delta Type to use to provide elapsed time.
+ */
+template<typename Func, typename Delta>
+struct ProcessAdaptor: Process<ProcessAdaptor<Func, Delta>, Delta>, private Func {
+    /**
+     * @brief Constructs a process adaptor from a lambda or a functor.
+     * @tparam Args Types of arguments to use to initialize the actual process.
+     * @param args Parameters to use to initialize the actual process.
+     */
+    template<typename... Args>
+    ProcessAdaptor(Args&&... args)
+        : Func{std::forward<Args>(args)...}
+    {}
+
+    /**
+     * @brief Updates a process and its internal state if required.
+     * @param delta Elapsed time.
+     */
+    void update(Delta delta) {
+        Func::operator()(delta, [this](){ this->succeed(); }, [this](){ this->fail(); });
+    }
+};
+
+
+}
+
+
+#endif // ENTT_PROCESS_PROCESS_HPP

+ 319 - 0
src/entt/process/scheduler.hpp

@@ -0,0 +1,319 @@
+#ifndef ENTT_PROCESS_SCHEDULER_HPP
+#define ENTT_PROCESS_SCHEDULER_HPP
+
+
+#include <vector>
+#include <memory>
+#include <utility>
+#include <iterator>
+#include <algorithm>
+#include <type_traits>
+#include "process.hpp"
+
+
+namespace entt {
+
+
+/**
+ * @brief Cooperative scheduler for processes.
+ *
+ * A cooperative scheduler runs processes and helps managing their life cycles.
+ *
+ * Each process is invoked once per tick. If a process terminates, it's
+ * removed automatically from the scheduler and it's never invoked again.<br/>
+ * A process can also have a child. In this case, the process is replaced with
+ * its child when it terminates if it returns with success. In case of errors,
+ * both the process and its child are discarded.
+ *
+ * Example of use (pseudocode):
+ *
+ * @code{.cpp}
+ * scheduler.attach([](auto delta, auto succeed, auto fail) {
+ *     // code
+ * }).then<MyProcess>(arguments...);
+ * @endcode
+ *
+ * In order to invoke all scheduled processes, call the `update` member function
+ * passing it the elapsed time to forward to the tasks.
+ *
+ * @sa Process
+ *
+ * @tparam Delta Type to use to provide elapsed time.
+ */
+template<typename Delta>
+class Scheduler final {
+    template<typename T>
+    struct tag { using type = T; };
+
+    struct ProcessHandler final {
+        using instance_type = std::unique_ptr<void, void(*)(void *)>;
+        using update_type = bool(*)(ProcessHandler &, Delta);
+        using abort_type = void(*)(ProcessHandler &, bool);
+        using next_type = std::unique_ptr<ProcessHandler>;
+
+        instance_type instance;
+        update_type update;
+        abort_type abort;
+        next_type next;
+    };
+
+    template<typename Lambda>
+    struct Then final: Lambda {
+        Then(Lambda &&lambda, ProcessHandler *handler)
+            : Lambda{std::forward<Lambda>(lambda)}, handler{handler}
+        {}
+
+        template<typename Proc, typename... Args>
+        decltype(auto) then(Args&&... args) && {
+            static_assert(std::is_base_of<Process<Proc, Delta>, Proc>::value, "!");
+            handler = Lambda::operator()(handler, tag<Proc>{}, std::forward<Args>(args)...);
+            return std::move(*this);
+        }
+
+        template<typename Func>
+        decltype(auto) then(Func &&func) && {
+            using Proc = ProcessAdaptor<std::decay_t<Func>, Delta>;
+            return std::move(*this).template then<Proc>(std::forward<Func>(func));
+        }
+
+    private:
+        ProcessHandler *handler;
+    };
+
+    template<typename Proc>
+    static bool update(ProcessHandler &handler, Delta delta) {
+        auto *process = static_cast<Proc *>(handler.instance.get());
+        process->tick(delta);
+
+        auto dead = process->dead();
+
+        if(dead) {
+            if(handler.next && !process->rejected()) {
+                handler = std::move(*handler.next);
+                dead = handler.update(handler, delta);
+            } else {
+                handler.instance.reset();
+            }
+        }
+
+        return dead;
+    }
+
+    template<typename Proc>
+    static void abort(ProcessHandler &handler, bool immediately) {
+        static_cast<Proc *>(handler.instance.get())->abort(immediately);
+    }
+
+    template<typename Proc>
+    static void deleter(void *proc) {
+        delete static_cast<Proc *>(proc);
+    }
+
+    auto then(ProcessHandler *handler) {
+        auto lambda = [this](ProcessHandler *handler, auto next, auto... args) {
+            using Proc = typename decltype(next)::type;
+
+            if(handler) {
+                auto proc = typename ProcessHandler::instance_type{ new Proc{std::forward<decltype(args)>(args)...}, &deleter<Proc> };
+                handler->next.reset(new ProcessHandler{std::move(proc), &this->update<Proc>, &this->abort<Proc>, nullptr});
+                handler = handler->next.get();
+            }
+
+            return handler;
+        };
+
+        return Then<decltype(lambda)>{std::move(lambda), handler};
+    }
+
+public:
+    /*! @brief Unsigned integer type. */
+    using size_type = typename std::vector<ProcessHandler>::size_type;
+
+    /*! @brief Default constructor. */
+    Scheduler() noexcept= default;
+
+    /*! @brief Copying a scheduler isn't allowed. */
+    Scheduler(const Scheduler &) = delete;
+    /*! @brief Default move constructor. */
+    Scheduler(Scheduler &&) = default;
+
+    /*! @brief Copying a scheduler isn't allowed. @return This scheduler. */
+    Scheduler & operator=(const Scheduler &) = delete;
+    /*! @brief Default move assignament operator. @return This scheduler. */
+    Scheduler & operator=(Scheduler &&) = default;
+
+    /**
+     * @brief Number of processes currently scheduled.
+     * @return Number of processes currently scheduled.
+     */
+    size_type size() const noexcept {
+        return handlers.size();
+    }
+
+    /**
+     * @brief Returns true if at least a process is currently scheduled.
+     * @return True if there are scheduled processes, false otherwise.
+     */
+    bool empty() const noexcept {
+        return handlers.empty();
+    }
+
+    /**
+     * @brief Discards all scheduled processes.
+     *
+     * Processes aren't aborted. They are discarded along with their children
+     * and never executed again.
+     */
+    void clear() {
+        handlers.clear();
+    }
+
+    /**
+     * @brief Schedules a process for the next tick.
+     *
+     * Returned value is an opaque object that can be used to attach a child to
+     * the given process. The child is automatically scheduled when the process
+     * terminates and only if the process returns with success.
+     *
+     * Example of use (pseudocode):
+     *
+     * @code{.cpp}
+     * // schedules a task in the form of a process class
+     * scheduler.attach<MyProcess>(arguments...)
+     * // appends a child in the form of a lambda function
+     * .then([](auto delta, auto succeed, auto fail) {
+     *     // code
+     * })
+     * // appends a child in the form of another process class
+     * .then<MyOtherProcess>();
+     * @endcode
+     *
+     * @tparam Proc Type of process to schedule.
+     * @tparam Args Types of arguments to use to initialize the process.
+     * @param args Parameters to use to initialize the process.
+     * @return An opaque object to use to concatenate processes.
+     */
+    template<typename Proc, typename... Args>
+    auto attach(Args&&... args) {
+        static_assert(std::is_base_of<Process<Proc, Delta>, Proc>::value, "!");
+
+        auto proc = typename ProcessHandler::instance_type{ new Proc{std::forward<Args>(args)...}, &deleter<Proc> };
+        ProcessHandler handler{std::move(proc), &update<Proc>, &abort<Proc>, nullptr};
+        handlers.push_back(std::move(handler));
+
+        return then(&handlers.back());
+    }
+
+    /**
+     * @brief Schedules a process for the next tick.
+     *
+     * A process can be either a lambda or a functor. The scheduler wraps both
+     * of them in a process adaptor internally.<br/>
+     * The signature of the function call operator should be equivalent to the
+     * following:
+     *
+     * @code{.cpp}
+     * void(Delta delta, auto succeed, auto fail);
+     * @endcode
+     *
+     * Where:
+     *
+     * * `delta` is the elapsed time.
+     * * `succeed` is a function to call when a process terminates with success.
+     * * `fail` is a function to call when a process terminates with errors.
+     *
+     * The signature of the function call operator of both `succeed` and `fail`
+     * is equivalent to the following:
+     *
+     * @code{.cpp}
+     * void();
+     * @endcode
+     *
+     * Returned value is an opaque object that can be used to attach a child to
+     * the given process. The child is automatically scheduled when the process
+     * terminates and only if the process returns with success.
+     *
+     * Example of use (pseudocode):
+     *
+     * @code{.cpp}
+     * // schedules a task in the form of a lambda function
+     * scheduler.attach([](auto delta, auto succeed, auto fail) {
+     *     // code
+     * })
+     * // appends a child in the form of another lambda function
+     * .then([](auto delta, auto succeed, auto fail) {
+     *     // code
+     * })
+     * // appends a child in the form of a process class
+     * .then<MyProcess>(arguments...);
+     * @endcode
+     *
+     * @sa ProcessAdaptor
+     *
+     * @tparam Func Type of process to schedule.
+     * @param func Either a lambda or a functor to use as a process.
+     * @return An opaque object to use to concatenate processes.
+     */
+    template<typename Func>
+    auto attach(Func &&func) {
+        using Proc = ProcessAdaptor<std::decay_t<Func>, Delta>;
+        return attach<Proc>(std::forward<Func>(func));
+    }
+
+    /**
+     * @brief Updates all scheduled processes.
+     *
+     * All scheduled processes are executed in no specific order.<br/>
+     * If a process terminates with success, it's replaced with its child, if
+     * any. Otherwise, if a process terminates with an error, it's removed along
+     * with its child.
+     *
+     * @param delta Elapsed time.
+     */
+    void update(Delta delta) {
+        bool clean = false;
+
+        for(auto i = handlers.size(); i > 0; --i) {
+            auto &handler = handlers[i-1];
+            const bool dead = handler.update(handler, delta);
+            clean = clean || dead;
+        }
+
+        if(clean) {
+            handlers.erase(std::remove_if(handlers.begin(), handlers.end(), [delta](auto &handler) {
+                return !handler.instance;
+            }), handlers.end());
+        }
+    }
+
+    /**
+     * @brief Aborts all scheduled processes.
+     *
+     * Unless an immediate operation is requested, the abort is scheduled for
+     * the next tick. Processes won't be executed anymore in any case.<br/>
+     * Once a process is fully aborted and thus finished, it's discarded along
+     * with its child if any.
+     *
+     * @param immediately Requests an immediate operation.
+     */
+    void abort(bool immediately = false) {
+        decltype(handlers) exec;
+        exec.swap(handlers);
+
+        std::for_each(exec.begin(), exec.end(), [immediately](auto &handler) {
+            handler.abort(handler, immediately);
+        });
+
+        std::move(handlers.begin(), handlers.end(), std::back_inserter(exec));
+        handlers.swap(exec);
+    }
+
+private:
+    std::vector<ProcessHandler> handlers{};
+};
+
+
+}
+
+
+#endif // ENTT_PROCESS_SCHEDULER_HPP

+ 162 - 0
test/entt/process/process.cpp

@@ -0,0 +1,162 @@
+#include <gtest/gtest.h>
+#include <entt/process/process.hpp>
+
+struct FakeProcess: entt::Process<FakeProcess, int> {
+    using process_type = entt::Process<FakeProcess, int>;
+
+    void succeed() noexcept { process_type::succeed(); }
+    void fail() noexcept { process_type::fail(); }
+    void pause() noexcept { process_type::pause(); }
+    void unpause() noexcept { process_type::unpause(); }
+
+    void init() { initInvoked = true; }
+    void update(delta_type) { updateInvoked = true; }
+    void succeeded() { succeededInvoked = true; }
+    void failed() { failedInvoked = true; }
+    void aborted() { abortedInvoked = true; }
+
+    bool initInvoked{false};
+    bool updateInvoked{false};
+    bool succeededInvoked{false};
+    bool failedInvoked{false};
+    bool abortedInvoked{false};
+};
+
+TEST(Process, Basics) {
+    FakeProcess process;
+
+    ASSERT_FALSE(process.alive());
+    ASSERT_FALSE(process.dead());
+    ASSERT_FALSE(process.paused());
+
+    process.succeed();
+    process.fail();
+    process.abort();
+    process.pause();
+    process.unpause();
+
+    ASSERT_FALSE(process.alive());
+    ASSERT_FALSE(process.dead());
+    ASSERT_FALSE(process.paused());
+
+    process.tick(0);
+
+    ASSERT_TRUE(process.alive());
+    ASSERT_FALSE(process.dead());
+    ASSERT_FALSE(process.paused());
+
+    process.pause();
+
+    ASSERT_TRUE(process.alive());
+    ASSERT_FALSE(process.dead());
+    ASSERT_TRUE(process.paused());
+
+    process.unpause();
+
+    ASSERT_TRUE(process.alive());
+    ASSERT_FALSE(process.dead());
+    ASSERT_FALSE(process.paused());
+}
+
+TEST(Process, Succeeded) {
+    FakeProcess process;
+
+    process.tick(0);
+    process.succeed();
+    process.tick(0);
+
+    ASSERT_FALSE(process.alive());
+    ASSERT_TRUE(process.dead());
+    ASSERT_FALSE(process.paused());
+
+    ASSERT_TRUE(process.initInvoked);
+    ASSERT_TRUE(process.updateInvoked);
+    ASSERT_TRUE(process.succeededInvoked);
+    ASSERT_FALSE(process.failedInvoked);
+    ASSERT_FALSE(process.abortedInvoked);
+}
+
+TEST(Process, Fail) {
+    FakeProcess process;
+
+    process.tick(0);
+    process.fail();
+    process.tick(0);
+
+    ASSERT_FALSE(process.alive());
+    ASSERT_TRUE(process.dead());
+    ASSERT_FALSE(process.paused());
+
+    ASSERT_TRUE(process.initInvoked);
+    ASSERT_TRUE(process.updateInvoked);
+    ASSERT_FALSE(process.succeededInvoked);
+    ASSERT_TRUE(process.failedInvoked);
+    ASSERT_FALSE(process.abortedInvoked);
+}
+
+TEST(Process, AbortNextTick) {
+    FakeProcess process;
+
+    process.tick(0);
+    process.abort();
+    process.tick(0);
+
+    ASSERT_FALSE(process.alive());
+    ASSERT_TRUE(process.dead());
+    ASSERT_FALSE(process.paused());
+
+    ASSERT_TRUE(process.initInvoked);
+    ASSERT_TRUE(process.updateInvoked);
+    ASSERT_FALSE(process.succeededInvoked);
+    ASSERT_FALSE(process.failedInvoked);
+    ASSERT_TRUE(process.abortedInvoked);
+}
+
+TEST(Process, AbortImmediately) {
+    FakeProcess process;
+
+    process.tick(0);
+    process.abort(true);
+
+    ASSERT_FALSE(process.alive());
+    ASSERT_TRUE(process.dead());
+    ASSERT_FALSE(process.paused());
+
+    ASSERT_TRUE(process.initInvoked);
+    ASSERT_TRUE(process.updateInvoked);
+    ASSERT_FALSE(process.succeededInvoked);
+    ASSERT_FALSE(process.failedInvoked);
+    ASSERT_TRUE(process.abortedInvoked);
+}
+
+TEST(ProcessAdaptor, Resolved) {
+    bool updated = false;
+    auto lambda = [&updated](uint64_t, auto resolve, auto) {
+        ASSERT_FALSE(updated);
+        updated = true;
+        resolve();
+    };
+
+    auto process = entt::ProcessAdaptor<decltype(lambda), uint64_t>{lambda};
+
+    process.tick(0);
+
+    ASSERT_TRUE(process.dead());
+    ASSERT_TRUE(updated);
+}
+
+TEST(ProcessAdaptor, Rejected) {
+    bool updated = false;
+    auto lambda = [&updated](uint64_t, auto, auto rejected) {
+        ASSERT_FALSE(updated);
+        updated = true;
+        rejected();
+    };
+
+    auto process = entt::ProcessAdaptor<decltype(lambda), uint64_t>{lambda};
+
+    process.tick(0);
+
+    ASSERT_TRUE(process.rejected());
+    ASSERT_TRUE(updated);
+}

+ 113 - 0
test/entt/process/scheduler.cpp

@@ -0,0 +1,113 @@
+#include <functional>
+#include <gtest/gtest.h>
+#include <entt/process/scheduler.hpp>
+#include <entt/process/process.hpp>
+
+struct FooProcess: entt::Process<FooProcess, int> {
+    FooProcess(std::function<void()> onUpdate, std::function<void()> onAborted)
+        : onUpdate{onUpdate}, onAborted{onAborted}
+    {}
+
+    void update(delta_type) { onUpdate(); }
+    void aborted() { onAborted(); }
+
+    std::function<void()> onUpdate;
+    std::function<void()> onAborted;
+};
+
+struct SucceededProcess: entt::Process<SucceededProcess, int> {
+    void update(delta_type) {
+        ASSERT_FALSE(updated);
+        updated = true;
+        ++invoked;
+        succeed();
+    }
+
+    static unsigned int invoked;
+    bool updated = false;
+};
+
+unsigned int SucceededProcess::invoked = 0;
+
+struct FailedProcess: entt::Process<FailedProcess, int> {
+    void update(delta_type) {
+        ASSERT_FALSE(updated);
+        updated = true;
+        fail();
+    }
+
+    bool updated = false;
+};
+
+TEST(Scheduler, Functionalities) {
+    entt::Scheduler<int> scheduler{};
+
+    bool updated = false;
+    bool aborted = false;
+
+    ASSERT_EQ(scheduler.size(), entt::Scheduler<int>::size_type{});
+    ASSERT_TRUE(scheduler.empty());
+
+    scheduler.attach<FooProcess>(
+                [&updated](){ updated = true; },
+                [&aborted](){ aborted = true; }
+    );
+
+    ASSERT_NE(scheduler.size(), entt::Scheduler<int>::size_type{});
+    ASSERT_FALSE(scheduler.empty());
+
+    scheduler.update(0);
+    scheduler.abort(true);
+
+    ASSERT_TRUE(updated);
+    ASSERT_TRUE(aborted);
+
+    ASSERT_NE(scheduler.size(), entt::Scheduler<int>::size_type{});
+    ASSERT_FALSE(scheduler.empty());
+
+    scheduler.clear();
+
+    ASSERT_EQ(scheduler.size(), entt::Scheduler<int>::size_type{});
+    ASSERT_TRUE(scheduler.empty());
+}
+
+TEST(Scheduler, Then) {
+    entt::Scheduler<int> scheduler;
+
+    scheduler.attach<SucceededProcess>()
+            .then<SucceededProcess>()
+            .then<FailedProcess>()
+            .then<SucceededProcess>();
+
+    for(auto i = 0; i < 8; ++i) {
+        scheduler.update(0);
+    }
+
+    ASSERT_EQ(SucceededProcess::invoked, 2u);
+}
+
+TEST(Scheduler, Functor) {
+    entt::Scheduler<int> scheduler;
+
+    bool firstFunctor = false;
+    bool secondFunctor = false;
+
+    scheduler.attach([&firstFunctor](auto, auto resolve, auto){
+        ASSERT_FALSE(firstFunctor);
+        firstFunctor = true;
+        resolve();
+    }).then([&secondFunctor](auto, auto, auto reject){
+        ASSERT_FALSE(secondFunctor);
+        secondFunctor = true;
+        reject();
+    }).then([](auto...){
+        FAIL();
+    });
+
+    for(auto i = 0; i < 8; ++i) {
+        scheduler.update(0);
+    }
+
+    ASSERT_TRUE(firstFunctor);
+    ASSERT_TRUE(secondFunctor);
+}