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