feat: Add observable value util (#2831)

This implements a simple observable value. Can be used for a more
reactive approach. Will be used in ETL state and across the codebase
with time.
This commit is contained in:
Alex Kremer
2025-12-08 16:44:43 +00:00
committed by GitHub
parent 33dc4ad95a
commit 69b8e5bd06
4 changed files with 1712 additions and 0 deletions

View File

@@ -0,0 +1,426 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2025, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <boost/signals2/connection.hpp>
#include <boost/signals2/signal.hpp>
#include <boost/signals2/variadic_signal.hpp>
#include <atomic>
#include <concepts>
#include <type_traits>
namespace util {
template <typename T>
concept SomeAtomic = std::same_as<std::remove_cvref_t<T>, std::atomic<std::remove_cvref_t<typename T::value_type>>>;
/**
* @brief Concept defining types that can be observed for changes.
*
* A type is Observable if it satisfies all requirements for being stored
* and monitored in an ObservableValue container:
*
* - Must be equality comparable to detect changes
* - Must be copy constructible for capturing old values in guards
* - Must be move constructible for efficient value updates
*
* @note Copy assignment is intentionally not required since we use move semantics
* for value updates and only need copy construction for change detection.
*/
template <typename T>
concept Observable = std::equality_comparable<T> && std::copy_constructible<T> && std::move_constructible<T>;
namespace impl {
/**
* @brief Base class containing common ObservableValue functionality.
*
* This class contains all the observer management and notification logic
* that is shared between regular and atomic ObservableValue specializations.
*
* @tparam T The value type (for atomic specializations, this is the underlying type, not std::atomic<T>)
*/
template <Observable T>
class ObservableValueBase {
protected:
boost::signals2::signal<void(T const&)> onUpdate_;
public:
virtual ~ObservableValueBase() = default;
/**
* @brief Registers an observer callback for value changes.
* @param fn Callback function/lambda that accepts T const&
* @return Connection object for managing the subscription
*/
boost::signals2::connection
observe(std::invocable<T const&> auto&& fn)
{
return onUpdate_.connect(std::forward<decltype(fn)>(fn));
}
/**
* @brief Checks if there are any active observers.
* @return true if there are observers, false otherwise
*/
[[nodiscard]] bool
hasObservers() const
{
return not onUpdate_.empty();
}
/**
* @brief Forces notification of all observers with the current value.
*
* This method will notify all observers with the current value regardless
* of whether the value has changed since the last notification.
*/
virtual void
forceNotify() = 0;
protected:
/**
* @brief Notifies all observers with the given value.
* @param value The value to send to observers
*/
void
notifyObservers(T const& value)
{
onUpdate_(value);
}
};
} // namespace impl
// Forward declaration
template <typename T>
class ObservableValue;
/**
* @brief An observable value container that notifies observers when the value changes.
*
* ObservableValue wraps a value of type T and provides a mechanism to observe changes to that value.
* When the value is modified (and actually changes), all registered observers are notified.
*
* @tparam T The type of value to observe. Must satisfy the Observable concept.
*
* @par Thread Safety
* - Observer subscription/unsubscription (observe() and connection.disconnect()) are thread-safe
* - Value modification operations (set(), operator=) are NOT thread-safe and require external synchronization
* - Observer callbacks are invoked synchronously on the same thread that triggered the value change
* - If observers need to perform work on different threads, they must handle dispatch themselves
* (e.g., using an async execution context or message queue)
*
* @par Exception Handling
* - If an observer callback throws an exception, the exception will propagate to the caller
* - The value will still be updated even if observers throw exceptions
* - No guarantee is made about whether other observers will be called if one throws
* - It is the caller's responsibility to handle exceptions from observer callbacks
*/
template <Observable T>
requires(not SomeAtomic<T>)
class ObservableValue<T> : public impl::ObservableValueBase<T> {
T value_;
/**
* @brief RAII guard for deferred notification of value changes.
*
* ObservableGuard captures the current value when created and compares it
* with the final value when destroyed. If the values differ, observers
* are notified. This allows for multiple modifications to the value with
* only a single notification at the end.
*
* @note This class is returned by operator->() and should not be used directly.
*/
struct ObservableGuard {
T const oldValue; ///< Value captured at construction time
ObservableValue<T>& ref; ///< Reference to the observable value
/**
* @brief Constructs guard and captures current value.
* @param observable The ObservableValue to guard
*/
ObservableGuard(ObservableValue<T>& observable) : oldValue(observable), ref(observable)
{
}
/**
* @brief Destructor that triggers notification if value changed.
*
* Compares the captured value with the current value. If they differ,
* notifies all observers with the current value.
*/
~ObservableGuard()
{
if (oldValue != ref.value_)
ref.notifyObservers(ref.value_);
}
/**
* @brief Provides mutable access to the underlying value.
* @return Mutable reference to the wrapped value
*/
[[nodiscard]]
operator T&()
{
return ref.value_;
}
};
public:
/**
* @brief Constructs ObservableValue with initial value.
* @param value Initial value (must be convertible to T)
*/
ObservableValue(std::convertible_to<T> auto&& value) : value_{std::forward<decltype(value)>(value)}
{
}
/**
* @brief Constructs ObservableValue with default initial value.
*/
ObservableValue()
requires std::default_initializable<T>
: value_{}
{
}
ObservableValue(ObservableValue const&) = delete;
ObservableValue(ObservableValue&&) = default;
ObservableValue&
operator=(ObservableValue const&) = delete;
ObservableValue&
operator=(ObservableValue&&) = default;
/**
* @brief Assignment operator that updates value and notifies observers.
*
* Updates the stored value and notifies observers if the new value
* differs from the current value (using operator!=).
*
* @param val New value (must be convertible to T)
* @return Reference to this object for chaining
*
* @throws Any exception thrown by observer callbacks will propagate
*/
ObservableValue&
operator=(std::convertible_to<T> auto&& val)
{
set(val);
return *this;
}
/**
* @brief Provides deferred notification access to the value.
*
* Returns an ObservableGuard that allows modification of the value
* with notification deferred until the guard is destroyed.
*
* @return ObservableGuard for deferred notification
*/
[[nodiscard]] ObservableGuard
operator->()
{
return {*this};
}
/**
* @brief Implicit conversion to const reference of the value.
* @return Const reference to the stored value
*/
[[nodiscard]]
operator T const&() const
{
return value_;
}
/**
* @brief Explicitly gets the current value.
* @return Const reference to the stored value
*/
[[nodiscard]] T const&
get() const
{
return value_;
}
/**
* @brief Sets a new value and notifies observers if changed.
*
* Updates the stored value and notifies all observers if the new value
* differs from the current value (using operator!=). If the values are
* equal, no notification occurs.
*
* @param val New value (must be convertible to T)
*
* @throws Any exception thrown by observer callbacks will propagate
*
* @par Thread Safety
* - This method is NOT thread-safe and requires external synchronization for concurrent access
* - Observer callbacks are invoked synchronously on the calling thread
*/
void
set(std::convertible_to<T> auto&& val)
{
if (value_ != val) {
value_ = std::forward<decltype(val)>(val);
this->notifyObservers(value_);
}
}
/**
* @brief Forces notification of all observers with the current value.
*
* This method will notify all observers with the current value regardless
* of whether the value has changed since the last notification.
*/
void
forceNotify() override
{
this->notifyObservers(value_);
}
};
/**
* @brief Partial specialization of ObservableValue for atomic types.
*
* This specialization provides thread-safe observation of atomic values while
* maintaining atomic semantics. It avoids the issues of copying atomic values
* and handles race conditions properly.
*
* @tparam T The underlying type stored in the atomic
*
* @par Thread Safety
* - All operations are thread-safe
* - Observer notifications are atomic with respect to value changes
* - Multiple threads can safely modify and observe the atomic value
*
* @par Performance Considerations
* - Uses atomic compare-and-swap operations for updates
* - Minimizes atomic reads during guard operations
* - Observer notifications happen outside of atomic operations when possible
*/
template <Observable T>
class ObservableValue<std::atomic<T>> : public impl::ObservableValueBase<T> {
std::atomic<T> value_;
public:
/**
* @brief Constructs ObservableValue with initial atomic value.
* @param value Initial value (will be stored in the atomic)
*/
ObservableValue(std::convertible_to<T> auto&& value) : value_{std::forward<decltype(value)>(value)}
{
}
/**
* @brief Constructs ObservableValue with default initial value.
*/
ObservableValue()
requires std::default_initializable<T>
: value_{}
{
}
ObservableValue(ObservableValue const&) = delete;
ObservableValue(ObservableValue&&) = default;
ObservableValue&
operator=(ObservableValue const&) = delete;
ObservableValue&
operator=(ObservableValue&&) = default;
/**
* @brief Assignment operator that updates atomic value and notifies observers.
*
* Uses atomic compare-and-swap to update the value and notifies observers
* only if the value actually changed.
*
* @param val New value
* @return Reference to this object for chaining
*/
ObservableValue&
operator=(std::convertible_to<T> auto&& val)
{
set(std::forward<decltype(val)>(val));
return *this;
}
/**
* @brief Gets the current atomic value.
* @return Current value stored in the atomic
*/
[[nodiscard]] T
get() const
{
return value_.load();
}
/**
* @brief Implicit conversion to the current atomic value.
* @return Current value stored in the atomic
*/
[[nodiscard]]
operator T() const
{
return get();
}
/**
* @brief Sets a new atomic value and notifies observers if changed.
*
* Uses atomic compare-and-swap to update the value. Notifies all observers
* if the value actually changed.
*
* @param val New value
*/
void
set(std::convertible_to<T> auto&& val)
{
T newValue = std::forward<decltype(val)>(val);
T oldValue = value_.load();
// Use compare-and-swap to atomically update
while (!value_.compare_exchange_weak(oldValue, newValue)) {
// compare_exchange_weak updates oldValue with current value on failure
// Continue until we succeed
}
// Notify observers if we actually changed the value
// Note: oldValue now contains the actual previous value that was replaced
if (oldValue != newValue) {
this->notifyObservers(newValue);
}
}
/**
* @brief Forces notification of all observers with the current value.
*
* This method will notify all observers with the current atomic value
* regardless of whether the value has changed since the last notification.
*/
void
forceNotify() override
{
this->notifyObservers(value_.load());
}
};
} // namespace util

View File

@@ -169,6 +169,8 @@ target_sources(
util/BytesConverterTests.cpp
util/CoroutineTest.cpp
util/MoveTrackerTests.cpp
util/ObservableValueTest.cpp
util/ObservableValueAtomicTest.cpp
util/RandomTests.cpp
util/RepeatTests.cpp
util/ResponseExpirationCacheTests.cpp

View File

@@ -0,0 +1,446 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2025, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include "util/ObservableValue.hpp"
#include <boost/signals2/connection.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <atomic>
#include <chrono>
#include <functional>
#include <mutex>
#include <thread>
#include <vector>
using namespace testing;
using namespace util;
namespace {
} // namespace
class ObservableValueAtomicTest : public ::testing::Test {};
TEST_F(ObservableValueAtomicTest, BasicConstruction)
{
ObservableValue<std::atomic<int>> obs{42};
EXPECT_EQ(obs.get(), 42);
EXPECT_EQ(static_cast<int>(obs), 42);
EXPECT_FALSE(obs.hasObservers());
}
TEST_F(ObservableValueAtomicTest, DefaultConstruction)
{
ObservableValue<std::atomic<int>> obsInt;
EXPECT_EQ(obsInt.get(), 0);
ObservableValue<std::atomic<bool>> obsBool;
EXPECT_FALSE(obsBool.get());
EXPECT_FALSE(obsInt.hasObservers());
EXPECT_FALSE(obsBool.hasObservers());
}
TEST_F(ObservableValueAtomicTest, BasicObservation)
{
ObservableValue<std::atomic<int>> obs{10};
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(mockObserver, Call(20));
obs = 20;
EXPECT_EQ(obs.get(), 20);
}
TEST_F(ObservableValueAtomicTest, SetMethod)
{
ObservableValue<std::atomic<int>> obs{5};
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(mockObserver, Call(15));
obs.set(15);
EXPECT_EQ(obs.get(), 15);
obs.set(15); // Same value should not notify
EXPECT_EQ(obs.get(), 15);
}
TEST_F(ObservableValueAtomicTest, AtomicBasicUsage)
{
ObservableValue<std::atomic<int>> obs{10};
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(mockObserver, Call(25));
obs.set(25);
EXPECT_EQ(obs.get(), 25);
}
TEST_F(ObservableValueAtomicTest, AtomicMultipleChanges)
{
ObservableValue<std::atomic<int>> obs{50};
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(mockObserver, Call(100)); // First change: 50→100
EXPECT_CALL(mockObserver, Call(50)); // Second change: 100→50
obs.set(100); // Should notify: 50→100
obs.set(50); // Should notify: 100→50
EXPECT_EQ(obs.get(), 50);
}
TEST_F(ObservableValueAtomicTest, AtomicNoChangeNoNotification)
{
ObservableValue<std::atomic<int>> obs{42};
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
// No EXPECT_CALL since no notification should occur
obs.set(42); // Same value, should not notify
obs.set(42); // Same value again, should not notify
EXPECT_EQ(obs.get(), 42);
}
TEST_F(ObservableValueAtomicTest, AtomicSequentialChanges)
{
ObservableValue<std::atomic<int>> obs{1};
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(mockObserver, Call(2));
obs.set(2);
EXPECT_CALL(mockObserver, Call(3));
obs.set(3);
EXPECT_EQ(obs.get(), 3);
}
TEST_F(ObservableValueAtomicTest, MultipleObservers)
{
ObservableValue<std::atomic<int>> obs{0};
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver1;
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver2;
auto conn1 = obs.observe(mockObserver1.AsStdFunction());
auto conn2 = obs.observe(mockObserver2.AsStdFunction());
EXPECT_CALL(mockObserver1, Call(42));
EXPECT_CALL(mockObserver2, Call(42));
obs = 42;
conn1.disconnect();
EXPECT_CALL(mockObserver2, Call(100));
obs = 100;
}
TEST_F(ObservableValueAtomicTest, ThreadSafetyBasic)
{
ObservableValue<std::atomic<int>> obs{0};
std::atomic<int> notificationCount{0};
std::vector<int> values;
std::mutex valuesMutex;
auto connection = obs.observe([&](int const& value) {
notificationCount.fetch_add(1);
std::lock_guard<std::mutex> lock(valuesMutex);
values.push_back(value);
});
static constexpr auto kNUM_THREADS = 4;
static constexpr auto kINCREMENTS_PER_THREAD = 100;
std::vector<std::thread> threads;
threads.reserve(kNUM_THREADS);
for (int i = 0; i < kNUM_THREADS; ++i) {
threads.emplace_back([&obs]() {
for (int j = 0; j < kINCREMENTS_PER_THREAD; ++j) {
int expected = obs.get();
int newValue = expected + 1;
obs.set(newValue);
std::this_thread::sleep_for(std::chrono::microseconds(1));
}
});
}
for (auto& thread : threads)
thread.join();
// Final value may be less than kNumThreads * kIncrementsPerThread due to race conditions
EXPECT_GT(obs.get(), 0);
EXPECT_GT(notificationCount.load(), 0);
std::lock_guard<std::mutex> lock(valuesMutex);
for (auto const& value : values) {
EXPECT_GT(value, 0);
}
}
TEST_F(ObservableValueAtomicTest, ThreadSafetyWithDirectAccess)
{
ObservableValue<std::atomic<int>> obs{0};
std::atomic<int> notificationCount{0};
auto connection = obs.observe([&](int const&) { notificationCount.fetch_add(1); });
static constexpr auto kNUM_THREADS = 4;
static constexpr auto kOPERATIONS_PER_THREAD = 50;
std::vector<std::thread> threads;
threads.reserve(kNUM_THREADS);
for (int i = 0; i < kNUM_THREADS; ++i) {
threads.emplace_back([&obs]() {
for (int j = 0; j < kOPERATIONS_PER_THREAD; ++j) {
int current = obs.get();
obs.set(current + 1);
std::this_thread::sleep_for(std::chrono::microseconds(1));
}
});
}
for (auto& thread : threads)
thread.join();
EXPECT_GT(obs.get(), 0);
EXPECT_GT(notificationCount.load(), 0);
}
TEST_F(ObservableValueAtomicTest, AtomicBoolSpecialization)
{
ObservableValue<std::atomic<bool>> obs{false};
testing::StrictMock<testing::MockFunction<void(bool const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(mockObserver, Call(true));
obs = true;
EXPECT_TRUE(obs.get());
obs = true; // Same value should not notify
EXPECT_CALL(mockObserver, Call(false));
obs.set(false);
EXPECT_FALSE(obs.get());
}
TEST_F(ObservableValueAtomicTest, CompareAndSwapBehavior)
{
ObservableValue<std::atomic<int>> obs{10};
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
// Test that compare-and-swap works correctly in set()
obs.set(10); // Same value, should not notify
EXPECT_CALL(mockObserver, Call(20));
obs.set(20); // Different value, should notify
}
TEST_F(ObservableValueAtomicTest, RaceConditionNotificationIntegrity)
{
ObservableValue<std::atomic<int>> obs{0};
std::atomic<int> notificationCount{0};
std::vector<int> values;
std::mutex valuesMutex;
auto connection = obs.observe([&](int const& value) {
notificationCount.fetch_add(1);
std::lock_guard<std::mutex> lock(valuesMutex);
values.push_back(value);
});
static constexpr auto kNUM_THREADS = 10;
static constexpr auto kOPERATIONS_PER_THREAD = 20;
std::vector<std::thread> threads;
threads.reserve(kNUM_THREADS);
for (int i = 0; i < kNUM_THREADS; ++i) {
threads.emplace_back([&obs]() {
for (int j = 0; j < kOPERATIONS_PER_THREAD; ++j) {
obs.set(j % 3);
std::this_thread::sleep_for(std::chrono::microseconds(1));
}
});
}
for (auto& thread : threads)
thread.join();
EXPECT_GT(notificationCount.load(), 0);
std::lock_guard<std::mutex> lock(valuesMutex);
for (auto const& value : values) {
EXPECT_GE(value, 0);
EXPECT_LE(value, 2);
}
int finalValue = obs.get();
EXPECT_GE(finalValue, 0);
EXPECT_LE(finalValue, 2);
}
TEST_F(ObservableValueAtomicTest, DeterministicNotificationTest)
{
ObservableValue<std::atomic<int>> obs{0};
std::atomic<int> notificationCount{0};
std::vector<int> values;
std::mutex valuesMutex;
auto connection = obs.observe([&](int const& value) {
notificationCount.fetch_add(1);
std::lock_guard<std::mutex> lock(valuesMutex);
values.push_back(value);
});
static constexpr auto kNUM_THREADS = 5;
std::vector<std::thread> threads;
threads.reserve(kNUM_THREADS);
for (int i = 0; i < kNUM_THREADS; ++i) {
threads.emplace_back([&obs, i]() { obs.set(i + 1); });
}
for (auto& thread : threads)
thread.join();
// Each thread sets a unique value, so expect exactly kNumThreads notifications
EXPECT_EQ(notificationCount.load(), kNUM_THREADS);
std::lock_guard<std::mutex> lock(valuesMutex);
EXPECT_EQ(values.size(), kNUM_THREADS);
for (auto const& value : values) {
EXPECT_GE(value, 1);
EXPECT_LE(value, kNUM_THREADS);
}
int finalValue = obs.get();
EXPECT_GE(finalValue, 1);
EXPECT_LE(finalValue, kNUM_THREADS);
}
TEST_F(ObservableValueAtomicTest, NoNotificationForSameValue)
{
ObservableValue<std::atomic<int>> obs{42};
std::atomic<int> notificationCount{0};
auto connection = obs.observe([&](int const&) { notificationCount.fetch_add(1); });
static constexpr auto kNUM_THREADS = 10;
std::vector<std::thread> threads;
threads.reserve(kNUM_THREADS);
for (int i = 0; i < kNUM_THREADS; ++i) {
threads.emplace_back([&obs]() { obs.set(42); });
}
for (auto& thread : threads)
thread.join();
EXPECT_EQ(notificationCount.load(), 0); // No notifications since value never changed
EXPECT_EQ(obs.get(), 42);
}
TEST_F(ObservableValueAtomicTest, AtomicRaceConditionCorrectness)
{
ObservableValue<std::atomic<int>> obs{0};
std::atomic<int> notificationCount{0};
std::vector<int> values;
std::mutex valuesMutex;
auto connection = obs.observe([&](int const& value) {
notificationCount.fetch_add(1);
std::lock_guard<std::mutex> lock(valuesMutex);
values.push_back(value);
});
static constexpr auto kNUM_THREADS = 3;
std::vector<std::thread> threads;
threads.reserve(kNUM_THREADS);
// Test that direct access properly notifies for all value changes
// Each thread will make unique changes to avoid race condition conflicts
for (int i = 0; i < kNUM_THREADS; ++i) {
threads.emplace_back([&obs, i]() {
int baseValue = (i + 1) * 10; // 10, 20, 30
obs.set(baseValue); // Store unique values
obs.set(baseValue + 1); // Then increment
});
}
for (auto& thread : threads)
thread.join();
// We should get some notifications (exact count depends on race conditions)
// but at least one per thread since they use unique base values
EXPECT_GE(notificationCount.load(), kNUM_THREADS);
std::lock_guard<std::mutex> lock(valuesMutex);
EXPECT_GE(values.size(), kNUM_THREADS);
for (auto const& value : values)
EXPECT_GT(value, 0);
}
TEST_F(ObservableValueAtomicTest, ForceNotify)
{
ObservableValue<std::atomic<int>> obs{42};
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver;
obs.forceNotify();
auto connection = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(mockObserver, Call(42));
obs.forceNotify();
EXPECT_CALL(mockObserver, Call(42));
obs.forceNotify();
EXPECT_CALL(mockObserver, Call(100));
obs.set(100);
EXPECT_CALL(mockObserver, Call(100));
obs.forceNotify();
EXPECT_CALL(mockObserver, Call(100)).Times(3);
obs.forceNotify();
obs.forceNotify();
obs.forceNotify();
}

View File

@@ -0,0 +1,838 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2025, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include "util/ObservableValue.hpp"
#include <boost/signals2/connection.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <concepts>
#include <map>
#include <set>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
using namespace testing;
using namespace util;
namespace {
struct TestStruct {
int value = 0;
std::string name;
bool
operator==(TestStruct const& other) const
{
return value == other.value && name == other.name;
}
bool
operator!=(TestStruct const& other) const
{
return !(*this == other);
}
};
} // namespace
class ObservableValueTest : public ::testing::Test {};
TEST_F(ObservableValueTest, ConceptCompliance)
{
static_assert(Observable<int>);
static_assert(Observable<std::string>);
static_assert(Observable<double>);
static_assert(Observable<TestStruct>);
static_assert(Observable<bool>);
static_assert(Observable<char>);
static_assert(Observable<float>);
struct NonCopyable {
int value = 0;
NonCopyable() = default;
NonCopyable(NonCopyable const&) = delete;
NonCopyable(NonCopyable&&) = default;
NonCopyable&
operator=(NonCopyable const&) = delete;
NonCopyable&
operator=(NonCopyable&&) = default;
bool
operator==(NonCopyable const& other) const
{
return value == other.value;
}
};
static_assert(!Observable<NonCopyable>);
struct NonMovable {
int value = 0;
NonMovable() = default;
NonMovable(NonMovable const&) = default;
NonMovable(NonMovable&&) = delete;
NonMovable&
operator=(NonMovable const&) = default;
NonMovable&
operator=(NonMovable&&) = delete;
bool
operator==(NonMovable const& other) const
{
return value == other.value;
}
};
static_assert(!Observable<NonMovable>);
struct NonComparable {
int value = 0;
NonComparable() = default;
NonComparable(NonComparable const&) = default;
NonComparable(NonComparable&&) = default;
NonComparable&
operator=(NonComparable const&) = default;
NonComparable&
operator=(NonComparable&&) = default;
};
static_assert(!Observable<NonComparable>);
struct NonDefaultInitializable {
int value;
NonDefaultInitializable() = delete;
explicit NonDefaultInitializable(int v) : value(v)
{
}
NonDefaultInitializable(NonDefaultInitializable const&) = default;
NonDefaultInitializable(NonDefaultInitializable&&) = default;
NonDefaultInitializable&
operator=(NonDefaultInitializable const&) = default;
NonDefaultInitializable&
operator=(NonDefaultInitializable&&) = default;
bool
operator==(NonDefaultInitializable const& other) const
{
return value == other.value;
}
};
static_assert(Observable<NonDefaultInitializable>);
static_assert(!std::default_initializable<NonDefaultInitializable>);
static_assert(Observable<std::vector<int>>);
static_assert(Observable<std::map<int, int>>);
static_assert(Observable<std::set<int>>);
static_assert(Observable<std::pair<int, std::string>>);
static_assert(std::default_initializable<int>);
static_assert(std::default_initializable<std::string>);
static_assert(std::default_initializable<std::vector<int>>);
static_assert(std::default_initializable<TestStruct>);
}
TEST_F(ObservableValueTest, Construction)
{
ObservableValue<int> obs{42};
EXPECT_EQ(static_cast<int>(obs), 42);
EXPECT_EQ(obs.get(), 42);
EXPECT_FALSE(obs.hasObservers());
}
TEST_F(ObservableValueTest, ConstructionWithDifferentTypes)
{
ObservableValue<std::string> obsStr{"hello"};
EXPECT_EQ(obsStr.get(), "hello");
ObservableValue<double> obsDouble{3.14};
EXPECT_DOUBLE_EQ(obsDouble.get(), 3.14);
ObservableValue<bool> obsBool{true};
EXPECT_TRUE(obsBool.get());
}
TEST_F(ObservableValueTest, DefaultConstruction)
{
ObservableValue<int> obsInt;
EXPECT_EQ(obsInt.get(), 0);
ObservableValue<double> obsDouble;
EXPECT_DOUBLE_EQ(obsDouble.get(), 0.0);
ObservableValue<bool> obsBool;
EXPECT_FALSE(obsBool.get());
ObservableValue<char> obsChar;
EXPECT_EQ(obsChar.get(), '\0');
EXPECT_FALSE(obsInt.hasObservers());
EXPECT_FALSE(obsDouble.hasObservers());
EXPECT_FALSE(obsBool.hasObservers());
EXPECT_FALSE(obsChar.hasObservers());
}
TEST_F(ObservableValueTest, DefaultConstructionWithContainers)
{
ObservableValue<std::string> obsString;
EXPECT_EQ(obsString.get(), "");
EXPECT_TRUE(obsString.get().empty());
ObservableValue<std::vector<int>> obsVector;
EXPECT_TRUE(obsVector.get().empty());
EXPECT_EQ(obsVector.get().size(), 0);
ObservableValue<std::set<int>> obsSet;
EXPECT_TRUE(obsSet.get().empty());
EXPECT_EQ(obsSet.get().size(), 0);
ObservableValue<std::map<int, std::string>> obsMap;
EXPECT_TRUE(obsMap.get().empty());
EXPECT_EQ(obsMap.get().size(), 0);
}
TEST_F(ObservableValueTest, DefaultConstructionWithCustomType)
{
ObservableValue<TestStruct> obsStruct;
EXPECT_EQ(obsStruct.get().value, 0);
EXPECT_EQ(obsStruct.get().name, "");
}
TEST_F(ObservableValueTest, DefaultConstructionThenAssignment)
{
ObservableValue<int> obs;
EXPECT_EQ(obs.get(), 0);
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(mockObserver, Call(42));
obs = 42;
EXPECT_EQ(obs.get(), 42);
obs = 42; // Same value, should not notify
EXPECT_CALL(mockObserver, Call(100));
obs.set(100);
EXPECT_EQ(obs.get(), 100);
}
TEST_F(ObservableValueTest, DefaultConstructionWithGuard)
{
ObservableValue<std::string> obs;
EXPECT_EQ(obs.get(), "");
testing::StrictMock<testing::MockFunction<void(std::string const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(mockObserver, Call("modified through guard"));
{
auto guard = obs.operator->();
std::string& ref = guard;
ref = "modified through guard";
}
EXPECT_EQ(obs.get(), "modified through guard");
}
TEST_F(ObservableValueTest, DefaultConstructionNotificationBehavior)
{
ObservableValue<int> obs;
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(mockObserver, Call(1));
obs = 1;
EXPECT_CALL(mockObserver, Call(0));
obs = 0;
obs = 0; // Same value, should not notify
}
TEST_F(ObservableValueTest, NonDefaultInitializableTypeWithParameterizedConstructor)
{
struct NonDefaultInitializable {
int value;
NonDefaultInitializable() = delete;
explicit NonDefaultInitializable(int v) : value(v)
{
}
NonDefaultInitializable(NonDefaultInitializable const&) = default;
NonDefaultInitializable(NonDefaultInitializable&&) = default;
NonDefaultInitializable&
operator=(NonDefaultInitializable const&) = default;
NonDefaultInitializable&
operator=(NonDefaultInitializable&&) = default;
bool
operator==(NonDefaultInitializable const& other) const
{
return value == other.value;
}
};
ObservableValue<NonDefaultInitializable> obs{NonDefaultInitializable{42}};
EXPECT_EQ(obs.get().value, 42);
testing::StrictMock<testing::MockFunction<void(NonDefaultInitializable const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(mockObserver, Call(testing::Field(&NonDefaultInitializable::value, 100)));
obs = NonDefaultInitializable{100};
EXPECT_EQ(obs.get().value, 100);
}
TEST_F(ObservableValueTest, MoveSemantics)
{
ObservableValue<int> obs1{100};
ObservableValue<int> obs2 = std::move(obs1);
EXPECT_EQ(obs2.get(), 100);
ObservableValue<int> obs3{200};
obs3 = std::move(obs2);
EXPECT_EQ(obs3.get(), 100);
}
TEST_F(ObservableValueTest, CopyOperationsDeleted)
{
static_assert(!std::is_copy_constructible_v<ObservableValue<int>>);
static_assert(!std::is_copy_assignable_v<ObservableValue<int>>);
}
TEST_F(ObservableValueTest, AssignmentOperator)
{
ObservableValue<int> obs{10};
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(mockObserver, Call(20));
obs = 20;
EXPECT_EQ(obs.get(), 20);
obs = 20; // Same value, should not notify
EXPECT_EQ(obs.get(), 20);
}
TEST_F(ObservableValueTest, SetMethod)
{
ObservableValue<int> obs{5};
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(mockObserver, Call(15));
obs.set(15);
EXPECT_EQ(obs.get(), 15);
obs.set(15); // Same value, should not notify
EXPECT_EQ(obs.get(), 15);
}
TEST_F(ObservableValueTest, ObserverManagement)
{
ObservableValue<int> obs{0};
EXPECT_FALSE(obs.hasObservers());
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver1;
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver2;
auto conn1 = obs.observe(mockObserver1.AsStdFunction());
EXPECT_TRUE(obs.hasObservers());
auto conn2 = obs.observe(mockObserver2.AsStdFunction());
EXPECT_TRUE(obs.hasObservers());
EXPECT_CALL(mockObserver1, Call(42));
EXPECT_CALL(mockObserver2, Call(42));
obs = 42;
conn1.disconnect();
EXPECT_CALL(mockObserver2, Call(100));
obs = 100;
conn2.disconnect();
EXPECT_FALSE(obs.hasObservers());
obs = 200; // No observers, no calls expected
}
TEST_F(ObservableValueTest, ObservableGuardBasicUsage)
{
ObservableValue<int> obs{10};
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(mockObserver, Call(25));
{
auto guard = obs.operator->();
int& ref = guard;
ref = 25;
}
EXPECT_EQ(obs.get(), 25);
}
TEST_F(ObservableValueTest, ObservableGuardNoChangeNoNotification)
{
ObservableValue<int> obs{50};
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
// No EXPECT_CALL since no notification should occur
{
auto guard = obs.operator->();
int& ref = guard;
ref = 100;
ref = 50; // Back to original value
}
EXPECT_EQ(obs.get(), 50);
}
TEST_F(ObservableValueTest, ObservableGuardMultipleChanges)
{
ObservableValue<int> obs{1};
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(mockObserver, Call(2));
{
auto guard = obs.operator->();
int& ref = guard;
ref = 2;
}
EXPECT_CALL(mockObserver, Call(3));
{
auto guard = obs.operator->();
int& ref = guard;
ref = 3;
}
EXPECT_EQ(obs.get(), 3);
}
TEST_F(ObservableValueTest, ComplexTypeObservation)
{
TestStruct initial{.value = 42, .name = "test"};
ObservableValue<TestStruct> obs{initial};
testing::StrictMock<testing::MockFunction<void(TestStruct const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
TestStruct newValue{.value = 100, .name = "changed"};
EXPECT_CALL(
mockObserver,
Call(testing::AllOf(testing::Field(&TestStruct::value, 100), testing::Field(&TestStruct::name, "changed")))
);
obs = newValue;
}
TEST_F(ObservableValueTest, ComplexTypeGuardModification)
{
TestStruct initial{.value = 10, .name = "initial"};
ObservableValue<TestStruct> obs{initial};
testing::StrictMock<testing::MockFunction<void(TestStruct const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(
mockObserver,
Call(testing::AllOf(testing::Field(&TestStruct::value, 20), testing::Field(&TestStruct::name, "modified")))
);
{
auto guard = obs.operator->();
TestStruct& ref = guard;
ref.value = 20;
ref.name = "modified";
}
EXPECT_EQ(obs.get().value, 20);
EXPECT_EQ(obs.get().name, "modified");
}
TEST_F(ObservableValueTest, StringObservation)
{
ObservableValue<std::string> obs{"initial"};
testing::StrictMock<testing::MockFunction<void(std::string const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(mockObserver, Call("changed"));
obs = "changed";
EXPECT_CALL(mockObserver, Call("set_method"));
obs.set("set_method");
obs = "set_method"; // Same value, should not notify
}
TEST_F(ObservableValueTest, MultipleObserversWithDifferentLifetimes)
{
ObservableValue<int> obs{0};
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver1;
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver2;
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver3;
auto conn1 = obs.observe(mockObserver1.AsStdFunction());
EXPECT_CALL(mockObserver1, Call(1));
obs = 1;
auto conn2 = obs.observe(mockObserver2.AsStdFunction());
EXPECT_CALL(mockObserver1, Call(2));
EXPECT_CALL(mockObserver2, Call(2));
obs = 2;
conn1.disconnect();
auto conn3 = obs.observe(mockObserver3.AsStdFunction());
EXPECT_CALL(mockObserver2, Call(3));
EXPECT_CALL(mockObserver3, Call(3));
obs = 3;
}
TEST_F(ObservableValueTest, NoNotificationWhenNoObservers)
{
ObservableValue<int> obs{0};
obs = 1;
obs.set(2);
{
auto guard = obs.operator->();
int& ref = guard;
ref = 3;
}
EXPECT_EQ(obs.get(), 3);
EXPECT_FALSE(obs.hasObservers());
}
TEST_F(ObservableValueTest, ManyObservers)
{
ObservableValue<int> obs{0};
std::vector<std::unique_ptr<testing::StrictMock<testing::MockFunction<void(int const&)>>>> mockObservers;
std::vector<boost::signals2::connection> connections;
constexpr int kNUM_OBSERVERS = 100;
for (int i = 0; i < kNUM_OBSERVERS; ++i) {
mockObservers.push_back(std::make_unique<testing::StrictMock<testing::MockFunction<void(int const&)>>>());
connections.push_back(obs.observe(mockObservers.back()->AsStdFunction()));
}
EXPECT_TRUE(obs.hasObservers());
for (auto const& mockObserver : mockObservers) {
EXPECT_CALL(*mockObserver, Call(42));
}
obs = 42;
for (auto& conn : connections) {
conn.disconnect();
}
EXPECT_FALSE(obs.hasObservers());
}
TEST_F(ObservableValueTest, TypeConversions)
{
ObservableValue<double> obs{1.0};
testing::StrictMock<testing::MockFunction<void(double const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(mockObserver, Call(testing::DoubleEq(2.0)));
obs = 2;
EXPECT_CALL(mockObserver, Call(testing::DoubleEq(3.14)));
obs = 3.14;
EXPECT_CALL(mockObserver, Call(testing::DoubleEq(4.0)));
obs = static_cast<double>(4.0f);
}
TEST_F(ObservableValueTest, EnhancedConceptRequirements)
{
struct ComplexObservable {
std::string name;
int value{};
std::vector<int> data;
ComplexObservable() = default;
ComplexObservable(std::string n, int v, std::vector<int> d) : name(std::move(n)), value(v), data(std::move(d))
{
}
ComplexObservable(ComplexObservable const& other) = default;
ComplexObservable(ComplexObservable&& other) noexcept = default;
ComplexObservable&
operator=(ComplexObservable&& other) noexcept
{
if (this != &other) {
name = std::move(other.name);
value = other.value;
data = std::move(other.data);
}
return *this;
}
bool
operator==(ComplexObservable const& other) const
{
return name == other.name && value == other.value && data == other.data;
}
ComplexObservable&
operator=(ComplexObservable const& other)
{
if (this != &other) {
name = other.name;
value = other.value;
data = other.data;
}
return *this;
}
};
static_assert(Observable<ComplexObservable>);
ComplexObservable initial{"test", 42, {1, 2, 3}};
ObservableValue<ComplexObservable> obs{std::move(initial)};
testing::StrictMock<testing::MockFunction<void(ComplexObservable const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
ComplexObservable newValue{"changed", 100, {4, 5, 6}};
EXPECT_CALL(
mockObserver,
Call(
testing::AllOf(
testing::Field(&ComplexObservable::name, "changed"),
testing::Field(&ComplexObservable::value, 100),
testing::Field(&ComplexObservable::data, std::vector<int>({4, 5, 6}))
)
)
);
obs = std::move(newValue);
ComplexObservable sameValue{"changed", 100, {4, 5, 6}};
obs = std::move(sameValue); // Same value, should not notify
}
TEST_F(ObservableValueTest, ExceptionInObserver)
{
ObservableValue<int> obs{0};
testing::StrictMock<testing::MockFunction<void(int const&)>> goodMockObserver;
auto goodConnection = obs.observe(goodMockObserver.AsStdFunction());
auto throwingConnection = obs.observe([](int const&) { throw std::runtime_error("Observer exception"); });
EXPECT_CALL(goodMockObserver, Call(42));
EXPECT_THROW(obs = 42, std::runtime_error);
// Value is still updated even when observers throw
EXPECT_EQ(obs.get(), 42);
}
TEST_F(ObservableValueTest, GuardExceptionSafety)
{
ObservableValue<int> obs{10};
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(mockObserver, Call(20));
try {
auto guard = obs.operator->();
int& ref = guard;
ref = 20;
throw std::runtime_error("Test exception");
} catch (...) {
[[maybe_unused]] auto nothing = true;
}
EXPECT_EQ(obs.get(), 20);
}
TEST_F(ObservableValueTest, ComprehensiveIntegrationTest)
{
ObservableValue<std::string> obs{"start"};
testing::StrictMock<testing::MockFunction<void(std::string const&)>> mockObserver1;
testing::StrictMock<testing::MockFunction<void(std::string const&)>> mockObserver2;
auto conn1 = obs.observe(mockObserver1.AsStdFunction());
auto conn2 = obs.observe(mockObserver2.AsStdFunction());
EXPECT_CALL(mockObserver1, Call("first"));
EXPECT_CALL(mockObserver2, Call("first"));
obs = "first";
EXPECT_CALL(mockObserver1, Call("second"));
EXPECT_CALL(mockObserver2, Call("second"));
obs.set("second");
obs = "second"; // Same value, should not notify
EXPECT_CALL(mockObserver1, Call("third"));
EXPECT_CALL(mockObserver2, Call("third"));
{
auto guard = obs.operator->();
std::string& ref = guard;
ref = "third";
}
conn1.disconnect();
EXPECT_CALL(mockObserver2, Call("fourth"));
obs = "fourth";
EXPECT_EQ(obs.get(), "fourth");
EXPECT_TRUE(obs.hasObservers());
conn2.disconnect();
EXPECT_FALSE(obs.hasObservers());
}
TEST_F(ObservableValueTest, RegularConnectionPersistsAfterDestruction)
{
ObservableValue<int> obs{0};
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver;
{
auto connection = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(mockObserver, Call(1));
obs = 1;
}
EXPECT_CALL(mockObserver, Call(2));
obs = 2;
EXPECT_TRUE(obs.hasObservers());
}
TEST_F(ObservableValueTest, ScopedConnectionDisconnectsOnDestruction)
{
ObservableValue<int> obs{0};
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver;
{
boost::signals2::scoped_connection scoped = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(mockObserver, Call(1));
obs = 1;
EXPECT_TRUE(obs.hasObservers());
}
obs = 2; // No call expected since connection was destroyed
EXPECT_FALSE(obs.hasObservers());
}
TEST_F(ObservableValueTest, ManualDisconnectWithRegularConnection)
{
ObservableValue<int> obs{0};
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(mockObserver, Call(1));
obs = 1;
EXPECT_TRUE(obs.hasObservers());
connection.disconnect();
obs = 2; // No call expected since connection was disconnected
EXPECT_FALSE(obs.hasObservers());
}
TEST_F(ObservableValueTest, ScopedConnectionCanBeDisconnectedManually)
{
ObservableValue<int> obs{0};
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver;
boost::signals2::scoped_connection scoped = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(mockObserver, Call(1));
obs = 1;
EXPECT_TRUE(obs.hasObservers());
scoped.disconnect();
obs = 2; // No call expected since connection was disconnected
EXPECT_FALSE(obs.hasObservers());
}
TEST_F(ObservableValueTest, MixedConnectionTypes)
{
ObservableValue<int> obs{0};
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver1;
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver2;
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver3;
auto regularConn = obs.observe(mockObserver1.AsStdFunction());
{
boost::signals2::scoped_connection scoped1 = obs.observe(mockObserver2.AsStdFunction());
boost::signals2::scoped_connection scoped2 = obs.observe(mockObserver3.AsStdFunction());
EXPECT_CALL(mockObserver1, Call(1));
EXPECT_CALL(mockObserver2, Call(1));
EXPECT_CALL(mockObserver3, Call(1));
obs = 1;
EXPECT_TRUE(obs.hasObservers());
}
EXPECT_CALL(mockObserver1, Call(2));
obs = 2; // Only mockObserver1 should be called since scoped connections were destroyed
EXPECT_TRUE(obs.hasObservers());
regularConn.disconnect();
EXPECT_FALSE(obs.hasObservers());
}
TEST_F(ObservableValueTest, ForceNotify)
{
ObservableValue<int> obs{42};
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver;
obs.forceNotify();
auto connection = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(mockObserver, Call(42));
obs.forceNotify();
EXPECT_CALL(mockObserver, Call(42));
obs.forceNotify();
EXPECT_CALL(mockObserver, Call(100));
obs.set(100);
EXPECT_CALL(mockObserver, Call(100));
obs.forceNotify();
EXPECT_CALL(mockObserver, Call(100)).Times(3);
obs.forceNotify();
obs.forceNotify();
obs.forceNotify();
}