From 3f5499cebc06356ed99159be3fb9676292cf7b8b Mon Sep 17 00:00:00 2001 From: Felix Morgner Date: Wed, 24 Jul 2024 10:44:13 +0200 Subject: turns: rename domain to core --- core/CMakeLists.txt | 57 ++++++++ core/include/turns/core/disposition.hpp | 27 ++++ core/include/turns/core/participant.hpp | 58 ++++++++ core/include/turns/core/turn_order.hpp | 85 ++++++++++++ core/src/disposition.cpp | 25 ++++ core/src/participant.cpp | 35 +++++ core/src/turn_order.cpp | 225 ++++++++++++++++++++++++++++++++ core/tests/disposition.cpp | 32 +++++ core/tests/participant.cpp | 113 ++++++++++++++++ core/tests/register_types.cpp | 13 ++ core/tests/turn_order.cpp | 225 ++++++++++++++++++++++++++++++++ core/tests/turn_order_bugs.cpp | 42 ++++++ 12 files changed, 937 insertions(+) create mode 100644 core/CMakeLists.txt create mode 100644 core/include/turns/core/disposition.hpp create mode 100644 core/include/turns/core/participant.hpp create mode 100644 core/include/turns/core/turn_order.hpp create mode 100644 core/src/disposition.cpp create mode 100644 core/src/participant.cpp create mode 100644 core/src/turn_order.cpp create mode 100644 core/tests/disposition.cpp create mode 100644 core/tests/participant.cpp create mode 100644 core/tests/register_types.cpp create mode 100644 core/tests/turn_order.cpp create mode 100644 core/tests/turn_order_bugs.cpp (limited to 'core') diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt new file mode 100644 index 0000000..785421c --- /dev/null +++ b/core/CMakeLists.txt @@ -0,0 +1,57 @@ +set(COMPONENT "core") + +# Library + +add_library("${COMPONENT}" + "src/disposition.cpp" + "src/participant.cpp" + "src/turn_order.cpp" +) + +add_library("turns::${COMPONENT}" ALIAS "${COMPONENT}") + + +target_compile_options("${COMPONENT}" PUBLIC + "$<$:-Wall>" + "$<$:-Wextra>" + "$<$:-Werror>" + "$<$:-pedantic-errors>" + PRIVATE + "$<$,$>:-fprofile-arcs>" + "$<$,$>:-ftest-coverage>" +) + +target_include_directories("${COMPONENT}" PUBLIC + "include" +) + +target_link_libraries("${COMPONENT}" PUBLIC + "$<$,$>:gcov>" + + "PkgConfig::giomm" + "PkgConfig::glibmm" +) + +target_link_options("${COMPONENT}" PRIVATE + "$<$,$>:--coverage>" +) + +# Tests + +add_executable("${COMPONENT}-tests" + "tests/register_types.cpp" + + "tests/disposition.cpp" + "tests/participant.cpp" + "tests/turn_order_bugs.cpp" + "tests/turn_order.cpp" +) + +target_link_libraries("${COMPONENT}-tests" + "Catch2::Catch2" + + "turns::core" + "turns::glib-test-main" +) + +catch_discover_tests("${COMPONENT}-tests") \ No newline at end of file diff --git a/core/include/turns/core/disposition.hpp b/core/include/turns/core/disposition.hpp new file mode 100644 index 0000000..291aaf5 --- /dev/null +++ b/core/include/turns/core/disposition.hpp @@ -0,0 +1,27 @@ +#ifndef TURNS_DOMAIN_DISPOSITION_HPP +#define TURNS_DOMAIN_DISPOSITION_HPP + +#include +#include + +#include + +namespace turns::core +{ + + enum struct disposition : std::uint8_t + { + neutral, + friendly, + hostile, + secret, + + ///! End marker + END + }; + + auto presentation_name_for(disposition value) -> Glib::ustring; + +} // namespace turns::core + +#endif \ No newline at end of file diff --git a/core/include/turns/core/participant.hpp b/core/include/turns/core/participant.hpp new file mode 100644 index 0000000..9b5dab4 --- /dev/null +++ b/core/include/turns/core/participant.hpp @@ -0,0 +1,58 @@ +#ifndef TURNS_DOMAIN_PARTICIPANT_HPP +#define TURNS_DOMAIN_PARTICIPANT_HPP + +#include "turns/core/disposition.hpp" + +#include + +#include +#include +#include +#include +#include + +namespace turns::core +{ + struct participant : Glib::Object + { + auto static create(Glib::ustring name, float priority, disposition disposition) -> Glib::RefPtr; + + participant(); + participant(Glib::ustring name, float priority, disposition disposition); + + auto operator<=>(participant const & other) const noexcept -> std::partial_ordering; + + template + auto disposition(this Self && self) + { + return self.m_disposition.get_proxy(); + } + + template + auto is_active(this Self && self) + { + return self.m_is_active.get_proxy(); + } + + template + auto name(this Self && self) + { + return self.m_name.get_proxy(); + } + + template + auto priority(this Self && self) + { + return self.m_priority.get_proxy(); + } + + private: + Glib::Property m_disposition{*this, "disposition", core::disposition::neutral}; + Glib::Property m_is_active{*this, "active", false}; + Glib::Property m_name{*this, "name", ""}; + Glib::Property m_priority{*this, "priority", 0.0f}; + }; + +} // namespace turns::core + +#endif \ No newline at end of file diff --git a/core/include/turns/core/turn_order.hpp b/core/include/turns/core/turn_order.hpp new file mode 100644 index 0000000..43ee075 --- /dev/null +++ b/core/include/turns/core/turn_order.hpp @@ -0,0 +1,85 @@ +#ifndef TURNS_DOMAIN_TURN_ORDER_HPP +#define TURNS_DOMAIN_TURN_ORDER_HPP + +#include "turns/core/disposition.hpp" +#include "turns/core/participant.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace turns::core +{ + + struct turn_order : Gio::ListModel, + Glib::Object + { + using value_type = Glib::RefPtr; + using container_type = std::vector; + using iterator = container_type::iterator; + using const_iterator = container_type::const_iterator; + + using active_participant_type = unsigned int; + using is_empty_type = bool; + using has_next_type = bool; + using has_previous_type = bool; + using is_running_type = bool; + using round_number_type = unsigned int; + + auto static constexpr invalid_participant_index = std::numeric_limits::max(); + auto static constexpr invalid_round_number = std::numeric_limits::max(); + + /** Life-time */ + turn_order(); + + auto static create() -> Glib::RefPtr; + + /** Properties */ + auto is_empty() const -> Glib::PropertyProxy_ReadOnly; + auto has_next() const -> Glib::PropertyProxy_ReadOnly; + auto has_previous() const -> Glib::PropertyProxy_ReadOnly; + auto is_running() const -> Glib::PropertyProxy_ReadOnly; + auto round_number() const -> Glib::PropertyProxy_ReadOnly; + + /** Element Modifications */ + auto add(Glib::ustring const & name, float priority, disposition disposition) -> void; + auto clear() -> void; + auto remove(unsigned index) -> void; + + /** Turn Modification */ + auto next() -> void; + auto previous() -> void; + auto start() -> void; + auto stop() -> void; + + private: + auto get_item_type_vfunc() -> GType override; + auto get_n_items_vfunc() -> unsigned override; + auto get_item_vfunc(unsigned position) -> void * override; + + /** Signal handlers */ + auto handle_priority_changed(value_type entry) -> void; + + /** Data management */ + auto find(value_type entry) const -> const_iterator; + auto insert(value_type entry) -> const_iterator; + + container_type m_data{}; + std::optional m_active{}; + + Glib::Property m_has_next{*this, "has-next", false}; + Glib::Property m_has_previous{*this, "has-previous", false}; + Glib::Property m_is_empty{*this, "is-empty", true}; + Glib::Property m_is_running{*this, "is-running", false}; + Glib::Property m_round_number{*this, "round-number", invalid_round_number}; + }; + +} // namespace turns::core + +#endif \ No newline at end of file diff --git a/core/src/disposition.cpp b/core/src/disposition.cpp new file mode 100644 index 0000000..4eec33e --- /dev/null +++ b/core/src/disposition.cpp @@ -0,0 +1,25 @@ +#include "turns/core/disposition.hpp" + +#include + +namespace turns::core +{ + + auto presentation_name_for(disposition value) -> Glib::ustring + { + switch (value) + { + case disposition::neutral: + return _("Neutral"); + case disposition::friendly: + return _("Friendly"); + case disposition::hostile: + return _("Hostile"); + case disposition::secret: + return _("Secret"); + default: + return _("Unknown disposition value"); + } + } + +} // namespace turns::core \ No newline at end of file diff --git a/core/src/participant.cpp b/core/src/participant.cpp new file mode 100644 index 0000000..45b02bd --- /dev/null +++ b/core/src/participant.cpp @@ -0,0 +1,35 @@ +#include "turns/core/participant.hpp" + +#include +#include + +#include +#include + +namespace turns::core +{ + auto participant::create(Glib::ustring name, float priority, core::disposition disposition) -> Glib::RefPtr + { + return Glib::make_refptr_for_instance(new participant{name, priority, disposition}); + } + + participant::participant() + : Glib::ObjectBase{typeid(participant)} + , Glib::Object{} + { + } + + participant::participant(Glib::ustring name, float priority, core::disposition disposition) + : participant() + { + m_name = name; + m_priority = priority; + m_disposition = disposition; + } + + auto participant::operator<=>(participant const & other) const noexcept -> std::partial_ordering + { + return m_priority <=> other.m_priority; + } + +} // namespace turns::core \ No newline at end of file diff --git a/core/src/turn_order.cpp b/core/src/turn_order.cpp new file mode 100644 index 0000000..ae3511e --- /dev/null +++ b/core/src/turn_order.cpp @@ -0,0 +1,225 @@ +#include "turns/core/turn_order.hpp" + +#include "turns/core/participant.hpp" + +#include +#include +#include +#include + +#include + +namespace turns::core +{ + + namespace + { + auto constexpr comparator = [](auto lhs, auto rhs) { + return *lhs > *rhs; + }; + + auto constexpr equal_comparator = [](auto lhs, auto rhs) { + return (lhs->get_name() == rhs->get_name()) && (lhs->get_priority() && rhs->get_priority()); + }; + } // namespace + + /** Construction */ + + turn_order::turn_order() + : Glib::ObjectBase{typeid(turn_order)} + , Gio::ListModel{} + { + } + + auto turn_order::create() -> Glib::RefPtr + { + return Glib::make_refptr_for_instance(new turn_order{}); + } + + /** Queries */ + + auto turn_order::is_empty() const -> Glib::PropertyProxy_ReadOnly + { + return m_is_empty.get_proxy(); + } + + auto turn_order::has_next() const -> Glib::PropertyProxy_ReadOnly + { + return m_has_next.get_proxy(); + } + + auto turn_order::has_previous() const -> Glib::PropertyProxy_ReadOnly + { + return m_has_previous.get_proxy(); + } + + auto turn_order::is_running() const -> Glib::PropertyProxy_ReadOnly + { + return m_is_running.get_proxy(); + } + + auto turn_order::round_number() const -> Glib::PropertyProxy_ReadOnly + { + return m_round_number.get_proxy(); + } + + /** Modifiers */ + + auto turn_order::add(Glib::ustring const & name, float priority, disposition disposition) -> void + { + auto entry = participant::create(name, priority, disposition); + entry->priority().signal_changed().connect(sigc::bind(sigc::mem_fun(*this, &turn_order::handle_priority_changed), entry)); + auto position = std::distance(m_data.cbegin(), insert(entry)); + items_changed(position, 0, 1); + + if (get_n_items() == 1) + { + m_is_empty = false; + m_has_next = true; + } + } + + auto turn_order::clear() -> void + { + m_is_running = false; + m_is_empty = true; + m_has_next = false; + m_has_previous = false; + m_active.reset(); + m_round_number = invalid_round_number; + + auto old_size = get_n_items(); + m_data.clear(); + items_changed(0, old_size, 0); + } + + auto turn_order::next() -> void + { + auto old_active = *m_active; + m_active = m_active.transform([this](auto index) { return (index + 1) % get_n_items(); }); + + m_has_previous = true; + m_data[old_active]->is_active() = false; + m_data[*m_active]->is_active() = true; + + if (m_active == 0) + { + m_round_number = m_round_number + 1; + } + } + + auto turn_order::previous() -> void + { + if (!(m_has_previous && m_is_running)) + { + return; + } + + auto old_active = *m_active; + m_active = m_active.transform([this](auto index) { return index ? index - 1 : get_n_items() - 1; }); + + m_has_previous = m_round_number > 0 || m_active > 0; + m_data[old_active]->is_active() = false; + m_data[*m_active]->is_active() = true; + + if (m_active == 0 && m_round_number > 0) + { + m_round_number = m_round_number - 1; + } + } + + auto turn_order::remove(unsigned index) -> void + { + if (index >= get_n_items()) + { + return; + } + + auto position = m_data.begin() + index; + m_data.erase(position); + items_changed(index, 1, 0); + if (get_n_items() == 0) + { + m_is_empty = true; + m_is_running = false; + m_has_next = false; + } + } + + auto turn_order::start() -> void + { + if (!m_active) + { + m_active = 0; + m_data[*m_active]->is_active() = true; + } + if (m_round_number == invalid_round_number) + { + m_round_number = 0; + } + m_is_running = true; + } + + auto turn_order::stop() -> void + { + m_is_running = false; + } + + /** ListModel implementation */ + + auto turn_order::get_item_type_vfunc() -> GType + { + return participant::get_type(); + } + + auto turn_order::get_n_items_vfunc() -> unsigned + { + return m_data.size(); + } + + auto turn_order::get_item_vfunc(unsigned position) -> void * + { + if (position >= get_n_items()) + { + return nullptr; + } + auto item = m_data[position]; + item->reference(); + return item->gobj(); + } + + /** Signal handlers */ + + auto turn_order::handle_priority_changed(value_type entry) -> void + { + auto original_position = find(entry); + auto original_index = distance(m_data.cbegin(), original_position); + auto target_position = std::ranges::upper_bound(m_data, entry, comparator); + if (original_position == target_position) + { + return; + } + + m_data.erase(original_position); + auto inserted_position = insert(entry); + items_changed(0, get_n_items(), get_n_items()); + if (m_active == original_index) + { + m_active = distance(m_data.cbegin(), inserted_position); + m_has_previous = m_round_number > 0 || m_active > 0; + } + } + + /** Data management */ + + auto turn_order::find(value_type entry) const -> const_iterator + { + return std::ranges::find(m_data, entry); + } + + auto turn_order::insert(value_type entry) -> const_iterator + { + return m_data.insert(std::ranges::upper_bound(m_data, entry, comparator), entry); + } + +} // namespace turns::core \ No newline at end of file diff --git a/core/tests/disposition.cpp b/core/tests/disposition.cpp new file mode 100644 index 0000000..3a35741 --- /dev/null +++ b/core/tests/disposition.cpp @@ -0,0 +1,32 @@ +#include "turns/core/disposition.hpp" + +#include +#include + +#include +#include +#include +#include + +#include +#include + +namespace turns::core::tests +{ + + TEST_CASE("to_presentation_name returns the correct string for the current language", "[disposition]") + { + auto [value, name] = GENERATE(std::pair{disposition::neutral, Glib::ustring{_("Neutral")}}, + std::pair{disposition::friendly, Glib::ustring{_("Friendly")}}, + std::pair{disposition::hostile, Glib::ustring{_("Hostile")}}, + std::pair{disposition::secret, Glib::ustring{_("Secret")}}, + std::pair{static_cast(std::numeric_limits>::max()), + Glib::ustring{_("Unknown disposition value")}}); + + SECTION(std::format("the presentation name for '{}' is '{}'", static_cast>(value), name.c_str())) + { + REQUIRE(presentation_name_for(value) == name); + } + } + +} // namespace turns::core::tests \ No newline at end of file diff --git a/core/tests/participant.cpp b/core/tests/participant.cpp new file mode 100644 index 0000000..14fb1ae --- /dev/null +++ b/core/tests/participant.cpp @@ -0,0 +1,113 @@ +#include "turns/core/participant.hpp" +#include "turns/core/disposition.hpp" + +#include + +#include + +#include + +namespace turns::core::tests +{ + + TEST_CASE("A freshly constructed participant") + { + auto constexpr constructed_name = "Vana Thistletop"; + auto constexpr constructed_priority = 17; + auto constexpr constructed_disposition = disposition::friendly; + auto instance = participant{constructed_name, constructed_priority, constructed_disposition}; + + SECTION("can be created") + { + REQUIRE(participant::create(constructed_name, constructed_priority, constructed_disposition)); + } + + SECTION("allows access to its disposition") + { + SECTION("allowing to get it") + { + REQUIRE(instance.disposition() == constructed_disposition); + } + + SECTION("allowing to get it via a constant object") + { + auto const & cref = instance; + REQUIRE(cref.disposition() == constructed_disposition); + } + + SECTION("allowing to set it") + { + instance.disposition() = disposition::hostile; + REQUIRE(instance.disposition() == disposition::hostile); + } + } + + SECTION("allows access to its name") + { + SECTION("allowing to get it") + { + REQUIRE(instance.name() == constructed_name); + } + + SECTION("allowing to get it via a constant object") + { + auto const & cref = instance; + REQUIRE(cref.name() == constructed_name); + } + + SECTION("allowing to set it") + { + instance.name() = "replaced"; + REQUIRE(instance.name() == "replaced"); + } + } + + SECTION("allows access to its priority") + { + SECTION("allowing to get it") + { + REQUIRE(instance.priority() == constructed_priority); + } + + SECTION("allowing to get it via a constant object") + { + auto const & cref = instance; + REQUIRE(cref.priority() == constructed_priority); + } + + SECTION("allowing to set it") + { + instance.priority() = 4; + REQUIRE(instance.priority() == 4); + } + } + + SECTION("can be compared with another participant") + { + auto equivalent_instance = participant{"Equivalent", constructed_priority, constructed_disposition}; + auto lesser_instance = participant{"Lesser", constructed_priority - 1, constructed_disposition}; + auto greater_instance = participant{"Greater", constructed_priority + 1, constructed_disposition}; + + SECTION("yielding std::partial_ordering::equivalent for itself") + { + REQUIRE((instance <=> equivalent_instance) == std::partial_ordering::equivalent); + } + + SECTION("yielding std::partial_ordering::equivalent for an equivalent participant") + { + REQUIRE((instance <=> equivalent_instance) == std::partial_ordering::equivalent); + } + + SECTION("yielding std::partial_ordering::greater for a lesser participant") + { + REQUIRE((instance <=> lesser_instance) == std::partial_ordering::greater); + } + + SECTION("yielding std::partial_ordering::less for a greater participant") + { + REQUIRE((instance <=> greater_instance) == std::partial_ordering::less); + } + } + } + +} // namespace turns::core::tests \ No newline at end of file diff --git a/core/tests/register_types.cpp b/core/tests/register_types.cpp new file mode 100644 index 0000000..2ad0628 --- /dev/null +++ b/core/tests/register_types.cpp @@ -0,0 +1,13 @@ +#include "turns/core/participant.hpp" +#include "turns/core/turn_order.hpp" + +namespace turns::tests +{ + + auto register_types() -> void + { + static_cast(core::participant{}); + static_cast(core::turn_order{}); + } + +} // namespace turns::tests \ No newline at end of file diff --git a/core/tests/turn_order.cpp b/core/tests/turn_order.cpp new file mode 100644 index 0000000..fc779d7 --- /dev/null +++ b/core/tests/turn_order.cpp @@ -0,0 +1,225 @@ +#include "turns/core/turn_order.hpp" + +#include "turns/core/participant.hpp" + +#include + +#include + +namespace turns::core::tests +{ + SCENARIO("Queries on a fresh turn_order instance", "[turn_order]") + { + GIVEN("an empty turn_order") + { + auto instance = turn_order::create(); + + THEN("get_n_items() returns 0") + { + auto str = Gio::ListStore::create(); + REQUIRE(instance->get_n_items() == str->get_n_items()); + } + + THEN("get_type() returns participant::get_type()") + { + REQUIRE(instance->get_item_type() == participant::get_type()); + } + + THEN("get_typed_object(0) returns nullptr") + { + REQUIRE(instance->get_typed_object(0) == nullptr); + } + + THEN("has_next() returns false") + { + REQUIRE_FALSE(instance->has_next()); + } + + THEN("has_previous() returns false") + { + REQUIRE_FALSE(instance->has_previous()); + } + + THEN("is_empty() returns true") + { + REQUIRE(instance->is_empty()); + } + + THEN("is_running() returns false") + { + REQUIRE_FALSE(instance->is_running()); + } + + THEN("round_number() returns invalid_round_number") + { + REQUIRE(instance->round_number() == turn_order::invalid_round_number); + } + } + } + + SCENARIO("Adding participants") + { + auto instance = turn_order::create(); + + GIVEN("a participant has been added to a turn_order") + { + instance->add("Participant #0", 0, disposition::neutral); + + THEN("get_n_items() returns 1") + { + REQUIRE(instance->get_n_items() == 1); + } + + THEN("get_typed_object(0) returns a non-null pointer") + { + REQUIRE(instance->get_typed_object(0) != nullptr); + } + + THEN("has_next() returns true") + { + REQUIRE(instance->has_next()); + } + + THEN("has_previous() returns false") + { + REQUIRE_FALSE(instance->has_previous()); + } + + THEN("is_empty() returns false") + { + REQUIRE_FALSE(instance->is_empty()); + } + + THEN("is_running() returns false") + { + REQUIRE_FALSE(instance->is_running()); + } + + THEN("round_number() returns invalid_round_number") + { + REQUIRE(instance->round_number() == turn_order::invalid_round_number); + } + + WHEN("the turn_order is start()ed") + { + instance->start(); + + THEN("get_n_items() still returns 1") + { + REQUIRE(instance->get_n_items() == 1); + } + + THEN("get_typed_object(0) still returns a non-null pointer") + { + REQUIRE(instance->get_typed_object(0) != nullptr); + } + + THEN("has_next() still returns true") + { + REQUIRE(instance->has_next()); + } + + THEN("has_previous() still returns false") + { + REQUIRE_FALSE(instance->has_previous()); + } + + THEN("is_empty() still returns false") + { + REQUIRE_FALSE(instance->is_empty()); + } + + THEN("is_running() returns true") + { + REQUIRE(instance->is_running()); + } + + THEN("round_number() returns 0") + { + REQUIRE(instance->round_number() == 0); + } + + AND_WHEN("invoking previous()") + { + instance->previous(); + + THEN("get_n_items() still returns 1") + { + REQUIRE(instance->get_n_items() == 1); + } + + THEN("get_typed_object(0) still returns a non-null pointer") + { + REQUIRE(instance->get_typed_object(0) != nullptr); + } + + THEN("has_next() still returns true") + { + REQUIRE(instance->has_next()); + } + + THEN("has_previous() still returns false") + { + REQUIRE_FALSE(instance->has_previous()); + } + + THEN("is_empty() still returns false") + { + REQUIRE_FALSE(instance->is_empty()); + } + + THEN("is_running() returns true") + { + REQUIRE(instance->is_running()); + } + + THEN("round_number() returns 0") + { + REQUIRE(instance->round_number() == 0); + } + } + + AND_WHEN("invoking next()") + { + instance->next(); + + THEN("get_n_items() still returns 1") + { + REQUIRE(instance->get_n_items() == 1); + } + + THEN("get_typed_object(0) still returns a non-null pointer") + { + REQUIRE(instance->get_typed_object(0) != nullptr); + } + + THEN("has_next() still returns true") + { + REQUIRE(instance->has_next()); + } + + THEN("has_previous() returns true") + { + REQUIRE(instance->has_previous()); + } + + THEN("is_empty() still returns false") + { + REQUIRE_FALSE(instance->is_empty()); + } + + THEN("is_running() returns true") + { + REQUIRE(instance->is_running()); + } + + THEN("round_number() returns 1") + { + REQUIRE(instance->round_number() == 1); + } + } + } + } + } + +} // namespace turns::core::tests \ No newline at end of file diff --git a/core/tests/turn_order_bugs.cpp b/core/tests/turn_order_bugs.cpp new file mode 100644 index 0000000..0fa0720 --- /dev/null +++ b/core/tests/turn_order_bugs.cpp @@ -0,0 +1,42 @@ +#include "turns/core/participant.hpp" +#include "turns/core/turn_order.hpp" + +#include + +#include + +namespace turns::core::tests +{ + /** + * Bug description: + * + * After having stepped according to the step pattern below, tt was possible to step backward often enough to underflow the round number: + * - forward + * - backward + * - forward + */ + SCENARIO("Can step back infinitely", "[turn_order][bug]") + { + GIVEN("a non-empty turn_order") + { + auto instance = turn_order::create(); + + instance->add("A", 0, disposition::neutral); + + WHEN("it is started and then stepped forward, backward, forward") + { + instance->start(); + instance->next(); + instance->previous(); + instance->next(); + + THEN("it is not possible to step backwards more than once") + { + instance->previous(); + instance->previous(); + REQUIRE(instance->round_number() == 0); + } + } + } + } +} // namespace turns::core::tests \ No newline at end of file -- cgit v1.2.3