From 3350a93a13d6841e8220eba098287af15ef1781b Mon Sep 17 00:00:00 2001 From: Trent Houliston Date: Sat, 26 Oct 2024 14:28:12 +1100 Subject: [PATCH] Add a scope concept to the DSL words (#156) Adds a new concept called `scope` to the NUClear DSL. This concept can be used to add words which will be able to know when they are running within a specific reaction. For example, if you want to have a DSL word that modifies how events are emitted within that task you can use this to track when the current task is one with that word. Then when another system needs to know if it is running in that context it can call the in_scope function. --- src/Reactor.hpp | 8 + src/dsl/Fusion.hpp | 10 +- src/dsl/Parse.hpp | 27 +++- src/dsl/fusion/NoOp.hpp | 20 ++- ...tconditionFusion.hpp => PostRunFusion.hpp} | 36 ++--- src/dsl/fusion/PreRunFusion.hpp | 76 +++++++++ src/dsl/fusion/ScopeFusion.hpp | 84 ++++++++++ src/dsl/word/Always.hpp | 2 +- src/dsl/word/IO.hpp | 2 +- src/dsl/word/Once.hpp | 4 +- src/dsl/word/TaskScope.hpp | 108 +++++++++++++ src/util/CallbackGenerator.hpp | 10 +- tests/tests/dsl/TaskScope.cpp | 145 ++++++++++++++++++ 13 files changed, 491 insertions(+), 41 deletions(-) rename src/dsl/fusion/{PostconditionFusion.hpp => PostRunFusion.hpp} (63%) create mode 100644 src/dsl/fusion/PreRunFusion.hpp create mode 100644 src/dsl/fusion/ScopeFusion.hpp create mode 100644 src/dsl/word/TaskScope.hpp create mode 100644 tests/tests/dsl/TaskScope.cpp diff --git a/src/Reactor.hpp b/src/Reactor.hpp index d21740d5..f072002e 100644 --- a/src/Reactor.hpp +++ b/src/Reactor.hpp @@ -70,6 +70,9 @@ namespace dsl { struct MainThread; + template + struct TaskScope; + template struct Network; @@ -225,6 +228,10 @@ class Reactor { /// @copydoc dsl::word::MainThread using MainThread = dsl::word::MainThread; + /// @copydoc dsl::word::TaskScope + template + using TaskScope = dsl::word::TaskScope; + /// @copydoc dsl::word::Startup using Startup = dsl::word::Startup; @@ -474,6 +481,7 @@ class Reactor { #include "dsl/word/Startup.hpp" #include "dsl/word/Sync.hpp" #include "dsl/word/TCP.hpp" +#include "dsl/word/TaskScope.hpp" #include "dsl/word/Trigger.hpp" #include "dsl/word/UDP.hpp" #include "dsl/word/Watchdog.hpp" diff --git a/src/dsl/Fusion.hpp b/src/dsl/Fusion.hpp index ab5ce5d9..f80b3553 100644 --- a/src/dsl/Fusion.hpp +++ b/src/dsl/Fusion.hpp @@ -29,9 +29,11 @@ #include "fusion/GroupFusion.hpp" #include "fusion/InlineFusion.hpp" #include "fusion/PoolFusion.hpp" -#include "fusion/PostconditionFusion.hpp" +#include "fusion/PostRunFusion.hpp" +#include "fusion/PreRunFusion.hpp" #include "fusion/PreconditionFusion.hpp" #include "fusion/PriorityFusion.hpp" +#include "fusion/ScopeFusion.hpp" namespace NUClear { namespace dsl { @@ -43,10 +45,12 @@ namespace dsl { , fusion::GetFusion , fusion::GroupFusion , fusion::InlineFusion + , fusion::PoolFusion + , fusion::PostRunFusion + , fusion::PreRunFusion , fusion::PreconditionFusion , fusion::PriorityFusion - , fusion::PoolFusion - , fusion::PostconditionFusion {}; + , fusion::ScopeFusion {}; } // namespace dsl } // namespace NUClear diff --git a/src/dsl/Parse.hpp b/src/dsl/Parse.hpp index 58983ab8..1e38116d 100644 --- a/src/dsl/Parse.hpp +++ b/src/dsl/Parse.hpp @@ -57,25 +57,36 @@ namespace dsl { Parse>(task); } - static bool precondition(threading::ReactionTask& task) { - return std::conditional_t::value, DSL, fusion::NoOp>::template precondition< + static std::shared_ptr pool(threading::ReactionTask& task) { + return std::conditional_t::value, DSL, fusion::NoOp>::template pool< Parse>(task); } - static int priority(threading::ReactionTask& task) { - return std::conditional_t::value, DSL, fusion::NoOp>::template priority< + static void post_run(threading::ReactionTask& task) { + std::conditional_t::value, DSL, fusion::NoOp>::template post_run< + Parse>(task); + } + static void pre_run(threading::ReactionTask& task) { + std::conditional_t::value, DSL, fusion::NoOp>::template pre_run< Parse>(task); } - static std::shared_ptr pool(threading::ReactionTask& task) { - return std::conditional_t::value, DSL, fusion::NoOp>::template pool< + static bool precondition(threading::ReactionTask& task) { + return std::conditional_t::value, DSL, fusion::NoOp>::template precondition< Parse>(task); } - static void postcondition(threading::ReactionTask& task) { - std::conditional_t::value, DSL, fusion::NoOp>::template postcondition< + static int priority(threading::ReactionTask& task) { + return std::conditional_t::value, DSL, fusion::NoOp>::template priority< Parse>(task); } + + static auto scope(threading::ReactionTask& task) + -> decltype(std::conditional_t::value, DSL, fusion::NoOp>::template scope< + Parse>(task)) { + return std::conditional_t::value, DSL, fusion::NoOp>::template scope>( + task); + } }; } // namespace dsl diff --git a/src/dsl/fusion/NoOp.hpp b/src/dsl/fusion/NoOp.hpp index 7ee1d03e..5b20e5ee 100644 --- a/src/dsl/fusion/NoOp.hpp +++ b/src/dsl/fusion/NoOp.hpp @@ -69,6 +69,16 @@ namespace dsl { return true; } + template + static void post_run(const threading::ReactionTask& /*task*/) { + // Empty as this is a no-op placeholder + } + + template + static void pre_run(const threading::ReactionTask& /*task*/) { + // Empty as this is a no-op placeholder + } + template static int priority(const threading::ReactionTask& /*task*/) { return word::Priority::NORMAL::value; @@ -80,8 +90,8 @@ namespace dsl { } template - static void postcondition(const threading::ReactionTask& /*task*/) { - // Empty as this is a no-op placeholder + static std::tuple<> scope(const threading::ReactionTask& /*task*/) { + return {}; } }; @@ -102,11 +112,15 @@ namespace dsl { static bool precondition(threading::ReactionTask&); + static void post_run(threading::ReactionTask&); + + static void pre_run(threading::ReactionTask&); + static int priority(threading::ReactionTask&); static std::shared_ptr pool(threading::ReactionTask&); - static void postcondition(threading::ReactionTask&); + static std::tuple<> scope(threading::ReactionTask&); }; } // namespace fusion diff --git a/src/dsl/fusion/PostconditionFusion.hpp b/src/dsl/fusion/PostRunFusion.hpp similarity index 63% rename from src/dsl/fusion/PostconditionFusion.hpp rename to src/dsl/fusion/PostRunFusion.hpp index ad2d1bad..a02c15ad 100644 --- a/src/dsl/fusion/PostconditionFusion.hpp +++ b/src/dsl/fusion/PostRunFusion.hpp @@ -20,8 +20,8 @@ * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -#ifndef NUCLEAR_DSL_FUSION_POSTCONDITION_FUSION_HPP -#define NUCLEAR_DSL_FUSION_POSTCONDITION_FUSION_HPP +#ifndef NUCLEAR_DSL_FUSION_POST_RUN_FUSION_HPP +#define NUCLEAR_DSL_FUSION_POST_RUN_FUSION_HPP #include "../../threading/ReactionTask.hpp" #include "../operation/DSLProxy.hpp" @@ -32,45 +32,45 @@ namespace NUClear { namespace dsl { namespace fusion { - /// Make a SFINAE type to check if a word has a postcondition method - HAS_NUCLEAR_DSL_METHOD(postcondition); + /// Make a SFINAE type to check if a word has a post_run method + HAS_NUCLEAR_DSL_METHOD(post_run); - // Default case where there are no postcondition words + // Default case where there are no post_run words template - struct PostconditionFuser {}; + struct PostRunFuser {}; // Case where there is only a single word remaining template - struct PostconditionFuser> { + struct PostRunFuser> { template - static void postcondition(threading::ReactionTask& task) { + static void post_run(threading::ReactionTask& task) { - // Run our remaining postcondition - Word::template postcondition(task); + // Run our remaining post_run + Word::template post_run(task); } }; // Case where there is more 2 more more words remaining template - struct PostconditionFuser> { + struct PostRunFuser> { template - static void postcondition(threading::ReactionTask& task) { + static void post_run(threading::ReactionTask& task) { - // Run our postcondition - Word1::template postcondition(task); + // Run our post_run + Word1::template post_run(task); - // Run the rest of our postconditions - PostconditionFuser>::template postcondition(task); + // Run the rest of our post_runs + PostRunFuser>::template post_run(task); } }; template - struct PostconditionFusion : PostconditionFuser> {}; + struct PostRunFusion : PostRunFuser> {}; } // namespace fusion } // namespace dsl } // namespace NUClear -#endif // NUCLEAR_DSL_FUSION_POSTCONDITION_FUSION_HPP +#endif // NUCLEAR_DSL_FUSION_POST_RUN_FUSION_HPP diff --git a/src/dsl/fusion/PreRunFusion.hpp b/src/dsl/fusion/PreRunFusion.hpp new file mode 100644 index 00000000..4ba83c0a --- /dev/null +++ b/src/dsl/fusion/PreRunFusion.hpp @@ -0,0 +1,76 @@ +/* + * MIT License + * + * Copyright (c) 2024 NUClear Contributors + * + * This file is part of the NUClear codebase. + * See https://github.com/Fastcode/NUClear for further info. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#ifndef NUCLEAR_DSL_FUSION_PRE_RUN_FUSION_HPP +#define NUCLEAR_DSL_FUSION_PRE_RUN_FUSION_HPP + +#include "../../threading/ReactionTask.hpp" +#include "../operation/DSLProxy.hpp" +#include "FindWords.hpp" +#include "has_nuclear_dsl_method.hpp" + +namespace NUClear { +namespace dsl { + namespace fusion { + + /// Make a SFINAE type to check if a word has a pre_run method + HAS_NUCLEAR_DSL_METHOD(pre_run); + + // Default case where there are no pre_run words + template + struct PreRunFuser {}; + + // Case where there is only a single word remaining + template + struct PreRunFuser> { + + template + static void pre_run(threading::ReactionTask& task) { + + // Run our remaining pre_run + Word::template pre_run(task); + } + }; + + // Case where there is more 2 more more words remaining + template + struct PreRunFuser> { + + template + static void pre_run(threading::ReactionTask& task) { + + // Run our pre_run + Word1::template pre_run(task); + + // Run the rest of our pre_runs + PreRunFuser>::template pre_run(task); + } + }; + + template + struct PreRunFusion : PreRunFuser> {}; + + } // namespace fusion +} // namespace dsl +} // namespace NUClear + +#endif // NUCLEAR_DSL_FUSION_PRE_RUN_FUSION_HPP diff --git a/src/dsl/fusion/ScopeFusion.hpp b/src/dsl/fusion/ScopeFusion.hpp new file mode 100644 index 00000000..3257c6d3 --- /dev/null +++ b/src/dsl/fusion/ScopeFusion.hpp @@ -0,0 +1,84 @@ +/* + * MIT License + * + * Copyright (c) 2024 NUClear Contributors + * + * This file is part of the NUClear codebase. + * See https://github.com/Fastcode/NUClear for further info. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#ifndef NUCLEAR_DSL_FUSION_SCOPE_FUSION_HPP +#define NUCLEAR_DSL_FUSION_SCOPE_FUSION_HPP + +#include "../../threading/Reaction.hpp" +#include "../../util/tuplify.hpp" +#include "../operation/DSLProxy.hpp" +#include "FindWords.hpp" +#include "has_nuclear_dsl_method.hpp" + +namespace NUClear { +namespace dsl { + namespace fusion { + + /// Make a SFINAE type to check if a word has a scope method + HAS_NUCLEAR_DSL_METHOD(scope); + + /** + * This is our Function Fusion wrapper class that allows it to call scope functions. + * + * @tparam Function the scope function that we are wrapping for + * @tparam DSL the DSL that we pass to our scope function + */ + template + struct ScopeCaller { + static auto call(threading::ReactionTask& task) -> decltype(Function::template scope(task)) { + return Function::template scope(task); + } + }; + + // Default case where there are no scope words + template + struct ScopeFuser {}; + + // Case where there is at least one get word + template + struct ScopeFuser> { + + template + static auto scope(threading::ReactionTask& task) + -> decltype(util::FunctionFusion, + decltype(std::forward_as_tuple(task)), + ScopeCaller, + std::tuple, + 1>::call(task)) { + + // Perform our function fusion + return util::FunctionFusion, + decltype(std::forward_as_tuple(task)), + ScopeCaller, + std::tuple, + 1>::call(task); + } + }; + + template + struct ScopeFusion : ScopeFuser> {}; + + } // namespace fusion +} // namespace dsl +} // namespace NUClear + +#endif // NUCLEAR_DSL_FUSION_SCOPE_FUSION_HPP diff --git a/src/dsl/word/Always.hpp b/src/dsl/word/Always.hpp index 24506c33..9292f739 100644 --- a/src/dsl/word/Always.hpp +++ b/src/dsl/word/Always.hpp @@ -110,7 +110,7 @@ namespace dsl { } template - static void postcondition(threading::ReactionTask& task) { + static void post_run(threading::ReactionTask& task) { // Get a task for the always reaction and submit it to the scheduler PowerPlant::powerplant->submit(task.parent->get_task()); } diff --git a/src/dsl/word/IO.hpp b/src/dsl/word/IO.hpp index 9b6e2648..6b7f3cb0 100644 --- a/src/dsl/word/IO.hpp +++ b/src/dsl/word/IO.hpp @@ -154,7 +154,7 @@ namespace dsl { } template - static void postcondition(threading::ReactionTask& task) { + static void post_run(threading::ReactionTask& task) { task.parent->reactor.emit(std::make_unique(task.parent->id)); } }; diff --git a/src/dsl/word/Once.hpp b/src/dsl/word/Once.hpp index d0bb339b..7699971f 100644 --- a/src/dsl/word/Once.hpp +++ b/src/dsl/word/Once.hpp @@ -35,13 +35,13 @@ namespace dsl { * * @code on() @endcode * Any reactions listed with this DSL word will run only once. - * This is the only time these reactions will run as the postcondition Unbinds the current reaction. + * This is the only time these reactions will run as the post_run Unbinds the current reaction. */ struct Once : Single { // Post condition to unbind this reaction. template - static void postcondition(threading::ReactionTask& task) { + static void post_run(threading::ReactionTask& task) { // Unbind: task.parent->unbind(); } diff --git a/src/dsl/word/TaskScope.hpp b/src/dsl/word/TaskScope.hpp new file mode 100644 index 00000000..18f2e0a3 --- /dev/null +++ b/src/dsl/word/TaskScope.hpp @@ -0,0 +1,108 @@ +/* + * MIT License + * + * Copyright (c) 2024 NUClear Contributors + * + * This file is part of the NUClear codebase. + * See https://github.com/Fastcode/NUClear for further info. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef NUCLEAR_DSL_WORD_TASK_SCOPE_HPP +#define NUCLEAR_DSL_WORD_TASK_SCOPE_HPP +#include + +#include "../../id.hpp" +#include "../../threading/ReactionTask.hpp" +#include "../../util/platform.hpp" + +namespace NUClear { +namespace dsl { + namespace word { + + /** + * This template can be used when you need other DSL words to know that they are running in a specific context. + * + * For example, if you want to have a DSL word that modifies how events are emitted within that task you can use + * this to track when the current task is one with that word. + * It utilises scope to track when the task is running by storing the task id in a thread local variable. + * Then when another system needs to know if it is running in that context it can call the in_scope function. + * + * @tparam Group a unique type to identify this scope + */ + template + struct TaskScope { + + /** + * Locks the current task id to the word until the lock goes out of scope + */ + struct Lock { + explicit Lock(NUClear::id_t old_id) : old_id(old_id) {} + + // No copying the lock + Lock(const Lock&) = delete; + Lock& operator=(const Lock&) = delete; + + // Moving the lock must invalidate the old lock + Lock(Lock&& other) noexcept : valid(std::exchange(other.valid, false)), old_id(other.old_id) {} + Lock& operator=(Lock&& other) noexcept { + if (this != &other) { + valid = std::exchange(other.valid, false); + old_id = other.old_id; + } + } + ~Lock() { // Only restore if this lock hasn't been invalidated + current_task_id = valid ? old_id : current_task_id; + } + + private: + /// If this lock is still valid (hasn't been moved) + bool valid = true; + /// The old task id to restore + NUClear::id_t old_id; + }; + + template + static Lock scope(const threading::ReactionTask& task) { + // Store the old task id + NUClear::id_t old_id = current_task_id; + // Set the current task id to the word + current_task_id = task.id; + // Return a lock that will restore the old task id + return Lock(old_id); + } + + static bool in_scope() { + // Get the current task id + const auto* task = threading::ReactionTask::get_current_task(); + + // Check if the current task id is the word + return task != nullptr && task->id == current_task_id; + } + + private: + /// The current task id that is running + // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) + static ATTRIBUTE_TLS NUClear::id_t current_task_id; + }; + + // Initialise the current task id + template // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) + ATTRIBUTE_TLS NUClear::id_t TaskScope::current_task_id{0}; + + } // namespace word +} // namespace dsl +} // namespace NUClear + +#endif // NUCLEAR_DSL_WORD_TASK_SCOPE_HPP diff --git a/src/util/CallbackGenerator.hpp b/src/util/CallbackGenerator.hpp index 2c2b039a..93ada18c 100644 --- a/src/util/CallbackGenerator.hpp +++ b/src/util/CallbackGenerator.hpp @@ -132,8 +132,11 @@ namespace util { // We have to catch any exceptions try { - // We call with only the relevant arguments to the passed function - util::apply_relevant(c, std::move(data)); + auto scope = DSL::scope(task); // Acquire the scope + DSL::pre_run(task); // Pre run tasks + util::apply_relevant(c, std::move(data)); // Run the callback + DSL::post_run(task); // Post run tasks + std::ignore = scope; // Ignore unused variable warning } catch (...) { // Catch our exception if it happens @@ -142,9 +145,6 @@ namespace util { } } - // Run our postconditions - DSL::postcondition(task); - if (task.statistics != nullptr) { task.statistics->finished = message::ReactionStatistics::Event::now(); PowerPlant::powerplant->emit(std::make_unique(Event::FINISHED, task.statistics)); diff --git a/tests/tests/dsl/TaskScope.cpp b/tests/tests/dsl/TaskScope.cpp new file mode 100644 index 00000000..eb192a5a --- /dev/null +++ b/tests/tests/dsl/TaskScope.cpp @@ -0,0 +1,145 @@ +/* + * MIT License + * + * Copyright (c) 2013 NUClear Contributors + * + * This file is part of the NUClear codebase. + * See https://github.com/Fastcode/NUClear for further info. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include + +#include "test_util/TestBase.hpp" +#include "test_util/common.hpp" + +/** + * Holds information about a single step in the test + */ +struct StepData { + int scope; ///< The scope that this step was run in + bool next_inline; ///< If the next step was run inline + std::array scope_states; ///< The state of the scopes during this step +}; + +namespace Catch { +template <> +struct StringMaker> { + static std::string convert(const std::map& value) { + std::stringstream event; + for (const auto& items : value) { + event << items.first << "(" << items.second.scope << "):"; + for (const auto& scope : items.second.scope_states) { + event << (scope ? "t" : "f"); + } + event << (items.second.next_inline ? " -> " : " -| "); + } + return event.str(); + } +}; +} // namespace Catch + +class TestReactor : public test_util::TestBase { +public: + /** + * Holds the accumulated data for the test + */ + template + struct Data { + std::map steps; ///< The steps that have been run + }; + + template + void process_step(const Message& d) { + + using NextData = Data; + + // Get the scope state before the inline event + auto next_inline = std::make_unique(); + next_inline->steps = d.steps; + next_inline->steps[Current] = { + ScopeID, + true, + {TaskScope>::in_scope(), TaskScope>::in_scope(), TaskScope>::in_scope()}, + }; + emit(next_inline); + + // Get the scope state after the inline event + auto next_normal = std::make_unique(); + next_normal->steps = d.steps; + next_normal->steps[Current] = { + ScopeID, + false, + {TaskScope>::in_scope(), TaskScope>::in_scope(), TaskScope>::in_scope()}}; + emit(next_normal); + } + + TestReactor(std::unique_ptr environment) : TestBase(std::move(environment)) { + + on>>().then([this](const Data<0>& a) { process_step<0, -1>(a); }); + on>, TaskScope>>().then([this](const Data<0>& m) { process_step<0, 0>(m); }); + on>, TaskScope>>().then([this](const Data<0>& m) { process_step<0, 1>(m); }); + on>, TaskScope>>().then([this](const Data<0>& m) { process_step<0, 2>(m); }); + + on>>().then([this](const Data<1>& m) { process_step<1, -1>(m); }); + on>, TaskScope>>().then([this](const Data<1>& m) { process_step<1, 0>(m); }); + on>, TaskScope>>().then([this](const Data<1>& m) { process_step<1, 1>(m); }); + on>, TaskScope>>().then([this](const Data<1>& m) { process_step<1, 2>(m); }); + + on>>().then([this](const Data<2>& m) { process_step<2, -1>(m); }); + on>, TaskScope>>().then([this](const Data<2>& m) { process_step<2, 0>(m); }); + on>, TaskScope>>().then([this](const Data<2>& m) { process_step<2, 1>(m); }); + on>, TaskScope>>().then([this](const Data<2>& m) { process_step<2, 2>(m); }); + + // Store the results of the test + on>>().then([this](const Data<3>& m) { events.push_back(m.steps); }); + + // Start the test + on().then([this] { emit(std::make_unique>()); }); + } + + /// A vector of events that have happened + std::vector> events; +}; + + +TEST_CASE("Test that Trigger statements get the correct data", "[api][trigger]") { + + NUClear::Configuration config; + config.default_pool_concurrency = 1; + NUClear::PowerPlant plant(config); + // test_util::add_tracing(plant); + const auto& reactor = plant.install(); + plant.start(); + + CHECK(reactor.events.size() == 512); + for (const auto& test : reactor.events) { + CAPTURE(test); + CHECK(test.size() == 3); + for (const auto& step : test) { + const auto& step_no = step.first; + const auto& data = step.second; + CAPTURE(step_no); + CAPTURE(data.scope); + + // Only the active scope (if there is one) should be true + CHECK(data.scope_states[0] == (0 == data.scope)); + CHECK(data.scope_states[1] == (1 == data.scope)); + CHECK(data.scope_states[2] == (2 == data.scope)); + } + } +}