Add extracted tools: CitrineOS, OpenOCPP, ShapeShifter
- CitrineOS core extracted (CSMS OCPP 2.0.1) - OpenOCPP extracted (firmware OCPP 1.6J/2.0.1) - ShapeShifter library installed (pip install -e) - ShapeShifter specification extracted - EVerest extracted TODO updated with progress
This commit is contained in:
19
tools/EVerest-main/lib/everest/util/BUILD.bazel
Normal file
19
tools/EVerest-main/lib/everest/util/BUILD.bazel
Normal file
@@ -0,0 +1,19 @@
|
||||
load("@rules_cc//cc:defs.bzl", "cc_library", "cc_test")
|
||||
load("//third-party/bazel/toolchains:defs.bzl", "CROSS_TEST_INCOMPATIBLE")
|
||||
|
||||
cc_library(
|
||||
name = "util",
|
||||
hdrs = glob(["include/**/*.hpp"]),
|
||||
includes = ["include"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
cc_test(
|
||||
name = "async_tests",
|
||||
srcs = glob(["tests/async/*.cpp"]),
|
||||
target_compatible_with = CROSS_TEST_INCOMPATIBLE,
|
||||
deps = [
|
||||
":util",
|
||||
"@googletest//:gtest_main",
|
||||
],
|
||||
)
|
||||
65
tools/EVerest-main/lib/everest/util/CMakeLists.txt
Normal file
65
tools/EVerest-main/lib/everest/util/CMakeLists.txt
Normal file
@@ -0,0 +1,65 @@
|
||||
add_library(everest_util INTERFACE)
|
||||
add_library(everest::util ALIAS everest_util)
|
||||
|
||||
include(CMakePackageConfigHelpers)
|
||||
include(GNUInstallDirs)
|
||||
|
||||
set_target_properties(everest_util PROPERTIES
|
||||
VERSION 0.0.1
|
||||
)
|
||||
|
||||
|
||||
target_include_directories(everest_util
|
||||
INTERFACE
|
||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
||||
)
|
||||
|
||||
set_target_properties(everest_util
|
||||
PROPERTIES
|
||||
BUILD_INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/include"
|
||||
EXPORT_NAME util
|
||||
)
|
||||
|
||||
install(TARGETS everest_util
|
||||
EXPORT everest-core-targets
|
||||
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
|
||||
)
|
||||
|
||||
install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/include/everest
|
||||
DESTINATION include
|
||||
FILES_MATCHING PATTERN "*.hpp"
|
||||
)
|
||||
|
||||
if(DISABLE_EDM)
|
||||
# Do not use evc_setup_package() here: it unconditionally installs an
|
||||
# export file for the package, which would export everest_util a second
|
||||
# time alongside everest-core-targets and break non-EDM consumers.
|
||||
set(EVEREST_UTIL_CMAKE_INSTALL_DIR "${CMAKE_INSTALL_LIBDIR}/cmake/everest-util")
|
||||
|
||||
configure_package_config_file(
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/config.cmake.in
|
||||
${CMAKE_CURRENT_BINARY_DIR}/everest-util-config.cmake
|
||||
INSTALL_DESTINATION ${EVEREST_UTIL_CMAKE_INSTALL_DIR}
|
||||
PATH_VARS CMAKE_INSTALL_INCLUDEDIR
|
||||
)
|
||||
|
||||
write_basic_package_version_file(
|
||||
${CMAKE_CURRENT_BINARY_DIR}/everest-util-config-version.cmake
|
||||
VERSION ${PROJECT_VERSION}
|
||||
COMPATIBILITY ExactVersion
|
||||
)
|
||||
|
||||
install(
|
||||
FILES
|
||||
${CMAKE_CURRENT_BINARY_DIR}/everest-util-config.cmake
|
||||
${CMAKE_CURRENT_BINARY_DIR}/everest-util-config-version.cmake
|
||||
DESTINATION ${EVEREST_UTIL_CMAKE_INSTALL_DIR}
|
||||
)
|
||||
endif()
|
||||
|
||||
if (BUILD_TESTING)
|
||||
add_subdirectory(tests)
|
||||
endif()
|
||||
10
tools/EVerest-main/lib/everest/util/config.cmake.in
Normal file
10
tools/EVerest-main/lib/everest/util/config.cmake.in
Normal file
@@ -0,0 +1,10 @@
|
||||
@PACKAGE_INIT@
|
||||
|
||||
if(NOT TARGET everest::util)
|
||||
add_library(everest::util INTERFACE IMPORTED)
|
||||
set_target_properties(everest::util PROPERTIES
|
||||
INTERFACE_INCLUDE_DIRECTORIES "${PACKAGE_PREFIX_DIR}/@CMAKE_INSTALL_INCLUDEDIR@"
|
||||
)
|
||||
endif()
|
||||
|
||||
check_required_components(everest-util)
|
||||
@@ -0,0 +1,287 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
/**
|
||||
* @file Wrapping of a resource and serializing the access to the resource
|
||||
* @brief The content is based on the conference talk 'Concurrency-and-Parallelism'
|
||||
* held by Herb Sutter at C-and-Beyond-2012.
|
||||
* The original pattern has been extend with exhaustive handling of corner cases and errors.
|
||||
* Policies allow for the adaptation to common use cases.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <everest/util/queue/thread_safe_queue.hpp>
|
||||
#include <exception>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
#include <type_traits>
|
||||
|
||||
namespace everest::lib::util::testing_interface {
|
||||
// Forward declaration of the test fixture used as a friend
|
||||
class AsyncWrapperTest;
|
||||
} // namespace everest::lib::util::testing_interface
|
||||
|
||||
namespace everest::lib::util {
|
||||
|
||||
// --- EXCEPTION POLICIES (Policy 1) ---
|
||||
|
||||
/**
|
||||
* @brief Policy that triggers a global shutdown if a user-supplied task throws an exception.
|
||||
* @details Used for guarded resources where an exception implies resource corruption.
|
||||
*/
|
||||
struct GlobalFailurePolicy {
|
||||
/**
|
||||
* @brief Sets the global promise with the given exception pointer, permanently failing the executor.
|
||||
*/
|
||||
static void handle_user_exception(std::shared_ptr<std::promise<void>> const& global_promise_ptr,
|
||||
std::exception_ptr current_exception_ptr) {
|
||||
try {
|
||||
global_promise_ptr->set_exception(current_exception_ptr);
|
||||
} catch (const std::future_error&) {
|
||||
// Ignore: promise was already satisfied by an earlier thread/task
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Policy that contains a user-supplied exception locally (does not cause global failure).
|
||||
* @details Used for background tasks where exceptions do not corrupt the resource state.
|
||||
*/
|
||||
struct LocalFailurePolicy {
|
||||
/**
|
||||
* @brief Handles the user exception by doing nothing to the global promise.
|
||||
*/
|
||||
static void handle_user_exception([[maybe_unused]] std::shared_ptr<std::promise<void>> const& global_promise_ptr,
|
||||
[[maybe_unused]] std::exception_ptr current_exception_ptr) {
|
||||
}
|
||||
};
|
||||
|
||||
// --- SHUTDOWN POLICIES (Policy 2) ---
|
||||
|
||||
/**
|
||||
* @brief Destructor policy that waits for all queued tasks to finish execution before joining the worker thread.
|
||||
*/
|
||||
struct WaitToFinishPolicy {
|
||||
/**
|
||||
* @brief Performs a graceful shutdown by pushing a sentinel task and joining.
|
||||
*/
|
||||
template <typename QueueT, typename ThreadT>
|
||||
static void shutdown(QueueT& queue, ThreadT& worker, std::atomic_bool& done_flag) {
|
||||
// Push the final stop signal, which will be executed after all pending tasks.
|
||||
queue.push([&done_flag] { done_flag = true; });
|
||||
worker.join();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Destructor policy that signals the worker to exit immediately, potentially dropping queued tasks.
|
||||
*/
|
||||
struct FastQuitPolicy {
|
||||
/**
|
||||
* @brief Performs an immediate shutdown by setting the flag and unblocking the thread.
|
||||
*/
|
||||
template <typename QueueT, typename ThreadT>
|
||||
static void shutdown(QueueT& queue, ThreadT& worker, std::atomic_bool& done_flag) {
|
||||
// Signal termination immediately
|
||||
done_flag = true;
|
||||
// Send a dummy task to unblock the worker if it's currently blocking on pop()
|
||||
queue.push([] {});
|
||||
worker.join();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A single-threaded asynchronous executor that serializes access to a resource T.
|
||||
* @details All operations on T are performed sequentially on a dedicated worker thread. It uses two policies
|
||||
* (ExceptionPolicy and ShutdownPolicy) and a QueueT template parameter to define its entire contract.
|
||||
* @tparam T The resource type being wrapped.
|
||||
* @tparam ExceptionPolicy Defines global failure behavior (e.g., GlobalFailurePolicy).
|
||||
* @tparam ShutdownPolicy Defines destructor behavior (e.g., WaitToFinishPolicy).
|
||||
* @tparam QueueT The underlying thread-safe queue implementation.
|
||||
*/
|
||||
template <typename T, typename ExceptionPolicy, typename ShutdownPolicy,
|
||||
template <class> typename QueueT = thread_safe_queue>
|
||||
class async_wrapper_impl {
|
||||
public:
|
||||
friend class everest::lib::util::testing_interface::AsyncWrapperTest; ///< Allows GTest fixture to access
|
||||
///< protected/private members for testing.
|
||||
|
||||
using ThisT = async_wrapper_impl<T, ExceptionPolicy, ShutdownPolicy, QueueT>;
|
||||
|
||||
/**
|
||||
* @brief Constructs the wrapper, initializing the resource and starting the worker thread.
|
||||
*/
|
||||
template <class... ArgsT>
|
||||
explicit async_wrapper_impl(ArgsT&&... args) :
|
||||
m_resource(std::forward<ArgsT>(args)...), m_global_promise(std::make_shared<std::promise<void>>()) {
|
||||
|
||||
m_global_future = m_global_promise->get_future();
|
||||
|
||||
m_worker = std::thread([this] {
|
||||
while (not m_done) {
|
||||
try {
|
||||
m_queue.pop()(); // Execute the task lambda
|
||||
} catch (const std::exception& e) {
|
||||
|
||||
// Critical infrastructure failure handling
|
||||
try {
|
||||
m_global_promise->set_exception(
|
||||
std::make_exception_ptr(std::runtime_error("Async worker infrastructure failure.")));
|
||||
} catch (const std::future_error&) {
|
||||
// Ignore: Promise was already set by a concurrent thread/user task
|
||||
}
|
||||
m_done = true; // Signal thread termination
|
||||
} catch (...) {
|
||||
m_done = true; // Handle non-standard exceptions
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Rule of Five members
|
||||
async_wrapper_impl(ThisT&& other) noexcept = default;
|
||||
ThisT& operator=(ThisT&&) noexcept = default;
|
||||
async_wrapper_impl(ThisT const& other) = delete;
|
||||
|
||||
/**
|
||||
* @brief Destructor that shuts down the worker thread according to the ShutdownPolicy.
|
||||
*/
|
||||
~async_wrapper_impl() {
|
||||
ShutdownPolicy::shutdown(m_queue, m_worker, m_done);
|
||||
}
|
||||
|
||||
private:
|
||||
template <typename Fut, typename F> void promise_set_value(std::promise<Fut>& prom, F& f, T& t) const {
|
||||
prom.set_value(f(t));
|
||||
}
|
||||
|
||||
template <typename F> void promise_set_value(std::promise<void>& prom, F& f, T& t) const {
|
||||
f(t);
|
||||
prom.set_value();
|
||||
}
|
||||
|
||||
bool is_global_failure_signaled() const {
|
||||
return m_global_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready;
|
||||
}
|
||||
|
||||
template <typename F, typename R> void enqueue_task(F&& f, std::shared_ptr<std::promise<R>> prom) const {
|
||||
auto global_promise_ptr = m_global_promise;
|
||||
|
||||
// --- SYNCHRONOUS FAILURE CHECK (Gatekeeper, executed on calling thread) ---
|
||||
if (is_global_failure_signaled()) {
|
||||
try {
|
||||
m_global_future.get();
|
||||
} catch (const std::exception&) {
|
||||
prom->set_exception(std::current_exception());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
m_queue.push([f = std::forward<F>(f), prom, global_promise_ptr, this] {
|
||||
// --- ASYNCHRONOUS FAILURE CHECK (Mandatory Final Gatekeeper) ---
|
||||
if (is_global_failure_signaled()) {
|
||||
try {
|
||||
m_global_future.get();
|
||||
} catch (const std::exception&) {
|
||||
prom->set_exception(std::current_exception());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
promise_set_value(*prom, f, m_resource);
|
||||
} catch (...) {
|
||||
prom->set_exception(std::current_exception());
|
||||
|
||||
ExceptionPolicy::handle_user_exception(global_promise_ptr, std::current_exception());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --- ENQUEUE FOR FIRE-AND-FORGET TASKS FOR (No Promise, No Wait) ---
|
||||
template <typename F> void enqueue_task(F&& f) const {
|
||||
if (is_global_failure_signaled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto global_promise_ptr = m_global_promise;
|
||||
m_queue.push([f = std::forward<F>(f), global_promise_ptr, this] {
|
||||
if (is_global_failure_signaled()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
f(m_resource);
|
||||
} catch (...) {
|
||||
ExceptionPolicy::handle_user_exception(global_promise_ptr, std::current_exception());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
mutable T m_resource;
|
||||
mutable QueueT<std::function<void()>> m_queue;
|
||||
std::thread m_worker;
|
||||
std::atomic_bool m_done{false};
|
||||
std::shared_ptr<std::promise<void>> m_global_promise;
|
||||
std::shared_future<void> m_global_future;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Submits a function object to the worker thread and returns a future for the result.
|
||||
* @details The function is guaranteed to be executed sequentially with respect to other tasks.
|
||||
* It receives the managed resource as it's only parameter.
|
||||
* @tparam F Function object callable with T&.
|
||||
* @return std::future<R> where R is the return type of F. The future will contain an exception
|
||||
* if the task fails or if a global failure signal has been set.
|
||||
*/
|
||||
template <typename F> auto operator()(F f) const {
|
||||
using ReturnT = decltype(f(m_resource));
|
||||
auto prom = std::make_shared<std::promise<ReturnT>>();
|
||||
auto fut = prom->get_future();
|
||||
|
||||
enqueue_task(std::forward<F>(f), prom);
|
||||
|
||||
return fut;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Submits a function object to the worker thread without returning a future (fire-and-forget).
|
||||
* @details This uses an optimized path that avoids creating a std::promise, relying only on the
|
||||
* ExceptionPolicy for failure handling.
|
||||
* @tparam F Function object callable with T&.
|
||||
*/
|
||||
template <typename F> void run(F f) const {
|
||||
enqueue_task<F>(std::forward<F>(f));
|
||||
}
|
||||
};
|
||||
|
||||
// --- TYPEDEFS ---
|
||||
|
||||
/** @brief Base alias for resources where a task failure causes GLOBAL corruption (Guarded). */
|
||||
template <typename T, typename ShutdownPolicy>
|
||||
using async_wrapper_guarded = async_wrapper_impl<T, GlobalFailurePolicy, ShutdownPolicy>;
|
||||
|
||||
/** @brief Intermediate base alias for resources where a task failure is LOCALIZED (Background). */
|
||||
template <typename T, typename ShutdownPolicy>
|
||||
using async_wrapper_local = async_wrapper_impl<T, LocalFailurePolicy, ShutdownPolicy>;
|
||||
|
||||
// Final Usage Types (Combine Exception Policy and Shutdown Policy)
|
||||
|
||||
/** * @brief Primary default wrapper: Local Failure (Background) with Fast Quit.
|
||||
* @tparam T The resource type.
|
||||
*/
|
||||
template <typename T> using async_wrapper = async_wrapper_local<T, FastQuitPolicy>;
|
||||
|
||||
/** @brief Guarded resource with graceful (wait-to-finish) shutdown. */
|
||||
template <typename T> using async_wrapper_guarded_wait = async_wrapper_guarded<T, WaitToFinishPolicy>;
|
||||
/** @brief Local resource with graceful (wait-to-finish) shutdown. */
|
||||
template <typename T> using async_wrapper_wait = async_wrapper_local<T, WaitToFinishPolicy>;
|
||||
/** @brief Guarded resource with fast (potentially lossy) shutdown. */
|
||||
template <typename T> using async_wrapper_guarded_fast = async_wrapper_guarded<T, FastQuitPolicy>;
|
||||
/** @brief Local resource with fast (potentially lossy) shutdown. */
|
||||
template <typename T> using async_wrapper_fast = async_wrapper_local<T, FastQuitPolicy>;
|
||||
|
||||
} // namespace everest::lib::util
|
||||
@@ -0,0 +1,295 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
/**
|
||||
* @file monitor.hpp
|
||||
* @brief Provides a generic RAII Monitor pattern implementation for thread-safe access to a shared resource.
|
||||
*
|
||||
* The Monitor pattern bundles shared data with a synchronization mechanism (mutex and condition variable)
|
||||
* to ensure only one thread can access the data at any given time, and provides tools for thread
|
||||
* coordination (waiting and signaling).
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
namespace everest::lib::util {
|
||||
|
||||
template <typename T, typename = void> struct has_arrow_operator : std::false_type {};
|
||||
|
||||
template <typename T>
|
||||
struct has_arrow_operator<T, std::void_t<decltype(std::declval<T>().operator->())>> : std::true_type {};
|
||||
|
||||
template <typename T> inline constexpr bool has_arrow_operator_v = has_arrow_operator<T>::value;
|
||||
|
||||
/**
|
||||
* @brief The RAII guard that provides locked access to the shared data T.
|
||||
* * This object is non-copyable but movable. Its existence guarantees that the
|
||||
* underlying mutex in the parent monitor object is held. When this handle
|
||||
* goes out of scope, the lock is automatically released.
|
||||
*
|
||||
* @tparam T The type of the protected resource.
|
||||
* @tparam MTX The mutex type used for locking (e.g., std::mutex, std::recursive_mutex).
|
||||
*/
|
||||
template <class T, class MTX> class monitor_handle {
|
||||
public:
|
||||
/**
|
||||
* @brief Constructs the monitor handle and takes ownership of the acquired lock.
|
||||
* @param obj Reference to the protected resource within the monitor.
|
||||
* @param mtx R-value reference to the acquired unique lock, ownership is moved.
|
||||
* @param cv Reference to the condition variable in the monitor.
|
||||
*/
|
||||
monitor_handle(T& obj, std::unique_lock<MTX>&& mtx, std::condition_variable& cv) :
|
||||
m_obj(obj), m_lock(std::move(mtx)), m_cv(cv) {
|
||||
}
|
||||
|
||||
// Monitor handles should not be copied, as that would duplicate a unique lock.
|
||||
monitor_handle(monitor_handle<T, MTX> const& other) = delete;
|
||||
monitor_handle<T, MTX>& operator=(monitor_handle<T, MTX> const& rhs) = delete;
|
||||
|
||||
// Defaulted move operations allow the handle to be moved (e.g., returned from monitor::handle).
|
||||
/** @brief generated default move constructor*/
|
||||
monitor_handle(monitor_handle<T, MTX>&& other) = default;
|
||||
/** @brief generated default move assignment*/
|
||||
monitor_handle<T, MTX>& operator=(monitor_handle<T, MTX>&& rhs) = default;
|
||||
|
||||
/**
|
||||
* @brief Destructor. Automatically releases the lock held by m_lock.
|
||||
*/
|
||||
~monitor_handle() = default;
|
||||
|
||||
/**
|
||||
* @brief Overloads the dereference operator to allow reference access to the protected object.
|
||||
* * This provides direct reference access to the guarded object T (or the wrapper T, e.g., std::unique_ptr<...>&).
|
||||
* The lock is held during the access.
|
||||
*
|
||||
* @return Reference to the protected object T.
|
||||
*/
|
||||
T& operator*() {
|
||||
return m_obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Overloads the arrow operator to provide unified pointer-like access to the protected object.
|
||||
* * This implementation uses C++17's `if constexpr` to support three primary access patterns:
|
||||
* 1. **Chaining (Returns T&):** Used when T is a pointer-like wrapper (e.g., std::unique_ptr<T>) or a raw pointer
|
||||
* (T*). This allows the compiler's built-in chaining mechanism to continue indirection until the final object is
|
||||
* reached.
|
||||
* 2. **Direct Pointer Access (Returns T*):** Used when T is the final object type (e.g., a simple struct or class).
|
||||
* This terminates the chain immediately with a pointer to the object.
|
||||
* * @note This method holds the mutex lock for the duration of the access.
|
||||
*
|
||||
* @return `decltype(auto)` returns T& for chaining/pointers, or T* for direct access.
|
||||
*/
|
||||
decltype(auto) operator->() {
|
||||
if constexpr (has_arrow_operator_v<T> || std::is_pointer_v<T>) {
|
||||
return m_obj;
|
||||
} else {
|
||||
return &m_obj;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Blocks the thread until the provided predicate returns true.
|
||||
* @details This function atomically releases the lock (allowing other threads to acquire it)
|
||||
* and waits for a notification on the condition variable. When woken, it re-acquires
|
||||
* the lock and re-checks the predicate.
|
||||
* * @note This method is only available when MTX is std::mutex.
|
||||
* @tparam Predicate The callable type (e.g., lambda) that returns a boolean.
|
||||
* @param pred The condition that must become true to stop waiting.
|
||||
*/
|
||||
template <class Predicate, class U = MTX, std::enable_if_t<std::is_same_v<U, std::mutex>>* = nullptr>
|
||||
void wait(Predicate&& pred) {
|
||||
m_cv.wait(m_lock, std::forward<Predicate>(pred));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Blocks the thread until the predicate returns true or the timeout expires.
|
||||
* @note This method is only available when MTX is std::mutex.
|
||||
* @tparam Rep The type representing the duration count (e.g., int, long).
|
||||
* @tparam Period The type representing the duration period (e.g., std::milli, std::ratio<1>).
|
||||
* @tparam Predicate The callable type (e.g., lambda) that returns a boolean.
|
||||
* @param pred The condition that must become true to stop waiting.
|
||||
* @param timeout The maximum time to wait for the condition to become true.
|
||||
* @return true if the predicate became true, false if the timeout expired.
|
||||
*/
|
||||
template <class Rep, class Period, class Predicate, class U = MTX,
|
||||
std::enable_if_t<std::is_same_v<U, std::mutex>>* = nullptr>
|
||||
bool wait_for(Predicate&& pred, std::chrono::duration<Rep, Period> timeout) {
|
||||
return m_cv.wait_for(m_lock, timeout, std::forward<Predicate>(pred));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Blocks the thread until the predicate returns true or the absolute time point is reached.
|
||||
* * If the predicate is false, the lock is atomically released, and the thread sleeps until
|
||||
* a notification is received or abs_time is reached. When woken, the lock is re-acquired
|
||||
* and the predicate is re-checked.
|
||||
* @note This method is only available when MTX is std::mutex.
|
||||
* @tparam Clock The clock type used for the time point (e.g., std::system_clock, std::steady_clock).
|
||||
* @tparam Duration The duration type used for the time point.
|
||||
* @tparam Predicate The callable type (e.g., lambda) that returns a boolean.
|
||||
* @param abs_time The absolute time point at which the wait will cease, regardless of predicate state.
|
||||
* @param pred The condition that must become true to stop waiting.
|
||||
* @return true if the predicate became true, false if the absolute time was reached.
|
||||
*/
|
||||
template <class Clock, class Duration, class Predicate, class U = MTX,
|
||||
std::enable_if_t<std::is_same_v<U, std::mutex>>* = nullptr>
|
||||
bool wait_until(std::chrono::time_point<Clock, Duration> const& abs_time, Predicate&& pred) {
|
||||
return m_cv.wait_until(m_lock, abs_time, std::forward<Predicate>(pred));
|
||||
}
|
||||
|
||||
private:
|
||||
T& m_obj; ///< Reference to the protected resource.
|
||||
std::unique_lock<MTX> m_lock; ///< The unique lock, holding the mutex during the handle's lifetime.
|
||||
std::condition_variable& m_cv; ///< Reference to the monitor's condition variable.
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A generic monitor class that manages RAII access to its resource T.
|
||||
* * Provides thread-safe data encapsulation using a mutex and thread coordination
|
||||
* via a condition variable. Access to the internal resource T is only possible
|
||||
* by obtaining a monitor_handle.
|
||||
*
|
||||
* @tparam T The type of the resource being protected.
|
||||
* @tparam MTX The mutex type to use, defaults to std::mutex.
|
||||
*/
|
||||
template <class T, class MTX = std::mutex> class monitor {
|
||||
public:
|
||||
monitor() = default;
|
||||
/**
|
||||
* @brief Constructs the internal object T using move construction.
|
||||
*/
|
||||
explicit monitor(T&& obj) : m_obj(std::move(obj)) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Constructs the internal object T using perfect forwarding.
|
||||
* @tparam ArgsT Types of arguments used to construct T.
|
||||
* @param args Arguments passed to the constructor of T.
|
||||
*/
|
||||
template <class... ArgsT> explicit monitor(ArgsT&&... args) : m_obj(std::forward<ArgsT>(args)...) {
|
||||
}
|
||||
|
||||
~monitor() = default;
|
||||
|
||||
// Monitors should not be copied.
|
||||
monitor(monitor<T, MTX> const& other) = delete;
|
||||
monitor<T, MTX>& operator=(monitor<T, MTX> const& rhs) = delete;
|
||||
|
||||
/**
|
||||
* @brief Thread-safe move constructor. Locks the source mutex before swapping.
|
||||
* @details The move constructor is 'noexcept' if the monitor with it's template parameters
|
||||
* is no-throw swappable.
|
||||
*/
|
||||
monitor(monitor<T, MTX>&& other) noexcept(std::is_nothrow_swappable_v<T>) {
|
||||
// Lock the source monitor's mutex before moving its data to ensure thread safety
|
||||
std::unique_lock lock(other.m_mtx);
|
||||
// This pattern is important, don't just use std::swap, but enable std::swap for the case
|
||||
// no specialized optimazation is available. The following always prefers the the specialized version
|
||||
// via ADL lookup
|
||||
using std::swap;
|
||||
swap(m_obj, other.m_obj);
|
||||
// Note: m_mtx and m_cv are not swapped; they remain tied to the current object.
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Thread-safe move assignment operator. Locks the source mutex before swapping.
|
||||
* @details The move assignment operator is 'noexcept' if the monitor with it's template parameters
|
||||
* is no-throw swappable.
|
||||
* @return Reference to the current object.
|
||||
*/
|
||||
monitor<T, MTX>&
|
||||
operator=(monitor<T, MTX>&& rhs) noexcept(noexcept(std::declval<monitor&>().swap(std::declval<monitor&>()))) {
|
||||
if (this != &rhs) {
|
||||
this->swap(rhs);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Blocks indefinitely to acquire the lock and return a handle.
|
||||
* @return monitor_handle<T, MTX> with ownership of the acquired lock.
|
||||
*/
|
||||
monitor_handle<T, MTX> handle() {
|
||||
std::unique_lock lock(m_mtx);
|
||||
return monitor_handle<T, MTX>(m_obj, std::move(lock), m_cv);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Attempts to acquire the lock within the specified timeout duration.
|
||||
* @note This method is only available when MTX is std::timed_mutex.
|
||||
* @tparam Rep The type representing the duration count.
|
||||
* @tparam Period The type representing the duration period.
|
||||
* @param timeout The maximum time to wait for the lock.
|
||||
* @return An optional handle: contains the handle if the lock was acquired, std::nullopt otherwise.
|
||||
*/
|
||||
template <class Rep, class Period, class U = MTX, std::enable_if_t<std::is_same_v<U, std::timed_mutex>>* = nullptr>
|
||||
std::optional<monitor_handle<T, MTX>> handle(std::chrono::duration<Rep, Period> timeout) {
|
||||
auto deadline = std::chrono::steady_clock::now() + timeout;
|
||||
|
||||
std::unique_lock lock(m_mtx, std::defer_lock);
|
||||
if (not lock.try_lock_until(deadline)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return monitor_handle<T, MTX>(m_obj, std::move(lock), m_cv);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Wakes up one thread currently waiting on the monitor's condition variable.
|
||||
* @note This method is only available when MTX is std::mutex.
|
||||
*/
|
||||
template <class U = MTX, std::enable_if_t<std::is_same_v<U, std::mutex>>* = nullptr> void notify_one() {
|
||||
m_cv.notify_one();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Wakes up all threads currently waiting on the monitor's condition variable.
|
||||
* @note This method is only available when MTX is std::mutex.
|
||||
*/
|
||||
template <class U = MTX, std::enable_if_t<std::is_same_v<U, std::mutex>>* = nullptr> void notify_all() {
|
||||
m_cv.notify_all();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Member swap function for thread-safe and exception-safe exchange of resources.
|
||||
* * Locks both mutexes using RAII and deadlock avoidance (via std::scoped_lock)
|
||||
* before swapping the protected resource T.
|
||||
* @details This function is 'noexcept' if T is no-throw swappable
|
||||
* @param other The monitor to swap resources with.
|
||||
*/
|
||||
void swap(monitor<T, MTX>& other) noexcept(std::is_nothrow_swappable_v<T>) {
|
||||
std::scoped_lock lock(m_mtx, other.m_mtx);
|
||||
// This pattern is important, don't just use std::swap, but enable std::swap for the case
|
||||
// no specialized optimazation is available. The following always prefers the the specialized version
|
||||
// via ADL lookup
|
||||
using std::swap;
|
||||
swap(m_obj, other.m_obj);
|
||||
}
|
||||
|
||||
private:
|
||||
MTX m_mtx; ///< The mutex protecting the resource T.
|
||||
T m_obj; ///< The protected resource.
|
||||
std::condition_variable m_cv; ///< The condition variable for thread coordination.
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Non-member swap function for standard ADL (Argument-Dependent Lookup) swap.
|
||||
* * This function delegates the call to the thread-safe member swap function, ensuring
|
||||
* a safe, deadlock-avoiding exchange of resources between two monitor objects.
|
||||
* * @tparam T The type of the resource being protected.
|
||||
* @details This function is 'noexcept' if the monitor with its template parameters is no-throw swappable
|
||||
* @tparam MTX The mutex type used for locking.
|
||||
* @param lhs The first monitor object.
|
||||
* @param rhs The second monitor object.
|
||||
*/
|
||||
template <class T, class MTX> void swap(monitor<T, MTX>& lhs, monitor<T, MTX>& rhs) noexcept(noexcept(lhs.swap(rhs))) {
|
||||
lhs.swap(rhs);
|
||||
}
|
||||
|
||||
} // namespace everest::lib::util
|
||||
@@ -0,0 +1,135 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
/**
|
||||
* @file thread_pool.hpp
|
||||
* @brief Simple fixed-size thread pool implementation.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <everest/util/queue/thread_safe_queue.hpp>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <thread>
|
||||
|
||||
namespace everest::lib::util {
|
||||
|
||||
/**
|
||||
* @brief A thread safe fixed-size pool for task execution.
|
||||
* @details This pool maintains a constant number of worker threads. It provides two
|
||||
* interfaces for task submission: operator() for tasks requiring a return value
|
||||
* (via std::future) and run() for fire-and-forget tasks.
|
||||
*/
|
||||
class thread_pool {
|
||||
public:
|
||||
/** @brief Type definition for the tasks held in the queue. */
|
||||
using action = std::function<void()>;
|
||||
|
||||
/**
|
||||
* @brief Constructs the thread pool and spawns worker threads.
|
||||
* @param thread_count The number of worker threads to maintain.
|
||||
*/
|
||||
thread_pool(unsigned int thread_count) {
|
||||
auto worker_loop = [this] {
|
||||
while (auto task = m_action_queue.wait_and_pop()) {
|
||||
// Task successful, execute it while handling exceptions
|
||||
try {
|
||||
task.value()();
|
||||
} catch (...) {
|
||||
// Keep the worker alive even if the task fails.
|
||||
// Exceptions for operator() are handled in the promise wrapper.
|
||||
}
|
||||
}
|
||||
};
|
||||
for (std::size_t i = 0; i < thread_count; ++i) {
|
||||
m_threads.emplace_back(worker_loop);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Destructor. Signals all threads to stop and joins them.
|
||||
* @details Unblocks any threads waiting on the queue before joining.
|
||||
*/
|
||||
~thread_pool() {
|
||||
m_action_queue.stop();
|
||||
for (auto& elem : m_threads) {
|
||||
if (elem.joinable()) {
|
||||
elem.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Submits a task to the pool and returns a future for its result.
|
||||
* @tparam F The type of the callable.
|
||||
* @tparam Args The types of the arguments to pass to the callable.
|
||||
* @param f The callable to execute.
|
||||
* @param args The arguments to pass to the callable.
|
||||
* @return A std::future that will eventually contain the result of the callable.
|
||||
*/
|
||||
template <typename F, typename... Args>
|
||||
auto operator()(F&& f, Args&&... args) const -> std::future<std::invoke_result_t<F, Args...>> {
|
||||
using R = std::invoke_result_t<F, Args...>;
|
||||
|
||||
auto prom = std::make_shared<std::promise<R>>();
|
||||
auto fut = prom->get_future();
|
||||
enqueue_task(std::forward<F>(f), prom, std::forward<Args>(args)...);
|
||||
|
||||
return fut;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Submits a "fire-and-forget" task to the pool.
|
||||
* @details This method is highly efficient as it avoids the overhead of creating
|
||||
* std::promise and std::future objects. It returns immediately after the task
|
||||
* is added to the queue.
|
||||
* @tparam F The type of the callable.
|
||||
* @tparam Args The types of the arguments to pass to the callable.
|
||||
* @param f The callable to execute.
|
||||
* @param args The arguments to pass to the callable.
|
||||
*/
|
||||
template <typename F, typename... Args> void run(F&& f, Args&&... args) const {
|
||||
m_action_queue.push(std::bind(std::forward<F>(f), std::forward<Args>(args)...));
|
||||
}
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Helper to set a promise value for non-void return types.
|
||||
*/
|
||||
template <typename Fut, typename F> static void promise_set_value(std::promise<Fut>& prom, F& f) {
|
||||
prom.set_value(f());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Helper to set a promise value for void return types.
|
||||
*/
|
||||
template <typename F> static void promise_set_value(std::promise<void>& prom, F& f) {
|
||||
f();
|
||||
prom.set_value();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Wraps a task with a promise and enqueues it.
|
||||
*/
|
||||
template <typename F, typename... Args, typename R>
|
||||
void enqueue_task(F&& f, std::shared_ptr<std::promise<R>>& prom, Args&&... args) const {
|
||||
auto bound_f = std::bind(std::forward<F>(f), std::forward<Args>(args)...);
|
||||
m_action_queue.push([prom, task_f = std::move(bound_f)]() mutable {
|
||||
try {
|
||||
promise_set_value(*prom, task_f);
|
||||
} catch (...) {
|
||||
// Ensure promise is settled with an exception if task throws
|
||||
prom->set_exception(std::current_exception());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** @brief Thread safe queue for incoming tasks. */
|
||||
mutable thread_safe_queue<action> m_action_queue;
|
||||
|
||||
/** @brief Container for worker thread handles. */
|
||||
std::vector<std::thread> m_threads;
|
||||
};
|
||||
|
||||
} // namespace everest::lib::util
|
||||
@@ -0,0 +1,401 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
/** \file */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "everest/util/async/monitor.hpp"
|
||||
#include "everest/util/queue/thread_safe_bounded_queue.hpp"
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <list>
|
||||
#include <optional>
|
||||
#include <thread>
|
||||
#include <type_traits>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
namespace everest::lib::util {
|
||||
|
||||
/**
|
||||
* @brief A task wrapper that tracks when the task was enqueued.
|
||||
*/
|
||||
struct TrackedAction {
|
||||
std::function<void()> func; ///< The actual work to perform.
|
||||
std::chrono::steady_clock::time_point arrival; ///< Timestamp of enqueueing.
|
||||
|
||||
/**
|
||||
* @brief Constructs a tracked action with the current timestamp.
|
||||
* @param[in] f The functional object to be executed.
|
||||
*/
|
||||
explicit TrackedAction(std::function<void()> f) : func(std::move(f)), arrival(std::chrono::steady_clock::now()) {
|
||||
}
|
||||
};
|
||||
|
||||
// --- Scaling Policies ---
|
||||
|
||||
// A policy advertises whether it needs a background supervisor (and at what
|
||||
// cadence) via a single constexpr: `supervisor_tick`. `std::nullopt` means no
|
||||
// supervisor; a value means "re-evaluate scaling every <tick> ms".
|
||||
|
||||
/**
|
||||
* @brief Greedy scaling policy: grows whenever there is any backlog.
|
||||
*/
|
||||
struct GreedyScaling {
|
||||
static constexpr std::optional<std::chrono::milliseconds> supervisor_tick = std::nullopt;
|
||||
/**
|
||||
* @brief Decides to grow if there is any backlog.
|
||||
* @param current_workers Number of threads currently in the registry.
|
||||
* @param queue_size Number of tasks waiting in the queue.
|
||||
* @return true if we should spawn a new thread.
|
||||
*/
|
||||
static bool should_grow([[maybe_unused]] std::size_t current_workers, std::size_t queue_size,
|
||||
std::optional<std::chrono::steady_clock::time_point> oldest_task) {
|
||||
// If queue_size > 1, it means even if a worker is currently
|
||||
// popping, there is at least one other task that will be stuck waiting.
|
||||
return queue_size > 1;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Conservative scaling policy: grows only when backlog is significant.
|
||||
*/
|
||||
struct ConservativeScaling {
|
||||
static constexpr std::optional<std::chrono::milliseconds> supervisor_tick = std::nullopt;
|
||||
static bool should_grow(std::size_t current_workers, std::size_t queue_size,
|
||||
[[maybe_unused]] std::optional<std::chrono::steady_clock::time_point> oldest_task) {
|
||||
return queue_size > (current_workers * 2);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Fixed size scaling policy: grows after a specific queue depth limit is reached.
|
||||
* @tparam Limit The queue size threshold.
|
||||
*/
|
||||
template <std::size_t Limit> struct FixedSizeScaling {
|
||||
static constexpr std::optional<std::chrono::milliseconds> supervisor_tick = std::nullopt;
|
||||
static bool should_grow([[maybe_unused]] std::size_t current_workers, std::size_t queue_size,
|
||||
[[maybe_unused]] std::optional<std::chrono::steady_clock::time_point> oldest_arrival) {
|
||||
return queue_size >= Limit;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Latency-based scaling policy: grows if the oldest task has waited too long.
|
||||
* @tparam ThresholdMs Maximum tolerable wait time in milliseconds.
|
||||
* @tparam TickMs Cadence at which the supervisor re-evaluates the policy.
|
||||
*/
|
||||
template <std::size_t ThresholdMs = 10, std::size_t TickMs = 5> struct LatencyScaling {
|
||||
static constexpr std::optional<std::chrono::milliseconds> supervisor_tick = std::chrono::milliseconds(TickMs);
|
||||
static bool should_grow([[maybe_unused]] std::size_t current_workers, std::size_t queue_size,
|
||||
std::optional<std::chrono::steady_clock::time_point> oldest_arrival) {
|
||||
if (queue_size < 1 or not oldest_arrival.has_value()) {
|
||||
return false;
|
||||
}
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
const auto wait = std::chrono::duration_cast<std::chrono::milliseconds>(now - oldest_arrival.value());
|
||||
return wait.count() > static_cast<long long>(ThresholdMs);
|
||||
}
|
||||
};
|
||||
|
||||
// --- Thread Pool ---
|
||||
|
||||
// +--------------+--------------------------+----------------------------------------+
|
||||
// | POLICY | GROWTH TRIGGER LOGIC | CHARACTER / INTENT |
|
||||
// +--------------+--------------------------+----------------------------------------+
|
||||
// | Greedy | queue_size > 1 | Minimizes latency at all costs. Scales |
|
||||
// | | | the moment a backlog is detected. |
|
||||
// +--------------+--------------------------+----------------------------------------+
|
||||
// | Latency | wait > ThresholdMs | Balances resources with SLA. Scales |
|
||||
// | | | only if tasks sit too long in queue. |
|
||||
// +--------------+--------------------------+----------------------------------------+
|
||||
// | Conservative | queue_size > (workers*2) | Prioritizes stability. Scales only |
|
||||
// | | | when tasks significantly outnumber |
|
||||
// | | | current worker capacity. |
|
||||
// +--------------+--------------------------+----------------------------------------+
|
||||
// | FixedSize | queue_size >= Limit | Rigid and predictable. Grows only |
|
||||
// | | | when a specific depth limit is hit. |
|
||||
// +--------------+--------------------------+----------------------------------------+
|
||||
|
||||
// --- Exception Handling Policies ---
|
||||
|
||||
/**
|
||||
* @brief Exception policy: silently swallow exceptions (fire-and-forget semantics).
|
||||
*/
|
||||
struct SuppressExceptions {
|
||||
static void handle_exception([[maybe_unused]] std::exception_ptr) noexcept {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Exception policy: rethrow from the worker thread, terminating the process if uncaught.
|
||||
*/
|
||||
struct RethrowExceptions {
|
||||
[[noreturn]] static void handle_exception(std::exception_ptr eptr) {
|
||||
std::rethrow_exception(eptr);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A thread pool that dynamically scales its worker count based on a policy.
|
||||
* * @details This pool maintains a minimum number of threads and expands up to a maximum
|
||||
* when the ScalingPolicy (e.g., LatencyScaling or GreedyScaling) signals that growth
|
||||
* is necessary. Idle surplus threads are automatically retired after a specified timeout.
|
||||
* * @tparam ScalingPolicy A policy class implementing should_grow(size_t, size_t, std::optional<time_point>).
|
||||
* * @tparam ExceptionPolicy A policy class implementing a static handle_exception() called inside the catch block.
|
||||
*/
|
||||
template <typename ScalingPolicy = LatencyScaling<10>, typename ExceptionPolicy = SuppressExceptions>
|
||||
class thread_pool_scaling {
|
||||
public:
|
||||
using action = std::function<void()>;
|
||||
|
||||
/**
|
||||
* @brief Constructs the scalable thread pool.
|
||||
* @tparam Rep The representation type of the duration.
|
||||
* @tparam Period The period type of the duration.
|
||||
* @param[in] min Minimum worker threads to keep alive.
|
||||
* @param[in] max Maximum allowed worker threads.
|
||||
* @param[in] timeout Idle duration before a surplus worker retires. Defaults to 60s.
|
||||
* @param[in] queue_limit Maximum tasks allowed in the queue. Defaults to 0 (unbounded).
|
||||
*
|
||||
* The supervisor tick (if any) is carried by the ScalingPolicy itself; see
|
||||
* @ref LatencyScaling for an example.
|
||||
*/
|
||||
template <class Rep, class Period>
|
||||
thread_pool_scaling(std::size_t min, std::size_t max,
|
||||
std::chrono::duration<Rep, Period> timeout = std::chrono::seconds(60),
|
||||
std::size_t queue_limit = 0) :
|
||||
m_min_threads(min),
|
||||
m_max_threads(max),
|
||||
m_idle_timeout(std::chrono::duration_cast<std::chrono::milliseconds>(timeout)),
|
||||
m_action_queue(queue_limit) {
|
||||
|
||||
{
|
||||
auto reg_h = m_reg.handle();
|
||||
for (std::size_t i = 0; i < m_min_threads; ++i) {
|
||||
spawn_worker_internal(reg_h);
|
||||
}
|
||||
}
|
||||
if constexpr (ScalingPolicy::supervisor_tick.has_value()) {
|
||||
m_supervisor = std::thread([this] { run_supervisor(*ScalingPolicy::supervisor_tick); });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Destructor. Signals shutdown and joins all active worker threads.
|
||||
*/
|
||||
~thread_pool_scaling() {
|
||||
// 1. Signal shutdown and wake the supervisor + producers/consumers.
|
||||
{
|
||||
auto reg_h = m_reg.handle();
|
||||
reg_h->shutdown = true;
|
||||
}
|
||||
m_reg.notify_all();
|
||||
m_action_queue.stop();
|
||||
|
||||
// 2. Join the supervisor before tearing down the worker list: the supervisor
|
||||
// can spawn new workers, and we must not race with the steal in step 3.
|
||||
if constexpr (ScalingPolicy::supervisor_tick.has_value()) {
|
||||
if (m_supervisor.joinable()) {
|
||||
m_supervisor.join();
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Steal the active workers list. Explicitly clear the source so that any
|
||||
// worker that acquires the lock afterwards sees size()==0 and cannot
|
||||
// voluntarily retire into the zombies deque after step 5's final reap.
|
||||
std::list<std::thread> workers_to_join;
|
||||
{
|
||||
auto reg_h = m_reg.handle();
|
||||
workers_to_join = std::move(reg_h->workers);
|
||||
reg_h->workers.clear();
|
||||
}
|
||||
|
||||
// 4. Join everything in our stolen list
|
||||
for (auto& t : workers_to_join) {
|
||||
if (t.joinable()) {
|
||||
t.join();
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Join any zombies that retired before the steal. Steal the deque first
|
||||
// so the join happens outside the lock (same pattern as the worker loop).
|
||||
std::deque<std::thread> zombies_to_join;
|
||||
{
|
||||
auto reg_h = m_reg.handle();
|
||||
zombies_to_join = std::move(reg_h->zombies);
|
||||
}
|
||||
for (auto& t : zombies_to_join) {
|
||||
if (t.joinable()) {
|
||||
t.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Submits a "fire-and-forget" task for execution.
|
||||
* @details Optimized path that avoids promise/future overhead.
|
||||
* @param[in] f The task to execute.
|
||||
* @param[in] args Arguments to pass to the task.
|
||||
*/
|
||||
template <typename F, typename... Args> void run(F&& f, Args&&... args) {
|
||||
submit_to_queue(std::bind(std::forward<F>(f), std::forward<Args>(args)...));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Submits a task and returns a future for the result.
|
||||
* @return A std::future containing the result of the task.
|
||||
*/
|
||||
template <typename F, typename... Args>
|
||||
auto operator()(F&& f, Args&&... args) -> std::future<std::invoke_result_t<F, Args...>> {
|
||||
using R = std::invoke_result_t<F, Args...>;
|
||||
auto prom = std::make_shared<std::promise<R>>();
|
||||
auto fut = prom->get_future();
|
||||
|
||||
submit_to_queue([prom, bound_f = std::bind(std::forward<F>(f), std::forward<Args>(args)...)]() mutable {
|
||||
try {
|
||||
if constexpr (std::is_void_v<R>) {
|
||||
bound_f();
|
||||
prom->set_value();
|
||||
} else {
|
||||
prom->set_value(bound_f());
|
||||
}
|
||||
} catch (...) {
|
||||
prom->set_exception(std::current_exception());
|
||||
}
|
||||
});
|
||||
return fut;
|
||||
}
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Data structure representing the internal state of worker management.
|
||||
*/
|
||||
struct RegistryData {
|
||||
std::list<std::thread> workers; ///< List of active worker threads.
|
||||
std::deque<std::thread> zombies; ///< Threads that have exited but not yet been joined.
|
||||
bool shutdown = false; ///< Global shutdown flag.
|
||||
};
|
||||
|
||||
using handle = monitor_handle<RegistryData, std::mutex>; ///< Alias for monitor access.
|
||||
|
||||
/**
|
||||
* @brief Internal helper to push tasks and trigger the scaling heuristic.
|
||||
* @param[in] func The functional object to enqueue.
|
||||
*/
|
||||
void submit_to_queue(action&& func) {
|
||||
std::size_t size_after_push = m_action_queue.push(TrackedAction(std::move(func)));
|
||||
auto oldest_arrival = m_action_queue.oldest_arrival();
|
||||
|
||||
if (size_after_push > 0) {
|
||||
auto reg_h = m_reg.handle();
|
||||
if (reg_h->workers.size() < m_max_threads &&
|
||||
ScalingPolicy::should_grow(reg_h->workers.size(), size_after_push, oldest_arrival)) {
|
||||
spawn_worker_internal(reg_h);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Spawns a new worker thread.
|
||||
* @param[in] reg_h Handle to the monitor-protected registry data.
|
||||
*/
|
||||
void spawn_worker_internal(handle& reg_h) {
|
||||
reg_h->workers.emplace_back();
|
||||
auto it = std::prev(reg_h->workers.end());
|
||||
|
||||
*it = std::thread([this, it]() {
|
||||
while (true) {
|
||||
auto task_opt = m_action_queue.try_pop(m_idle_timeout);
|
||||
if (task_opt) {
|
||||
try {
|
||||
task_opt->func();
|
||||
} catch (...) {
|
||||
ExceptionPolicy::handle_exception(std::current_exception());
|
||||
}
|
||||
// Steal the zombie deque under the lock, then join outside it.
|
||||
// Joining while holding the lock is safe in practice (the zombie has already
|
||||
// released the lock before it can appear in the deque), but it blocks the
|
||||
// registry mutex for the duration of the join — delaying scaling decisions
|
||||
// and the destructor. Stealing first bounds the critical section to a cheap
|
||||
// list move.
|
||||
std::deque<std::thread> zombies_to_join;
|
||||
{
|
||||
auto reg_h = m_reg.handle();
|
||||
if (!reg_h->zombies.empty()) {
|
||||
zombies_to_join = std::move(reg_h->zombies);
|
||||
}
|
||||
}
|
||||
for (auto& t : zombies_to_join) {
|
||||
if (t.joinable()) {
|
||||
t.join();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
auto reg_h = m_reg.handle();
|
||||
|
||||
// 1. THE CRITICAL CHECK:
|
||||
// If shutdown is true, the destructor has already moved (or is moving)
|
||||
// the 'workers' list. We must NOT touch 'it' or the 'workers' list.
|
||||
if (reg_h->shutdown) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. VOLUNTARY RETIREMENT:
|
||||
// This only executes if we are NOT shutting down.
|
||||
// Since we are holding the monitor lock and shutdown is false,
|
||||
// we know 'it' is still valid in reg_h->workers.
|
||||
if (reg_h->workers.size() > m_min_threads) {
|
||||
reg_h->zombies.push_back(std::move(*it));
|
||||
reg_h->workers.erase(it);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Supervisor loop. Periodically re-evaluates the scaling policy so that
|
||||
* time-based policies (e.g. LatencyScaling) scale up when tasks sit in the queue
|
||||
* without any new submission to trigger a check.
|
||||
*/
|
||||
void run_supervisor(std::chrono::milliseconds tick) {
|
||||
while (true) {
|
||||
auto reg_h = m_reg.handle();
|
||||
// wait_for returns true when the predicate is satisfied (shutdown requested),
|
||||
// false on timeout.
|
||||
if (reg_h.wait_for([&]() { return reg_h->shutdown; }, tick)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::size_t queue_size = m_action_queue.size();
|
||||
if (queue_size == 0) {
|
||||
continue;
|
||||
}
|
||||
if (reg_h->workers.size() >= m_max_threads) {
|
||||
continue;
|
||||
}
|
||||
const auto oldest_arrival = m_action_queue.oldest_arrival();
|
||||
if (ScalingPolicy::should_grow(reg_h->workers.size(), queue_size, oldest_arrival)) {
|
||||
spawn_worker_internal(reg_h);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const std::size_t m_min_threads; ///< Minimum persistent thread count.
|
||||
const std::size_t m_max_threads; ///< Maximum allowed thread count.
|
||||
const std::chrono::milliseconds m_idle_timeout; ///< Surplus thread idle timeout.
|
||||
|
||||
thread_safe_bounded_queue<TrackedAction> m_action_queue; ///< Task queue.
|
||||
monitor<RegistryData> m_reg; ///< Worker registry.
|
||||
/// Background scaling supervisor. Only materialized as a real `std::thread`
|
||||
/// for policies whose `supervisor_tick` has a value; otherwise collapses to
|
||||
/// a `std::monostate` so non-supervisor pools don't carry a dead thread handle.
|
||||
std::conditional_t<ScalingPolicy::supervisor_tick.has_value(), std::thread, std::monostate> m_supervisor;
|
||||
};
|
||||
|
||||
} // namespace everest::lib::util
|
||||
@@ -0,0 +1,244 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2024 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
/**
|
||||
* \file convert an enum into bit flags
|
||||
* \note enum must contain item "last" which has the highest value
|
||||
*/
|
||||
|
||||
#ifndef ENUMFLAGS_HPP
|
||||
#define ENUMFLAGS_HPP
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <type_traits>
|
||||
|
||||
namespace everest::lib::util {
|
||||
|
||||
/**
|
||||
* \brief templated class to use an enumeration as bit flags
|
||||
* \note Enumeration must have the last element called last
|
||||
*
|
||||
* Example:
|
||||
* \code
|
||||
* enum class example : std::uin8_t {
|
||||
* item1,
|
||||
* item2,
|
||||
* item3,
|
||||
* last = item3,
|
||||
* };
|
||||
*
|
||||
* util::EnumFlags<example> flags;
|
||||
*
|
||||
* flags.set(example::item1);
|
||||
* flags.is_set(example::item1); // true
|
||||
* flags.reset(example::item1);
|
||||
* flags.is_set(example::item1); // false
|
||||
* \endcode
|
||||
*
|
||||
* Multiple flags can be combined:
|
||||
* \code
|
||||
* flags.reset();
|
||||
* flags.set(example::item1, example::item2);
|
||||
* flags.is_set(example::item1, example::item2); // true
|
||||
* flags.is_set(example::item3, example::item2); // false
|
||||
* flags.is_any_set(example::item3, example::item2); // true
|
||||
* \endcode
|
||||
*/
|
||||
|
||||
template <typename T>
|
||||
using SelectedUInt = std::conditional_t<
|
||||
(static_cast<std::size_t>(T::last) < 8), std::uint8_t,
|
||||
std::conditional_t<(static_cast<std::size_t>(T::last) < 16), std::uint16_t,
|
||||
std::conditional_t<(static_cast<std::size_t>(T::last) < 32), std::uint32_t,
|
||||
std::conditional_t<(static_cast<std::size_t>(T::last) < 64), std::uint64_t,
|
||||
void // invalid, triggers static_assert below
|
||||
>>>>;
|
||||
|
||||
template <typename T, typename B> class EnumFlagsBase {
|
||||
public:
|
||||
static_assert(std::is_enum<T>(), "Not enum");
|
||||
static_assert(std::is_integral<SelectedUInt<T>>(), "Not supported");
|
||||
|
||||
private:
|
||||
B _value{0ULL};
|
||||
|
||||
constexpr auto max_value() const {
|
||||
if constexpr (static_cast<std::underlying_type_t<T>>(T::last) == 64) {
|
||||
return std::numeric_limits<std::uint64_t>::max();
|
||||
} else {
|
||||
return (1ULL << (static_cast<std::underlying_type_t<T>>(T::last) + 1)) - 1;
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
/**
|
||||
* \brief return the bit position for the specified enum value
|
||||
* \param[in] flag the enum value
|
||||
* \returns an unsigned integer with the equivalent bit set
|
||||
*/
|
||||
static constexpr std::size_t bit(T flag) {
|
||||
return 1ULL << static_cast<std::underlying_type_t<T>>(flag);
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief set the state of a specific flag
|
||||
* \param[in] flag - the enum value to update
|
||||
* \param[in] value - set/reset the flag
|
||||
*/
|
||||
constexpr void set(T flag, bool value) {
|
||||
if (value) {
|
||||
set(flag);
|
||||
} else {
|
||||
reset(flag);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief set the specific flag
|
||||
* \param[in] flag - the enum value to set
|
||||
*/
|
||||
constexpr void set(T flag) {
|
||||
_value |= bit(flag);
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief set flags to a specific value
|
||||
* \note not recommended
|
||||
*/
|
||||
constexpr void set(std::underlying_type_t<T> v) {
|
||||
_value = v & max_value();
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief same as above but for multiple flags
|
||||
*/
|
||||
template <typename... Flags> constexpr void set(T flag, const Flags... flags) {
|
||||
set(flag);
|
||||
set(flags...);
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief reset the specific flag
|
||||
* \param[in] flag - the enum value to reset
|
||||
*/
|
||||
constexpr void reset(T flag) {
|
||||
_value &= ~bit(flag);
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief same as above but for multiple flags
|
||||
*/
|
||||
template <typename... Flags> constexpr void reset(T flag, const Flags... flags) {
|
||||
reset(flag);
|
||||
reset(flags...);
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief reset all flags
|
||||
*/
|
||||
constexpr void reset() {
|
||||
_value = 0ULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief set all flags
|
||||
*/
|
||||
constexpr void set() {
|
||||
_value = max_value();
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief test if all flags are reset (i.e. 0)
|
||||
* \returns true when no flag is set
|
||||
*/
|
||||
[[nodiscard]] constexpr bool all_reset() const {
|
||||
return _value == 0ULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief test if any flags are set
|
||||
* \returns true when any flag is set
|
||||
*/
|
||||
[[nodiscard]] constexpr bool any_reset() const {
|
||||
return _value != max_value();
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief test if all flags are reset (i.e. 0)
|
||||
* \returns true when no flag is set
|
||||
*/
|
||||
[[nodiscard]] constexpr bool all_set() const {
|
||||
return _value == max_value();
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief test if any flags are set
|
||||
* \returns true when any flag is set
|
||||
*/
|
||||
[[nodiscard]] constexpr bool any_set() const {
|
||||
return _value != 0ULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief test if a flag is set
|
||||
* \returns true when the flag is set
|
||||
*/
|
||||
constexpr bool is_set(T flag) const {
|
||||
return (_value & bit(flag)) != 0;
|
||||
}
|
||||
constexpr bool is_any_set(T flag) const {
|
||||
return is_set(flag);
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief test if a flag is reset
|
||||
* \returns true when the flag is reset
|
||||
*/
|
||||
constexpr bool is_reset(T flag) const {
|
||||
return (_value & bit(flag)) == 0;
|
||||
}
|
||||
constexpr bool is_any_reset(T flag) const {
|
||||
return is_reset(flag);
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief retrieve all flags
|
||||
* \returns the internal _value
|
||||
*/
|
||||
constexpr auto get() const {
|
||||
return _value;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief same as above but for multiple flags
|
||||
* \note is_set() all specified flags must be set for a true result
|
||||
* \note is_any_set() at least one of the specified flags must be set for a true result
|
||||
* \note is_reset() all specified flags must be reset for a true result
|
||||
* \note is_any_reset() at least one of the specified flags must be reset for a true result
|
||||
*/
|
||||
template <typename... Flags> constexpr bool is_set(T flag, const Flags... flags) const {
|
||||
return is_set(flag) && is_set(flags...);
|
||||
}
|
||||
|
||||
template <typename... Flags> constexpr bool is_any_set(T flag, const Flags... flags) const {
|
||||
return is_any_set(flag) || is_any_set(flags...);
|
||||
}
|
||||
|
||||
template <typename... Flags> constexpr bool is_reset(T flag, const Flags... flags) const {
|
||||
return is_reset(flag) && is_reset(flags...);
|
||||
}
|
||||
|
||||
template <typename... Flags> constexpr bool is_any_reset(T flag, const Flags... flags) const {
|
||||
return is_any_reset(flag) || is_any_reset(flags...);
|
||||
}
|
||||
};
|
||||
|
||||
template <typename T> struct EnumFlags : public EnumFlagsBase<T, SelectedUInt<T>> {};
|
||||
|
||||
template <typename T> struct AtomicEnumFlags : public EnumFlagsBase<T, std::atomic<SelectedUInt<T>>> {};
|
||||
|
||||
} // namespace everest::lib::util
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,234 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
// Originally from https://github.com/EVerest/libfsm/blob/draft/v2/include/fsm/v2/fsm.hpp
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace fsm::v2 {
|
||||
|
||||
namespace detail {
|
||||
|
||||
// detection idiom, see https://blog.tartanllama.xyz/detection-idiom/
|
||||
template <typename T, typename = void> struct output_type {
|
||||
using type = void;
|
||||
};
|
||||
|
||||
template <typename T> struct output_type<T, std::void_t<decltype(std::declval<T>().output)>> {
|
||||
using type = decltype(std::declval<T>().output);
|
||||
};
|
||||
|
||||
template <typename T> using output_type_t = typename output_type<T>::type;
|
||||
|
||||
// clang-format off
|
||||
template <typename, typename = void> constexpr bool is_template_state_compliant = false;
|
||||
|
||||
template <typename T>
|
||||
constexpr bool is_template_state_compliant<
|
||||
T, std::void_t<
|
||||
// Check for a 'ContainerType' used by the FSM/NestedFSM
|
||||
typename T::ContainerType,
|
||||
// Check for a 'EventType' used by the 'feed' function
|
||||
typename T::EventType,
|
||||
// Check for functions enter/feed/leave/get_id
|
||||
decltype(std::declval<T>().enter()), decltype(std::declval<T>().feed(std::declval<typename T::EventType>())),
|
||||
decltype(std::declval<T>().leave()), decltype(std::declval<T>().get_id()),
|
||||
// TODO(ioan): also check for types of this members?
|
||||
// Check that the return of 'feed' has the 'new_state' and 'unhandled' members
|
||||
decltype(std::declval<T>().feed(std::declval<typename T::EventType>()).unhandled),
|
||||
decltype(std::declval<T>().feed(std::declval<typename T::EventType>()).new_state)>> = true;
|
||||
// clang-format on
|
||||
|
||||
struct FeedResult {
|
||||
|
||||
FeedResult() = default;
|
||||
FeedResult(bool transition) : m_state(transition ? State::Transition : State::NoTransition) {
|
||||
}
|
||||
|
||||
operator bool() const {
|
||||
return m_state != State::Unhandled;
|
||||
}
|
||||
|
||||
bool transitioned() const {
|
||||
return (m_state == State::Transition);
|
||||
}
|
||||
|
||||
private:
|
||||
enum class State {
|
||||
Unhandled,
|
||||
Transition,
|
||||
NoTransition,
|
||||
};
|
||||
State m_state{State::Unhandled};
|
||||
};
|
||||
} // namespace detail
|
||||
|
||||
template <typename OutputType> struct FeedResult : public detail::FeedResult {
|
||||
using detail::FeedResult::FeedResult;
|
||||
|
||||
FeedResult(OutputType feed_output, bool transition) :
|
||||
detail::FeedResult(transition), output(std::move(feed_output)) {
|
||||
}
|
||||
|
||||
OutputType output; // we're requiring this to be default constructible?
|
||||
};
|
||||
|
||||
template <> struct FeedResult<void> : public detail::FeedResult {
|
||||
using detail::FeedResult::FeedResult; // inherit ctors
|
||||
};
|
||||
|
||||
template <typename StateType> class AbstractFSM {
|
||||
static_assert(detail::is_template_state_compliant<StateType>,
|
||||
"State must define a 'using EventType'! "
|
||||
"State must define a 'using ContainerType'! "
|
||||
"State must implement 'enter', 'feed', 'leave' functions! "
|
||||
"Return of 'feed' must have the 'unhandled' and 'new_state' members! ");
|
||||
};
|
||||
|
||||
template <typename StateType> class FSM : public AbstractFSM<StateType> {
|
||||
public:
|
||||
using StateContainerType = typename StateType::ContainerType;
|
||||
|
||||
FSM(StateContainerType initial_state) : m_current_state(std::move(initial_state)) {
|
||||
m_current_state->enter();
|
||||
}
|
||||
|
||||
~FSM() {
|
||||
m_current_state->leave();
|
||||
}
|
||||
|
||||
template <typename... Args> auto feed(Args&&... args) {
|
||||
auto result = m_current_state->feed(std::forward<Args>(args)...);
|
||||
|
||||
using OutputType = detail::output_type_t<decltype(result)>;
|
||||
|
||||
if (result.unhandled) {
|
||||
return FeedResult<OutputType>();
|
||||
}
|
||||
|
||||
const bool transitioned = (result.new_state != nullptr);
|
||||
|
||||
if (transitioned) {
|
||||
m_current_state->leave();
|
||||
m_current_state = std::move(result.new_state);
|
||||
m_current_state->enter();
|
||||
}
|
||||
|
||||
if constexpr (std::is_same_v<OutputType, void>) {
|
||||
return FeedResult<OutputType>(transitioned);
|
||||
} else {
|
||||
return FeedResult<OutputType>(std::move(result.output), transitioned);
|
||||
}
|
||||
}
|
||||
|
||||
auto get_current_state_id() const {
|
||||
return m_current_state->get_id();
|
||||
}
|
||||
|
||||
private:
|
||||
StateContainerType m_current_state;
|
||||
};
|
||||
|
||||
namespace detail {
|
||||
template <typename StateStackType> void unroll_child_states(StateStackType& state_stack) {
|
||||
while (true) {
|
||||
auto& leaf = state_stack.back();
|
||||
leaf->enter();
|
||||
|
||||
auto child = leaf->get_initial();
|
||||
|
||||
if (child == nullptr) {
|
||||
break;
|
||||
}
|
||||
|
||||
state_stack.emplace_back(std::move(child));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
template <typename StateType> class NestedFSM : public AbstractFSM<StateType> {
|
||||
public:
|
||||
using StateContainerType = typename StateType::ContainerType;
|
||||
|
||||
NestedFSM(StateContainerType initial_state) {
|
||||
m_state_stack.emplace_back(std::move(initial_state));
|
||||
detail::unroll_child_states(m_state_stack);
|
||||
}
|
||||
|
||||
~NestedFSM() {
|
||||
// leave all the states in order
|
||||
for (auto leaf = m_state_stack.rbegin(); leaf != m_state_stack.rend();) {
|
||||
(*leaf)->leave();
|
||||
leaf = std::next(leaf);
|
||||
m_state_stack.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
template <typename... Args> auto feed(Args&&... args) {
|
||||
auto leaf = m_state_stack.rbegin();
|
||||
|
||||
auto result = (*leaf)->feed(std::forward<Args>(args)...);
|
||||
using OutputType = detail::output_type_t<decltype(result)>;
|
||||
|
||||
auto next_state = std::next(leaf);
|
||||
|
||||
// descend stack as long as no state handles the event or the
|
||||
// stack's bottom is reached
|
||||
while (result.unhandled and (next_state != m_state_stack.rend())) {
|
||||
// check next state on stack
|
||||
// FIXME (aw): this will break with rvalue reference, as
|
||||
// they are moved multiple times
|
||||
result = (*next_state)->feed(std::forward<Args>(args)...);
|
||||
next_state = std::next(next_state);
|
||||
}
|
||||
|
||||
// if not handled at all, don't do anything at all
|
||||
if (result.unhandled) {
|
||||
return FeedResult<OutputType>();
|
||||
}
|
||||
|
||||
const bool transitioned = (result.new_state != nullptr);
|
||||
|
||||
if (transitioned) {
|
||||
while (leaf != next_state) {
|
||||
(*leaf)->leave();
|
||||
leaf = std::next(leaf);
|
||||
|
||||
m_state_stack.pop_back();
|
||||
}
|
||||
|
||||
m_state_stack.emplace_back(std::move(result.new_state));
|
||||
detail::unroll_child_states(m_state_stack);
|
||||
}
|
||||
|
||||
if constexpr (std::is_same_v<OutputType, void>) {
|
||||
return FeedResult<OutputType>(transitioned);
|
||||
} else {
|
||||
return FeedResult<OutputType>(std::move(result.output), transitioned);
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE (aw): could be computed along the feed
|
||||
auto get_current_state_id() const {
|
||||
using StateIDType = decltype(std::declval<StateType>().get_id());
|
||||
|
||||
std::vector<StateIDType> current_ids;
|
||||
current_ids.reserve(m_state_stack.size());
|
||||
|
||||
for (const auto& state : m_state_stack) {
|
||||
current_ids.emplace_back(state->get_id());
|
||||
}
|
||||
|
||||
return current_ids;
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<StateContainerType> m_state_stack;
|
||||
};
|
||||
|
||||
} // namespace fsm::v2
|
||||
@@ -0,0 +1,190 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2022 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <type_traits>
|
||||
|
||||
/**
|
||||
* @file comparison.hpp
|
||||
* @brief Mathematical and Optional utility functions for the EVerest framework.
|
||||
*/
|
||||
|
||||
namespace everest::lib::util {
|
||||
|
||||
// --- Floating Point Utilities ---
|
||||
|
||||
/**
|
||||
* @brief Calculates a threshold based on powers of ten (10^-n).
|
||||
* * * Positive `digits_of_precision` generate fractional limits (e.g., 3 yields 0.001).
|
||||
* * Negative `digits_of_precision` generate magnitude limits (e.g., -2 yields 100.0).
|
||||
* * A value of 0 yields 1.0.
|
||||
* * @tparam T The floating point type (float, double, long double).
|
||||
* @param digits_of_precision The exponent modifier for the threshold.
|
||||
* @return constexpr T The calculated threshold value.
|
||||
*/
|
||||
template <class T, std::enable_if_t<std::is_floating_point_v<T>>* = nullptr>
|
||||
constexpr T range_limit(int digits_of_precision) {
|
||||
T result = static_cast<T>(1.0);
|
||||
|
||||
if (digits_of_precision > 0) {
|
||||
// Handle fractions (10^-n)
|
||||
for (int i = 0; i < digits_of_precision; ++i) {
|
||||
result /= static_cast<T>(10.0);
|
||||
}
|
||||
} else if (digits_of_precision < 0) {
|
||||
// Handle magnitudes (10^+n)
|
||||
for (int i = 0; i > digits_of_precision; --i) {
|
||||
result *= static_cast<T>(10.0);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Compares two floating point numbers for equality within a decimal precision.
|
||||
* * Uses a fixed epsilon (absolute difference) approach. This is ideal for
|
||||
* physical values (like Amps or Volts) where the scale of the numbers is
|
||||
* relatively consistent and known.
|
||||
* * @tparam Prec The number of decimal digits of precision to use for comparison.
|
||||
* @tparam T1, T2 Floating point types of the input values.
|
||||
* @param lhs Left hand side value.
|
||||
* @param rhs Right hand side value.
|
||||
* @return true if the absolute difference is less than 10^-Prec.
|
||||
*/
|
||||
template <int Prec, class T1, class T2,
|
||||
std::enable_if_t<std::is_floating_point_v<T1> && std::is_floating_point_v<T2>>* = nullptr>
|
||||
constexpr bool almost_eq(T1 lhs, T2 rhs) {
|
||||
using Common = std::common_type_t<T1, T2>;
|
||||
const auto val_lhs = static_cast<Common>(lhs);
|
||||
const auto val_rhs = static_cast<Common>(rhs);
|
||||
// std::abs is not guaranteed to be constexpr in C++17, using the ternary operate instead.
|
||||
const auto diff = (val_lhs > val_rhs) ? (val_lhs - val_rhs) : (val_rhs - val_lhs);
|
||||
return diff < range_limit<Common>(Prec);
|
||||
}
|
||||
/**
|
||||
* @brief Compares two std::optional floating point values for equality.
|
||||
* * Logic follows these rules:
|
||||
* 1. If both contain values, performs almost_eq on the underlying values.
|
||||
* 2. If both are empty, they are considered equal (true).
|
||||
* 3. If only one is empty, they are not equal (false).
|
||||
* * @tparam Prec Decimal digits of precision.
|
||||
* @param lhs Optional value A.
|
||||
* @param rhs Optional value B.
|
||||
*/
|
||||
template <int Prec, class T> constexpr bool almost_eq(std::optional<T> const& lhs, std::optional<T> const& rhs) {
|
||||
if (lhs.has_value() and rhs.has_value()) {
|
||||
return almost_eq<Prec>(lhs.value(), rhs.value());
|
||||
}
|
||||
return lhs.has_value() == rhs.has_value();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Determines if the difference between two values is within a specific noise level.
|
||||
* * Useful for filtering out jitter in sensor readings or small fluctuations
|
||||
* that should not trigger logic changes.
|
||||
* * @param val_a First value.
|
||||
* @param val_b Second value.
|
||||
* @param noise_level The maximum allowed difference to be considered "noise".
|
||||
* @return true if |val_a - val_b| <= noise_level.
|
||||
*/
|
||||
template <class T, std::enable_if_t<std::is_floating_point_v<T>>* = nullptr>
|
||||
bool in_noise_range(T val_a, T val_b, T noise_level) {
|
||||
return std::abs(val_a - val_b) <= noise_level;
|
||||
}
|
||||
|
||||
// --- Optional Math Utilities (Minimum) ---
|
||||
|
||||
/**
|
||||
* @brief Finds the minimum between two std::optional values.
|
||||
* * In this context, an empty std::optional is treated as "no limit" or "infinity".
|
||||
* * @param a First optional value.
|
||||
* @param b Second optional value.
|
||||
* @return The smaller of the two if both exist, otherwise the existing value.
|
||||
*/
|
||||
template <class T, std::enable_if_t<std::is_floating_point_v<T>>* = nullptr>
|
||||
std::optional<T> min_optional(std::optional<T> const& a, std::optional<T> const& b) {
|
||||
if (not a.has_value()) {
|
||||
return b;
|
||||
}
|
||||
if (not b.has_value()) {
|
||||
return a;
|
||||
}
|
||||
return std::min(a.value(), b.value());
|
||||
}
|
||||
|
||||
template <class T> T min_optional(T a, std::optional<T> const& b) {
|
||||
if (b.has_value() and b.value() < a) {
|
||||
return b.value();
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
template <class T> T min_optional(std::optional<T> const& a, T b) {
|
||||
if (a.has_value() and a.value() < b) {
|
||||
return a.value();
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
// --- Optional Math Utilities (Maximum) ---
|
||||
|
||||
/**
|
||||
* @brief Finds the maximum between two std::optional values.
|
||||
* * In this context, an empty std::optional is treated as "negative infinity".
|
||||
* * @param a First optional value.
|
||||
* @param b Second optional value.
|
||||
* @return The larger of the two if both exist, otherwise the existing value.
|
||||
*/
|
||||
template <class T, std::enable_if_t<std::is_floating_point_v<T>>* = nullptr>
|
||||
std::optional<T> max_optional(std::optional<T> const& a, std::optional<T> const& b) {
|
||||
if (not a.has_value()) {
|
||||
return b;
|
||||
}
|
||||
if (not b.has_value()) {
|
||||
return a;
|
||||
}
|
||||
return std::max(a.value(), b.value());
|
||||
}
|
||||
|
||||
template <class T> T max_optional(T a, std::optional<T> const& b) {
|
||||
if (b.has_value() and b.value() > a) {
|
||||
return b.value();
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
template <class T> T max_optional(std::optional<T> const& a, T b) {
|
||||
if (a.has_value() and a.value() > b) {
|
||||
return a.value();
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
// --- Optional Math Utilities (Clamping) ---
|
||||
|
||||
/**
|
||||
* @brief Clamps a value between an optional minimum and an optional maximum.
|
||||
* * If a bound is empty, it is ignored (e.g., if min is empty, only the max is applied).
|
||||
* * @param v The value to clamp.
|
||||
* @param min The lower bound constraint.
|
||||
* @param max The upper bound constraint.
|
||||
* @return The clamped value.
|
||||
*/
|
||||
template <class T> T clamp_optional(T v, std::optional<T> const& min, std::optional<T> const& max) {
|
||||
T result = v;
|
||||
if (min.has_value() and result < min.value()) {
|
||||
result = min.value();
|
||||
}
|
||||
if (max.has_value() and result > max.value()) {
|
||||
result = max.value();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace everest::lib::util
|
||||
@@ -0,0 +1,66 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
|
||||
namespace everest::lib::util {
|
||||
/**
|
||||
* @brief Bind member function to std::function
|
||||
* @param[in] func Member function
|
||||
* @param[in] ptr to object (likely this)
|
||||
*/
|
||||
|
||||
template <typename R, typename C, typename... Args>
|
||||
std::function<R(Args...)> function_bind_obj(R (C::*func)(Args...), C* obj) {
|
||||
return [func, obj](Args... args) -> R { return (obj->*func)(std::forward<Args>(args)...); };
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Binds a member function to a specific object instance without std::function overhead.
|
||||
*
|
||||
* This utility creates a lightweight lambda closure that captures a member function pointer
|
||||
* and an object pointer.
|
||||
*
|
||||
* @tparam R The return type of the member function.
|
||||
* @tparam C The class type owning the member function.
|
||||
* @tparam Args The argument types expected by the member function.
|
||||
*
|
||||
* @param func A pointer to the member function (e.g., &MyClass::handle_data).
|
||||
* @param obj A pointer to the instance the function should be called on.
|
||||
*
|
||||
* @return A lambda closure that, when called, invokes the member function on @p obj.
|
||||
*
|
||||
* @note By returning 'auto', this function avoids the type-erasure overhead of std::function.
|
||||
* The compiler can often inline the resulting call entirely. It remains implicitly
|
||||
* convertible to std::function if required by an API.
|
||||
*
|
||||
* @par Example:
|
||||
* @code
|
||||
* auto processor = bind_obj(&MyCalss::handle_data, this);
|
||||
* consume(queue, processor);
|
||||
* @endcode
|
||||
*/
|
||||
template <typename R, typename C, typename... Args> auto bind_obj(R (C::*func)(Args...), C* obj) {
|
||||
return [func, obj](Args&&... args) -> R { return (obj->*func)(std::forward<Args>(args)...); };
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Const-qualified version of bind_obj.
|
||||
*
|
||||
* Overload to support binding to member functions marked with 'const'.
|
||||
* @tparam R The return type of the member function.
|
||||
* @tparam C The class type owning the member function.
|
||||
* @tparam Args The argument types expected by the member function.
|
||||
*
|
||||
* @param func A pointer to the const member function.
|
||||
* @param obj A pointer to the const instance.
|
||||
*
|
||||
* @return A lambda closure that invokes the const member function on @p obj.
|
||||
*/
|
||||
template <typename R, typename C, typename... Args> auto bind_obj(R (C::*func)(Args...) const, const C* obj) {
|
||||
return [func, obj](Args&&... args) -> R { return (obj->*func)(std::forward<Args>(args)...); };
|
||||
}
|
||||
|
||||
} // namespace everest::lib::util
|
||||
@@ -0,0 +1,128 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <iterator>
|
||||
#include <optional>
|
||||
#include <type_traits>
|
||||
|
||||
/**
|
||||
* @file container_utils.hpp
|
||||
* @brief Utility functions for generic STL container operations.
|
||||
*/
|
||||
|
||||
namespace everest::lib::util {
|
||||
|
||||
/**
|
||||
* @brief Internal type trait to detect if a container has a .find() member function.
|
||||
* @tparam T The container type to check.
|
||||
* @tparam Value The type of the value to search for.
|
||||
*/
|
||||
template <typename T, typename Value, typename = void> struct has_find : std::false_type {};
|
||||
|
||||
template <typename T, typename Value>
|
||||
struct has_find<T, Value, std::void_t<decltype(std::declval<const T&>().find(std::declval<const Value&>()))>>
|
||||
: std::true_type {};
|
||||
|
||||
/**
|
||||
* @brief Checks if a value exists in a sequence container (vector, list, etc.).
|
||||
* @note Internal implementation using O(n) linear search.
|
||||
*/
|
||||
template <typename Container, typename T> auto exists_impl(const Container& c, const T& val, std::false_type) -> bool {
|
||||
return std::find(std::begin(c), std::end(c), val) != std::end(c);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Checks if a value exists in an associative container (set, map, etc.).
|
||||
* @note Internal implementation using the container's optimized .find() method.
|
||||
*/
|
||||
template <typename Container, typename T> auto exists_impl(const Container& c, const T& val, std::true_type) -> bool {
|
||||
return c.find(val) != c.end();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Generically checks if a specific item exists within a container.
|
||||
* * This function automatically selects the most efficient search algorithm
|
||||
* available for the provided container type at compile-time.
|
||||
* This is supposed to be a replacement for C++20 'contains' method
|
||||
* * @tparam Container The type of the STL-compatible container.
|
||||
* @tparam T The type of the value to search for.
|
||||
* @param c The constant reference to the container to search.
|
||||
* @param val The value to look for.
|
||||
* @return true if the item is found, false otherwise.
|
||||
* * @par Complexity:
|
||||
* - **O(log n)** for associative containers (std::set, std::map).
|
||||
* - **O(1)** average for unordered containers (std::unordered_set/map).
|
||||
* - **O(n)** for sequence containers (std::vector, std::list).
|
||||
*/
|
||||
template <typename Container, typename T> bool exists(const Container& c, const T& val) {
|
||||
return exists_impl(c, val, has_find<Container, T>{});
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Implementation for sequence containers (vector, list, etc.).
|
||||
* @return Pointer to the found element or nullptr.
|
||||
*/
|
||||
template <typename Container, typename T> auto find_ptr_impl(Container& c, const T& val, std::false_type) {
|
||||
auto it = std::find(std::begin(c), std::end(c), val);
|
||||
return (it != std::end(c)) ? &(*it) : nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Implementation for associative containers (set, map, etc.).
|
||||
* @return Pointer to the found element or nullptr.
|
||||
*/
|
||||
template <typename Container, typename T> auto find_ptr_impl(Container& c, const T& val, std::true_type) {
|
||||
auto it = c.find(val);
|
||||
return (it != std::end(c)) ? &(*it) : nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Generically finds an item and returns a pointer to it.
|
||||
* * This utility provides a unified interface to find an element across different
|
||||
* STL containers. It returns a pointer to the element if found, allowing
|
||||
* for both existence checking and immediate access.
|
||||
* * @tparam Container The STL container type.
|
||||
* @tparam T The type of the value/key to search for.
|
||||
* @param c The container (can be const or non-const).
|
||||
* @param val The value to search for.
|
||||
* @return A pointer to the element within the container, or `nullptr` if not found.
|
||||
* * @example
|
||||
* if (auto* item = utils::find_ptr(my_vector, 42)) {
|
||||
* *item = 43; // Modify if non-const
|
||||
* }
|
||||
*/
|
||||
template <typename Container, typename T> auto find_ptr(Container& c, const T& val) {
|
||||
return find_ptr_impl(c, val, has_find<Container, T>{});
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Searches for an element and returns an optional iterator.
|
||||
* * This function abstracts the difference between sequence containers (like vector)
|
||||
* and associative containers (like set/map) to provide the most efficient
|
||||
* lookup possible.
|
||||
* * @tparam Container The STL container type.
|
||||
* @tparam T The value or key to search for.
|
||||
* @param c The container to search within.
|
||||
* @param val The value/key to find.
|
||||
* @return std::optional wrapping the iterator. Returns std::nullopt if not found.
|
||||
* * @note If found, the iterator can be used to access the element (*it) or
|
||||
* pass to c.erase(it) for efficient deletion.
|
||||
*/
|
||||
template <typename Container, typename T>
|
||||
auto find_optional(Container& c, const T& val) -> std::optional<decltype(std::begin(c))> {
|
||||
if constexpr (has_find<Container, T>::value) {
|
||||
auto it = c.find(val);
|
||||
if (it != std::end(c))
|
||||
return it;
|
||||
} else {
|
||||
auto it = std::find(std::begin(c), std::end(c), val);
|
||||
if (it != std::end(c))
|
||||
return it;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
} // namespace everest::lib::util
|
||||
@@ -0,0 +1,113 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
/** \file */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <queue>
|
||||
|
||||
namespace everest::lib::util {
|
||||
|
||||
/**
|
||||
* Simplified interface for a queue based
|
||||
* on <a href="https://en.cppreference.com/w/cpp/container/queue">std::queue</a>
|
||||
* @tparam T Datatype held by the queue
|
||||
*/
|
||||
template <class T> class simple_queue {
|
||||
public:
|
||||
/**
|
||||
* @var reference
|
||||
* @brief Inherited type definition from STL
|
||||
*/
|
||||
using reference = typename std::queue<T>::reference;
|
||||
|
||||
/**
|
||||
* @var const_reference
|
||||
* @brief Inherited type definition from STL
|
||||
*/
|
||||
using const_reference = typename std::queue<T>::const_reference;
|
||||
|
||||
/**
|
||||
* @var value_type
|
||||
* @brief Inherited type definition from STL
|
||||
*/
|
||||
using value_type = typename std::queue<T>::value_type;
|
||||
|
||||
/**
|
||||
* @var size_type
|
||||
* @brief Inherited type definition from STL
|
||||
*/
|
||||
using size_type = typename std::queue<T>::size_type;
|
||||
|
||||
/**
|
||||
* @brief Maps to std::queue::front()
|
||||
*/
|
||||
const_reference front() const {
|
||||
return m_queue.front();
|
||||
}
|
||||
|
||||
/**
|
||||
* Non-const front() is crucial for move semantics
|
||||
**/
|
||||
reference front() {
|
||||
return m_queue.front();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Maps to std::queue::back()
|
||||
*/
|
||||
const_reference back() const {
|
||||
return m_queue.back();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Maps to std::queue::push(const value_type&)
|
||||
*/
|
||||
void push(const value_type& value) {
|
||||
m_queue.push(value);
|
||||
}
|
||||
/**
|
||||
* @brief Maps to std::queue::push(value_type&&)
|
||||
*/
|
||||
void push(value_type&& value) {
|
||||
m_queue.push(std::forward<value_type>(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the front element of the queue if available.
|
||||
* @return Maybe the front element
|
||||
*/
|
||||
std::optional<value_type> pop() {
|
||||
if (m_queue.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
value_type tmp = std::move(front());
|
||||
m_queue.pop();
|
||||
return tmp;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Maps to std::queue::empty()
|
||||
*/
|
||||
bool empty() const {
|
||||
return m_queue.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Maps to std::queue::size()
|
||||
*/
|
||||
size_type size() const {
|
||||
return m_queue.size();
|
||||
}
|
||||
|
||||
template <class... Args> decltype(auto) emplace(Args&&... args) {
|
||||
m_queue.emplace(std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
private:
|
||||
std::queue<T> m_queue;
|
||||
};
|
||||
|
||||
} // namespace everest::lib::util
|
||||
@@ -0,0 +1,197 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
/** \file */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "simple_queue.hpp"
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
|
||||
namespace everest::lib::util {
|
||||
|
||||
/**
|
||||
* A thread safe bounded queue implemented on top of \ref queue::simple_queue. <br>
|
||||
* The common resource \ref simple_queue is guarded by a mutex in every member function.
|
||||
* A caller blocking on \p push will be unblocked when space becomes available via \p pop.
|
||||
* A caller blocking on \p pop or \p try_pop will be unblocked when new data is made available via \p push.
|
||||
* @tparam T Datatype held by the queue
|
||||
*/
|
||||
template <class T> class thread_safe_bounded_queue {
|
||||
public:
|
||||
/**
|
||||
* @var value_type
|
||||
* @brief Inherited type definition.
|
||||
*/
|
||||
using value_type = typename simple_queue<T>::value_type;
|
||||
|
||||
/**
|
||||
* @var size_type
|
||||
* @brief Inherited size type definition.
|
||||
*/
|
||||
using size_type = typename simple_queue<T>::size_type;
|
||||
|
||||
/**
|
||||
* @brief Constructor for the bounded queue.
|
||||
* @param[in] max_size The maximum number of elements allowed in the queue.
|
||||
* A value of 0 indicates an unbounded queue.
|
||||
*/
|
||||
explicit thread_safe_bounded_queue(size_type max_size = 0) : m_max_size(max_size) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Push new data into the queue
|
||||
* @details Blocks the caller if the queue has reached its \p max_size.
|
||||
* @param[in] value data
|
||||
* @return The size of the queue after push. Returns 0 if the queue is stopped.
|
||||
*/
|
||||
size_type push(const value_type& value) {
|
||||
std::unique_lock lock(m_mtx);
|
||||
if (m_max_size > 0) {
|
||||
m_cv_producer.wait(lock, [this]() { return m_queue.size() < m_max_size || m_stop; });
|
||||
}
|
||||
|
||||
if (m_stop) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
m_queue.push(value);
|
||||
auto result = m_queue.size();
|
||||
lock.unlock();
|
||||
m_cv_consumer.notify_one();
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Push new data into the queue
|
||||
* @details Blocks the caller if the queue has reached its \p max_size.
|
||||
* @param[in] value data
|
||||
* @return The size of the queue after push. Returns 0 if the queue is stopped.
|
||||
*/
|
||||
size_type push(value_type&& value) {
|
||||
std::unique_lock lock(m_mtx);
|
||||
if (m_max_size > 0) {
|
||||
m_cv_producer.wait(lock, [this]() { return m_queue.size() < m_max_size || m_stop; });
|
||||
}
|
||||
|
||||
if (m_stop) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
m_queue.push(std::forward<value_type>(value));
|
||||
auto result = m_queue.size();
|
||||
lock.unlock();
|
||||
m_cv_consumer.notify_one();
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Try to get an element from the queue.
|
||||
* @details Returns immediately.
|
||||
* @return An element from the queue, if one is available. \p std::nullopt otherwise
|
||||
*/
|
||||
std::optional<value_type> try_pop() {
|
||||
return pop_impl(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Try to get an element from the queue.
|
||||
* @details Returns as soon as data is availble or after timeout.
|
||||
* @param[in] timeout as <a href="https://en.cppreference.com/w/cpp/chrono/duration">std::chrono::duration</a>.
|
||||
* Smallest unit acceptable is milliseconds.
|
||||
* @return An element from the queue, if one is available. \p std::nullopt otherwise
|
||||
*/
|
||||
template <class Rep, class Period> std::optional<value_type> try_pop(std::chrono::duration<Rep, Period> timeout) {
|
||||
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(timeout);
|
||||
return pop_impl(ms.count());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get an element from the queue
|
||||
* @details Only returns, when data is available. Implicitly throws on stop().
|
||||
* @return An element from the queue.
|
||||
*/
|
||||
value_type pop() {
|
||||
return pop_impl(-1).value();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get an element from the queue
|
||||
* @details Only returns, when data is available, or the queue is stopped.
|
||||
* @return An element from the queue. Empty optional if stopped.
|
||||
*/
|
||||
std::optional<value_type> wait_and_pop() {
|
||||
return pop_impl(-1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Signals that no more items will be pushed and unblocks all waiting consumers and producers.
|
||||
* @details Remaining items in the queue can still be popped until it is empty.
|
||||
*/
|
||||
void stop() {
|
||||
std::unique_lock lock(m_mtx);
|
||||
m_stop = true;
|
||||
lock.unlock();
|
||||
m_cv_consumer.notify_all();
|
||||
m_cv_producer.notify_all();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Safely returns the arrival time of the oldest task.
|
||||
* @return std::optional containing the time_point of the oldest task,
|
||||
* or std::nullopt if the queue is empty.
|
||||
*/
|
||||
std::optional<std::chrono::steady_clock::time_point> oldest_arrival() const {
|
||||
std::lock_guard lock(m_mtx);
|
||||
if (m_queue.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return m_queue.front().arrival;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Safely returns the current number of elements in the queue.
|
||||
*/
|
||||
size_type size() const {
|
||||
std::lock_guard lock(m_mtx);
|
||||
return m_queue.size();
|
||||
}
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Internal implementation of the pop logic.
|
||||
* @param[in] timeout_ms Timeout in milliseconds. -1 for infinite wait, 0 for immediate return.
|
||||
* @return An optional containing the popped value or std::nullopt.
|
||||
*/
|
||||
std::optional<value_type> pop_impl(int timeout_ms) {
|
||||
std::unique_lock lock(m_mtx);
|
||||
auto wait_predicate = [this]() { return not m_queue.empty() or m_stop; };
|
||||
|
||||
if (timeout_ms < 0) {
|
||||
m_cv_consumer.wait(lock, wait_predicate);
|
||||
} else if (timeout_ms > 0) {
|
||||
(void)m_cv_consumer.wait_for(lock, std::chrono::milliseconds(timeout_ms), wait_predicate);
|
||||
}
|
||||
|
||||
// if the queue is still empty, we return a nullopt. Note that this would be implicitly
|
||||
// handled by simple_queue::pop, but it is added here to be more explcit
|
||||
if (m_queue.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto result = m_queue.pop();
|
||||
lock.unlock();
|
||||
m_cv_producer.notify_one();
|
||||
return result;
|
||||
}
|
||||
|
||||
simple_queue<T> m_queue; ///< The underlying non-thread-safe container.
|
||||
const size_type m_max_size; ///< Maximum capacity of the queue.
|
||||
mutable std::mutex m_mtx; ///< Mutex guarding access to the queue and state.
|
||||
std::condition_variable m_cv_consumer; ///< Condition variable for consumers waiting for data.
|
||||
std::condition_variable m_cv_producer; ///< Condition variable for producers waiting for space.
|
||||
bool m_stop{false}; ///< Flag indicating the queue is shutting down.
|
||||
};
|
||||
} // namespace everest::lib::util
|
||||
@@ -0,0 +1,131 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
/** \file */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "simple_queue.hpp"
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
|
||||
namespace everest::lib::util {
|
||||
|
||||
/**
|
||||
* A thread safe queue implemented on top of \ref queue::simple_queue. <br>
|
||||
* The common resource \ref simple_queue is guarded by a mutex in every member function. A caller blocking on \p pop
|
||||
* or \p try_pop will be unblocked when new data made available via \p push
|
||||
* @tparam T Datatype held by the queue
|
||||
*/
|
||||
template <class T> class thread_safe_queue {
|
||||
public:
|
||||
/**
|
||||
* @var value_type
|
||||
* @brief Inherited type definition.
|
||||
*/
|
||||
using value_type = typename simple_queue<T>::value_type;
|
||||
|
||||
/**
|
||||
* @brief Push new data into the queue
|
||||
* @param[in] value data
|
||||
* @return The size of the queue after push
|
||||
*/
|
||||
typename simple_queue<T>::size_type push(const value_type& value) {
|
||||
std::unique_lock lock(m_mtx);
|
||||
m_queue.push(value);
|
||||
auto result = m_queue.size();
|
||||
lock.unlock();
|
||||
m_cv.notify_one();
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* @brief Push new data into the queue
|
||||
* @param[in] value data
|
||||
* @return The size of the queue after push
|
||||
*/
|
||||
typename simple_queue<T>::size_type push(value_type&& value) {
|
||||
std::unique_lock lock(m_mtx);
|
||||
m_queue.push(std::forward<value_type>(value));
|
||||
auto result = m_queue.size();
|
||||
lock.unlock();
|
||||
m_cv.notify_one();
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Try to get an element from the queue.
|
||||
* @details Returns immediately.
|
||||
* @return An element from the queue, if one is available. \p std::nullopt otherwise
|
||||
*/
|
||||
std::optional<value_type> try_pop() {
|
||||
return pop_impl(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Try to get an element from the queue.
|
||||
* @details Returns as soon as data is availble or after timeout.
|
||||
* @param[in] timeout as <a href="https://en.cppreference.com/w/cpp/chrono/duration">std::chrono::duration</a>.
|
||||
* Smallest unit acceptable is milliseconds.
|
||||
* @return An element from the queue, if one is available. \p std::nullopt otherwise
|
||||
*/
|
||||
template <class Rep, class Period> std::optional<value_type> try_pop(std::chrono::duration<Rep, Period> timeout) {
|
||||
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(timeout);
|
||||
return pop_impl(ms.count());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get an element from the queue
|
||||
* @details Only returns, when data is available. Implicitly throws on stop().
|
||||
* @return An element from the queue.
|
||||
*/
|
||||
value_type pop() {
|
||||
return pop_impl(-1).value();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get an element from the queue
|
||||
* @details Only returns, when data is available, or the queue is stopped.
|
||||
* @return An element from the queue. Empty optional if stopped.
|
||||
*/
|
||||
std::optional<value_type> wait_and_pop() {
|
||||
return pop_impl(-1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Signals that no more items will be pushed and unblocks all waiting consumers.
|
||||
*/
|
||||
void stop() {
|
||||
std::unique_lock lock(m_mtx);
|
||||
m_stop = true;
|
||||
lock.unlock();
|
||||
m_cv.notify_all();
|
||||
}
|
||||
|
||||
private:
|
||||
std::optional<value_type> pop_impl(int timeout_ms) {
|
||||
std::unique_lock lock(m_mtx);
|
||||
auto wait_predicate = [this]() { return not m_queue.empty() or m_stop; };
|
||||
|
||||
if (timeout_ms < 0) {
|
||||
m_cv.wait(lock, wait_predicate);
|
||||
} else if (timeout_ms > 0) {
|
||||
(void)m_cv.wait_for(lock, std::chrono::milliseconds(timeout_ms), wait_predicate);
|
||||
}
|
||||
|
||||
// if the queue is still empty, we return a nullopt. Note that this would be implicitly
|
||||
// handled by simple_queue::pop, but it is added here to be more explcit
|
||||
if (m_queue.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return m_queue.pop();
|
||||
}
|
||||
|
||||
simple_queue<T> m_queue;
|
||||
std::mutex m_mtx;
|
||||
std::condition_variable m_cv;
|
||||
bool m_stop{false};
|
||||
};
|
||||
|
||||
} // namespace everest::lib::util
|
||||
@@ -0,0 +1,658 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
/**
|
||||
* @file fixed_vector.hpp
|
||||
* @brief Provides a std::vector-like container with fixed capacity, avoiding dynamic memory allocation.
|
||||
*
|
||||
* The fixed_vector is a sequence container that encapsulates a fixed-size array. It provides an interface
|
||||
* similar to std::vector but does not allocate memory on the heap. Its capacity is determined at compile time
|
||||
* by the template parameter N. This makes it suitable for real-time and embedded applications where
|
||||
* dynamic memory allocation is disallowed or undesirable.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <initializer_list>
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace everest::lib::util {
|
||||
|
||||
/**
|
||||
* @brief A container with std::vector-like interface but with fixed capacity.
|
||||
*
|
||||
* This class mimics the behavior of std::vector but stores its elements in a fixed-size internal buffer,
|
||||
* avoiding heap allocations. The capacity is specified at compile time.
|
||||
* If the number of elements exceeds the capacity, it results in an exception (`std::length_error`).
|
||||
*
|
||||
* @tparam T The type of elements.
|
||||
* @tparam N The maximum number of elements the vector can hold (its capacity).
|
||||
*/
|
||||
template <typename T, std::size_t N> class fixed_vector {
|
||||
static_assert(!std::is_move_constructible_v<T> || std::is_nothrow_move_constructible_v<T>,
|
||||
"fixed_vector requires T's move constructor to be noexcept. "
|
||||
"Types with throwing move constructors are not supported because "
|
||||
"fixed_vector cannot propagate move-construction failures safely.");
|
||||
static_assert(!std::is_move_assignable_v<T> || std::is_nothrow_move_assignable_v<T>,
|
||||
"fixed_vector requires T's move assignment to be noexcept. "
|
||||
"Types with throwing move assignment are not supported because "
|
||||
"fixed_vector cannot propagate move-assignment failures safely.");
|
||||
|
||||
public:
|
||||
//- Member types
|
||||
using value_type = T;
|
||||
using size_type = std::size_t;
|
||||
using difference_type = std::ptrdiff_t;
|
||||
using reference = value_type&;
|
||||
using const_reference = const value_type&;
|
||||
using pointer = value_type*;
|
||||
using const_pointer = const value_type*;
|
||||
using iterator = pointer;
|
||||
using const_iterator = const_pointer;
|
||||
using reverse_iterator = std::reverse_iterator<iterator>;
|
||||
using const_reverse_iterator = std::reverse_iterator<const_iterator>;
|
||||
|
||||
/**
|
||||
* @brief Constructs an empty fixed_vector.
|
||||
*/
|
||||
constexpr fixed_vector() noexcept = default;
|
||||
|
||||
/**
|
||||
* @brief Destroys the fixed_vector, calling destructors for all contained elements.
|
||||
*/
|
||||
~fixed_vector() {
|
||||
clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Copy constructor. Constructs the vector with a copy of the contents of other.
|
||||
* @param other another fixed_vector object to be used as source to initialize the elements of the container with.
|
||||
*/
|
||||
fixed_vector(const fixed_vector& other) {
|
||||
copy_construct_from(other);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Move constructor. Constructs the vector with the contents of other using move semantics.
|
||||
* @details Since T is required to be nothrow move-constructible, this operation is always noexcept.
|
||||
* After the move, `other` is empty.
|
||||
* @param other another fixed_vector object to be used as source.
|
||||
*/
|
||||
fixed_vector(fixed_vector&& other) noexcept {
|
||||
move_construct_from(std::move(other));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Constructs the vector with the contents of the initializer list.
|
||||
* @param init initializer list to initialize the elements of the container with.
|
||||
* @throws std::length_error if the size of the initializer list is greater than the capacity of the vector.
|
||||
*/
|
||||
fixed_vector(std::initializer_list<T> init) {
|
||||
if (init.size() > N) {
|
||||
throw std::length_error("Initializer list size exceeds fixed_vector capacity");
|
||||
}
|
||||
|
||||
// std::uninitialized_copy constructs elements in-place. If an element's
|
||||
// constructor throws, it destroys any elements already created.
|
||||
// We only update `size_` after all elements are successfully constructed,
|
||||
// which provides the strong exception guarantee.
|
||||
std::uninitialized_copy(init.begin(), init.end(), data());
|
||||
size_ = init.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Constructs the vector with the contents of an std::vector.
|
||||
* @param vec std::vector to initialize the elements of the container with.
|
||||
* @throws std::length_error if the size of the std::vector is greater than the capacity of the fixed_vector.
|
||||
*/
|
||||
explicit fixed_vector(const std::vector<T>& vec) {
|
||||
if (vec.size() > N) {
|
||||
throw std::length_error("std::vector size exceeds fixed_vector capacity");
|
||||
}
|
||||
|
||||
std::uninitialized_copy(vec.begin(), vec.end(), data());
|
||||
size_ = vec.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Copy assignment operator. Replaces the contents with a copy of the contents of other.
|
||||
* @param other another fixed_vector object to be used as source.
|
||||
* @return *this
|
||||
*/
|
||||
fixed_vector& operator=(const fixed_vector& other) {
|
||||
if (this != &other) {
|
||||
copy_assign_from(other);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Move assignment operator. Replaces the contents with those of other using move semantics.
|
||||
* @details Since T is required to have nothrow move operations, this is always noexcept.
|
||||
* After the move, `other` is empty.
|
||||
* @param other another fixed_vector object to be used as source.
|
||||
* @return *this
|
||||
*/
|
||||
fixed_vector& operator=(fixed_vector&& other) noexcept {
|
||||
if (this != &other) {
|
||||
move_assign_from(std::move(other));
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
// Element access
|
||||
/**
|
||||
* @brief Returns a reference to the element at specified location `pos`, with bounds checking.
|
||||
* @param pos position of the element to return.
|
||||
* @return Reference to the requested element.
|
||||
* @throws std::out_of_range if `pos >= size()`.
|
||||
*/
|
||||
constexpr reference at(size_type pos) {
|
||||
if (pos >= size_) {
|
||||
throw std::out_of_range("fixed_vector::at");
|
||||
}
|
||||
return data()[pos];
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns a const reference to the element at specified location `pos`, with bounds checking.
|
||||
* @param pos position of the element to return.
|
||||
* @return Const reference to the requested element.
|
||||
* @throws std::out_of_range if `pos >= size()`.
|
||||
*/
|
||||
constexpr const_reference at(size_type pos) const {
|
||||
if (pos >= size_) {
|
||||
throw std::out_of_range("fixed_vector::at");
|
||||
}
|
||||
return data()[pos];
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns a reference to the element at specified location `pos`. No bounds checking is performed.
|
||||
* @param pos position of the element to return.
|
||||
* @return Reference to the element at `pos`.
|
||||
*/
|
||||
constexpr reference operator[](size_type pos) {
|
||||
return data()[pos];
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns a const reference to the element at specified location `pos`. No bounds checking is performed.
|
||||
* @param pos position of the element to return.
|
||||
* @return Const reference to the element at `pos`.
|
||||
*/
|
||||
constexpr const_reference operator[](size_type pos) const {
|
||||
return data()[pos];
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns a reference to the first element in the container.
|
||||
* @details Calling front on an empty container is undefined.
|
||||
* @return Reference to the first element.
|
||||
*/
|
||||
constexpr reference front() {
|
||||
return data()[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns a const reference to the first element in the container.
|
||||
* @details Calling front on an empty container is undefined.
|
||||
* @return Const reference to the first element.
|
||||
*/
|
||||
constexpr const_reference front() const {
|
||||
return data()[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns a reference to the last element in the container.
|
||||
* @details Calling back on an empty container is undefined.
|
||||
* @return Reference to the last element.
|
||||
*/
|
||||
constexpr reference back() {
|
||||
return data()[size_ - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns a const reference to the last element in the container.
|
||||
* @details Calling back on an empty container is undefined.
|
||||
* @return Const reference to the last element.
|
||||
*/
|
||||
constexpr const_reference back() const {
|
||||
return data()[size_ - 1];
|
||||
}
|
||||
|
||||
// Iterators
|
||||
/**
|
||||
* @brief Returns an iterator to the first element of the vector.
|
||||
* @return Iterator to the first element.
|
||||
*/
|
||||
constexpr iterator begin() noexcept {
|
||||
return data();
|
||||
}
|
||||
/**
|
||||
* @brief Returns a const iterator to the first element of the vector.
|
||||
* @return Const iterator to the first element.
|
||||
*/
|
||||
constexpr const_iterator begin() const noexcept {
|
||||
return data();
|
||||
}
|
||||
/**
|
||||
* @brief Returns a const iterator to the first element of the vector.
|
||||
* @return Const iterator to the first element.
|
||||
*/
|
||||
constexpr const_iterator cbegin() const noexcept {
|
||||
return data();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns an iterator to the element following the last element of the vector.
|
||||
* @return Iterator to the element following the last element.
|
||||
*/
|
||||
constexpr iterator end() noexcept {
|
||||
return data() + size_;
|
||||
}
|
||||
/**
|
||||
* @brief Returns a const iterator to the element following the last element of the vector.
|
||||
* @return Const iterator to the element following the last element.
|
||||
*/
|
||||
constexpr const_iterator end() const noexcept {
|
||||
return data() + size_;
|
||||
}
|
||||
/**
|
||||
* @brief Returns a const iterator to the element following the last element of the vector.
|
||||
* @return Const iterator to the element following the last element.
|
||||
*/
|
||||
constexpr const_iterator cend() const noexcept {
|
||||
return data() + size_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns a reverse iterator to the first element of the reversed vector.
|
||||
* @return Reverse iterator to the first element.
|
||||
*/
|
||||
constexpr reverse_iterator rbegin() noexcept {
|
||||
return reverse_iterator(end());
|
||||
}
|
||||
/**
|
||||
* @brief Returns a const reverse iterator to the first element of the reversed vector.
|
||||
* @return Const reverse iterator to the first element.
|
||||
*/
|
||||
constexpr const_reverse_iterator rbegin() const noexcept {
|
||||
return const_reverse_iterator(end());
|
||||
}
|
||||
/**
|
||||
* @brief Returns a const reverse iterator to the first element of the reversed vector.
|
||||
* @return Const reverse iterator to the first element.
|
||||
*/
|
||||
constexpr const_reverse_iterator crbegin() const noexcept {
|
||||
return const_reverse_iterator(end());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns a reverse iterator to the element following the last element of the reversed vector.
|
||||
* @return Reverse iterator to the element following the last element.
|
||||
*/
|
||||
constexpr reverse_iterator rend() noexcept {
|
||||
return reverse_iterator(begin());
|
||||
}
|
||||
/**
|
||||
* @brief Returns a const reverse iterator to the element following the last element of the reversed vector.
|
||||
* @return Const reverse iterator to the element following the last element.
|
||||
*/
|
||||
constexpr const_reverse_iterator rend() const noexcept {
|
||||
return const_reverse_iterator(begin());
|
||||
}
|
||||
/**
|
||||
* @brief Returns a const reverse iterator to the element following the last element of the reversed vector.
|
||||
* @return Const reverse iterator to the element following the last element.
|
||||
*/
|
||||
constexpr const_reverse_iterator crend() const noexcept {
|
||||
return const_reverse_iterator(begin());
|
||||
}
|
||||
|
||||
// Capacity
|
||||
/**
|
||||
* @brief Checks if the container has no elements.
|
||||
* @return true if the container is empty, false otherwise.
|
||||
*/
|
||||
[[nodiscard]] constexpr bool empty() const noexcept {
|
||||
return size_ == 0;
|
||||
}
|
||||
/**
|
||||
* @brief Returns the number of elements in the container.
|
||||
* @return The number of elements in the container.
|
||||
*/
|
||||
[[nodiscard]] constexpr size_type size() const noexcept {
|
||||
return size_;
|
||||
}
|
||||
/**
|
||||
* @brief Returns the maximum number of elements the container is able to hold.
|
||||
* @return The maximum number of elements.
|
||||
*/
|
||||
[[nodiscard]] constexpr size_type max_size() const noexcept {
|
||||
return N;
|
||||
}
|
||||
/**
|
||||
* @brief Returns the number of elements that the container has currently allocated space for. For fixed_vector,
|
||||
* this is always N.
|
||||
* @return The capacity of the container.
|
||||
*/
|
||||
[[nodiscard]] constexpr size_type capacity() const noexcept {
|
||||
return N;
|
||||
}
|
||||
|
||||
// Modifiers
|
||||
/**
|
||||
* @brief Erases all elements from the container.
|
||||
*/
|
||||
void clear() noexcept {
|
||||
if constexpr (!std::is_trivially_destructible_v<T>) {
|
||||
for (size_type i = 0; i < size_; ++i) {
|
||||
std::destroy_at(data() + i);
|
||||
}
|
||||
}
|
||||
size_ = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Appends a new element to the end of the container, constructed in-place.
|
||||
* @param args arguments to forward to the constructor of the element.
|
||||
* @return Reference to the emplaced element.
|
||||
* @throws std::length_error if the container is full.
|
||||
*/
|
||||
template <class... Args> reference emplace_back(Args&&... args) {
|
||||
if (size_ >= N) {
|
||||
throw std::length_error("fixed_vector is full");
|
||||
}
|
||||
pointer ptr = data() + size_;
|
||||
new (ptr) T(std::forward<Args>(args)...);
|
||||
++size_;
|
||||
return *ptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Appends a new element to the end of the container, constructed in-place, without throwing exceptions.
|
||||
* @details If the container is full, or if the constructor of the element throws, this function does nothing
|
||||
* and returns `nullptr`.
|
||||
* @param args arguments to forward to the constructor of the element.
|
||||
* @return A pointer to the new element if successful, or `nullptr` otherwise.
|
||||
*/
|
||||
template <class... Args> pointer try_emplace_back(Args&&... args) noexcept {
|
||||
try {
|
||||
return &emplace_back(std::forward<Args>(args)...);
|
||||
} catch (...) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Appends the given element `value` to the end of the container.
|
||||
* @param value the value of the element to append.
|
||||
* @throws std::length_error if the container is full.
|
||||
*/
|
||||
void push_back(const T& value) {
|
||||
emplace_back(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Appends the given element `value` to the end of the container using move semantics.
|
||||
* @param value the value of the element to append.
|
||||
* @throws std::length_error if the container is full.
|
||||
*/
|
||||
void push_back(T&& value) {
|
||||
emplace_back(std::move(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Removes the last element of the container.
|
||||
* @details Calling pop_back on an empty container is a no-op in this implementation.
|
||||
*/
|
||||
void pop_back() {
|
||||
if (size_ > 0) {
|
||||
--size_;
|
||||
if constexpr (!std::is_trivially_destructible_v<T>) {
|
||||
std::destroy_at(data() + size_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Erases the element at `pos`.
|
||||
* @param pos iterator to the element to remove.
|
||||
* @return Iterator following the last removed element.
|
||||
*/
|
||||
iterator erase(const_iterator pos) {
|
||||
const auto offset = std::distance(cbegin(), pos);
|
||||
iterator ita = begin() + offset;
|
||||
|
||||
std::move(ita + 1, end(), ita);
|
||||
|
||||
pop_back();
|
||||
|
||||
return ita;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Erases the elements in the range `[first, last)`.
|
||||
* @param first the first element to remove.
|
||||
* @param last the last element to remove.
|
||||
* @return Iterator following the last removed element.
|
||||
*/
|
||||
iterator erase(const_iterator first, const_iterator last) {
|
||||
iterator it_first = begin() + std::distance(cbegin(), first);
|
||||
iterator it_last = begin() + std::distance(cbegin(), last);
|
||||
auto count = std::distance(it_first, it_last);
|
||||
|
||||
if (count > 0) {
|
||||
std::move(it_last, end(), it_first);
|
||||
|
||||
auto new_size = size_ - count;
|
||||
if constexpr (!std::is_trivially_destructible_v<T>) {
|
||||
for (size_type i = new_size; i < size_; ++i) {
|
||||
std::destroy_at(data() + i);
|
||||
}
|
||||
}
|
||||
size_ = new_size;
|
||||
}
|
||||
|
||||
return it_first;
|
||||
}
|
||||
|
||||
private:
|
||||
alignas(T) std::array<std::byte, N * sizeof(T)> storage_; ///< Internal storage for elements.
|
||||
size_type size_{0}; ///< Current number of elements.
|
||||
|
||||
// --- Implementation helpers for special members ---
|
||||
|
||||
// Copy-construct
|
||||
/**
|
||||
* @brief Helper for copy construction: constructs this fixed_vector by copy-constructing elements from another.
|
||||
* @details This overload is enabled if `T` is copy-constructible.
|
||||
* @param other The fixed_vector to copy elements from.
|
||||
*/
|
||||
template <typename U = T, std::enable_if_t<std::is_copy_constructible_v<U>>* = nullptr>
|
||||
void copy_construct_from(const fixed_vector& other) {
|
||||
for (const auto& elem : other) {
|
||||
push_back(elem);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Helper for copy construction: provides a compile-time error if `T` is not copy-constructible.
|
||||
* @details This overload is enabled if `T` is not copy-constructible, triggering a `static_assert`.
|
||||
* @param other Unused, present for signature matching.
|
||||
*/
|
||||
template <typename U = T, std::enable_if_t<!std::is_copy_constructible_v<U>>* = nullptr>
|
||||
void copy_construct_from(const fixed_vector& /*other*/) {
|
||||
static_assert(std::is_copy_constructible_v<U>,
|
||||
"fixed_vector requires T to be copy-constructible for copy construction.");
|
||||
}
|
||||
|
||||
// Copy-assign
|
||||
/**
|
||||
* @brief Helper for copy assignment: assigns elements from another fixed_vector.
|
||||
* @details This overload is enabled if `T` is both copy-constructible and copy-assignable.
|
||||
* It uses an efficient element-wise assignment strategy.
|
||||
* @param other The fixed_vector to assign elements from.
|
||||
* @return A reference to this fixed_vector.
|
||||
*/
|
||||
template <typename U = T,
|
||||
std::enable_if_t<std::is_copy_constructible_v<U> && std::is_copy_assignable_v<U>>* = nullptr>
|
||||
fixed_vector& copy_assign_from(const fixed_vector& other) {
|
||||
const size_type copy_len = std::min(size_, other.size_);
|
||||
std::copy(other.begin(), other.begin() + copy_len, begin());
|
||||
|
||||
if (size_ < other.size_) {
|
||||
for (size_type i = size_; i < other.size_; ++i) {
|
||||
push_back(other[i]);
|
||||
}
|
||||
} else if (size_ > other.size_) {
|
||||
const size_type old_size = size_;
|
||||
size_ = other.size_;
|
||||
if constexpr (!std::is_trivially_destructible_v<T>) {
|
||||
for (size_type i = size_; i < old_size; ++i) {
|
||||
std::destroy_at(data() + i);
|
||||
}
|
||||
}
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Helper for copy assignment: provides a compile-time error if `T` is not copy-constructible or not
|
||||
* copy-assignable.
|
||||
* @details This overload is enabled if `T` does not meet the requirements, triggering `static_assert`s.
|
||||
* @param other Unused, present for signature matching.
|
||||
* @return A reference to this fixed_vector (never reached due to static_assert).
|
||||
*/
|
||||
template <typename U = T,
|
||||
std::enable_if_t<!(std::is_copy_constructible_v<U> && std::is_copy_assignable_v<U>)>* = nullptr>
|
||||
fixed_vector& copy_assign_from(const fixed_vector& /*other*/) {
|
||||
static_assert(std::is_copy_constructible_v<U>,
|
||||
"fixed_vector requires T to be copy-constructible for copy assignment.");
|
||||
static_assert(std::is_copy_assignable_v<U>,
|
||||
"fixed_vector requires T to be copy-assignable for copy assignment.");
|
||||
return *this;
|
||||
}
|
||||
|
||||
// Move-construct
|
||||
/**
|
||||
* @brief Helper for move construction.
|
||||
* @details This overload is enabled if `T` is nothrow move-constructible (enforced by class-level
|
||||
* static_assert). It is `noexcept`.
|
||||
*/
|
||||
template <typename U = T, std::enable_if_t<std::is_nothrow_move_constructible_v<U>>* = nullptr>
|
||||
void move_construct_from(fixed_vector&& other) noexcept {
|
||||
for (auto& elem : other) {
|
||||
if (try_emplace_back(std::move(elem)) == nullptr) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
other.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Helper for move construction: provides a compile-time error if `T` is not move-constructible.
|
||||
* @details This overload is enabled if `T` is not move-constructible, triggering a `static_assert`.
|
||||
* @param other Unused, present for signature matching.
|
||||
*/
|
||||
template <typename U = T, std::enable_if_t<!std::is_nothrow_move_constructible_v<U>>* = nullptr>
|
||||
void move_construct_from(fixed_vector&& /*other*/) {
|
||||
static_assert(std::is_nothrow_move_constructible_v<U>,
|
||||
"fixed_vector requires T to be nothrow move-constructible for move construction.");
|
||||
}
|
||||
|
||||
// Move-assign
|
||||
/**
|
||||
* @brief Helper for move assignment.
|
||||
* @details This overload is enabled if `T`'s move operations are nothrow (enforced by class-level
|
||||
* static_assert). It is `noexcept`.
|
||||
*/
|
||||
template <typename U = T, std::enable_if_t<(std::is_nothrow_move_assignable_v<U> &&
|
||||
std::is_nothrow_move_constructible_v<U>)>* = nullptr>
|
||||
void move_assign_from(fixed_vector&& other) noexcept {
|
||||
const size_type move_len = std::min(size_, other.size_);
|
||||
std::move(other.begin(), other.begin() + move_len, begin());
|
||||
|
||||
if (size_ < other.size_) {
|
||||
for (size_type i = size_; i < other.size_; ++i) {
|
||||
if (try_emplace_back(std::move(other[i])) == nullptr) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (size_ > other.size_) {
|
||||
const size_type old_size = size_;
|
||||
size_ = other.size_;
|
||||
if constexpr (!std::is_trivially_destructible_v<T>) {
|
||||
for (size_type i = size_; i < old_size; ++i) {
|
||||
std::destroy_at(data() + i);
|
||||
}
|
||||
}
|
||||
}
|
||||
other.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Helper for move assignment: provides a compile-time error if `T` does not meet the requirements.
|
||||
* @details This overload is enabled if `T`'s move operations are not nothrow, triggering `static_assert`s.
|
||||
* @param other Unused, present for signature matching.
|
||||
*/
|
||||
template <typename U = T, std::enable_if_t<!(std::is_nothrow_move_assignable_v<U> &&
|
||||
std::is_nothrow_move_constructible_v<U>)>* = nullptr>
|
||||
void move_assign_from(fixed_vector&& /*other*/) {
|
||||
static_assert(std::is_nothrow_move_constructible_v<U>,
|
||||
"fixed_vector requires T to be nothrow move-constructible for move assignment.");
|
||||
static_assert(std::is_nothrow_move_assignable_v<U>,
|
||||
"fixed_vector requires T to be nothrow move-assignable for move assignment.");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns a pointer to the underlying storage.
|
||||
*/
|
||||
constexpr pointer data() noexcept {
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): needed for appropriate type
|
||||
return reinterpret_cast<pointer>(storage_.data());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns a const pointer to the underlying storage.
|
||||
*/
|
||||
constexpr const_pointer data() const noexcept {
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): needed for appropriate type
|
||||
return reinterpret_cast<const_pointer>(storage_.data());
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Checks if the contents of two fixed_vectors are equal.
|
||||
* @relates fixed_vector
|
||||
* @param lhs The left-hand side vector.
|
||||
* @param rhs The right-hand side vector.
|
||||
* @return true if the contents are equal, false otherwise.
|
||||
*/
|
||||
template <typename T, std::size_t N> bool operator==(const fixed_vector<T, N>& lhs, const fixed_vector<T, N>& rhs) {
|
||||
if (lhs.size() != rhs.size()) {
|
||||
return false;
|
||||
}
|
||||
return std::equal(lhs.begin(), lhs.end(), rhs.begin());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Checks if the contents of two fixed_vectors are not equal.
|
||||
* @relates fixed_vector
|
||||
* @param lhs The left-hand side vector.
|
||||
* @param rhs The right-hand side vector.
|
||||
* @return true if the contents are not equal, false otherwise.
|
||||
*/
|
||||
template <typename T, std::size_t N> bool operator!=(const fixed_vector<T, N>& lhs, const fixed_vector<T, N>& rhs) {
|
||||
return !(lhs == rhs);
|
||||
}
|
||||
|
||||
} // namespace everest::lib::util
|
||||
23
tools/EVerest-main/lib/everest/util/tests/CMakeLists.txt
Normal file
23
tools/EVerest-main/lib/everest/util/tests/CMakeLists.txt
Normal file
@@ -0,0 +1,23 @@
|
||||
add_executable(everest_util_tests
|
||||
async/monitor_tests.cpp
|
||||
async/thread_pool_tests.cpp
|
||||
async/thread_pool_scaling_tests.cpp
|
||||
async/wrapper_tests.cpp
|
||||
enum/EnumFlagsTest.cpp
|
||||
enum/EnumFlagsTest_B.cpp
|
||||
math/comparison_tests.cpp
|
||||
queue/simple_queue_tests.cpp
|
||||
queue/thread_safe_queue_tests.cpp
|
||||
queue/thread_safe_bounded_queue_tests.cpp
|
||||
vector/fixed_vector_tests.cpp
|
||||
fsm/fsm_tests.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(everest_util_tests
|
||||
PRIVATE
|
||||
GTest::gtest_main
|
||||
everest::util
|
||||
)
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(everest_util_tests)
|
||||
@@ -0,0 +1,437 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include <everest/util/async/monitor.hpp>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
using namespace everest::lib::util;
|
||||
|
||||
struct SharedData {
|
||||
int value = 0;
|
||||
std::string name = "initial";
|
||||
// Unique ID to track object identity across moves/swaps
|
||||
long long id = std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||
std::chrono::high_resolution_clock::now().time_since_epoch())
|
||||
.count();
|
||||
};
|
||||
|
||||
// --- Test Fixtures for Noexcept Checks ---
|
||||
// 1. Type that is NOT nothrow-swappable (due to non-noexcept move constructor)
|
||||
struct ThrowingMover {
|
||||
int* ptr;
|
||||
|
||||
// Move Constructor: NOT noexcept (This is the key difference)
|
||||
ThrowingMover(ThrowingMover&& other) noexcept(false) : ptr(std::exchange(other.ptr, nullptr)) {
|
||||
if (!ptr)
|
||||
throw std::runtime_error("simulated throw on move");
|
||||
}
|
||||
|
||||
// Move Assignment: NOT noexcept
|
||||
ThrowingMover& operator=(ThrowingMover&& other) noexcept(false) {
|
||||
if (this != &other) {
|
||||
std::swap(ptr, other.ptr);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
ThrowingMover() : ptr(new int(42)) {
|
||||
}
|
||||
~ThrowingMover() {
|
||||
delete ptr;
|
||||
}
|
||||
ThrowingMover(const ThrowingMover&) = delete;
|
||||
ThrowingMover& operator=(const ThrowingMover&) = delete;
|
||||
|
||||
// Define custom swap for ADL (must use the same non-noexcept status)
|
||||
friend void swap(ThrowingMover& lhs, ThrowingMover& rhs) noexcept(false) {
|
||||
std::swap(lhs.ptr, rhs.ptr);
|
||||
}
|
||||
};
|
||||
|
||||
// 2. Type that IS nothrow-swappable
|
||||
struct NoThrowMover {
|
||||
int* ptr;
|
||||
|
||||
// Move Constructor: IS noexcept
|
||||
NoThrowMover(NoThrowMover&& other) noexcept : ptr(std::exchange(other.ptr, nullptr)) {
|
||||
}
|
||||
|
||||
// Move Assignment: IS noexcept
|
||||
NoThrowMover& operator=(NoThrowMover&& other) noexcept {
|
||||
if (this != &other) {
|
||||
std::swap(ptr, other.ptr);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
NoThrowMover() : ptr(new int(42)) {
|
||||
}
|
||||
~NoThrowMover() {
|
||||
delete ptr;
|
||||
}
|
||||
NoThrowMover(const NoThrowMover&) = delete;
|
||||
NoThrowMover& operator=(const NoThrowMover&) = delete;
|
||||
|
||||
// Define custom swap for ADL (must be noexcept)
|
||||
friend void swap(NoThrowMover& lhs, NoThrowMover& rhs) noexcept {
|
||||
std::swap(lhs.ptr, rhs.ptr);
|
||||
}
|
||||
};
|
||||
|
||||
// --- The Static Assert Tests ---
|
||||
// We use basic static_asserts to verify the compiler's calculated noexcept status.
|
||||
|
||||
namespace NoexceptTests {
|
||||
using namespace everest::lib::util;
|
||||
template <typename T> using monitor = everest::lib::util::monitor<T>;
|
||||
using M_NT = monitor<NoThrowMover>; // Monitor protecting the safe type
|
||||
using M_T = monitor<ThrowingMover>; // Monitor protecting the unsafe type
|
||||
|
||||
// --- 1. Test against NoThrowMover (T is noexcept swappable) ---
|
||||
// All move and swap operations on the monitor should be noexcept(true).
|
||||
|
||||
static_assert(std::is_nothrow_swappable_v<NoThrowMover>,
|
||||
"Prerequisite 1 failed: NoThrowMover must be noexcept swappable.");
|
||||
|
||||
// Move Constructor: Should be noexcept(true)
|
||||
static_assert(std::is_nothrow_move_constructible_v<M_NT>, "NT Test 1 failed: Move Constructor must be noexcept.");
|
||||
|
||||
// Member Swap: Should be noexcept(true)
|
||||
static_assert(noexcept(std::declval<M_NT>().swap(std::declval<M_NT&>())),
|
||||
"NT Test 2 failed: Member Swap must be noexcept.");
|
||||
|
||||
// Move Assignment: Should be noexcept(true)
|
||||
static_assert(std::is_nothrow_move_assignable_v<M_NT>, "NT Test 3 failed: Move Assignment must be noexcept.");
|
||||
|
||||
// --- 2. Test against ThrowingMover (T is NOT noexcept swappable) ---
|
||||
// All move and swap operations on the monitor should be noexcept(false).
|
||||
|
||||
static_assert(!std::is_nothrow_swappable_v<ThrowingMover>,
|
||||
"Prerequisite 2 failed: ThrowingMover must NOT be noexcept swappable.");
|
||||
|
||||
// Move Constructor: Should be noexcept(false) (allows exceptions)
|
||||
static_assert(!std::is_nothrow_move_constructible_v<M_T>, "T Test 1 failed: Move Constructor must NOT be noexcept.");
|
||||
|
||||
// Member Swap: Should be noexcept(false) (allows exceptions)
|
||||
static_assert(!noexcept(std::declval<M_T>().swap(std::declval<M_T&>())),
|
||||
"T Test 2 failed: Member Swap must NOT be noexcept.");
|
||||
|
||||
// Move Assignment: Should be noexcept(false) (allows exceptions)
|
||||
static_assert(!std::is_nothrow_move_assignable_v<M_T>, "T Test 3 failed: Move Assignment must NOT be noexcept.");
|
||||
} // namespace NoexceptTests
|
||||
|
||||
using namespace everest::lib::util;
|
||||
|
||||
class MonitorTest : public ::testing::Test {
|
||||
protected:
|
||||
monitor<SharedData> simple_monitor_;
|
||||
monitor<std::unique_ptr<SharedData>> ptr_monitor_;
|
||||
// A timed mutex enabled monitor::handle(timeout)
|
||||
monitor<SharedData, std::timed_mutex> timed_mtx_monitor_;
|
||||
|
||||
// Time constants for tests
|
||||
const std::chrono::milliseconds BLOCK_TIME = std::chrono::milliseconds(200);
|
||||
const std::chrono::milliseconds SHORT_WAIT = std::chrono::milliseconds(10);
|
||||
const std::chrono::milliseconds LONG_WAIT = std::chrono::milliseconds(300);
|
||||
};
|
||||
|
||||
TEST_F(MonitorTest, SingleThreadedAccess) {
|
||||
// Block 1: Access and Modify (Lock acquired by handle, then released)
|
||||
{
|
||||
// Acquire the handle (locks the mutex)
|
||||
auto handle = simple_monitor_.handle();
|
||||
|
||||
// Access and modify the data using operator->
|
||||
handle->value = 100;
|
||||
handle->name = "updated";
|
||||
|
||||
// When 'handle' goes out of scope here, the lock is released (RAII).
|
||||
}
|
||||
|
||||
// Block 2: Verify changes (Lock acquired, then released)
|
||||
{
|
||||
// Now acquiring the lock succeeds because it was released above.
|
||||
auto handle_check = simple_monitor_.handle();
|
||||
|
||||
// Verify
|
||||
EXPECT_EQ(100, handle_check->value);
|
||||
EXPECT_EQ("updated", handle_check->name);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(MonitorTest, PointerLikeAccessChaining) {
|
||||
// Block 1: Initialization (Ensures the unique_ptr is created)
|
||||
{
|
||||
auto h = ptr_monitor_.handle();
|
||||
*h = std::make_unique<SharedData>();
|
||||
} // h is destroyed, lock released.
|
||||
|
||||
// Block 2: Access and Modify (Lock acquired by handle, then released)
|
||||
{
|
||||
// Acquire lock
|
||||
auto handle = ptr_monitor_.handle();
|
||||
|
||||
handle->value = 42;
|
||||
handle->name = "chained";
|
||||
} // handle is destroyed, lock released.
|
||||
|
||||
// Block 3: Verify (Lock acquired, then released)
|
||||
{
|
||||
// Acquire lock
|
||||
auto handle_check = ptr_monitor_.handle();
|
||||
|
||||
// Access via chaining
|
||||
EXPECT_EQ(42, handle_check->value);
|
||||
EXPECT_EQ("chained", handle_check->name);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(MonitorTest, ThreadSafeIncrement) {
|
||||
const int num_threads = 10;
|
||||
const int increments_per_thread = 1000;
|
||||
std::vector<std::thread> threads;
|
||||
|
||||
// Set initial value to 0
|
||||
simple_monitor_.handle()->value = 0;
|
||||
|
||||
for (int i = 0; i < num_threads; ++i) {
|
||||
threads.emplace_back([&] {
|
||||
for (int j = 0; j < increments_per_thread; ++j) {
|
||||
// Handle scope ensures RAII locking on every single increment
|
||||
auto handle = simple_monitor_.handle();
|
||||
handle->value++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (auto& t : threads) {
|
||||
t.join();
|
||||
}
|
||||
|
||||
// Verify the final value is correct
|
||||
auto final_handle = simple_monitor_.handle();
|
||||
EXPECT_EQ(num_threads * increments_per_thread, final_handle->value);
|
||||
}
|
||||
|
||||
TEST_F(MonitorTest, ConditionVariableWaitNotify) {
|
||||
bool done = false;
|
||||
|
||||
// Future/Promise pair 1: Waiter signals it is ready to wait
|
||||
std::promise<void> waiter_ready_promise;
|
||||
std::future<void> waiter_ready_future = waiter_ready_promise.get_future();
|
||||
|
||||
std::thread waiter([&] {
|
||||
// Acquire handle
|
||||
auto handle = simple_monitor_.handle();
|
||||
|
||||
// Signal that we are holding the lock and about to wait
|
||||
waiter_ready_promise.set_value();
|
||||
|
||||
// Wait until 'done' is true.
|
||||
handle.wait([&] { return done; });
|
||||
|
||||
EXPECT_EQ(99, handle->value);
|
||||
});
|
||||
|
||||
// Main thread waits until the waiter has acquired the lock and set the promise
|
||||
waiter_ready_future.get();
|
||||
|
||||
// Notifier thread operation (guaranteed to happen after waiter has locked/signaled)
|
||||
{
|
||||
// Acquire lock
|
||||
auto handle = simple_monitor_.handle();
|
||||
handle->value = 99;
|
||||
done = true;
|
||||
}
|
||||
|
||||
// Notify the waiting thread
|
||||
simple_monitor_.notify_one();
|
||||
|
||||
waiter.join();
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------
|
||||
|
||||
TEST_F(MonitorTest, TryLockHandleTimeout) {
|
||||
// Future/Promise pair 1: Blocker signals it has acquired the lock
|
||||
std::promise<void> blocker_locked_promise;
|
||||
std::future<void> blocker_locked_future = blocker_locked_promise.get_future();
|
||||
|
||||
// The shared object is used as the resource for the lock
|
||||
|
||||
std::thread blocker([&] {
|
||||
// Acquire the lock
|
||||
auto handle = timed_mtx_monitor_.handle(); // 🔒 Lock acquired
|
||||
|
||||
// Signal to the main thread that the lock is held
|
||||
blocker_locked_promise.set_value();
|
||||
|
||||
// Hold the lock for a specified duration
|
||||
std::this_thread::sleep_for(BLOCK_TIME);
|
||||
|
||||
// Lock released when handle goes out of scope 🔓
|
||||
});
|
||||
|
||||
// Main thread waits until the blocker explicitly confirms it is holding the lock
|
||||
blocker_locked_future.get();
|
||||
|
||||
// Test 1: Try to acquire the lock with a short timeout (Expected to FAIL)
|
||||
auto handle_opt = timed_mtx_monitor_.handle(SHORT_WAIT);
|
||||
EXPECT_FALSE(handle_opt.has_value());
|
||||
|
||||
// Test 2: Try to acquire the lock with a long timeout (Expected to SUCCEED eventually)
|
||||
// The total wait time will be slightly longer than BLOCK_TIME (200ms).
|
||||
auto start_success = std::chrono::steady_clock::now();
|
||||
auto handle_long_opt = timed_mtx_monitor_.handle(LONG_WAIT);
|
||||
|
||||
auto duration_success = std::chrono::steady_clock::now() - start_success;
|
||||
|
||||
EXPECT_TRUE(handle_long_opt.has_value());
|
||||
// FIX 2: Explicitly compare the count() to ensure stable comparison and output
|
||||
EXPECT_GE(duration_success.count(), BLOCK_TIME.count());
|
||||
|
||||
blocker.join();
|
||||
}
|
||||
|
||||
TEST_F(MonitorTest, TimedMutexLockAcquisition) {
|
||||
// This test ensures the complex timing logic for acquisition is sound.
|
||||
|
||||
// Synchronization barrier: Blocker signals it has acquired the lock
|
||||
std::promise<void> blocker_locked_promise;
|
||||
std::future<void> blocker_locked_future = blocker_locked_promise.get_future();
|
||||
|
||||
// THREAD A: The Blocker (Holds the lock on timed_mtx_monitor_)
|
||||
std::thread blocker([&] {
|
||||
// 1. Acquire lock
|
||||
auto handle = timed_mtx_monitor_.handle();
|
||||
blocker_locked_promise.set_value(); // Signal: Lock is now held
|
||||
|
||||
// 2. Hold the lock for the required duration
|
||||
std::this_thread::sleep_for(BLOCK_TIME); // 200ms
|
||||
|
||||
// Lock released when handle goes out of scope
|
||||
});
|
||||
|
||||
// 3. Main thread waits until the lock is actively held
|
||||
blocker_locked_future.get();
|
||||
|
||||
// --- Test 1: Fail Case (Wait is shorter than remaining lock time) ---
|
||||
auto fail_handle = timed_mtx_monitor_.handle(SHORT_WAIT);
|
||||
EXPECT_FALSE(fail_handle.has_value());
|
||||
|
||||
// --- Test 2: Success Case (Wait is longer than remaining lock time) ---
|
||||
auto start_success_timing = std::chrono::steady_clock::now();
|
||||
|
||||
// Acquire the lock with a sufficient timeout (300ms)
|
||||
auto success_handle = timed_mtx_monitor_.handle(LONG_WAIT);
|
||||
|
||||
auto duration_success = std::chrono::steady_clock::now() - start_success_timing;
|
||||
|
||||
// Must succeed acquisition
|
||||
EXPECT_TRUE(success_handle.has_value());
|
||||
|
||||
// FIX 3: Explicitly compare the count() to ensure stable comparison and output
|
||||
EXPECT_GE(duration_success.count(), BLOCK_TIME.count());
|
||||
|
||||
blocker.join();
|
||||
}
|
||||
|
||||
TEST_F(MonitorTest, ConditionVariableAtomicity) {
|
||||
bool notification_sent = false;
|
||||
// Use the SharedData member to track state
|
||||
simple_monitor_.handle()->value = 0;
|
||||
|
||||
// THREAD A: The Waiter
|
||||
std::thread waiter([&] {
|
||||
auto handle = simple_monitor_.handle();
|
||||
|
||||
// Waiter signals that it is holding the lock and about to enter the wait state
|
||||
// (This is implicitly tested by the notifier having to wait for the lock)
|
||||
|
||||
// Predicate check: Ensure 'notification_sent' is only true IF the lock is reacquired
|
||||
handle.wait([&] {
|
||||
// The predicate will be checked spuriously, but the critical check is on wake
|
||||
return notification_sent;
|
||||
});
|
||||
|
||||
// After waking up, the lock is held. Verify the resource state.
|
||||
// This checks that the state modification (value=1) happened while the lock was released.
|
||||
EXPECT_EQ(1, handle->value);
|
||||
});
|
||||
|
||||
// Give the waiter time to acquire the lock and block on the CV (Crucial setup time)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
|
||||
// THREAD B: The Notifier (This thread modifies the state and notifies)
|
||||
{
|
||||
// Must acquire the lock. This proves the waiter released it atomically inside wait().
|
||||
auto handle = simple_monitor_.handle();
|
||||
|
||||
// 1. Modify the resource while holding the lock
|
||||
handle->value = 1;
|
||||
|
||||
// 2. Set the wait condition *after* modification
|
||||
notification_sent = true;
|
||||
|
||||
// Lock is released here, which allows the waiter to potentially reacquire it.
|
||||
}
|
||||
|
||||
simple_monitor_.notify_one();
|
||||
|
||||
waiter.join();
|
||||
|
||||
// Final check that the state is 1, confirming the waiter successfully completed its EXPECT.
|
||||
EXPECT_EQ(1, simple_monitor_.handle()->value);
|
||||
}
|
||||
|
||||
TEST_F(MonitorTest, ThreadSafeMoveOperations) {
|
||||
// Setup: Monitor m1 (Source) starts with data, Monitor m2 (Destination) starts empty.
|
||||
monitor<SharedData> m1;
|
||||
m1.handle()->value = 10;
|
||||
auto m1_initial_id = m1.handle()->id; // Track resource identity
|
||||
|
||||
// Create m2 with different data
|
||||
monitor<SharedData> m2;
|
||||
m2.handle()->value = 99;
|
||||
auto m2_initial_id = m2.handle()->id;
|
||||
|
||||
std::promise<void> blocker_locked_promise;
|
||||
std::future<void> blocker_locked_future = blocker_locked_promise.get_future();
|
||||
|
||||
// THREAD A: The Blocker (Holds the lock on m1 to force the move operation to wait)
|
||||
std::thread blocker([&] {
|
||||
auto handle = m1.handle(); // Lock m1
|
||||
blocker_locked_promise.set_value();
|
||||
std::this_thread::sleep_for(BLOCK_TIME);
|
||||
});
|
||||
|
||||
// Main thread waits until m1 is locked by the blocker
|
||||
blocker_locked_future.get();
|
||||
|
||||
// --- Move Assignment Test: m2 = std::move(m1) ---
|
||||
|
||||
// Because the move assignment operator calls monitor::swap(m2, m1), and swap locks both,
|
||||
// it must wait for m1's lock (held by blocker thread) to be released.
|
||||
auto start_move = std::chrono::steady_clock::now();
|
||||
m2 = std::move(m1); // Should block here until blocker releases m1's lock
|
||||
auto duration_move = std::chrono::steady_clock::now() - start_move;
|
||||
|
||||
// Verify the move blocked until the blocker thread finished (duration > hold time)
|
||||
EXPECT_GE(duration_move, BLOCK_TIME);
|
||||
|
||||
// Verify data transfer (m2 now has m1's initial data)
|
||||
EXPECT_EQ(10, m2.handle()->value);
|
||||
EXPECT_EQ(m1_initial_id, m2.handle()->id); // m2 now owns m1's resource
|
||||
|
||||
// Verify source state (m1 now has m2's initial data)
|
||||
// The move assignment resulted in a SWAP.
|
||||
EXPECT_EQ(99, m1.handle()->value);
|
||||
EXPECT_EQ(m2_initial_id, m1.handle()->id); // m1 now owns m2's original resource
|
||||
|
||||
blocker.join();
|
||||
}
|
||||
@@ -0,0 +1,537 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <everest/util/async/thread_pool_scaling.hpp>
|
||||
#include <future>
|
||||
#include <vector>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
using namespace everest::lib::util;
|
||||
|
||||
// =================================================================
|
||||
// 1. Latency Scaling Tests
|
||||
// =================================================================
|
||||
|
||||
/**
|
||||
* @test ScalesOnLatencyThreshold
|
||||
* @brief Verifies that the pool spawns a new thread when a task waits too long.
|
||||
*/
|
||||
TEST(ThreadPoolScalingTest, ScalesOnLatencyThreshold) {
|
||||
thread_pool_scaling<LatencyScaling<10>> pool(1, 2, 1s);
|
||||
|
||||
std::promise<void> block_first_task;
|
||||
std::shared_future<void> block_future = block_first_task.get_future();
|
||||
|
||||
pool.run([block_future]() { block_future.wait(); });
|
||||
|
||||
std::this_thread::sleep_for(5ms);
|
||||
|
||||
std::atomic<bool> second_task_started{false};
|
||||
// This should spawn a new thread already.
|
||||
pool.run([&]() { second_task_started = true; });
|
||||
|
||||
ASSERT_FALSE(second_task_started.load());
|
||||
|
||||
std::this_thread::sleep_for(50ms);
|
||||
EXPECT_TRUE(second_task_started.load());
|
||||
|
||||
block_first_task.set_value();
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 2. Thread Retirement Tests
|
||||
// =================================================================
|
||||
|
||||
/**
|
||||
* @test SurplusThreadsRetireAfterTimeout
|
||||
* @brief Verifies that threads exceeding the minimum count retire when idle.
|
||||
*/
|
||||
TEST(ThreadPoolScalingTest, SurplusThreadsRetireAfterTimeout) {
|
||||
thread_pool_scaling<GreedyScaling> pool(1, 2, 100ms);
|
||||
|
||||
std::promise<void> p1, p2;
|
||||
auto f1 = p1.get_future().share();
|
||||
auto f2 = p2.get_future().share();
|
||||
|
||||
pool.run([f1]() { f1.wait(); });
|
||||
pool.run([f2]() { f2.wait(); });
|
||||
|
||||
p1.set_value();
|
||||
p2.set_value();
|
||||
|
||||
std::this_thread::sleep_for(300ms);
|
||||
|
||||
std::atomic<int> counter{0};
|
||||
for (int i = 0; i < 5; ++i)
|
||||
pool.run([&]() { counter++; });
|
||||
|
||||
std::this_thread::sleep_for(50ms);
|
||||
EXPECT_EQ(counter.load(), 5);
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 3. Backpressure Tests
|
||||
// =================================================================
|
||||
|
||||
/**
|
||||
* @test BackpressureBlocksProducer
|
||||
* @brief Ensures the calling thread blocks when the queue limit is reached.
|
||||
*/
|
||||
TEST(ThreadPoolScalingTest, BackpressureBlocksProducer) {
|
||||
thread_pool_scaling<GreedyScaling> pool(1, 1, 1s, 1);
|
||||
|
||||
std::promise<void> block;
|
||||
auto fut = block.get_future().share();
|
||||
|
||||
pool.run([fut]() { fut.wait(); });
|
||||
pool.run([]() {});
|
||||
|
||||
std::atomic<bool> producer_unblocked{false};
|
||||
std::thread producer([&]() {
|
||||
pool.run([]() {});
|
||||
producer_unblocked = true;
|
||||
});
|
||||
|
||||
std::this_thread::sleep_for(100ms);
|
||||
EXPECT_FALSE(producer_unblocked.load());
|
||||
|
||||
block.set_value();
|
||||
producer.join();
|
||||
EXPECT_TRUE(producer_unblocked.load());
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 4. Future Interface Tests
|
||||
// =================================================================
|
||||
|
||||
/**
|
||||
* @test OperatorReturnsValidFuture
|
||||
*/
|
||||
TEST(ThreadPoolScalingTest, OperatorReturnsValidFuture) {
|
||||
thread_pool_scaling<GreedyScaling> pool(1, 2, 1s);
|
||||
constexpr int lhs = 10;
|
||||
constexpr int rhs = 32;
|
||||
auto fut = pool([](int first, int second) { return first + second; }, lhs, rhs);
|
||||
EXPECT_EQ(fut.get(), 42);
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 5. Scaling and Retirement Stress Tests
|
||||
// =================================================================
|
||||
|
||||
/**
|
||||
* @test RapidScalingThrash
|
||||
* @brief Verifies stability during high-frequency fluctuations in workload.
|
||||
* @details Updated with more robust timing to handle OS scheduling jitter.
|
||||
*/
|
||||
TEST(ThreadPoolScalingStressTest, RapidScalingThrash) {
|
||||
constexpr std::size_t max_threads = 20;
|
||||
constexpr int burst_tasks = 40;
|
||||
constexpr int trickle_tasks = 5;
|
||||
constexpr int tasks_per_iteration = burst_tasks + trickle_tasks;
|
||||
thread_pool_scaling<GreedyScaling> pool(1, max_threads, 20ms);
|
||||
std::atomic<int> completed_tasks{0};
|
||||
const int iterations = 30;
|
||||
|
||||
for (int i = 0; i < iterations; ++i) {
|
||||
for (int j = 0; j < burst_tasks; ++j) {
|
||||
pool.run([&]() {
|
||||
std::this_thread::sleep_for(2ms);
|
||||
completed_tasks++;
|
||||
});
|
||||
}
|
||||
std::this_thread::sleep_for(30ms); // Allow some threads to start idling/retiring
|
||||
for (int j = 0; j < trickle_tasks; ++j) {
|
||||
pool.run([&]() { completed_tasks++; });
|
||||
}
|
||||
}
|
||||
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
while (completed_tasks < (iterations * tasks_per_iteration)) {
|
||||
std::this_thread::sleep_for(50ms);
|
||||
if (std::chrono::steady_clock::now() - start > 10s) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
EXPECT_EQ(completed_tasks.load(), iterations * tasks_per_iteration);
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 6. High Contention and Race Condition Tests
|
||||
// =================================================================
|
||||
|
||||
/**
|
||||
* @test HighContentionProducers
|
||||
*/
|
||||
TEST(ThreadPoolScalingStressTest, HighContentionProducers) {
|
||||
constexpr int num_producers = 8;
|
||||
constexpr int tasks_per_producer = 2000;
|
||||
constexpr std::size_t max_threads = 16;
|
||||
thread_pool_scaling<LatencyScaling<5>> pool(4, max_threads, 1s);
|
||||
|
||||
std::atomic<size_t> total_sum{0};
|
||||
std::vector<std::thread> producers;
|
||||
producers.reserve(static_cast<std::size_t>(num_producers));
|
||||
|
||||
for (int prod = 0; prod < num_producers; ++prod) {
|
||||
producers.emplace_back([&]() {
|
||||
for (int i = 0; i < tasks_per_producer; ++i) {
|
||||
pool.run([&total_sum]() { total_sum.fetch_add(1, std::memory_order_relaxed); });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (auto& thr : producers) {
|
||||
thr.join();
|
||||
}
|
||||
|
||||
const auto expected = static_cast<std::size_t>(num_producers) * static_cast<std::size_t>(tasks_per_producer);
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
while (total_sum.load() < expected) {
|
||||
std::this_thread::sleep_for(50ms);
|
||||
if (std::chrono::steady_clock::now() - start > 10s) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
EXPECT_EQ(total_sum.load(), expected);
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 7. Thread retirement versus pool destruction
|
||||
// =================================================================
|
||||
|
||||
/**
|
||||
* @test DestructorVsActiveScalingRace
|
||||
*/
|
||||
TEST(ThreadPoolScalingStressTest, DestructorVsActiveScalingRace) {
|
||||
constexpr int repetitions = 50;
|
||||
constexpr std::size_t max_threads = 10;
|
||||
constexpr int tasks_per_rep = 20;
|
||||
for (int i = 0; i < repetitions; ++i) {
|
||||
{
|
||||
thread_pool_scaling<GreedyScaling> pool(1, max_threads, 5ms);
|
||||
for (int j = 0; j < tasks_per_rep; ++j) {
|
||||
pool.run([]() { std::this_thread::sleep_for(1ms); });
|
||||
}
|
||||
std::this_thread::sleep_for(6ms);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 8. Edge Case: Full Idle Reset
|
||||
// =================================================================
|
||||
|
||||
/**
|
||||
* @test FullIdleResetToMinimum
|
||||
*/
|
||||
TEST(ThreadPoolScalingTest, FullIdleResetToMinimum) {
|
||||
const size_t min = 2;
|
||||
const size_t max = 5;
|
||||
const auto timeout = 50ms;
|
||||
thread_pool_scaling<GreedyScaling> pool(min, max, timeout);
|
||||
|
||||
std::vector<std::promise<void>> promises(max);
|
||||
for (int i = 0; i < max; ++i) {
|
||||
pool.run([&promises, i]() { promises[i].get_future().wait(); });
|
||||
}
|
||||
|
||||
for (auto& prom : promises) {
|
||||
prom.set_value();
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(timeout * 3);
|
||||
|
||||
std::atomic<bool> functional_check{false};
|
||||
pool.run([&]() { functional_check = true; });
|
||||
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
while (!functional_check.load() && std::chrono::steady_clock::now() - start < 1s) {
|
||||
std::this_thread::yield();
|
||||
}
|
||||
|
||||
ASSERT_TRUE(functional_check.load());
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 9. Re-entrancy and Policy Boundary Tests
|
||||
// =================================================================
|
||||
|
||||
/**
|
||||
* @test ReentrantScaling
|
||||
*/
|
||||
TEST(ThreadPoolScalingStressTest, ReentrantScaling) {
|
||||
constexpr int inner_tasks = 10;
|
||||
constexpr int total_tasks = inner_tasks + 1; // outer task + inner tasks
|
||||
thread_pool_scaling<LatencyScaling<10>> pool(1, 4, 1s);
|
||||
std::atomic<int> completed{0};
|
||||
|
||||
pool.run([&]() {
|
||||
for (int i = 0; i < inner_tasks; ++i) {
|
||||
pool.run([&]() { completed++; });
|
||||
}
|
||||
completed++;
|
||||
});
|
||||
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
while (completed < total_tasks && std::chrono::steady_clock::now() - start < 2s) {
|
||||
std::this_thread::sleep_for(10ms);
|
||||
}
|
||||
EXPECT_EQ(completed.load(), total_tasks);
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 10. Latency Boundary Check
|
||||
// =================================================================
|
||||
|
||||
/**
|
||||
* @test LatencyThresholdBoundary
|
||||
*/
|
||||
TEST(ThreadPoolScalingTest, LatencyThresholdBoundary) {
|
||||
// 100 ms threshold: tasks that wait less than 100 ms should NOT trigger scaling
|
||||
constexpr std::size_t threshold_ms = 100;
|
||||
thread_pool_scaling<LatencyScaling<threshold_ms>> pool(1, 2, 1s);
|
||||
|
||||
std::promise<void> block;
|
||||
auto fut = block.get_future().share();
|
||||
pool.run([fut]() { fut.wait(); });
|
||||
|
||||
std::atomic<bool> task2_started{false};
|
||||
pool.run([&]() { task2_started = true; });
|
||||
|
||||
std::this_thread::sleep_for(50ms);
|
||||
|
||||
EXPECT_FALSE(task2_started.load());
|
||||
|
||||
std::this_thread::sleep_for(100ms);
|
||||
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
while (!task2_started.load() && std::chrono::steady_clock::now() - start < 1s) {
|
||||
std::this_thread::sleep_for(10ms);
|
||||
}
|
||||
|
||||
EXPECT_TRUE(task2_started.load());
|
||||
block.set_value();
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 11. ConservativeScaling Policy Tests
|
||||
// =================================================================
|
||||
|
||||
/**
|
||||
* @test ConservativeScalingDoesNotGrowBelowThreshold
|
||||
* @brief With 1 worker, queue_size must exceed workers*2 (>2) to trigger growth.
|
||||
* At exactly 2 queued tasks the policy should NOT scale.
|
||||
*/
|
||||
TEST(ThreadPoolScalingTest, ConservativeScalingDoesNotGrowBelowThreshold) {
|
||||
// min=1, max=2 — second thread must NOT appear unless queue_size > 2
|
||||
thread_pool_scaling<ConservativeScaling> pool(1, 2, 1s);
|
||||
|
||||
std::promise<void> block;
|
||||
auto fut = block.get_future().share();
|
||||
|
||||
// Task 1: occupies the sole min thread
|
||||
pool.run([fut]() { fut.wait(); });
|
||||
|
||||
// Task 2: queue_size after push == 2, workers == 1, 2 > (1*2) is false → no growth
|
||||
std::atomic<bool> task2_ran{false};
|
||||
pool.run([&]() { task2_ran = true; });
|
||||
|
||||
std::this_thread::sleep_for(100ms);
|
||||
EXPECT_FALSE(task2_ran.load()); // still blocked behind task 1
|
||||
|
||||
block.set_value();
|
||||
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
while (!task2_ran.load() && std::chrono::steady_clock::now() - start < 2s) {
|
||||
std::this_thread::sleep_for(10ms);
|
||||
}
|
||||
EXPECT_TRUE(task2_ran.load());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test ConservativeScalingGrowsAboveThreshold
|
||||
* @brief With 1 worker, submitting 3 tasks (queue_size==3 > workers*2==2) must trigger growth.
|
||||
*/
|
||||
TEST(ThreadPoolScalingTest, ConservativeScalingGrowsAboveThreshold) {
|
||||
thread_pool_scaling<ConservativeScaling> pool(1, 3, 1s);
|
||||
|
||||
std::promise<void> block;
|
||||
auto fut = block.get_future().share();
|
||||
|
||||
// Task 1: pins the min thread
|
||||
pool.run([fut]() { fut.wait(); });
|
||||
|
||||
// Tasks 2 and 3: queue_size after task 3 == 3, workers == 1, 3 > 2 → growth
|
||||
std::atomic<int> ran{0};
|
||||
pool.run([&]() { ran++; });
|
||||
pool.run([&]() { ran++; });
|
||||
|
||||
block.set_value();
|
||||
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
while (ran.load() < 2 && std::chrono::steady_clock::now() - start < 2s) {
|
||||
std::this_thread::sleep_for(10ms);
|
||||
}
|
||||
EXPECT_EQ(ran.load(), 2);
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 12. FixedSizeScaling Policy Tests
|
||||
// =================================================================
|
||||
|
||||
/**
|
||||
* @test FixedSizeScalingDoesNotGrowBeforeLimit
|
||||
* @brief Pool must not scale when queue_size is below the fixed limit.
|
||||
*/
|
||||
TEST(ThreadPoolScalingTest, FixedSizeScalingDoesNotGrowBeforeLimit) {
|
||||
// Limit=3: grows only when queue_size >= 3
|
||||
thread_pool_scaling<FixedSizeScaling<3>> pool(1, 2, 1s);
|
||||
|
||||
std::promise<void> block;
|
||||
auto fut = block.get_future().share();
|
||||
|
||||
pool.run([fut]() { fut.wait(); });
|
||||
|
||||
// queue_size == 2 after this push, 2 < 3 → no growth
|
||||
std::atomic<bool> task2_ran{false};
|
||||
pool.run([&]() { task2_ran = true; });
|
||||
|
||||
std::this_thread::sleep_for(100ms);
|
||||
EXPECT_FALSE(task2_ran.load());
|
||||
|
||||
block.set_value();
|
||||
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
while (!task2_ran.load() && std::chrono::steady_clock::now() - start < 2s) {
|
||||
std::this_thread::sleep_for(10ms);
|
||||
}
|
||||
EXPECT_TRUE(task2_ran.load());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test FixedSizeScalingGrowsAtLimit
|
||||
* @brief Pool must spawn a new thread exactly when queue_size reaches the fixed limit.
|
||||
*/
|
||||
TEST(ThreadPoolScalingTest, FixedSizeScalingGrowsAtLimit) {
|
||||
// Limit=2: grows when queue_size >= 2
|
||||
thread_pool_scaling<FixedSizeScaling<2>> pool(1, 2, 1s);
|
||||
|
||||
std::promise<void> block;
|
||||
auto fut = block.get_future().share();
|
||||
|
||||
pool.run([fut]() { fut.wait(); });
|
||||
|
||||
// queue_size == 2 after this push, 2 >= 2 → growth
|
||||
std::atomic<bool> task2_ran{false};
|
||||
pool.run([&]() { task2_ran = true; });
|
||||
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
while (!task2_ran.load() && std::chrono::steady_clock::now() - start < 2s) {
|
||||
std::this_thread::sleep_for(10ms);
|
||||
}
|
||||
EXPECT_TRUE(task2_ran.load());
|
||||
|
||||
block.set_value();
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 13. min == max Degenerate Case
|
||||
// =================================================================
|
||||
|
||||
/**
|
||||
* @test FixedSizePoolNeverGrows
|
||||
* @brief When min == max the pool must never spawn additional threads regardless of backlog.
|
||||
*/
|
||||
TEST(ThreadPoolScalingTest, FixedSizePoolNeverGrows) {
|
||||
// min == max == 1: only ever one worker
|
||||
thread_pool_scaling<GreedyScaling> pool(1, 1, 1s);
|
||||
|
||||
std::promise<void> block;
|
||||
auto fut = block.get_future().share();
|
||||
|
||||
pool.run([fut]() { fut.wait(); });
|
||||
|
||||
// Queue up several tasks; none can run until the first finishes
|
||||
std::atomic<int> ran{0};
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
pool.run([&]() { ran++; });
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(100ms);
|
||||
EXPECT_EQ(ran.load(), 0); // no second thread spawned → all queued behind task 1
|
||||
|
||||
block.set_value();
|
||||
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
while (ran.load() < 4 && std::chrono::steady_clock::now() - start < 2s) {
|
||||
std::this_thread::sleep_for(10ms);
|
||||
}
|
||||
EXPECT_EQ(ran.load(), 4); // all tasks complete once the single worker is unblocked
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 14. Zombie reaping correctness
|
||||
// =================================================================
|
||||
|
||||
/**
|
||||
* @test ZombiesAreJoinedAfterRetirement
|
||||
* @brief Surplus threads that voluntarily retire must be fully joined — verified by
|
||||
* the pool destructor completing without hanging or calling std::terminate().
|
||||
*/
|
||||
TEST(ThreadPoolScalingTest, ZombiesAreJoinedAfterRetirement) {
|
||||
// Short idle timeout so surplus threads retire quickly
|
||||
thread_pool_scaling<GreedyScaling> pool(1, 4, 20ms);
|
||||
|
||||
std::promise<void> gate;
|
||||
auto fut = gate.get_future().share();
|
||||
|
||||
// Flood the pool to force scale-up to max
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
pool.run([fut]() { fut.wait(); });
|
||||
}
|
||||
gate.set_value();
|
||||
|
||||
// Let all surplus threads go idle and retire into the zombie deque
|
||||
std::this_thread::sleep_for(200ms);
|
||||
|
||||
// Destructor must complete cleanly: all zombies are joined before destruction
|
||||
}
|
||||
|
||||
/**
|
||||
* @test ZombiesReapedConcurrentlyWithTaskExecution
|
||||
* @brief Zombies created during task execution must be reaped correctly
|
||||
* by the worker loop while other tasks continue to execute.
|
||||
*/
|
||||
TEST(ThreadPoolScalingStressTest, ZombiesReapedConcurrentlyWithTaskExecution) {
|
||||
constexpr std::size_t max_threads = 8;
|
||||
constexpr int num_waves = 5;
|
||||
constexpr int tasks_per_wave = 10;
|
||||
constexpr int total_tasks = num_waves * tasks_per_wave;
|
||||
|
||||
// Short timeout forces retirement while tasks keep arriving
|
||||
thread_pool_scaling<GreedyScaling> pool(1, max_threads, 10ms);
|
||||
std::atomic<int> completed{0};
|
||||
|
||||
// Submit waves of tasks separated by the idle timeout to repeatedly
|
||||
// grow-then-shrink the pool, generating zombies during active execution
|
||||
for (int wave = 0; wave < num_waves; ++wave) {
|
||||
for (int i = 0; i < tasks_per_wave; ++i) {
|
||||
pool.run([&]() {
|
||||
std::this_thread::sleep_for(5ms);
|
||||
completed++;
|
||||
});
|
||||
}
|
||||
std::this_thread::sleep_for(15ms); // retire surplus threads between waves
|
||||
}
|
||||
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
while (completed.load() < total_tasks && std::chrono::steady_clock::now() - start < 5s) {
|
||||
std::this_thread::sleep_for(20ms);
|
||||
}
|
||||
EXPECT_EQ(completed.load(), total_tasks);
|
||||
// Destructor must complete cleanly with no unjoined zombie threads
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <everest/util/async/thread_pool.hpp>
|
||||
#include <iostream>
|
||||
#include <numeric>
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
using namespace everest::lib::util;
|
||||
|
||||
// --- Test Fixture ---
|
||||
// A fixture allows us to set up and tear down the thread pool easily.
|
||||
class ThreadPoolTest : public ::testing::Test {
|
||||
protected:
|
||||
// Pool size chosen small to encourage contention
|
||||
const unsigned int POOL_SIZE = 4;
|
||||
|
||||
// The thread_pool object will be initialized here and automatically
|
||||
// destroyed (and joined) when the test ends.
|
||||
thread_pool* pool;
|
||||
|
||||
void SetUp() override {
|
||||
pool = new thread_pool(POOL_SIZE);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
delete pool;
|
||||
pool = nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
// --- Helper Functions for Binding ---
|
||||
// Example function to test argument passing
|
||||
int add(int a, int b) {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
// Example function to test void return
|
||||
void do_nothing() {
|
||||
// This is run by a worker thread
|
||||
}
|
||||
|
||||
// --- Test Cases ---
|
||||
|
||||
// 1. Basic Correctness Tests
|
||||
// -------------------------
|
||||
|
||||
TEST_F(ThreadPoolTest, Test_Immediate_Execution_And_Contention) {
|
||||
std::atomic<int> counter{0};
|
||||
const int num_tasks = 1000;
|
||||
std::vector<std::future<void>> futures;
|
||||
|
||||
// Submit many more tasks than threads to test queue contention
|
||||
for (int i = 0; i < num_tasks; ++i) {
|
||||
// Use a lambda with no arguments (uses the specialized operator() if available)
|
||||
futures.push_back((*pool)([&counter]() { counter++; }));
|
||||
}
|
||||
|
||||
// Wait for all tasks to complete
|
||||
for (auto& f : futures) {
|
||||
f.get();
|
||||
}
|
||||
|
||||
// Check if all tasks ran correctly
|
||||
ASSERT_EQ(counter.load(), num_tasks);
|
||||
}
|
||||
|
||||
TEST_F(ThreadPoolTest, Test_Future_Return_Value_And_Arguments) {
|
||||
// Test passing arguments to a simple function
|
||||
std::future<int> f1 = (*pool)(add, 10, 20);
|
||||
|
||||
// Test a lambda with a return value and local arguments
|
||||
int multiplier = 5;
|
||||
std::future<double> f2 = (*pool)([multiplier](double val) { return val * multiplier; }, 10.0);
|
||||
|
||||
ASSERT_EQ(f1.get(), 30);
|
||||
ASSERT_DOUBLE_EQ(f2.get(), 50.0);
|
||||
}
|
||||
|
||||
TEST_F(ThreadPoolTest, Test_Run_Is_NonBlocking) {
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
|
||||
// Submit a task that takes 500ms
|
||||
pool->run([]() { std::this_thread::sleep_for(std::chrono::milliseconds(500)); });
|
||||
|
||||
auto end = std::chrono::steady_clock::now();
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
|
||||
|
||||
// It should return in basically 0ms (well under 50ms)
|
||||
EXPECT_LT(elapsed.count(), 50) << "run() blocked the caller!";
|
||||
}
|
||||
|
||||
TEST_F(ThreadPoolTest, Test_Recursive_Submission) {
|
||||
std::atomic<int> result{0};
|
||||
|
||||
std::future<void> f = (*pool)([this, &result]() {
|
||||
// Task A submits Task B
|
||||
std::future<int> f2 = (*pool)([]() { return 42; });
|
||||
result = f2.get();
|
||||
});
|
||||
|
||||
f.get();
|
||||
ASSERT_EQ(result.load(), 42);
|
||||
}
|
||||
|
||||
TEST_F(ThreadPoolTest, Test_Multi_Producer_Contention) {
|
||||
std::atomic<int> counter{0};
|
||||
const int num_producers = 4;
|
||||
const int tasks_per_producer = 250;
|
||||
std::vector<std::thread> producers;
|
||||
|
||||
for (int i = 0; i < num_producers; ++i) {
|
||||
producers.emplace_back([this, &counter, tasks_per_producer]() {
|
||||
for (int j = 0; j < tasks_per_producer; ++j) {
|
||||
pool->run([&counter]() { counter++; });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (auto& t : producers)
|
||||
t.join();
|
||||
|
||||
// Give workers a moment to finish the queue
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
ASSERT_EQ(counter.load(), num_producers * tasks_per_producer);
|
||||
}
|
||||
|
||||
// 2. Exception and Error Handling Tests
|
||||
// ------------------------------------
|
||||
|
||||
TEST_F(ThreadPoolTest, Test_Task_Exception_Transfer) {
|
||||
// Submit a task that throws an exception
|
||||
auto throwing_task = []() -> int {
|
||||
throw std::runtime_error("Task failed intentionally");
|
||||
return 42;
|
||||
};
|
||||
|
||||
std::future<int> f = (*pool)(throwing_task);
|
||||
|
||||
// future::get() should re-throw the exception from the worker thread
|
||||
ASSERT_THROW(f.get(), std::runtime_error);
|
||||
}
|
||||
|
||||
// 3. Critical Shutdown Tests
|
||||
// -------------------------
|
||||
|
||||
TEST_F(ThreadPoolTest, Test_Clean_Destruction_Blocked_Workers) {
|
||||
const int num_threads = POOL_SIZE;
|
||||
std::vector<std::future<void>> futures;
|
||||
std::atomic<int> started_count{0};
|
||||
|
||||
// Submit exactly POOL_SIZE tasks that sleep for a long time
|
||||
for (int i = 0; i < num_threads; ++i) {
|
||||
futures.push_back((*pool)([&started_count]() {
|
||||
started_count++;
|
||||
// Block the thread, forcing the destructor to wait
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
}));
|
||||
}
|
||||
|
||||
// Wait briefly to ensure all threads have started their tasks
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
|
||||
// The destruction of the 'pool' fixture happens automatically in TearDown.
|
||||
// TearDown calls 'delete pool', which calls '~thread_pool()'.
|
||||
// If TearDown completes without crashing, the test passes.
|
||||
|
||||
ASSERT_EQ(started_count.load(), num_threads) << "Not all threads started blocking tasks.";
|
||||
}
|
||||
|
||||
TEST_F(ThreadPoolTest, Test_Clean_Destruction_Full_Queue) {
|
||||
const int num_tasks_to_queue = POOL_SIZE * 5; // Overwhelm the pool
|
||||
std::vector<std::future<void>> futures;
|
||||
|
||||
// Submit many tasks, including long-running ones
|
||||
for (int i = 0; i < num_tasks_to_queue; ++i) {
|
||||
futures.push_back((*pool)([]() {
|
||||
// Mix of fast and slow tasks
|
||||
if (rand() % 10 == 0) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// Do NOT call f.get() here. Let the pool be destroyed immediately,
|
||||
// simulating a sudden program exit.
|
||||
// The destruction in TearDown must complete without a deadlock.
|
||||
|
||||
SUCCEED(); // If TearDown completes, the shutdown was clean.
|
||||
}
|
||||
|
||||
TEST_F(ThreadPoolTest, Test_Void_Return) {
|
||||
std::future<void> f = (*pool)(do_nothing);
|
||||
|
||||
// future::get() must be called to ensure the task finished without exception
|
||||
// It returns void, but checks for exceptions set on the promise.
|
||||
f.get();
|
||||
|
||||
SUCCEED();
|
||||
}
|
||||
|
||||
TEST_F(ThreadPoolTest, Test_Parallel_Execution_Proved) {
|
||||
// A single task duration that is long enough to measure accurately.
|
||||
const auto task_duration = std::chrono::milliseconds(100);
|
||||
|
||||
// Number of tasks equals the number of threads in the pool
|
||||
const unsigned int num_tasks = POOL_SIZE;
|
||||
|
||||
// Calculate the expected sequential time (N tasks * T duration)
|
||||
const auto expected_sequential_duration = task_duration * num_tasks;
|
||||
|
||||
// The expected parallel time should be slightly more than one task's duration
|
||||
// We use a large tolerance factor (e.g., 2.5x the single task duration)
|
||||
// to account for thread creation, scheduling, and I/O overhead.
|
||||
const auto expected_parallel_limit = task_duration * 2.5;
|
||||
|
||||
std::vector<std::future<void>> futures;
|
||||
|
||||
auto start_time = std::chrono::steady_clock::now();
|
||||
|
||||
// 1. Submit N blocking tasks (one for each thread)
|
||||
for (unsigned int i = 0; i < num_tasks; ++i) {
|
||||
futures.push_back((*pool)([task_duration]() { std::this_thread::sleep_for(task_duration); }));
|
||||
}
|
||||
|
||||
// 2. Wait for all tasks to complete
|
||||
for (auto& f : futures) {
|
||||
f.get();
|
||||
}
|
||||
|
||||
auto end_time = std::chrono::steady_clock::now();
|
||||
auto actual_duration = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
|
||||
|
||||
// 3. Assert parallelism
|
||||
|
||||
// Log results for manual inspection if the test fails
|
||||
// std::cout << "\n[ PARALLELISM TEST ]" << std::endl;
|
||||
// std::cout << " Pool Size: " << POOL_SIZE << " threads" << std::endl;
|
||||
// std::cout << " Single Task Time: " << task_duration.count() << "ms" << std::endl;
|
||||
// std::cout << " Sequential Expected: " << expected_sequential_duration.count() << "ms" << std::endl;
|
||||
// std::cout << " Parallel Limit: " << expected_parallel_limit.count() << "ms" << std::endl;
|
||||
// std::cout << " Actual Time Taken: " << actual_duration.count() << "ms" << std::endl;
|
||||
|
||||
// CRITICAL ASSERTION: The actual time must be much less than the sequential time.
|
||||
// Use EXPECT_LT (less than) against the safe parallel limit.
|
||||
EXPECT_LT(actual_duration.count(), expected_parallel_limit.count())
|
||||
<< "The total time taken (" << actual_duration.count() << "ms) suggests tasks ran sequentially."
|
||||
<< "Expected time less than " << expected_parallel_limit.count() << "ms for parallel execution.";
|
||||
}
|
||||
|
||||
TEST(ThreadPoolStress, Test_Rapid_Lifecycle) {
|
||||
for (int i = 0; i < 50; ++i) {
|
||||
thread_pool temporary_pool(2);
|
||||
for (int j = 0; j < 10; ++j) {
|
||||
temporary_pool.run([]() { std::this_thread::yield(); });
|
||||
}
|
||||
// Destruction happens immediately
|
||||
}
|
||||
SUCCEED();
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include <everest/util/async/async_wrapper.hpp>
|
||||
#include <exception>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
struct Counter {
|
||||
Counter(int v) : value(v) {
|
||||
}
|
||||
int value = 0;
|
||||
std::thread::id worker_id;
|
||||
|
||||
// Mutator
|
||||
void add(int v) {
|
||||
value += v;
|
||||
worker_id = std::this_thread::get_id();
|
||||
}
|
||||
// Accessor
|
||||
int get() const {
|
||||
return value;
|
||||
}
|
||||
// Thrower
|
||||
int throw_if_equal(int v) {
|
||||
if (value == v) {
|
||||
throw std::runtime_error("User-defined fatal error.");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
using namespace everest::lib::util;
|
||||
|
||||
// --- GTEST FIXTURE ---
|
||||
namespace everest::lib::util::testing_interface {
|
||||
class AsyncWrapperTest : public ::testing::Test {
|
||||
public:
|
||||
template <class T> std::shared_ptr<std::promise<void>> const& get_global_promise_for_test(T& wrapper) const {
|
||||
return wrapper.m_global_promise;
|
||||
}
|
||||
|
||||
template <class T> auto& get_queue_for_test(T& wrapper) {
|
||||
return wrapper.m_queue;
|
||||
}
|
||||
};
|
||||
} // namespace everest::lib::util::testing_interface
|
||||
|
||||
using namespace everest::lib::util::testing_interface;
|
||||
|
||||
template <class T> class TestQueue : public thread_safe_queue<T> {
|
||||
public:
|
||||
using ThisT = thread_safe_queue<T>;
|
||||
void push(T item) {
|
||||
ThisT::push(item);
|
||||
}
|
||||
|
||||
T pop() {
|
||||
if (m_throw_on_next_pop) {
|
||||
throw std::runtime_error("oh no");
|
||||
}
|
||||
|
||||
auto tmp = ThisT::pop();
|
||||
|
||||
return tmp;
|
||||
}
|
||||
|
||||
void force_throw_on_next_pop() {
|
||||
m_throw_on_next_pop = true;
|
||||
push([]() {});
|
||||
}
|
||||
|
||||
private:
|
||||
bool m_throw_on_next_pop{false};
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
using async_guarded_testqueue = async_wrapper_impl<T, GlobalFailurePolicy, WaitToFinishPolicy, TestQueue>;
|
||||
|
||||
// Test 1: Basic functionality and thread serialization (Background/WaitToFinish)
|
||||
TEST_F(AsyncWrapperTest, CoreFunctionality) {
|
||||
async_wrapper_wait<Counter> wrapper(0);
|
||||
|
||||
// 1. Check asynchronous execution and result retrieval
|
||||
auto fut1 = wrapper([](Counter& c) {
|
||||
c.add(5);
|
||||
return c.get();
|
||||
});
|
||||
auto fut2 = wrapper([](Counter& c) {
|
||||
c.add(10);
|
||||
return c.get();
|
||||
});
|
||||
|
||||
EXPECT_EQ(fut1.get(), 5);
|
||||
EXPECT_EQ(fut2.get(), 15);
|
||||
|
||||
// 2. Check side effect on resource
|
||||
auto fut3 = wrapper([](Counter& c) { return c.get(); });
|
||||
EXPECT_EQ(fut3.get(), 15);
|
||||
|
||||
// 3. Check 'run' (fire-and-forget)
|
||||
wrapper.run([](Counter& c) { c.add(5); });
|
||||
|
||||
auto fut4 = wrapper([](Counter& c) { return c.get(); });
|
||||
EXPECT_EQ(fut4.get(), 20);
|
||||
|
||||
// 4. Check thread ID
|
||||
std::thread::id main_thread_id = std::this_thread::get_id();
|
||||
auto fut_id = wrapper([](Counter& c) { return c.worker_id; });
|
||||
|
||||
EXPECT_NE(fut_id.get(), main_thread_id);
|
||||
}
|
||||
|
||||
// Test 2: LocalFailurePolicy (Background) - User Exception is Isolated
|
||||
TEST_F(AsyncWrapperTest, LocalPolicy_UserExceptionIsContained) {
|
||||
async_wrapper_wait<Counter> wrapper(5);
|
||||
|
||||
auto fut_a = wrapper([](Counter& c) { return c.throw_if_equal(5); });
|
||||
auto fut_b = wrapper([](Counter& c) {
|
||||
c.add(10);
|
||||
return c.get();
|
||||
});
|
||||
|
||||
fut_a.wait();
|
||||
fut_b.wait();
|
||||
|
||||
ASSERT_THROW(fut_a.get(), std::runtime_error);
|
||||
|
||||
int received_value = 0;
|
||||
EXPECT_NO_THROW(received_value = fut_b.get());
|
||||
EXPECT_EQ(received_value, 15);
|
||||
|
||||
auto fut_c = wrapper([](Counter& c) { return c.get(); });
|
||||
fut_c.wait();
|
||||
EXPECT_EQ(fut_c.get(), 15);
|
||||
}
|
||||
|
||||
// Test 3: GlobalFailurePolicy (Guarded) - User Exception Shuts Down Instance
|
||||
TEST_F(AsyncWrapperTest, GlobalPolicy_UserExceptionCausesShutdown) {
|
||||
async_wrapper_guarded_wait<Counter> wrapper(5);
|
||||
|
||||
auto fut_a = wrapper([](Counter& c) { return c.throw_if_equal(5); });
|
||||
auto fut_b = wrapper([](Counter& c) {
|
||||
c.add(10);
|
||||
return c.get();
|
||||
});
|
||||
|
||||
fut_a.wait();
|
||||
fut_b.wait();
|
||||
|
||||
ASSERT_THROW(fut_a.get(), std::runtime_error);
|
||||
ASSERT_THROW(fut_b.get(), std::runtime_error);
|
||||
|
||||
auto fut_c = wrapper([](Counter& c) { return c.get(); });
|
||||
fut_c.wait();
|
||||
ASSERT_THROW(fut_c.get(), std::runtime_error);
|
||||
}
|
||||
|
||||
// Test 4: GlobalFailureSignal_BlocksNewTasks (Tests signal effect)
|
||||
TEST_F(AsyncWrapperTest, GlobalFailureSignal_BlocksNewTasks) {
|
||||
async_wrapper_guarded_wait<Counter> wrapper(0);
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
|
||||
// 1. Manually set the Global Promise (simulating infrastructure or user failure)
|
||||
try {
|
||||
throw std::runtime_error("Simulated Global Signal Set.");
|
||||
} catch (...) {
|
||||
get_global_promise_for_test(wrapper)->set_exception(std::current_exception());
|
||||
}
|
||||
|
||||
// 2. Submit a new task (Task B)
|
||||
auto fut_b = wrapper([](Counter& c) {
|
||||
c.add(50);
|
||||
return c.get();
|
||||
});
|
||||
|
||||
fut_b.wait();
|
||||
|
||||
// 3. Task B must immediately fail because the infrastructure flag is set
|
||||
ASSERT_THROW(fut_b.get(), std::runtime_error);
|
||||
}
|
||||
|
||||
// Test 5: Destructor Behavior - WaitToFinishPolicy vs FastQuitPolicy
|
||||
TEST_F(AsyncWrapperTest, DestructorShutdownPolicies) {
|
||||
// Setup 1: Test WaitToFinishPolicy (Guaranteed execution of queued task)
|
||||
int wait_result = 0;
|
||||
{
|
||||
async_wrapper_guarded_wait<Counter> wrapper(0);
|
||||
auto fut = wrapper([&wait_result](Counter& c) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
wait_result = 1;
|
||||
return c.get();
|
||||
});
|
||||
// Destructor runs here (WaitToFinishPolicy::shutdown), MUST wait 50ms.
|
||||
}
|
||||
// Result confirms the destructor waited for the task to finish.
|
||||
EXPECT_EQ(wait_result, 1);
|
||||
|
||||
// Setup 2: Test FastQuitPolicy (Drops queued task, joins quickly)
|
||||
int fast_result = 0;
|
||||
{
|
||||
async_wrapper_guarded_fast<Counter> wrapper(0);
|
||||
|
||||
// Push a task that runs briefly. If the task starts, the destructor must wait.
|
||||
// We rely on the race condition being won by the destructor for EXPECT_EQ(0) to pass.
|
||||
wrapper.run([&fast_result](Counter& c) {
|
||||
std::this_thread::sleep_for(std::chrono::microseconds(100)); // Very fast sleep
|
||||
fast_result = 2; // Should not reach here if the task is aborted while queued
|
||||
});
|
||||
|
||||
// Destructor runs here (FastQuitPolicy::shutdown), should join quickly.
|
||||
}
|
||||
// If fast_result == 0, the task was aborted while queued.
|
||||
// If fast_result == 2, the task started and the destructor waited for it to finish.
|
||||
EXPECT_EQ(fast_result, 0);
|
||||
}
|
||||
|
||||
// Test 6: Verify Worker's internal catch block works correctly
|
||||
TEST_F(AsyncWrapperTest, InfrastructureFailure_WorkerSetsSignalAndShutsDown) {
|
||||
async_guarded_testqueue<Counter> wrapper(0); // Use the specialized TestQueue type
|
||||
|
||||
// 1. Ensure worker thread is blocked on pop()
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
|
||||
// 2. Force the queue to throw an exception, waking up the worker thread
|
||||
get_queue_for_test(wrapper).force_throw_on_next_pop();
|
||||
|
||||
// 3. Give the worker time to execute the catch block and shut down.
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
|
||||
// 4. Submit a new task (Task A). This hits the Synchronous Gatekeeper check.
|
||||
auto fut_a = wrapper([](Counter& c) {
|
||||
c.add(99);
|
||||
return c.get();
|
||||
});
|
||||
|
||||
fut_a.wait();
|
||||
|
||||
// 5. Task A must fail with the infrastructure exception
|
||||
bool active_runtime_error = false;
|
||||
std::string message = "";
|
||||
try {
|
||||
fut_a.get();
|
||||
} catch (const std::runtime_error& e) {
|
||||
message = e.what();
|
||||
active_runtime_error = true;
|
||||
}
|
||||
EXPECT_TRUE(active_runtime_error);
|
||||
|
||||
EXPECT_EQ(message, "Async worker infrastructure failure.");
|
||||
}
|
||||
|
||||
// Test 7: Run method must not block the main thread (optimized fire-and-forget)
|
||||
TEST_F(AsyncWrapperTest, RunMethodDoesNotBlock) {
|
||||
// We use the WaitToFinish policy so we know the task will complete before the test ends.
|
||||
async_wrapper_wait<Counter> wrapper(0);
|
||||
const int SLOW_TASK_MS = 50;
|
||||
|
||||
auto slow_task = [SLOW_TASK_MS](Counter& c) {
|
||||
// Task takes significant time to execute
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(SLOW_TASK_MS));
|
||||
c.add(1);
|
||||
};
|
||||
|
||||
auto start_time = std::chrono::steady_clock::now();
|
||||
|
||||
// Submit the slow task using the optimized run() method
|
||||
wrapper.run(slow_task);
|
||||
|
||||
auto end_time = std::chrono::steady_clock::now();
|
||||
|
||||
// The main thread should not have blocked for the duration of the task.
|
||||
// The elapsed time should be much less than the task execution time (10ms tolerance).
|
||||
auto elapsed_ms = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time).count();
|
||||
|
||||
// 1. Assertion on timing: Proves non-blocking submission
|
||||
EXPECT_LT(elapsed_ms, 10);
|
||||
|
||||
// 2. Assert task eventually ran (rely on WaitToFinishPolicy destructor)
|
||||
auto fut_check = wrapper([](Counter& c) { return c.get(); });
|
||||
EXPECT_EQ(fut_check.get(), 1);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <everest/util/enum/EnumFlags.hpp>
|
||||
|
||||
enum class ErrorHandlingFlags : std::uint8_t {
|
||||
prevent_charging,
|
||||
prevent_charging_welded,
|
||||
all_errors_cleared,
|
||||
last = all_errors_cleared
|
||||
};
|
||||
|
||||
enum class BspErrors : std::uint8_t {
|
||||
DiodeFault,
|
||||
VentilationNotAvailable,
|
||||
BrownOut,
|
||||
EnergyManagement,
|
||||
PermanentFault,
|
||||
MREC2GroundFailure,
|
||||
MREC4OverCurrentFailure,
|
||||
MREC5OverVoltage,
|
||||
MREC6UnderVoltage,
|
||||
MREC8EmergencyStop,
|
||||
MREC10InvalidVehicleMode,
|
||||
MREC14PilotFault,
|
||||
MREC15PowerLoss,
|
||||
MREC17EVSEContactorFault,
|
||||
MREC19CableOverTempStop,
|
||||
MREC20PartialInsertion,
|
||||
MREC23ProximityFault,
|
||||
MREC24ConnectorVoltageHigh,
|
||||
MREC25BrokenLatch,
|
||||
MREC26CutCable,
|
||||
VendorError,
|
||||
last = VendorError
|
||||
};
|
||||
|
||||
using namespace everest::lib::util;
|
||||
|
||||
TEST(AtomicEnumFlagsTest, init) {
|
||||
AtomicEnumFlags<ErrorHandlingFlags> flags;
|
||||
EXPECT_TRUE(flags.all_reset());
|
||||
}
|
||||
|
||||
TEST(AtomicEnumFlagsTest, init_large) {
|
||||
AtomicEnumFlags<BspErrors> flags;
|
||||
EXPECT_TRUE(flags.all_reset());
|
||||
}
|
||||
|
||||
TEST(AtomicEnumFlagsTest, set_reset_one) {
|
||||
AtomicEnumFlags<ErrorHandlingFlags> flags;
|
||||
EXPECT_TRUE(flags.all_reset());
|
||||
|
||||
flags.set(ErrorHandlingFlags::all_errors_cleared);
|
||||
EXPECT_FALSE(flags.all_reset());
|
||||
flags.reset(ErrorHandlingFlags::all_errors_cleared);
|
||||
EXPECT_TRUE(flags.all_reset());
|
||||
}
|
||||
|
||||
TEST(AtomicEnumFlagsTest, set_reset_two) {
|
||||
AtomicEnumFlags<ErrorHandlingFlags> flags;
|
||||
EXPECT_TRUE(flags.all_reset());
|
||||
|
||||
flags.set(ErrorHandlingFlags::all_errors_cleared);
|
||||
EXPECT_FALSE(flags.all_reset());
|
||||
flags.set(ErrorHandlingFlags::prevent_charging);
|
||||
EXPECT_FALSE(flags.all_reset());
|
||||
flags.reset(ErrorHandlingFlags::all_errors_cleared);
|
||||
EXPECT_FALSE(flags.all_reset());
|
||||
flags.reset(ErrorHandlingFlags::prevent_charging);
|
||||
EXPECT_TRUE(flags.all_reset());
|
||||
}
|
||||
|
||||
TEST(AtomicEnumFlagsTest, set_reset_three) {
|
||||
AtomicEnumFlags<ErrorHandlingFlags> flags;
|
||||
EXPECT_TRUE(flags.all_reset());
|
||||
|
||||
flags.set(ErrorHandlingFlags::all_errors_cleared);
|
||||
EXPECT_FALSE(flags.all_reset());
|
||||
flags.set(ErrorHandlingFlags::prevent_charging);
|
||||
EXPECT_FALSE(flags.all_reset());
|
||||
flags.set(ErrorHandlingFlags::prevent_charging_welded);
|
||||
EXPECT_FALSE(flags.all_reset());
|
||||
flags.reset();
|
||||
EXPECT_TRUE(flags.all_reset());
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <everest/util/enum/EnumFlags.hpp>
|
||||
|
||||
namespace {
|
||||
using namespace everest::lib::util;
|
||||
|
||||
// needs an 8-bit value
|
||||
enum class small : std::uint8_t {
|
||||
one,
|
||||
two,
|
||||
three,
|
||||
four,
|
||||
five,
|
||||
six,
|
||||
seven,
|
||||
last = seven,
|
||||
};
|
||||
|
||||
// needs an 8-bit value
|
||||
enum class full : std::uint8_t {
|
||||
one,
|
||||
two,
|
||||
three,
|
||||
four,
|
||||
five,
|
||||
six,
|
||||
seven,
|
||||
eight,
|
||||
last = eight,
|
||||
};
|
||||
|
||||
// needs an 16-bit value
|
||||
enum class large : std::uint8_t {
|
||||
zero,
|
||||
one,
|
||||
two,
|
||||
three,
|
||||
four,
|
||||
five,
|
||||
six,
|
||||
seven,
|
||||
eight,
|
||||
last = eight,
|
||||
};
|
||||
|
||||
static_assert(sizeof(full) == sizeof(std::uint8_t));
|
||||
static_assert(sizeof(SelectedUInt<full>) == sizeof(std::uint8_t));
|
||||
|
||||
static_assert(sizeof(large) == sizeof(std::uint8_t));
|
||||
static_assert(sizeof(SelectedUInt<large>) == sizeof(std::uint16_t));
|
||||
|
||||
TEST(EnumFlags, InitFull) {
|
||||
EnumFlags<full> flags;
|
||||
|
||||
EXPECT_EQ(flags.get(), 0);
|
||||
EXPECT_TRUE(flags.all_reset());
|
||||
EXPECT_FALSE(flags.any_set());
|
||||
EXPECT_FALSE(flags.all_set());
|
||||
|
||||
EXPECT_FALSE(flags.is_set(full::one));
|
||||
EXPECT_FALSE(flags.is_set(full::two));
|
||||
EXPECT_FALSE(flags.is_set(full::three));
|
||||
EXPECT_FALSE(flags.is_set(full::four));
|
||||
EXPECT_FALSE(flags.is_set(full::five));
|
||||
EXPECT_FALSE(flags.is_set(full::six));
|
||||
EXPECT_FALSE(flags.is_set(full::seven));
|
||||
EXPECT_FALSE(flags.is_set(full::eight));
|
||||
|
||||
EXPECT_TRUE(flags.is_reset(full::one));
|
||||
EXPECT_TRUE(flags.is_reset(full::two));
|
||||
EXPECT_TRUE(flags.is_reset(full::three));
|
||||
EXPECT_TRUE(flags.is_reset(full::four));
|
||||
EXPECT_TRUE(flags.is_reset(full::five));
|
||||
EXPECT_TRUE(flags.is_reset(full::six));
|
||||
EXPECT_TRUE(flags.is_reset(full::seven));
|
||||
EXPECT_TRUE(flags.is_reset(full::eight));
|
||||
|
||||
flags.set(full::one);
|
||||
EXPECT_EQ(flags.get(), 1);
|
||||
EXPECT_FALSE(flags.all_reset());
|
||||
EXPECT_TRUE(flags.any_set());
|
||||
EXPECT_FALSE(flags.all_set());
|
||||
|
||||
EXPECT_TRUE(flags.is_set(full::one));
|
||||
EXPECT_FALSE(flags.is_set(full::two));
|
||||
EXPECT_FALSE(flags.is_set(full::three));
|
||||
EXPECT_FALSE(flags.is_set(full::four));
|
||||
EXPECT_FALSE(flags.is_set(full::five));
|
||||
EXPECT_FALSE(flags.is_set(full::six));
|
||||
EXPECT_FALSE(flags.is_set(full::seven));
|
||||
EXPECT_FALSE(flags.is_set(full::eight));
|
||||
|
||||
EXPECT_FALSE(flags.is_reset(full::one));
|
||||
EXPECT_TRUE(flags.is_reset(full::two));
|
||||
EXPECT_TRUE(flags.is_reset(full::three));
|
||||
EXPECT_TRUE(flags.is_reset(full::four));
|
||||
EXPECT_TRUE(flags.is_reset(full::five));
|
||||
EXPECT_TRUE(flags.is_reset(full::six));
|
||||
EXPECT_TRUE(flags.is_reset(full::seven));
|
||||
EXPECT_TRUE(flags.is_reset(full::eight));
|
||||
|
||||
flags.set(full::two, full::three, full::four);
|
||||
EXPECT_EQ(flags.get(), 0b1111);
|
||||
EXPECT_FALSE(flags.all_reset());
|
||||
EXPECT_TRUE(flags.any_set());
|
||||
EXPECT_FALSE(flags.all_set());
|
||||
|
||||
EXPECT_TRUE(flags.is_set(full::one));
|
||||
EXPECT_TRUE(flags.is_set(full::two));
|
||||
EXPECT_TRUE(flags.is_set(full::three));
|
||||
EXPECT_TRUE(flags.is_set(full::four));
|
||||
EXPECT_FALSE(flags.is_set(full::five));
|
||||
EXPECT_FALSE(flags.is_set(full::six));
|
||||
EXPECT_FALSE(flags.is_set(full::seven));
|
||||
EXPECT_FALSE(flags.is_set(full::eight));
|
||||
|
||||
EXPECT_FALSE(flags.is_reset(full::one));
|
||||
EXPECT_FALSE(flags.is_reset(full::two));
|
||||
EXPECT_FALSE(flags.is_reset(full::three));
|
||||
EXPECT_FALSE(flags.is_reset(full::four));
|
||||
EXPECT_TRUE(flags.is_reset(full::five));
|
||||
EXPECT_TRUE(flags.is_reset(full::six));
|
||||
EXPECT_TRUE(flags.is_reset(full::seven));
|
||||
EXPECT_TRUE(flags.is_reset(full::eight));
|
||||
|
||||
flags.set(full::five, full::six, full::seven, full::eight);
|
||||
EXPECT_EQ(flags.get(), 0xff);
|
||||
EXPECT_FALSE(flags.all_reset());
|
||||
EXPECT_TRUE(flags.any_set());
|
||||
EXPECT_TRUE(flags.all_set());
|
||||
|
||||
EXPECT_TRUE(flags.is_set(full::one));
|
||||
EXPECT_TRUE(flags.is_set(full::two));
|
||||
EXPECT_TRUE(flags.is_set(full::three));
|
||||
EXPECT_TRUE(flags.is_set(full::four));
|
||||
EXPECT_TRUE(flags.is_set(full::five));
|
||||
EXPECT_TRUE(flags.is_set(full::six));
|
||||
EXPECT_TRUE(flags.is_set(full::seven));
|
||||
EXPECT_TRUE(flags.is_set(full::eight));
|
||||
|
||||
EXPECT_FALSE(flags.is_reset(full::one));
|
||||
EXPECT_FALSE(flags.is_reset(full::two));
|
||||
EXPECT_FALSE(flags.is_reset(full::three));
|
||||
EXPECT_FALSE(flags.is_reset(full::four));
|
||||
EXPECT_FALSE(flags.is_reset(full::five));
|
||||
EXPECT_FALSE(flags.is_reset(full::six));
|
||||
EXPECT_FALSE(flags.is_reset(full::seven));
|
||||
EXPECT_FALSE(flags.is_reset(full::eight));
|
||||
|
||||
flags.reset(full::one, full::eight);
|
||||
EXPECT_EQ(flags.get(), 0b01111110);
|
||||
EXPECT_FALSE(flags.all_reset());
|
||||
EXPECT_TRUE(flags.any_set());
|
||||
EXPECT_FALSE(flags.all_set());
|
||||
EXPECT_FALSE(flags.is_set(full::one, full::eight));
|
||||
EXPECT_FALSE(flags.is_set(full::one, full::eight, full::five));
|
||||
EXPECT_TRUE(flags.is_set(full::two, full::five, full::seven));
|
||||
|
||||
flags.set(0xfe);
|
||||
EXPECT_EQ(flags.get(), 0b11111110);
|
||||
EXPECT_FALSE(flags.all_reset());
|
||||
EXPECT_TRUE(flags.any_set());
|
||||
EXPECT_FALSE(flags.all_set());
|
||||
|
||||
flags.set(full::one);
|
||||
EXPECT_EQ(flags.get(), 0b11111111);
|
||||
EXPECT_FALSE(flags.all_reset());
|
||||
EXPECT_TRUE(flags.any_set());
|
||||
EXPECT_TRUE(flags.all_set());
|
||||
}
|
||||
|
||||
TEST(EnumFlags, Set) {
|
||||
EnumFlags<small> sflags;
|
||||
EXPECT_TRUE(sflags.all_reset());
|
||||
sflags.set();
|
||||
EXPECT_TRUE(sflags.all_set());
|
||||
EXPECT_EQ(sflags.get(), 0b01111111);
|
||||
|
||||
EnumFlags<full> flags;
|
||||
EXPECT_TRUE(flags.all_reset());
|
||||
EXPECT_FALSE(flags.any_set());
|
||||
|
||||
flags.set(full::one);
|
||||
EXPECT_EQ(flags.get(), 0b1);
|
||||
|
||||
flags.reset();
|
||||
flags.set(full::one, full::two);
|
||||
EXPECT_EQ(flags.get(), 0b11);
|
||||
|
||||
flags.reset();
|
||||
flags.set(full::one, full::two, full::three);
|
||||
EXPECT_EQ(flags.get(), 0b111);
|
||||
|
||||
flags.reset();
|
||||
flags.set(full::one, full::two, full::three, full::four);
|
||||
EXPECT_EQ(flags.get(), 0b1111);
|
||||
}
|
||||
|
||||
TEST(EnumFlags, Reset) {
|
||||
EnumFlags<full> flags;
|
||||
flags.set();
|
||||
EXPECT_TRUE(flags.all_set());
|
||||
EXPECT_FALSE(flags.any_reset());
|
||||
|
||||
flags.reset(full::one);
|
||||
EXPECT_EQ(flags.get(), 0b11111110);
|
||||
|
||||
flags.set();
|
||||
flags.reset(full::one, full::two);
|
||||
EXPECT_EQ(flags.get(), 0b11111100);
|
||||
|
||||
flags.set();
|
||||
flags.reset(full::one, full::two, full::three);
|
||||
EXPECT_EQ(flags.get(), 0b11111000);
|
||||
|
||||
flags.set();
|
||||
flags.reset(full::one, full::two, full::three, full::four);
|
||||
EXPECT_EQ(flags.get(), 0b11110000);
|
||||
}
|
||||
|
||||
TEST(EnumFlags, AnySet) {
|
||||
EnumFlags<full> flags;
|
||||
flags.set(0x7e);
|
||||
EXPECT_EQ(flags.get(), 0b01111110);
|
||||
|
||||
EXPECT_TRUE(flags.is_set(full::two));
|
||||
EXPECT_FALSE(flags.is_set(full::one, full::two));
|
||||
EXPECT_FALSE(flags.is_set(full::two, full::one));
|
||||
EXPECT_FALSE(flags.is_set(full::one, full::two, full::three));
|
||||
EXPECT_FALSE(flags.is_set(full::three, full::two, full::one));
|
||||
|
||||
EXPECT_TRUE(flags.is_any_set(full::one, full::two));
|
||||
EXPECT_TRUE(flags.is_any_set(full::two, full::one));
|
||||
EXPECT_TRUE(flags.is_any_set(full::one, full::two, full::three));
|
||||
EXPECT_TRUE(flags.is_any_set(full::one, full::three, full::two));
|
||||
EXPECT_TRUE(flags.is_any_set(full::three, full::two, full::one));
|
||||
|
||||
EXPECT_FALSE(flags.is_any_set(full::one, full::eight));
|
||||
EXPECT_TRUE(flags.is_any_set(full::eight, full::two, full::one));
|
||||
}
|
||||
|
||||
TEST(EnumFlags, AnyReSet) {
|
||||
EnumFlags<full> flags;
|
||||
flags.set(0x7e);
|
||||
EXPECT_EQ(flags.get(), 0b01111110);
|
||||
|
||||
EXPECT_TRUE(flags.is_reset(full::one));
|
||||
EXPECT_TRUE(flags.is_set(full::two));
|
||||
EXPECT_FALSE(flags.is_reset(full::one, full::two));
|
||||
EXPECT_FALSE(flags.is_reset(full::two, full::one));
|
||||
EXPECT_FALSE(flags.is_reset(full::one, full::two, full::three));
|
||||
EXPECT_FALSE(flags.is_reset(full::three, full::two, full::one));
|
||||
|
||||
EXPECT_TRUE(flags.is_any_reset(full::one, full::two));
|
||||
EXPECT_TRUE(flags.is_any_reset(full::two, full::one));
|
||||
EXPECT_TRUE(flags.is_any_reset(full::one, full::two, full::three));
|
||||
EXPECT_TRUE(flags.is_any_reset(full::three, full::two, full::one));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
435
tools/EVerest-main/lib/everest/util/tests/fsm/fsm_tests.cpp
Normal file
435
tools/EVerest-main/lib/everest/util/tests/fsm/fsm_tests.cpp
Normal file
@@ -0,0 +1,435 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <everest/util/fsm/fsm.hpp>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// --- Minimal test state infrastructure ---
|
||||
|
||||
enum class StateID {
|
||||
A,
|
||||
B,
|
||||
C,
|
||||
};
|
||||
|
||||
enum class Event {
|
||||
GoToB,
|
||||
GoToC,
|
||||
Stay,
|
||||
Unknown,
|
||||
};
|
||||
|
||||
struct TestState;
|
||||
using TestStatePtr = std::unique_ptr<TestState>;
|
||||
|
||||
struct FeedResult {
|
||||
FeedResult() = default;
|
||||
FeedResult(bool handled) : unhandled(!handled) {
|
||||
}
|
||||
FeedResult(TestStatePtr result_state) : unhandled(false), new_state(std::move(result_state)) {
|
||||
}
|
||||
|
||||
bool unhandled{true};
|
||||
TestStatePtr new_state{nullptr};
|
||||
};
|
||||
|
||||
struct TestState {
|
||||
using ContainerType = TestStatePtr;
|
||||
using EventType = Event;
|
||||
|
||||
TestState(StateID id, std::vector<std::string>& log) : m_id(id), m_log(log) {
|
||||
}
|
||||
|
||||
virtual ~TestState() = default;
|
||||
|
||||
StateID get_id() const {
|
||||
return m_id;
|
||||
}
|
||||
|
||||
virtual void enter() {
|
||||
m_log.push_back("enter:" + state_name());
|
||||
}
|
||||
|
||||
virtual FeedResult feed(Event ev) = 0;
|
||||
|
||||
virtual void leave() {
|
||||
m_log.push_back("leave:" + state_name());
|
||||
}
|
||||
|
||||
std::string state_name() const {
|
||||
switch (m_id) {
|
||||
case StateID::A:
|
||||
return "A";
|
||||
case StateID::B:
|
||||
return "B";
|
||||
case StateID::C:
|
||||
return "C";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
protected:
|
||||
StateID m_id;
|
||||
std::vector<std::string>& m_log;
|
||||
};
|
||||
|
||||
struct StateA : TestState {
|
||||
StateA(std::vector<std::string>& log) : TestState(StateID::A, log) {
|
||||
}
|
||||
|
||||
FeedResult feed(Event ev) override;
|
||||
};
|
||||
|
||||
struct StateB : TestState {
|
||||
StateB(std::vector<std::string>& log) : TestState(StateID::B, log) {
|
||||
}
|
||||
|
||||
FeedResult feed(Event ev) override {
|
||||
if (ev == Event::Stay) {
|
||||
return FeedResult(true);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
FeedResult StateA::feed(Event ev) {
|
||||
if (ev == Event::GoToB) {
|
||||
return FeedResult(std::make_unique<StateB>(m_log));
|
||||
}
|
||||
if (ev == Event::Stay) {
|
||||
return FeedResult(true);
|
||||
}
|
||||
return {}; // unhandled
|
||||
}
|
||||
|
||||
// --- FSM Tests ---
|
||||
|
||||
TEST(FsmV2Test, ConstructionCallsEnter) {
|
||||
std::vector<std::string> log;
|
||||
{
|
||||
fsm::v2::FSM<TestState> fsm(std::make_unique<StateA>(log));
|
||||
EXPECT_EQ(log.size(), 1u);
|
||||
EXPECT_EQ(log[0], "enter:A");
|
||||
}
|
||||
}
|
||||
|
||||
TEST(FsmV2Test, DestructionCallsLeave) {
|
||||
std::vector<std::string> log;
|
||||
{
|
||||
fsm::v2::FSM<TestState> fsm(std::make_unique<StateA>(log));
|
||||
log.clear();
|
||||
}
|
||||
EXPECT_EQ(log.size(), 1u);
|
||||
EXPECT_EQ(log[0], "leave:A");
|
||||
}
|
||||
|
||||
TEST(FsmV2Test, FeedNoTransition) {
|
||||
std::vector<std::string> log;
|
||||
fsm::v2::FSM<TestState> fsm(std::make_unique<StateA>(log));
|
||||
|
||||
auto result = fsm.feed(Event::Stay);
|
||||
EXPECT_TRUE(result);
|
||||
EXPECT_FALSE(result.transitioned());
|
||||
EXPECT_EQ(fsm.get_current_state_id(), StateID::A);
|
||||
}
|
||||
|
||||
TEST(FsmV2Test, FeedWithTransition) {
|
||||
std::vector<std::string> log;
|
||||
fsm::v2::FSM<TestState> fsm(std::make_unique<StateA>(log));
|
||||
log.clear();
|
||||
|
||||
auto result = fsm.feed(Event::GoToB);
|
||||
EXPECT_TRUE(result);
|
||||
EXPECT_TRUE(result.transitioned());
|
||||
EXPECT_EQ(fsm.get_current_state_id(), StateID::B);
|
||||
|
||||
ASSERT_EQ(log.size(), 2u);
|
||||
EXPECT_EQ(log[0], "leave:A");
|
||||
EXPECT_EQ(log[1], "enter:B");
|
||||
}
|
||||
|
||||
TEST(FsmV2Test, UnhandledEvent) {
|
||||
std::vector<std::string> log;
|
||||
fsm::v2::FSM<TestState> fsm(std::make_unique<StateA>(log));
|
||||
|
||||
auto result = fsm.feed(Event::Unknown);
|
||||
EXPECT_FALSE(result); // FeedResult is falsy for unhandled
|
||||
EXPECT_FALSE(result.transitioned());
|
||||
EXPECT_EQ(fsm.get_current_state_id(), StateID::A);
|
||||
}
|
||||
|
||||
TEST(FsmV2Test, GetCurrentStateId) {
|
||||
std::vector<std::string> log;
|
||||
fsm::v2::FSM<TestState> fsm(std::make_unique<StateA>(log));
|
||||
EXPECT_EQ(fsm.get_current_state_id(), StateID::A);
|
||||
|
||||
fsm.feed(Event::GoToB);
|
||||
EXPECT_EQ(fsm.get_current_state_id(), StateID::B);
|
||||
}
|
||||
|
||||
// --- FeedResult with output ---
|
||||
|
||||
struct OutputState;
|
||||
using OutputStatePtr = std::unique_ptr<OutputState>;
|
||||
|
||||
struct OutputFeedResult {
|
||||
OutputFeedResult() = default;
|
||||
OutputFeedResult(bool handled) : unhandled(!handled) {
|
||||
}
|
||||
OutputFeedResult(int output_value, bool handled) : unhandled(!handled), output(output_value) {
|
||||
}
|
||||
OutputFeedResult(OutputStatePtr result_state) : unhandled(false), new_state(std::move(result_state)) {
|
||||
}
|
||||
OutputFeedResult(OutputStatePtr result_state, int output_value) :
|
||||
unhandled(false), new_state(std::move(result_state)), output(output_value) {
|
||||
}
|
||||
|
||||
bool unhandled{true};
|
||||
OutputStatePtr new_state{nullptr};
|
||||
int output{0};
|
||||
};
|
||||
|
||||
struct OutputState {
|
||||
using ContainerType = OutputStatePtr;
|
||||
using EventType = Event;
|
||||
|
||||
OutputState(StateID id) : m_id(id) {
|
||||
}
|
||||
|
||||
virtual ~OutputState() = default;
|
||||
|
||||
StateID get_id() const {
|
||||
return m_id;
|
||||
}
|
||||
|
||||
virtual void enter() {
|
||||
}
|
||||
|
||||
virtual OutputFeedResult feed(Event ev) = 0;
|
||||
|
||||
virtual void leave() {
|
||||
}
|
||||
|
||||
protected:
|
||||
StateID m_id;
|
||||
};
|
||||
|
||||
struct OutputStateA : OutputState {
|
||||
OutputStateA() : OutputState(StateID::A) {
|
||||
}
|
||||
|
||||
OutputFeedResult feed(Event ev) override {
|
||||
if (ev == Event::Stay) {
|
||||
return OutputFeedResult(42, true);
|
||||
}
|
||||
if (ev == Event::GoToB) {
|
||||
return OutputFeedResult(std::make_unique<OutputStateA>(), 99);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
TEST(FsmV2Test, FeedResultWithOutput) {
|
||||
fsm::v2::FSM<OutputState> fsm(std::make_unique<OutputStateA>());
|
||||
|
||||
auto result = fsm.feed(Event::Stay);
|
||||
EXPECT_TRUE(result);
|
||||
EXPECT_FALSE(result.transitioned());
|
||||
EXPECT_EQ(result.output, 42);
|
||||
}
|
||||
|
||||
TEST(FsmV2Test, FeedResultWithOutputOnTransition) {
|
||||
fsm::v2::FSM<OutputState> fsm(std::make_unique<OutputStateA>());
|
||||
|
||||
auto result = fsm.feed(Event::GoToB);
|
||||
EXPECT_TRUE(result);
|
||||
EXPECT_TRUE(result.transitioned());
|
||||
EXPECT_EQ(result.output, 99);
|
||||
}
|
||||
|
||||
TEST(FsmV2Test, FeedResultVoidUnhandled) {
|
||||
std::vector<std::string> log;
|
||||
fsm::v2::FSM<TestState> fsm(std::make_unique<StateA>(log));
|
||||
|
||||
auto result = fsm.feed(Event::Unknown);
|
||||
// FeedResult<void> — no .output member, just check bool and transitioned
|
||||
EXPECT_FALSE(result);
|
||||
EXPECT_FALSE(result.transitioned());
|
||||
}
|
||||
|
||||
// --- NestedFSM tests ---
|
||||
|
||||
struct NestedState;
|
||||
using NestedStatePtr = std::unique_ptr<NestedState>;
|
||||
|
||||
struct NestedFeedResult {
|
||||
NestedFeedResult() = default;
|
||||
NestedFeedResult(bool handled) : unhandled(!handled) {
|
||||
}
|
||||
NestedFeedResult(NestedStatePtr result_state) : unhandled(false), new_state(std::move(result_state)) {
|
||||
}
|
||||
|
||||
bool unhandled{true};
|
||||
NestedStatePtr new_state{nullptr};
|
||||
};
|
||||
|
||||
struct NestedState {
|
||||
using ContainerType = NestedStatePtr;
|
||||
using EventType = Event;
|
||||
|
||||
NestedState(StateID id, std::vector<std::string>& log) : m_id(id), m_log(log) {
|
||||
}
|
||||
|
||||
virtual ~NestedState() = default;
|
||||
|
||||
StateID get_id() const {
|
||||
return m_id;
|
||||
}
|
||||
|
||||
virtual void enter() {
|
||||
m_log.push_back("enter:" + state_name());
|
||||
}
|
||||
|
||||
virtual NestedFeedResult feed(Event ev) = 0;
|
||||
|
||||
virtual void leave() {
|
||||
m_log.push_back("leave:" + state_name());
|
||||
}
|
||||
|
||||
virtual NestedStatePtr get_initial() {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::string state_name() const {
|
||||
switch (m_id) {
|
||||
case StateID::A:
|
||||
return "A";
|
||||
case StateID::B:
|
||||
return "B";
|
||||
case StateID::C:
|
||||
return "C";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
protected:
|
||||
StateID m_id;
|
||||
std::vector<std::string>& m_log;
|
||||
};
|
||||
|
||||
// ChildB is a leaf child of ParentA
|
||||
struct ChildB : NestedState {
|
||||
ChildB(std::vector<std::string>& log) : NestedState(StateID::B, log) {
|
||||
}
|
||||
|
||||
NestedFeedResult feed(Event ev) override {
|
||||
if (ev == Event::Stay) {
|
||||
return NestedFeedResult(true);
|
||||
}
|
||||
return {}; // bubble up to parent
|
||||
}
|
||||
};
|
||||
|
||||
// ParentA has ChildB as initial child state
|
||||
struct ParentA : NestedState {
|
||||
ParentA(std::vector<std::string>& log) : NestedState(StateID::A, log) {
|
||||
}
|
||||
|
||||
NestedFeedResult feed(Event ev) override {
|
||||
if (ev == Event::GoToC) {
|
||||
// Transition to a new state C (leaf, no children)
|
||||
return NestedFeedResult(std::make_unique<LeafC>(m_log));
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
NestedStatePtr get_initial() override {
|
||||
return std::make_unique<ChildB>(m_log);
|
||||
}
|
||||
|
||||
struct LeafC : NestedState {
|
||||
LeafC(std::vector<std::string>& log) : NestedState(StateID::C, log) {
|
||||
}
|
||||
|
||||
NestedFeedResult feed(Event) override {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
TEST(NestedFsmV2Test, ConstructionUnrollsChildren) {
|
||||
std::vector<std::string> log;
|
||||
{
|
||||
fsm::v2::NestedFSM<NestedState> fsm(std::make_unique<ParentA>(log));
|
||||
// Should enter ParentA, then enter ChildB
|
||||
ASSERT_EQ(log.size(), 2u);
|
||||
EXPECT_EQ(log[0], "enter:A");
|
||||
EXPECT_EQ(log[1], "enter:B");
|
||||
}
|
||||
}
|
||||
|
||||
TEST(NestedFsmV2Test, DestructionLeavesAllStates) {
|
||||
std::vector<std::string> log;
|
||||
{
|
||||
fsm::v2::NestedFSM<NestedState> fsm(std::make_unique<ParentA>(log));
|
||||
log.clear();
|
||||
}
|
||||
// Should leave ChildB then ParentA
|
||||
ASSERT_EQ(log.size(), 2u);
|
||||
EXPECT_EQ(log[0], "leave:B");
|
||||
EXPECT_EQ(log[1], "leave:A");
|
||||
}
|
||||
|
||||
TEST(NestedFsmV2Test, LeafHandlesEvent) {
|
||||
std::vector<std::string> log;
|
||||
fsm::v2::NestedFSM<NestedState> fsm(std::make_unique<ParentA>(log));
|
||||
|
||||
auto result = fsm.feed(Event::Stay);
|
||||
EXPECT_TRUE(result);
|
||||
EXPECT_FALSE(result.transitioned());
|
||||
}
|
||||
|
||||
TEST(NestedFsmV2Test, EventBubblesToParent) {
|
||||
std::vector<std::string> log;
|
||||
fsm::v2::NestedFSM<NestedState> fsm(std::make_unique<ParentA>(log));
|
||||
log.clear();
|
||||
|
||||
// GoToC is unhandled by ChildB, bubbles to ParentA which transitions
|
||||
auto result = fsm.feed(Event::GoToC);
|
||||
EXPECT_TRUE(result);
|
||||
EXPECT_TRUE(result.transitioned());
|
||||
|
||||
// ChildB leave, ParentA leave (popped off stack), then new LeafC enter
|
||||
ASSERT_EQ(log.size(), 3u);
|
||||
EXPECT_EQ(log[0], "leave:B");
|
||||
EXPECT_EQ(log[1], "leave:A");
|
||||
EXPECT_EQ(log[2], "enter:C");
|
||||
}
|
||||
|
||||
TEST(NestedFsmV2Test, UnhandledByAll) {
|
||||
std::vector<std::string> log;
|
||||
fsm::v2::NestedFSM<NestedState> fsm(std::make_unique<ParentA>(log));
|
||||
|
||||
auto result = fsm.feed(Event::Unknown);
|
||||
EXPECT_FALSE(result);
|
||||
}
|
||||
|
||||
TEST(NestedFsmV2Test, GetCurrentStateIdReturnsFullPath) {
|
||||
std::vector<std::string> log;
|
||||
fsm::v2::NestedFSM<NestedState> fsm(std::make_unique<ParentA>(log));
|
||||
|
||||
auto ids = fsm.get_current_state_id();
|
||||
ASSERT_EQ(ids.size(), 2u);
|
||||
EXPECT_EQ(ids[0], StateID::A);
|
||||
EXPECT_EQ(ids[1], StateID::B);
|
||||
|
||||
fsm.feed(Event::GoToC);
|
||||
ids = fsm.get_current_state_id();
|
||||
ASSERT_EQ(ids.size(), 1u);
|
||||
EXPECT_EQ(ids[0], StateID::C);
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
#include <everest/util/math/comparison.hpp>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
namespace everest::lib::util {
|
||||
|
||||
class ComparisonTest : public ::testing::Test {};
|
||||
|
||||
// --- Floating Point & almost_eq Tests ---
|
||||
|
||||
TEST_F(ComparisonTest, RangeLimit) {
|
||||
EXPECT_NEAR(range_limit<double>(1), 0.1, 1e-9);
|
||||
EXPECT_NEAR(range_limit<double>(3), 0.001, 1e-9);
|
||||
EXPECT_EQ(range_limit<double>(0), 1.0);
|
||||
EXPECT_DOUBLE_EQ(range_limit<double>(-1), 10.0);
|
||||
EXPECT_DOUBLE_EQ(range_limit<double>(-2), 100.0);
|
||||
EXPECT_DOUBLE_EQ(range_limit<double>(-3), 1000.0);
|
||||
}
|
||||
|
||||
TEST_F(ComparisonTest, AlmostEqBasic) {
|
||||
// 3 digits of precision = 0.001 threshold
|
||||
EXPECT_TRUE((almost_eq<3>(1.0001, 1.0002)));
|
||||
EXPECT_FALSE((almost_eq<3>(1.0, 1.002)));
|
||||
}
|
||||
|
||||
TEST_F(ComparisonTest, AlmostEqNegativePrecision) {
|
||||
// -2 digits of precision = 100.0 threshold
|
||||
EXPECT_TRUE((almost_eq<-2>(207.0, 250.0)));
|
||||
EXPECT_FALSE((almost_eq<-2>(100.0, 250.0)));
|
||||
|
||||
// -1 digit of precision = 10.0 threshold
|
||||
EXPECT_TRUE((almost_eq<-1>(15.0, 22.0))); // diff 7 < 10
|
||||
EXPECT_FALSE((almost_eq<-1>(15.0, 28.0))); // diff 13 > 10
|
||||
}
|
||||
|
||||
TEST_F(ComparisonTest, AlmostEqOptional) {
|
||||
std::optional<double> a = 1.0001;
|
||||
std::optional<double> b = 1.0002;
|
||||
std::optional<double> empty;
|
||||
|
||||
EXPECT_TRUE(almost_eq<3>(a, b));
|
||||
EXPECT_TRUE(almost_eq<3>(empty, empty));
|
||||
EXPECT_FALSE(almost_eq<3>(a, empty));
|
||||
}
|
||||
|
||||
// --- Min/Max Optional Tests ---
|
||||
|
||||
TEST_F(ComparisonTest, MinOptional) {
|
||||
std::optional<float> low = 10.0f;
|
||||
std::optional<float> high = 20.0f;
|
||||
std::optional<float> empty;
|
||||
|
||||
// Optional & Optional
|
||||
EXPECT_EQ(min_optional(low, high).value(), 10.0f);
|
||||
EXPECT_EQ(min_optional(low, empty).value(), 10.0f);
|
||||
EXPECT_FALSE(min_optional(empty, empty).has_value());
|
||||
|
||||
// Value & Optional
|
||||
EXPECT_EQ(min_optional(15.0f, high), 15.0f);
|
||||
EXPECT_EQ(min_optional(25.0f, high), 20.0f);
|
||||
EXPECT_EQ(min_optional(25.0f, empty), 25.0f);
|
||||
}
|
||||
|
||||
TEST_F(ComparisonTest, MaxOptional) {
|
||||
std::optional<float> low = 10.0f;
|
||||
std::optional<float> empty;
|
||||
|
||||
EXPECT_EQ(max_optional(low, 5.0f), 10.0f);
|
||||
EXPECT_EQ(max_optional(low, 15.0f), 15.0f);
|
||||
EXPECT_EQ(max_optional(empty, 15.0f), 15.0f);
|
||||
}
|
||||
|
||||
// --- Clamping Tests ---
|
||||
|
||||
TEST_F(ComparisonTest, ClampOptional) {
|
||||
std::optional<double> min_limit = 10.0;
|
||||
std::optional<double> max_limit = 20.0;
|
||||
std::optional<double> no_limit;
|
||||
|
||||
// Inside range
|
||||
EXPECT_DOUBLE_EQ(clamp_optional(15.0, min_limit, max_limit), 15.0);
|
||||
|
||||
// Underflow
|
||||
EXPECT_DOUBLE_EQ(clamp_optional(5.0, min_limit, max_limit), 10.0);
|
||||
|
||||
// Overflow
|
||||
EXPECT_DOUBLE_EQ(clamp_optional(25.0, min_limit, max_limit), 20.0);
|
||||
|
||||
// One-sided clamping
|
||||
EXPECT_DOUBLE_EQ(clamp_optional(5.0, no_limit, max_limit), 5.0);
|
||||
EXPECT_DOUBLE_EQ(clamp_optional(25.0, min_limit, no_limit), 25.0);
|
||||
|
||||
// No limits
|
||||
EXPECT_DOUBLE_EQ(clamp_optional(100.0, no_limit, no_limit), 100.0);
|
||||
}
|
||||
|
||||
// --- Noise Range Tests ---
|
||||
|
||||
TEST_F(ComparisonTest, InNoiseRange) {
|
||||
EXPECT_TRUE(in_noise_range(10.0, 10.05, 0.1));
|
||||
EXPECT_FALSE(in_noise_range(10.0, 10.11, 0.1));
|
||||
// Exact boundary
|
||||
EXPECT_TRUE(in_noise_range(10.0, 10.1, 0.1));
|
||||
}
|
||||
|
||||
} // namespace everest::lib::util
|
||||
@@ -0,0 +1,172 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include <everest/util/queue/simple_queue.hpp>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
using namespace everest::lib::util;
|
||||
|
||||
// =================================================================
|
||||
// 2. Test Fixture Setup
|
||||
// =================================================================
|
||||
|
||||
template <typename T> class SimpleQueueTest : public ::testing::Test {
|
||||
protected:
|
||||
simple_queue<T> queue;
|
||||
};
|
||||
|
||||
// Typed Test Suite for standard types
|
||||
using QueueTypes = ::testing::Types<int, std::string>;
|
||||
TYPED_TEST_SUITE(SimpleQueueTest, QueueTypes);
|
||||
|
||||
// =================================================================
|
||||
// A. Basic Functionality Tests (FIFO & Empty Checks)
|
||||
// =================================================================
|
||||
|
||||
TYPED_TEST(SimpleQueueTest, InitialStateIsEmpty) {
|
||||
ASSERT_TRUE(this->queue.empty());
|
||||
ASSERT_EQ(this->queue.size(), 0);
|
||||
ASSERT_FALSE(this->queue.pop().has_value());
|
||||
}
|
||||
|
||||
TYPED_TEST(SimpleQueueTest, PushAndEmptyCheck) {
|
||||
TypeParam value;
|
||||
|
||||
// Use if constexpr to initialize value correctly
|
||||
if constexpr (std::is_same_v<TypeParam, int>) {
|
||||
value = 10;
|
||||
} else if constexpr (std::is_same_v<TypeParam, std::string>) {
|
||||
value = "Test_10";
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
this->queue.push(value);
|
||||
|
||||
ASSERT_FALSE(this->queue.empty());
|
||||
ASSERT_EQ(this->queue.size(), 1);
|
||||
}
|
||||
|
||||
TYPED_TEST(SimpleQueueTest, PushPopAndEmptyCheck) {
|
||||
TypeParam expected_value;
|
||||
|
||||
// Use if constexpr to initialize value correctly
|
||||
if constexpr (std::is_same_v<TypeParam, int>) {
|
||||
expected_value = 42;
|
||||
} else if constexpr (std::is_same_v<TypeParam, std::string>) {
|
||||
expected_value = "Test_42";
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
this->queue.push(expected_value);
|
||||
|
||||
std::optional<TypeParam> result = this->queue.pop();
|
||||
|
||||
ASSERT_TRUE(result.has_value());
|
||||
ASSERT_EQ(result.value(), expected_value);
|
||||
ASSERT_TRUE(this->queue.empty());
|
||||
ASSERT_EQ(this->queue.size(), 0);
|
||||
}
|
||||
|
||||
TYPED_TEST(SimpleQueueTest, MultiplePushAndPopOrder) {
|
||||
const int count = 3;
|
||||
|
||||
// Push elements (0, 1, 2)
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if constexpr (std::is_same_v<TypeParam, int>) {
|
||||
this->queue.push(i);
|
||||
} else {
|
||||
this->queue.push(std::to_string(i));
|
||||
}
|
||||
}
|
||||
|
||||
// Pop elements and verify FIFO order (0, 1, 2)
|
||||
for (int i = 0; i < count; ++i) {
|
||||
std::optional<TypeParam> result = this->queue.pop();
|
||||
TypeParam expected_value;
|
||||
if constexpr (std::is_same_v<TypeParam, int>) {
|
||||
expected_value = i;
|
||||
} else {
|
||||
expected_value = std::to_string(i);
|
||||
}
|
||||
|
||||
ASSERT_TRUE(result.has_value());
|
||||
ASSERT_EQ(result.value(), expected_value) << "Element popped out of FIFO order.";
|
||||
}
|
||||
|
||||
ASSERT_TRUE(this->queue.empty());
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// B. Reference Tests (front() and back())
|
||||
// =================================================================
|
||||
|
||||
TYPED_TEST(SimpleQueueTest, FrontAndBackReferences) {
|
||||
TypeParam val1, val2;
|
||||
|
||||
if constexpr (std::is_same_v<TypeParam, int>) {
|
||||
val1 = 100;
|
||||
val2 = 200;
|
||||
} else if constexpr (std::is_same_v<TypeParam, std::string>) {
|
||||
val1 = "Front";
|
||||
val2 = "Back";
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
this->queue.push(val1);
|
||||
this->queue.push(val2);
|
||||
|
||||
// Verify front()
|
||||
ASSERT_EQ(this->queue.front(), val1);
|
||||
|
||||
// Verify back()
|
||||
ASSERT_EQ(this->queue.back(), val2);
|
||||
|
||||
// After pop, front should change
|
||||
this->queue.pop();
|
||||
ASSERT_EQ(this->queue.front(), val2);
|
||||
ASSERT_EQ(this->queue.back(), val2);
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// C. Move-Only Type Compatibility Test (Verifying the pop() fix)
|
||||
// =================================================================
|
||||
|
||||
// Test suite for std::unique_ptr<int> (a move-only type)
|
||||
class SimpleQueueMoveOnlyTest : public ::testing::Test {
|
||||
protected:
|
||||
simple_queue<std::unique_ptr<int>> queue;
|
||||
};
|
||||
|
||||
TEST_F(SimpleQueueMoveOnlyTest, PushAndPopMoveOnlyType) {
|
||||
const int value1 = 10;
|
||||
const int value2 = 20;
|
||||
|
||||
// Push: Requires the r-value push overload
|
||||
this->queue.push(std::make_unique<int>(value1));
|
||||
this->queue.push(std::make_unique<int>(value2));
|
||||
|
||||
ASSERT_EQ(this->queue.size(), 2);
|
||||
|
||||
// Pop: Requires the fixed move-based pop()
|
||||
std::optional<std::unique_ptr<int>> opt_result1 = this->queue.pop();
|
||||
|
||||
// Verify the value was retrieved
|
||||
ASSERT_TRUE(opt_result1.has_value());
|
||||
ASSERT_NE(opt_result1.value(), nullptr);
|
||||
ASSERT_EQ(*opt_result1.value(), value1);
|
||||
|
||||
// Pop the second item
|
||||
std::optional<std::unique_ptr<int>> opt_result2 = this->queue.pop();
|
||||
ASSERT_TRUE(opt_result2.has_value());
|
||||
ASSERT_EQ(*opt_result2.value(), value2);
|
||||
|
||||
ASSERT_TRUE(this->queue.empty());
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <everest/util/queue/thread_safe_bounded_queue.hpp>
|
||||
#include <optional>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
using namespace everest::lib::util;
|
||||
|
||||
/**
|
||||
* @brief Helper struct to mimic the TrackedAction used in the thread pool,
|
||||
* as the queue now expects a type with an .arrival member.
|
||||
*/
|
||||
struct TestTask {
|
||||
int value;
|
||||
std::chrono::steady_clock::time_point arrival;
|
||||
|
||||
explicit TestTask(int v = 0) : value(v), arrival(std::chrono::steady_clock::now()) {
|
||||
}
|
||||
};
|
||||
|
||||
// =================================================================
|
||||
// 1. Bounded Functionality Tests (Backpressure)
|
||||
// =================================================================
|
||||
|
||||
TEST(ThreadSafeBoundedQueueTest, PushBlocksWhenFull) {
|
||||
const size_t limit = 2;
|
||||
thread_safe_bounded_queue<TestTask> queue(limit);
|
||||
|
||||
// Fill the queue to the limit
|
||||
queue.push(TestTask(1));
|
||||
queue.push(TestTask(2));
|
||||
|
||||
std::atomic<bool> push_completed{false};
|
||||
std::thread producer([&] {
|
||||
// This should block until a consumer pops an item
|
||||
queue.push(TestTask(3));
|
||||
push_completed = true;
|
||||
});
|
||||
|
||||
// Give the thread a moment to start and block
|
||||
std::this_thread::sleep_for(50ms);
|
||||
ASSERT_FALSE(push_completed.load());
|
||||
|
||||
// Pop an item, which should unblock the producer
|
||||
auto popped = queue.try_pop(100ms);
|
||||
ASSERT_TRUE(popped.has_value());
|
||||
ASSERT_EQ(popped->value, 1);
|
||||
|
||||
producer.join();
|
||||
ASSERT_TRUE(push_completed.load());
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 2. Latency Interface Tests
|
||||
// =================================================================
|
||||
|
||||
TEST(ThreadSafeBoundedQueueTest, OldestArrivalTracking) {
|
||||
thread_safe_bounded_queue<TestTask> queue(10);
|
||||
|
||||
auto t1 = std::chrono::steady_clock::now();
|
||||
queue.push(TestTask(100));
|
||||
std::this_thread::sleep_for(10ms);
|
||||
|
||||
auto t2 = std::chrono::steady_clock::now();
|
||||
queue.push(TestTask(200));
|
||||
|
||||
auto oldest = queue.oldest_arrival();
|
||||
|
||||
// The oldest arrival should be close to t1, certainly before t2
|
||||
ASSERT_GE(oldest, t1);
|
||||
ASSERT_LT(oldest, t2);
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 3. Stop and Signaling Tests
|
||||
// =================================================================
|
||||
|
||||
TEST(ThreadSafeBoundedQueueTest, StopUnblocksBlockedProducers) {
|
||||
thread_safe_bounded_queue<TestTask> queue(1);
|
||||
queue.push(TestTask(1)); // Fill it
|
||||
|
||||
std::atomic<bool> producer_exited{false};
|
||||
std::thread producer([&] {
|
||||
// This blocks because queue is full
|
||||
size_t result = queue.push(TestTask(2));
|
||||
// result should be 0 because the queue was stopped
|
||||
if (result == 0) {
|
||||
producer_exited = true;
|
||||
}
|
||||
});
|
||||
|
||||
std::this_thread::sleep_for(50ms);
|
||||
queue.stop(); // This should wake the producer up
|
||||
|
||||
producer.join();
|
||||
ASSERT_TRUE(producer_exited.load());
|
||||
}
|
||||
|
||||
TEST(ThreadSafeBoundedQueueTest, StopReturnsNullOptToConsumers) {
|
||||
thread_safe_bounded_queue<TestTask> queue(5);
|
||||
|
||||
std::thread consumer([&] {
|
||||
auto result = queue.try_pop(1s);
|
||||
ASSERT_FALSE(result.has_value());
|
||||
});
|
||||
|
||||
std::this_thread::sleep_for(20ms);
|
||||
queue.stop();
|
||||
consumer.join();
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 4. Stress Tests (Concurrent Producers and Consumers)
|
||||
// =================================================================
|
||||
|
||||
TEST(ThreadSafeBoundedQueueTest, HighContentionStressTest) {
|
||||
const int num_producers = 4;
|
||||
const int num_consumers = 4;
|
||||
const int items_per_producer = 1000;
|
||||
const size_t queue_limit = 10;
|
||||
|
||||
thread_safe_bounded_queue<TestTask> queue(queue_limit);
|
||||
std::atomic<int> total_popped{0};
|
||||
std::atomic<int> sum_popped{0};
|
||||
|
||||
std::vector<std::thread> workers;
|
||||
|
||||
// Consumers
|
||||
for (int i = 0; i < num_consumers; ++i) {
|
||||
workers.emplace_back([&] {
|
||||
while (total_popped < (num_producers * items_per_producer)) {
|
||||
auto val = queue.try_pop(10ms);
|
||||
if (val) {
|
||||
sum_popped += val->value;
|
||||
total_popped++;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Producers
|
||||
for (int i = 0; i < num_producers; ++i) {
|
||||
workers.emplace_back([&] {
|
||||
for (int j = 0; j < items_per_producer; ++j) {
|
||||
queue.push(TestTask(1));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (auto& w : workers)
|
||||
w.join();
|
||||
|
||||
ASSERT_EQ(total_popped.load(), num_producers * items_per_producer);
|
||||
ASSERT_EQ(sum_popped.load(), num_producers * items_per_producer);
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <everest/util/queue/thread_safe_queue.hpp>
|
||||
#include <memory> // For std::unique_ptr
|
||||
#include <numeric>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <thread>
|
||||
#include <type_traits> // For std::is_same_v
|
||||
#include <vector>
|
||||
|
||||
// Note: Add includes for your simple_queue and thread_safe_queue here
|
||||
// #include "simple_queue.h"
|
||||
// #include "thread_safe_queue.h"
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
using namespace everest::lib::util;
|
||||
|
||||
// =================================================================
|
||||
// 1. Test Fixture Setup
|
||||
// =================================================================
|
||||
|
||||
// Helper to initialize TypeParam correctly in typed tests
|
||||
template <typename T> T initialize_value(int id) {
|
||||
if constexpr (std::is_same_v<T, int>) {
|
||||
return id;
|
||||
} else if constexpr (std::is_same_v<T, std::string>) {
|
||||
return "Value_" + std::to_string(id);
|
||||
} else {
|
||||
// Fallback for other non-tested types
|
||||
return T{};
|
||||
}
|
||||
}
|
||||
|
||||
// Test Fixture
|
||||
template <typename T> class ThreadSafeQueueTest : public ::testing::Test {
|
||||
protected:
|
||||
thread_safe_queue<T> queue;
|
||||
};
|
||||
|
||||
// Typed Test Suite for int and std::string
|
||||
using QueueTypes = ::testing::Types<int, std::string>;
|
||||
TYPED_TEST_SUITE(ThreadSafeQueueTest, QueueTypes);
|
||||
|
||||
// Define a test suite specifically for concurrency checks (using int)
|
||||
using ThreadSafeQueueIntTest = ThreadSafeQueueTest<int>;
|
||||
|
||||
// =================================================================
|
||||
// 2. Basic Functionality Tests (Single Thread)
|
||||
// =================================================================
|
||||
|
||||
TYPED_TEST(ThreadSafeQueueTest, PushAndPopSimple) {
|
||||
TypeParam expected_value = initialize_value<TypeParam>(42);
|
||||
|
||||
this->queue.push(expected_value);
|
||||
|
||||
// Test the non-blocking pop
|
||||
std::optional<TypeParam> result = this->queue.try_pop();
|
||||
|
||||
ASSERT_TRUE(result.has_value());
|
||||
ASSERT_EQ(result.value(), expected_value);
|
||||
ASSERT_FALSE(this->queue.try_pop().has_value());
|
||||
}
|
||||
|
||||
TYPED_TEST(ThreadSafeQueueTest, MultiplePushAndPopOrder) {
|
||||
const int count = 5;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
this->queue.push(initialize_value<TypeParam>(i));
|
||||
}
|
||||
|
||||
for (int i = 0; i < count; ++i) {
|
||||
TypeParam expected_value = initialize_value<TypeParam>(i);
|
||||
std::optional<TypeParam> result = this->queue.try_pop();
|
||||
|
||||
ASSERT_TRUE(result.has_value());
|
||||
ASSERT_EQ(result.value(), expected_value);
|
||||
}
|
||||
ASSERT_FALSE(this->queue.try_pop().has_value());
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 3. Time-Based Functionality Tests
|
||||
// =================================================================
|
||||
|
||||
TYPED_TEST(ThreadSafeQueueTest, TryPopWithTimeout_Timeout) {
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
|
||||
// Try to pop with a short timeout
|
||||
std::optional<TypeParam> result = this->queue.try_pop(10ms);
|
||||
|
||||
auto end = std::chrono::steady_clock::now();
|
||||
|
||||
ASSERT_FALSE(result.has_value());
|
||||
|
||||
auto elapsed = end - start;
|
||||
ASSERT_GE(elapsed, 9ms);
|
||||
ASSERT_LE(elapsed, 50ms);
|
||||
}
|
||||
|
||||
TYPED_TEST(ThreadSafeQueueTest, TryPopWithTimeout_ImmediateSuccess) {
|
||||
TypeParam value = initialize_value<TypeParam>(101);
|
||||
this->queue.push(value);
|
||||
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
std::optional<TypeParam> result = this->queue.try_pop(10s);
|
||||
auto end = std::chrono::steady_clock::now();
|
||||
|
||||
ASSERT_TRUE(result.has_value());
|
||||
ASSERT_EQ(result.value(), value);
|
||||
|
||||
ASSERT_LT(end - start, 5ms);
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 4. Synchronization and Blocking Tests
|
||||
// =================================================================
|
||||
|
||||
TEST_F(ThreadSafeQueueIntTest, BlockingPopUnblocksOnPush) {
|
||||
const int expected_value = 123;
|
||||
std::atomic<int> result = 0;
|
||||
|
||||
std::thread consumer([this, &result] {
|
||||
// Blocking pop() call
|
||||
result = this->queue.pop();
|
||||
});
|
||||
|
||||
std::this_thread::sleep_for(100ms);
|
||||
|
||||
this->queue.push(expected_value);
|
||||
|
||||
consumer.join();
|
||||
ASSERT_EQ(result.load(), expected_value);
|
||||
}
|
||||
|
||||
TEST_F(ThreadSafeQueueIntTest, MultipleWaitersUnblockedSequentially) {
|
||||
const int num_waiters = 5;
|
||||
std::vector<std::thread> consumers;
|
||||
std::atomic<int> pops_received = 0;
|
||||
|
||||
for (int i = 0; i < num_waiters; ++i) {
|
||||
consumers.emplace_back([this, &pops_received] {
|
||||
this->queue.pop();
|
||||
pops_received++;
|
||||
});
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(100ms);
|
||||
|
||||
// Push exactly the number of waiters—only one waiter should be released per push
|
||||
for (int i = 0; i < num_waiters; ++i) {
|
||||
this->queue.push(i);
|
||||
}
|
||||
|
||||
for (auto& t : consumers) {
|
||||
t.join();
|
||||
}
|
||||
|
||||
ASSERT_EQ(pops_received.load(), num_waiters);
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 5. Stress and Race Condition Tests
|
||||
// =================================================================
|
||||
|
||||
TEST_F(ThreadSafeQueueIntTest, ConcurrentPushConsistency) {
|
||||
const int num_producers = 10;
|
||||
const int items_per_producer = 1000;
|
||||
const int total_items = num_producers * items_per_producer;
|
||||
|
||||
std::vector<std::thread> producers;
|
||||
std::set<int> expected_values;
|
||||
|
||||
for (int i = 0; i < num_producers; ++i) {
|
||||
producers.emplace_back([this, i, items_per_producer] {
|
||||
int start_value = i * items_per_producer;
|
||||
for (int j = 0; j < items_per_producer; ++j) {
|
||||
this->queue.push(start_value + j);
|
||||
}
|
||||
});
|
||||
int start_value = i * items_per_producer;
|
||||
for (int j = 0; j < items_per_producer; ++j) {
|
||||
expected_values.insert(start_value + j);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& t : producers) {
|
||||
t.join();
|
||||
}
|
||||
|
||||
// Drain the queue and check for consistency
|
||||
std::set<int> retrieved_values;
|
||||
for (int i = 0; i < total_items; ++i) {
|
||||
auto val = this->queue.pop();
|
||||
retrieved_values.insert(val);
|
||||
}
|
||||
|
||||
ASSERT_EQ(retrieved_values.size(), total_items);
|
||||
ASSERT_EQ(retrieved_values, expected_values);
|
||||
}
|
||||
|
||||
TEST_F(ThreadSafeQueueIntTest, ConcurrentPopNoDuplicate) {
|
||||
const int total_items = 10000;
|
||||
const int num_consumers = 10;
|
||||
|
||||
// Producer pushes all items
|
||||
for (int i = 0; i < total_items; ++i) {
|
||||
this->queue.push(i);
|
||||
}
|
||||
|
||||
// Consumers pop concurrently
|
||||
std::vector<std::thread> consumers;
|
||||
std::mutex result_mtx;
|
||||
std::set<int> retrieved_values;
|
||||
std::atomic<int> pop_count = 0;
|
||||
|
||||
for (int i = 0; i < num_consumers; ++i) {
|
||||
consumers.emplace_back([this, &result_mtx, &retrieved_values, &pop_count, total_items] {
|
||||
while (pop_count.load() < total_items) {
|
||||
// Use try_pop so threads don't block indefinitely waiting for a push
|
||||
// that won't come until the other threads finish.
|
||||
if (auto val = this->queue.try_pop(); val.has_value()) {
|
||||
std::lock_guard lock(result_mtx);
|
||||
retrieved_values.insert(val.value());
|
||||
pop_count++;
|
||||
}
|
||||
std::this_thread::yield();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (auto& t : consumers) {
|
||||
t.join();
|
||||
}
|
||||
|
||||
// Check consistency
|
||||
ASSERT_EQ(pop_count.load(), total_items) << "Total pops do not match total pushed items.";
|
||||
ASSERT_EQ(retrieved_values.size(), total_items) << "Duplicate items were retrieved.";
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 6. Move-Only Type Compatibility Test (Verifying the push/pop fix)
|
||||
// =================================================================
|
||||
|
||||
// Test fixture for std::unique_ptr<int> (a move-only type)
|
||||
class ThreadSafeQueueMoveOnlyTest : public ::testing::Test {
|
||||
protected:
|
||||
thread_safe_queue<std::unique_ptr<int>> queue;
|
||||
};
|
||||
|
||||
TEST_F(ThreadSafeQueueMoveOnlyTest, HandlesConcurrentMoveOnlyTypes) {
|
||||
const int total_items = 1000;
|
||||
const int num_threads = 5;
|
||||
|
||||
std::vector<std::thread> threads;
|
||||
std::atomic<int> pop_count = 0;
|
||||
|
||||
// Producer/Consumer set for unique ownership verification
|
||||
std::set<int> retrieved_values;
|
||||
std::mutex result_mtx;
|
||||
|
||||
// Start 5 threads: 3 producers, 2 consumers
|
||||
for (int i = 0; i < num_threads; ++i) {
|
||||
if (i < 3) { // Producers
|
||||
threads.emplace_back([this, i, total_items] {
|
||||
int start_value = i * total_items;
|
||||
for (int j = 0; j < total_items; ++j) {
|
||||
// Requires thread_safe_queue::push(T&&)
|
||||
this->queue.push(std::make_unique<int>(start_value + j));
|
||||
}
|
||||
});
|
||||
} else { // Consumers
|
||||
threads.emplace_back([this, &pop_count, &result_mtx, &retrieved_values, total_items] {
|
||||
int pops = 0;
|
||||
while (pops < total_items * 3 / 2) { // Try to pop 1500 times
|
||||
// Requires thread_safe_queue::try_pop()
|
||||
if (auto opt_ptr = this->queue.try_pop(); opt_ptr.has_value()) {
|
||||
std::lock_guard lock(result_mtx);
|
||||
retrieved_values.insert(*opt_ptr.value());
|
||||
pop_count++;
|
||||
pops++;
|
||||
}
|
||||
std::this_thread::yield();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& t : threads) {
|
||||
if (t.joinable()) {
|
||||
t.join();
|
||||
}
|
||||
}
|
||||
|
||||
// Drain any remaining items in the main thread (should be few/none)
|
||||
while (auto opt_ptr = this->queue.try_pop()) {
|
||||
std::lock_guard lock(result_mtx);
|
||||
retrieved_values.insert(*opt_ptr.value());
|
||||
pop_count++;
|
||||
}
|
||||
|
||||
const int total_expected = total_items * 3; // 3 producers * 1000 items
|
||||
|
||||
ASSERT_EQ(pop_count.load(), total_expected) << "Total items popped does not match total pushed.";
|
||||
ASSERT_EQ(retrieved_values.size(), total_expected)
|
||||
<< "Duplicate pointers/values were retrieved, indicating a race condition failure or a failed move.";
|
||||
}
|
||||
@@ -0,0 +1,647 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <everest/util/vector/fixed_vector.hpp>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
using namespace everest::lib::util;
|
||||
|
||||
TEST(FixedVectorTest, BasicInt) {
|
||||
fixed_vector<int, 10> vec;
|
||||
EXPECT_TRUE(vec.empty());
|
||||
EXPECT_EQ(vec.size(), 0);
|
||||
|
||||
vec.push_back(1);
|
||||
vec.push_back(2);
|
||||
vec.push_back(3);
|
||||
|
||||
EXPECT_FALSE(vec.empty());
|
||||
EXPECT_EQ(vec.size(), 3);
|
||||
EXPECT_EQ(vec[0], 1);
|
||||
EXPECT_EQ(vec[1], 2);
|
||||
EXPECT_EQ(vec[2], 3);
|
||||
EXPECT_EQ(vec.at(1), 2);
|
||||
|
||||
int count = 1;
|
||||
for (const auto& val : vec) {
|
||||
EXPECT_EQ(val, count++);
|
||||
}
|
||||
|
||||
vec.clear();
|
||||
EXPECT_TRUE(vec.empty());
|
||||
EXPECT_EQ(vec.size(), 0);
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, StringAndMove) {
|
||||
fixed_vector<std::string, 5> vec;
|
||||
vec.push_back("hello");
|
||||
std::string s = "world";
|
||||
vec.push_back(std::move(s));
|
||||
|
||||
EXPECT_EQ(vec.size(), 2);
|
||||
EXPECT_EQ(vec[0], "hello");
|
||||
EXPECT_EQ(vec[1], "world");
|
||||
// Note: The state of a moved-from string is valid but unspecified.
|
||||
// In many implementations it is empty, but we shouldn't rely on it.
|
||||
// EXPECT_TRUE(s.empty());
|
||||
|
||||
vec.emplace_back(5, 'c');
|
||||
EXPECT_EQ(vec.size(), 3);
|
||||
EXPECT_EQ(vec[2], "ccccc");
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, Capacity) {
|
||||
fixed_vector<int, 3> vec;
|
||||
vec.push_back(1);
|
||||
vec.push_back(2);
|
||||
vec.push_back(3);
|
||||
EXPECT_EQ(vec.size(), 3);
|
||||
EXPECT_THROW(vec.push_back(4), std::length_error);
|
||||
EXPECT_THROW(vec.emplace_back(5), std::length_error);
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, AtThrows) {
|
||||
fixed_vector<int, 5> vec;
|
||||
vec.push_back(1);
|
||||
EXPECT_THROW(vec.at(1), std::out_of_range);
|
||||
const auto& cvec = vec;
|
||||
EXPECT_THROW(cvec.at(1), std::out_of_range);
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, Erase) {
|
||||
fixed_vector<int, 10> vec;
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
vec.push_back(i);
|
||||
}
|
||||
|
||||
// erase first
|
||||
auto it = vec.erase(vec.begin());
|
||||
EXPECT_EQ(*it, 1);
|
||||
EXPECT_EQ(vec.size(), 9);
|
||||
EXPECT_EQ(vec[0], 1);
|
||||
EXPECT_EQ(vec[8], 9);
|
||||
|
||||
// erase last
|
||||
it = vec.erase(vec.end() - 1);
|
||||
EXPECT_EQ(it, vec.end());
|
||||
EXPECT_EQ(vec.size(), 8);
|
||||
EXPECT_EQ(vec[7], 8);
|
||||
|
||||
// erase middle
|
||||
it = vec.erase(vec.begin() + 3); // erase '4' from {1,2,3,4,5,6,7,8}
|
||||
EXPECT_EQ(*it, 5);
|
||||
EXPECT_EQ(vec.size(), 7);
|
||||
EXPECT_EQ(vec[0], 1);
|
||||
EXPECT_EQ(vec[1], 2);
|
||||
EXPECT_EQ(vec[2], 3);
|
||||
EXPECT_EQ(vec[3], 5);
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, EraseRange) {
|
||||
fixed_vector<int, 10> vec;
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
vec.push_back(i);
|
||||
}
|
||||
|
||||
// erase range in the middle
|
||||
auto it = vec.erase(vec.begin() + 2, vec.begin() + 5); // erase 2, 3, 4
|
||||
EXPECT_EQ(*it, 5);
|
||||
EXPECT_EQ(vec.size(), 7);
|
||||
EXPECT_EQ(vec[0], 0);
|
||||
EXPECT_EQ(vec[1], 1);
|
||||
EXPECT_EQ(vec[2], 5);
|
||||
EXPECT_EQ(vec[3], 6);
|
||||
EXPECT_EQ(vec[6], 9);
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, MoveOnlyType) {
|
||||
fixed_vector<std::unique_ptr<int>, 5> vec;
|
||||
vec.emplace_back(std::make_unique<int>(1));
|
||||
vec.push_back(std::make_unique<int>(2));
|
||||
EXPECT_EQ(vec.size(), 2);
|
||||
EXPECT_EQ(*vec[0], 1);
|
||||
EXPECT_EQ(*vec[1], 2);
|
||||
|
||||
vec.erase(vec.begin());
|
||||
EXPECT_EQ(vec.size(), 1);
|
||||
EXPECT_EQ(*vec[0], 2);
|
||||
}
|
||||
|
||||
struct DestructorCheck {
|
||||
static int destructor_calls;
|
||||
DestructorCheck() = default;
|
||||
~DestructorCheck() {
|
||||
destructor_calls++;
|
||||
}
|
||||
};
|
||||
int DestructorCheck::destructor_calls = 0;
|
||||
|
||||
TEST(FixedVectorTest, DestructorCalledOnClear) {
|
||||
DestructorCheck::destructor_calls = 0;
|
||||
fixed_vector<DestructorCheck, 5> vec;
|
||||
vec.emplace_back();
|
||||
vec.emplace_back();
|
||||
vec.emplace_back();
|
||||
EXPECT_EQ(DestructorCheck::destructor_calls, 0);
|
||||
vec.clear();
|
||||
EXPECT_EQ(DestructorCheck::destructor_calls, 3);
|
||||
vec.clear();
|
||||
EXPECT_EQ(DestructorCheck::destructor_calls, 3);
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, DestructorCalledOnErase) {
|
||||
DestructorCheck::destructor_calls = 0;
|
||||
fixed_vector<DestructorCheck, 5> vec;
|
||||
vec.emplace_back();
|
||||
vec.emplace_back();
|
||||
vec.emplace_back();
|
||||
EXPECT_EQ(DestructorCheck::destructor_calls, 0);
|
||||
vec.erase(vec.begin());
|
||||
EXPECT_EQ(DestructorCheck::destructor_calls, 1);
|
||||
vec.erase(vec.begin(), vec.end());
|
||||
EXPECT_EQ(DestructorCheck::destructor_calls, 3);
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, DestructorCalledOnDestruction) {
|
||||
DestructorCheck::destructor_calls = 0;
|
||||
{
|
||||
fixed_vector<DestructorCheck, 5> vec;
|
||||
vec.emplace_back();
|
||||
vec.emplace_back();
|
||||
EXPECT_EQ(DestructorCheck::destructor_calls, 0);
|
||||
}
|
||||
EXPECT_EQ(DestructorCheck::destructor_calls, 2);
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, CopyConstructor) {
|
||||
fixed_vector<int, 5> original;
|
||||
original.push_back(1);
|
||||
original.push_back(2);
|
||||
|
||||
fixed_vector<int, 5> copy = original;
|
||||
EXPECT_EQ(copy.size(), 2);
|
||||
EXPECT_EQ(copy[0], 1);
|
||||
EXPECT_EQ(copy[1], 2);
|
||||
|
||||
// Ensure original is untouched
|
||||
EXPECT_EQ(original.size(), 2);
|
||||
EXPECT_EQ(original[0], 1);
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, CopyAssignment) {
|
||||
fixed_vector<int, 5> original;
|
||||
original.push_back(1);
|
||||
original.push_back(2);
|
||||
|
||||
fixed_vector<int, 5> copy;
|
||||
copy.push_back(99);
|
||||
copy = original;
|
||||
|
||||
EXPECT_EQ(copy.size(), 2);
|
||||
EXPECT_EQ(copy[0], 1);
|
||||
EXPECT_EQ(copy[1], 2);
|
||||
|
||||
// Ensure original is untouched
|
||||
EXPECT_EQ(original.size(), 2);
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, CopyAssignmentEdgeCases) {
|
||||
// Case 1: Destination is larger than source
|
||||
fixed_vector<DestructorCheck, 5> dest1;
|
||||
dest1.emplace_back();
|
||||
dest1.emplace_back();
|
||||
dest1.emplace_back();
|
||||
|
||||
fixed_vector<DestructorCheck, 5> source1;
|
||||
source1.emplace_back();
|
||||
source1.emplace_back();
|
||||
|
||||
DestructorCheck::destructor_calls = 0;
|
||||
dest1 = source1;
|
||||
EXPECT_EQ(dest1.size(), 2);
|
||||
EXPECT_EQ(DestructorCheck::destructor_calls, 1); // One surplus element should have been destroyed
|
||||
|
||||
// Case 2: Destination is smaller than source
|
||||
fixed_vector<int, 5> dest2;
|
||||
dest2.push_back(1);
|
||||
|
||||
fixed_vector<int, 5> source2;
|
||||
source2.push_back(10);
|
||||
source2.push_back(20);
|
||||
|
||||
dest2 = source2;
|
||||
EXPECT_EQ(dest2.size(), 2);
|
||||
EXPECT_EQ(dest2[0], 10);
|
||||
EXPECT_EQ(dest2[1], 20);
|
||||
|
||||
// Case 3: Sizes are equal
|
||||
fixed_vector<int, 5> dest3;
|
||||
dest3.push_back(1);
|
||||
dest3.push_back(2);
|
||||
|
||||
fixed_vector<int, 5> source3;
|
||||
source3.push_back(10);
|
||||
source3.push_back(20);
|
||||
|
||||
dest3 = source3;
|
||||
EXPECT_EQ(dest3.size(), 2);
|
||||
EXPECT_EQ(dest3[0], 10);
|
||||
EXPECT_EQ(dest3[1], 20);
|
||||
|
||||
// Case 4: Self-assignment
|
||||
fixed_vector<int, 5> self_assign;
|
||||
self_assign.push_back(123);
|
||||
self_assign = self_assign;
|
||||
EXPECT_EQ(self_assign.size(), 1);
|
||||
EXPECT_EQ(self_assign[0], 123);
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, MoveConstructor) {
|
||||
fixed_vector<std::string, 5> original;
|
||||
original.push_back("a");
|
||||
original.push_back("b");
|
||||
|
||||
fixed_vector<std::string, 5> moved = std::move(original);
|
||||
EXPECT_EQ(moved.size(), 2);
|
||||
EXPECT_EQ(moved[0], "a");
|
||||
|
||||
EXPECT_TRUE(original.empty());
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, MoveAssignment) {
|
||||
fixed_vector<std::string, 5> original;
|
||||
original.push_back("a");
|
||||
original.push_back("b");
|
||||
|
||||
fixed_vector<std::string, 5> moved;
|
||||
moved.push_back("c");
|
||||
moved = std::move(original);
|
||||
|
||||
EXPECT_EQ(moved.size(), 2);
|
||||
EXPECT_EQ(moved[0], "a");
|
||||
|
||||
EXPECT_TRUE(original.empty());
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, ZeroCapacity) {
|
||||
fixed_vector<int, 0> vec;
|
||||
EXPECT_TRUE(vec.empty());
|
||||
EXPECT_EQ(vec.size(), 0);
|
||||
EXPECT_EQ(vec.capacity(), 0);
|
||||
EXPECT_THROW(vec.push_back(1), std::length_error);
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, FrontAndBack) {
|
||||
fixed_vector<int, 3> vec;
|
||||
vec.push_back(10);
|
||||
vec.push_back(20);
|
||||
EXPECT_EQ(vec.front(), 10);
|
||||
EXPECT_EQ(vec.back(), 20);
|
||||
vec.front() = 11;
|
||||
EXPECT_EQ(vec[0], 11);
|
||||
|
||||
const auto& cvec = vec;
|
||||
EXPECT_EQ(cvec.front(), 11);
|
||||
EXPECT_EQ(cvec.back(), 20);
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, ReverseIteration) {
|
||||
fixed_vector<int, 3> vec;
|
||||
vec.push_back(1);
|
||||
vec.push_back(2);
|
||||
vec.push_back(3);
|
||||
|
||||
int expected = 3;
|
||||
for (auto it = vec.rbegin(); it != vec.rend(); ++it) {
|
||||
EXPECT_EQ(*it, expected--);
|
||||
}
|
||||
|
||||
const auto& cvec = vec;
|
||||
expected = 3;
|
||||
for (auto it = cvec.rbegin(); it != cvec.rend(); ++it) {
|
||||
EXPECT_EQ(*it, expected--);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, EraseEdgeCases) {
|
||||
fixed_vector<int, 5> vec;
|
||||
vec.push_back(1);
|
||||
vec.push_back(2);
|
||||
vec.push_back(3);
|
||||
vec.push_back(4);
|
||||
|
||||
// Erase empty range
|
||||
auto it = vec.erase(vec.begin() + 1, vec.begin() + 1);
|
||||
EXPECT_EQ(vec.size(), 4);
|
||||
EXPECT_EQ(*it, 2);
|
||||
|
||||
// Erase to the end
|
||||
it = vec.erase(vec.begin() + 2, vec.end());
|
||||
EXPECT_EQ(vec.size(), 2);
|
||||
EXPECT_EQ(it, vec.end());
|
||||
EXPECT_EQ(vec[0], 1);
|
||||
EXPECT_EQ(vec[1], 2);
|
||||
|
||||
// Erase everything
|
||||
it = vec.erase(vec.begin(), vec.end());
|
||||
EXPECT_TRUE(vec.empty());
|
||||
EXPECT_EQ(it, vec.end());
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, TryEmplaceBack) {
|
||||
fixed_vector<int, 3> vec;
|
||||
auto* elem1 = vec.try_emplace_back(10);
|
||||
ASSERT_NE(elem1, nullptr);
|
||||
EXPECT_EQ(*elem1, 10);
|
||||
EXPECT_EQ(vec.size(), 1);
|
||||
EXPECT_EQ(vec[0], 10);
|
||||
|
||||
vec.try_emplace_back(20);
|
||||
vec.try_emplace_back(30);
|
||||
|
||||
EXPECT_EQ(vec.size(), 3);
|
||||
|
||||
// Vector is full
|
||||
auto* elem4 = vec.try_emplace_back(40);
|
||||
EXPECT_EQ(elem4, nullptr);
|
||||
EXPECT_EQ(vec.size(), 3);
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, PopBack) {
|
||||
fixed_vector<int, 5> vec;
|
||||
vec.push_back(1);
|
||||
vec.push_back(2);
|
||||
vec.push_back(3);
|
||||
|
||||
EXPECT_EQ(vec.size(), 3);
|
||||
EXPECT_EQ(vec.back(), 3);
|
||||
|
||||
vec.pop_back();
|
||||
EXPECT_EQ(vec.size(), 2);
|
||||
EXPECT_EQ(vec.back(), 2);
|
||||
|
||||
vec.pop_back();
|
||||
EXPECT_EQ(vec.size(), 1);
|
||||
EXPECT_EQ(vec.back(), 1);
|
||||
|
||||
vec.pop_back();
|
||||
EXPECT_TRUE(vec.empty());
|
||||
EXPECT_EQ(vec.size(), 0);
|
||||
|
||||
// Popping from an empty vector should be a no-op
|
||||
vec.pop_back();
|
||||
EXPECT_TRUE(vec.empty());
|
||||
EXPECT_EQ(vec.size(), 0);
|
||||
|
||||
// Test with DestructorCheck
|
||||
DestructorCheck::destructor_calls = 0;
|
||||
fixed_vector<DestructorCheck, 5> vec_dc;
|
||||
vec_dc.emplace_back();
|
||||
vec_dc.emplace_back();
|
||||
vec_dc.emplace_back();
|
||||
EXPECT_EQ(DestructorCheck::destructor_calls, 0);
|
||||
|
||||
vec_dc.pop_back();
|
||||
EXPECT_EQ(DestructorCheck::destructor_calls, 1);
|
||||
vec_dc.pop_back();
|
||||
EXPECT_EQ(DestructorCheck::destructor_calls, 2);
|
||||
vec_dc.pop_back();
|
||||
EXPECT_EQ(DestructorCheck::destructor_calls, 3);
|
||||
EXPECT_TRUE(vec_dc.empty());
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, CapacityAndMaxSize) {
|
||||
fixed_vector<int, 5> vec;
|
||||
EXPECT_EQ(vec.capacity(), 5);
|
||||
EXPECT_EQ(vec.max_size(), 5);
|
||||
|
||||
const fixed_vector<int, 0> zero_vec;
|
||||
EXPECT_EQ(zero_vec.capacity(), 0);
|
||||
EXPECT_EQ(zero_vec.max_size(), 0);
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, ConstIteratorMethods) {
|
||||
fixed_vector<int, 5> vec;
|
||||
vec.push_back(1);
|
||||
vec.push_back(2);
|
||||
vec.push_back(3);
|
||||
|
||||
const auto& cvec = vec;
|
||||
|
||||
// Test cbegin() and cend()
|
||||
int expected_val = 1;
|
||||
for (auto it = cvec.cbegin(); it != cvec.cend(); ++it) {
|
||||
EXPECT_EQ(*it, expected_val++);
|
||||
}
|
||||
EXPECT_EQ(expected_val, 4); // Should have iterated 1, 2, 3
|
||||
|
||||
// Test crbegin() and crend()
|
||||
expected_val = 3;
|
||||
for (auto it = cvec.crbegin(); it != cvec.crend(); ++it) {
|
||||
EXPECT_EQ(*it, expected_val--);
|
||||
}
|
||||
EXPECT_EQ(expected_val, 0); // Should have iterated 3, 2, 1
|
||||
|
||||
// Test on empty vector
|
||||
fixed_vector<int, 5> empty_vec;
|
||||
const auto& cempty_vec = empty_vec;
|
||||
EXPECT_EQ(cempty_vec.cbegin(), cempty_vec.cend());
|
||||
EXPECT_EQ(cempty_vec.crbegin(), cempty_vec.crend());
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, EraseInvalidRange) {
|
||||
fixed_vector<int, 5> vec;
|
||||
vec.push_back(1);
|
||||
vec.push_back(2);
|
||||
vec.push_back(3);
|
||||
vec.push_back(4);
|
||||
|
||||
// Test with first > last, which should do nothing
|
||||
auto original_size = vec.size();
|
||||
auto it = vec.erase(vec.begin() + 2, vec.begin() + 1);
|
||||
|
||||
// Verify nothing happened
|
||||
EXPECT_EQ(vec.size(), original_size);
|
||||
EXPECT_EQ(vec[0], 1);
|
||||
EXPECT_EQ(vec[1], 2);
|
||||
EXPECT_EQ(vec[2], 3);
|
||||
EXPECT_EQ(vec[3], 4);
|
||||
|
||||
// The returned iterator should be the 'first' iterator passed in
|
||||
EXPECT_EQ(it, vec.begin() + 2);
|
||||
EXPECT_EQ(*it, 3);
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, ComparisonOperators) {
|
||||
fixed_vector<int, 5> vec1;
|
||||
vec1.push_back(1);
|
||||
vec1.push_back(2);
|
||||
|
||||
fixed_vector<int, 5> vec2;
|
||||
vec2.push_back(1);
|
||||
vec2.push_back(2);
|
||||
|
||||
fixed_vector<int, 5> vec3;
|
||||
vec3.push_back(1);
|
||||
vec3.push_back(99);
|
||||
|
||||
fixed_vector<int, 5> vec4;
|
||||
vec4.push_back(1);
|
||||
|
||||
fixed_vector<int, 5> empty1;
|
||||
fixed_vector<int, 5> empty2;
|
||||
|
||||
EXPECT_TRUE(vec1 == vec2);
|
||||
EXPECT_FALSE(vec1 != vec2);
|
||||
|
||||
EXPECT_FALSE(vec1 == vec3);
|
||||
EXPECT_TRUE(vec1 != vec3);
|
||||
|
||||
EXPECT_FALSE(vec1 == vec4);
|
||||
EXPECT_TRUE(vec1 != vec4);
|
||||
|
||||
EXPECT_TRUE(empty1 == empty2);
|
||||
EXPECT_FALSE(empty1 != empty2);
|
||||
|
||||
EXPECT_FALSE(vec1 == empty1);
|
||||
EXPECT_TRUE(vec1 != empty1);
|
||||
}
|
||||
|
||||
// Verify that fixed_vector enforces nothrow move requirements at compile time.
|
||||
// Types with throwing move constructors/assignments are rejected by static_assert.
|
||||
struct NothrowMovable {
|
||||
NothrowMovable() = default;
|
||||
NothrowMovable(NothrowMovable&&) noexcept = default;
|
||||
NothrowMovable& operator=(NothrowMovable&&) noexcept = default;
|
||||
NothrowMovable(const NothrowMovable&) = default;
|
||||
NothrowMovable& operator=(const NothrowMovable&) = default;
|
||||
};
|
||||
|
||||
TEST(FixedVectorTest, NothrowMoveConstraint) {
|
||||
// Verify that fixed_vector works with nothrow-movable types
|
||||
fixed_vector<NothrowMovable, 5> vec;
|
||||
vec.emplace_back();
|
||||
EXPECT_EQ(vec.size(), 1);
|
||||
|
||||
// Move construction should be noexcept
|
||||
static_assert(std::is_nothrow_move_constructible_v<fixed_vector<NothrowMovable, 5>>);
|
||||
static_assert(std::is_nothrow_move_assignable_v<fixed_vector<NothrowMovable, 5>>);
|
||||
static_assert(std::is_nothrow_move_constructible_v<fixed_vector<int, 5>>);
|
||||
static_assert(std::is_nothrow_move_assignable_v<fixed_vector<int, 5>>);
|
||||
static_assert(std::is_nothrow_move_constructible_v<fixed_vector<std::string, 5>>);
|
||||
static_assert(std::is_nothrow_move_assignable_v<fixed_vector<std::string, 5>>);
|
||||
}
|
||||
|
||||
// Types with throwing move operations — used only in compile-time rejection checks below.
|
||||
struct ThrowingMoveConstructor {
|
||||
ThrowingMoveConstructor() = default;
|
||||
ThrowingMoveConstructor(ThrowingMoveConstructor&&) noexcept(false) {
|
||||
}
|
||||
ThrowingMoveConstructor& operator=(ThrowingMoveConstructor&&) noexcept = default;
|
||||
};
|
||||
|
||||
struct ThrowingMoveAssignment {
|
||||
ThrowingMoveAssignment() = default;
|
||||
ThrowingMoveAssignment(ThrowingMoveAssignment&&) noexcept = default;
|
||||
ThrowingMoveAssignment& operator=(ThrowingMoveAssignment&&) noexcept(false) {
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
|
||||
struct ThrowingBothMoveOps {
|
||||
ThrowingBothMoveOps() = default;
|
||||
ThrowingBothMoveOps(ThrowingBothMoveOps&&) noexcept(false) {
|
||||
}
|
||||
ThrowingBothMoveOps& operator=(ThrowingBothMoveOps&&) noexcept(false) {
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
|
||||
// Verify at compile time that fixed_vector rejects types whose move operations can throw.
|
||||
// The static_asserts inside fixed_vector prevent instantiation of these types.
|
||||
// We verify this indirectly: if fixed_vector's constraint is working, these types must not
|
||||
// satisfy the nothrow move requirements.
|
||||
TEST(FixedVectorTest, ThrowingMoveTypesAreRejected) {
|
||||
// Confirm the types themselves have throwing move operations
|
||||
static_assert(!std::is_nothrow_move_constructible_v<ThrowingMoveConstructor>,
|
||||
"ThrowingMoveConstructor should not be nothrow move constructible");
|
||||
static_assert(!std::is_nothrow_move_constructible_v<ThrowingBothMoveOps>,
|
||||
"ThrowingBothMoveOps should not be nothrow move constructible");
|
||||
static_assert(!std::is_nothrow_move_assignable_v<ThrowingMoveAssignment>,
|
||||
"ThrowingMoveAssignment should not be nothrow move assignable");
|
||||
static_assert(!std::is_nothrow_move_assignable_v<ThrowingBothMoveOps>,
|
||||
"ThrowingBothMoveOps should not be nothrow move assignable");
|
||||
|
||||
// fixed_vector<ThrowingMoveConstructor, 5> would fail to compile due to static_assert.
|
||||
// fixed_vector<ThrowingMoveAssignment, 5> would fail to compile due to static_assert.
|
||||
// fixed_vector<ThrowingBothMoveOps, 5> would fail to compile due to static_assert.
|
||||
//
|
||||
// These cannot be tested at runtime since instantiation itself is a compile error.
|
||||
// The static_asserts above confirm the trait checks that fixed_vector relies on.
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, InitializerListConstructor) {
|
||||
// Basic construction
|
||||
fixed_vector<int, 5> vec1 = {1, 2, 3};
|
||||
EXPECT_EQ(vec1.size(), 3);
|
||||
EXPECT_EQ(vec1[0], 1);
|
||||
EXPECT_EQ(vec1[1], 2);
|
||||
EXPECT_EQ(vec1[2], 3);
|
||||
|
||||
// Empty list
|
||||
fixed_vector<int, 5> vec2 = {};
|
||||
EXPECT_TRUE(vec2.empty());
|
||||
EXPECT_EQ(vec2.size(), 0);
|
||||
|
||||
// Full capacity
|
||||
fixed_vector<int, 3> vec3 = {10, 20, 30};
|
||||
EXPECT_EQ(vec3.size(), 3);
|
||||
EXPECT_EQ(vec3[2], 30);
|
||||
|
||||
// Exceeding capacity - use a lambda to avoid comma issues with the macro
|
||||
EXPECT_THROW(([] { fixed_vector<int, 2> vec4 = {1, 2, 3}; }()), std::length_error);
|
||||
|
||||
// With strings
|
||||
fixed_vector<std::string, 4> vec5 = {"hello", "world"};
|
||||
EXPECT_EQ(vec5.size(), 2);
|
||||
EXPECT_EQ(vec5[0], "hello");
|
||||
EXPECT_EQ(vec5[1], "world");
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, StdVectorConstructor) {
|
||||
// Test case 1: Construct from an empty std::vector
|
||||
std::vector<int> empty_std_vec = {};
|
||||
fixed_vector<int, 5> vec_from_empty(empty_std_vec);
|
||||
EXPECT_TRUE(vec_from_empty.empty());
|
||||
EXPECT_EQ(vec_from_empty.size(), 0);
|
||||
|
||||
// Test case 2: Construct from a std::vector with elements (within capacity)
|
||||
std::vector<int> small_std_vec = {1, 2, 3};
|
||||
fixed_vector<int, 5> vec_from_small(small_std_vec);
|
||||
EXPECT_EQ(vec_from_small.size(), 3);
|
||||
EXPECT_EQ(vec_from_small[0], 1);
|
||||
EXPECT_EQ(vec_from_small[1], 2);
|
||||
EXPECT_EQ(vec_from_small[2], 3);
|
||||
|
||||
// Test case 3: Construct from a std::vector with elements (exactly capacity)
|
||||
std::vector<int> full_std_vec = {10, 20, 30, 40, 50};
|
||||
fixed_vector<int, 5> vec_from_full(full_std_vec);
|
||||
EXPECT_EQ(vec_from_full.size(), 5);
|
||||
EXPECT_EQ(vec_from_full[0], 10);
|
||||
EXPECT_EQ(vec_from_full[4], 50);
|
||||
|
||||
// Test case 4: Construct from a std::vector with elements exceeding capacity
|
||||
std::vector<int> large_std_vec = {1, 2, 3, 4, 5, 6};
|
||||
EXPECT_THROW(([large_std_vec] { fixed_vector<int, 5> vec_from_large(large_std_vec); }()), std::length_error);
|
||||
|
||||
// Test case 5: Construct with std::string elements
|
||||
std::vector<std::string> string_std_vec = {"apple", "banana"};
|
||||
fixed_vector<std::string, 3> vec_from_strings(string_std_vec);
|
||||
EXPECT_EQ(vec_from_strings.size(), 2);
|
||||
EXPECT_EQ(vec_from_strings[0], "apple");
|
||||
EXPECT_EQ(vec_from_strings[1], "banana");
|
||||
}
|
||||
Reference in New Issue
Block a user