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:
Eric F
2026-06-08 00:38:27 -04:00
parent 468cfeaa50
commit d398a6ced2
7326 changed files with 1177561 additions and 7 deletions

View 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",
],
)

View 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()

View 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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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)

View File

@@ -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();
}

View File

@@ -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
}

View File

@@ -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();
}

View File

@@ -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);
}

View File

@@ -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());
}

View File

@@ -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

View 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);
}

View File

@@ -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

View File

@@ -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());
}

View File

@@ -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);
}

View File

@@ -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.";
}

View File

@@ -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");
}