#include #include #include #include #include #include #include #include "renderive/base/observer/Observer.hpp" struct Observer_Test_Observation { int value{}; }; struct Observer_Test_Time_Source { std::shared_ptr> time{std::make_shared>(10)}; std::uint64_t now_ns() const noexcept { return time->fetch_add(5, std::memory_order_relaxed); } }; struct Observer_Test_Recorder { static constexpr bool enabled = true; std::shared_ptr value{std::make_shared()}; void observe(const Observer_Test_Observation& observation) noexcept { *value = observation.value; } }; using Observer_Test_State = Observer_State; static_assert(Observation_Struct); static_assert(Observer_Time_Source); static_assert(Struct_Observer); static_assert(Timed_Struct_Observer); TEST(observer_state_test, forwards_observation_structure) { Observer_Test_Recorder recorder; Observer_Test_State observer(recorder, Observer_Test_Time_Source{}); observer.observe(Observer_Test_Observation{7}); EXPECT_EQ(*recorder.value, 7); } TEST(observer_state_test, owns_time_source) { Observer_Test_State observer; EXPECT_EQ(observer.now_ns(), 10); EXPECT_EQ(observer.now_ns(), 15); } TEST(disabled_observer_test, accepts_any_observation_structure) { Observer_State<> observer; EXPECT_NO_THROW(observer.observe(Observer_Test_Observation{7})); EXPECT_GT(observer.now_ns(), 0); } struct Observer_Test_Concurrent_Time_Source_Data { std::atomic active{}; std::atomic overlap{}; std::uint64_t value{}; }; struct Observer_Test_Concurrent_Time_Source { std::shared_ptr data{std::make_shared()}; std::uint64_t now_ns() const noexcept { if (data->active.fetch_add(1, std::memory_order_acq_rel) != 0) { data->overlap.store(true, std::memory_order_release); } std::this_thread::sleep_for(std::chrono::milliseconds(2)); const auto value = data->value++; data->active.fetch_sub(1, std::memory_order_acq_rel); return value; } }; TEST(observer_state_test, serializes_time_source_access) { Observer_Test_Concurrent_Time_Source time_source; const auto data = time_source.data; Observer_State observer(Disabled_Observer{}, std::move(time_source)); std::atomic start{}; std::vector threads; for (int index = 0; index < 8; ++index) { threads.emplace_back([&] { while (!start.load(std::memory_order_acquire)) { std::this_thread::yield(); } observer.now_ns(); }); } start.store(true, std::memory_order_release); for (auto& thread : threads) { thread.join(); } EXPECT_FALSE(data->overlap.load(std::memory_order_acquire)); EXPECT_EQ(data->value, 8); } struct Observer_Test_Throwing_Move_Time_Source { Observer_Test_Throwing_Move_Time_Source() = default; Observer_Test_Throwing_Move_Time_Source(Observer_Test_Throwing_Move_Time_Source&&) { throw std::runtime_error("time source move failed"); } Observer_Test_Throwing_Move_Time_Source& operator=(Observer_Test_Throwing_Move_Time_Source&&) = delete; std::uint64_t now_ns() const noexcept { return 0; } }; TEST(observer_state_test, move_constructor_propagates_throwing_time_source_move) { using State = Observer_State; static_assert(std::move_constructible); static_assert(!std::is_nothrow_move_constructible_v); State source; EXPECT_THROW(State(std::move(source)), std::runtime_error); }