#include #include #include #include "State_Test_Types.hpp" TEST(double_state_strategy_test, builds_without_state_validator) { auto product = State_Plain_Product::Builder{}.set<&State_Plain_Value::count>(10).set<&State_Plain_Value::name>("alpha").build(); EXPECT_EQ(product->get<&State_Plain_Value::count>(), 10); EXPECT_EQ(product->get<&State_Plain_Value::name>(), "alpha"); } TEST(double_state_strategy_test, calls_optional_state_validator_when_present) { auto builder = State_Checked_Product::Builder{}.set<&State_Checked_Value::minimum>(10).set<&State_Checked_Value::maximum>(5); EXPECT_THROW(builder.build(1, "invalid"), std::invalid_argument); } TEST(double_state_strategy_test, keeps_cache_separate_until_publish) { State_Render_State_Reader render; auto product = State_Checked_Product::Builder{}.set<&State_Checked_Value::percent>(20).set<&State_Checked_Value::batch_size>(4).build(1, "state"); product->set<&State_Checked_Value::percent>(60); EXPECT_EQ(product->get<&State_Checked_Value::percent>(), 60); EXPECT_EQ(render.read(*product).percent.get(), 20); product->publish(); EXPECT_EQ(render.read(*product).percent.get(), 60); } TEST(double_state_strategy_test, keeps_acquired_render_reference_stable_until_publish) { State_Render_State_Reader render; State_Plain_Strategy strategy(State_Plain_Value{.count = 1, .name = "snapshot"}); const auto snapshot = render.read(strategy); strategy.set<&State_Plain_Value::count>(2); strategy.publish(); EXPECT_EQ(snapshot.count, 1); EXPECT_EQ(render.read(strategy).count, 2); } TEST(double_state_strategy_test, keeps_cached_value_when_runtime_validation_fails) { auto product = State_Checked_Product::Builder{}.set<&State_Checked_Value::percent>(20).set<&State_Checked_Value::batch_size>(4).build(1, "state"); EXPECT_THROW((product->set<&State_Checked_Value::percent>(200)), std::out_of_range); EXPECT_EQ(product->get<&State_Checked_Value::percent>(), 20); } TEST(double_state_strategy_test, supports_atomic_wait_mutex) { State_Render_State_Reader render; auto product = State_Wait_Product::Builder{State_Plain_Value{.count = 1, .name = "wait"}}.build(); product->set<&State_Plain_Value::count>(2); product->publish(); EXPECT_EQ(render.read(*product).count, 2); } TEST(double_state_strategy_test, passes_additional_builder_template_arguments) { static_assert(std::same_as); static_assert(std::same_as); auto product = State_Tagged_Product::Builder{State_Plain_Value{.count = 3, .name = "tagged"}}.build(); EXPECT_EQ(product->get<&State_Plain_Value::count>(), 3); } struct Double_State_Observer_Test_Time_Source { std::uint64_t value{100}; std::uint64_t now_ns() const noexcept { return value; } }; struct Double_State_Observer_Test_Data { std::uint64_t event_count{}; std::uint64_t publish_count{}; int value{}; }; struct Double_State_Observer_Test_Recorder { static constexpr bool enabled = true; std::shared_ptr data{std::make_shared()}; template void observe(const Observation& observation) noexcept { ++data->event_count; data->publish_count = observation.publish_count; data->value = observation.state.count; } }; TEST(double_state_strategy_test, notifies_observer_for_cache_update_and_publish) { using Observer = Observer_State; using Strategy = Double_State_Strategy; Double_State_Observer_Test_Recorder recorder; Strategy strategy(State_Plain_Value{}, With_Observer(Observer(recorder, Double_State_Observer_Test_Time_Source{}))); strategy.set<&State_Plain_Value::count>(8); strategy.publish(); EXPECT_EQ(recorder.data->event_count, 2); EXPECT_EQ(recorder.data->publish_count, 1); EXPECT_EQ(recorder.data->value, 8); } struct Double_State_Throwing_Assignment_State { int first{}; int second{}; Double_State_Throwing_Assignment_State() = default; Double_State_Throwing_Assignment_State(int first, int second) : first(first), second(second) {} Double_State_Throwing_Assignment_State(const Double_State_Throwing_Assignment_State&) = default; Double_State_Throwing_Assignment_State(Double_State_Throwing_Assignment_State&&) noexcept = default; Double_State_Throwing_Assignment_State& operator=(const Double_State_Throwing_Assignment_State& other) { first = other.first; if (throw_on_copy_assignment) { throw std::runtime_error("state assignment failed"); } second = other.second; return *this; } Double_State_Throwing_Assignment_State& operator=(Double_State_Throwing_Assignment_State&&) noexcept = default; inline static bool throw_on_copy_assignment{}; }; TEST(double_state_strategy_test, failed_publish_keeps_render_state_and_revision_unchanged) { State_Render_State_Reader render; using Strategy = Double_State_Strategy; Double_State_Throwing_Assignment_State::throw_on_copy_assignment = false; Strategy strategy(Double_State_Throwing_Assignment_State(1, 2)); strategy.set<&Double_State_Throwing_Assignment_State::first>(10); strategy.set<&Double_State_Throwing_Assignment_State::second>(20); Double_State_Throwing_Assignment_State::throw_on_copy_assignment = true; EXPECT_THROW(strategy.publish(), std::runtime_error); Double_State_Throwing_Assignment_State::throw_on_copy_assignment = false; EXPECT_EQ(strategy.state_revision(), 0); EXPECT_EQ(render.read(strategy).first, 1); EXPECT_EQ(render.read(strategy).second, 2); strategy.publish(); EXPECT_EQ(strategy.state_revision(), 1); EXPECT_EQ(render.read(strategy).first, 10); EXPECT_EQ(render.read(strategy).second, 20); }