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:
23
tools/EVerest-main/lib/everest/util/tests/CMakeLists.txt
Normal file
23
tools/EVerest-main/lib/everest/util/tests/CMakeLists.txt
Normal file
@@ -0,0 +1,23 @@
|
||||
add_executable(everest_util_tests
|
||||
async/monitor_tests.cpp
|
||||
async/thread_pool_tests.cpp
|
||||
async/thread_pool_scaling_tests.cpp
|
||||
async/wrapper_tests.cpp
|
||||
enum/EnumFlagsTest.cpp
|
||||
enum/EnumFlagsTest_B.cpp
|
||||
math/comparison_tests.cpp
|
||||
queue/simple_queue_tests.cpp
|
||||
queue/thread_safe_queue_tests.cpp
|
||||
queue/thread_safe_bounded_queue_tests.cpp
|
||||
vector/fixed_vector_tests.cpp
|
||||
fsm/fsm_tests.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(everest_util_tests
|
||||
PRIVATE
|
||||
GTest::gtest_main
|
||||
everest::util
|
||||
)
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(everest_util_tests)
|
||||
@@ -0,0 +1,437 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include <everest/util/async/monitor.hpp>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
using namespace everest::lib::util;
|
||||
|
||||
struct SharedData {
|
||||
int value = 0;
|
||||
std::string name = "initial";
|
||||
// Unique ID to track object identity across moves/swaps
|
||||
long long id = std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||
std::chrono::high_resolution_clock::now().time_since_epoch())
|
||||
.count();
|
||||
};
|
||||
|
||||
// --- Test Fixtures for Noexcept Checks ---
|
||||
// 1. Type that is NOT nothrow-swappable (due to non-noexcept move constructor)
|
||||
struct ThrowingMover {
|
||||
int* ptr;
|
||||
|
||||
// Move Constructor: NOT noexcept (This is the key difference)
|
||||
ThrowingMover(ThrowingMover&& other) noexcept(false) : ptr(std::exchange(other.ptr, nullptr)) {
|
||||
if (!ptr)
|
||||
throw std::runtime_error("simulated throw on move");
|
||||
}
|
||||
|
||||
// Move Assignment: NOT noexcept
|
||||
ThrowingMover& operator=(ThrowingMover&& other) noexcept(false) {
|
||||
if (this != &other) {
|
||||
std::swap(ptr, other.ptr);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
ThrowingMover() : ptr(new int(42)) {
|
||||
}
|
||||
~ThrowingMover() {
|
||||
delete ptr;
|
||||
}
|
||||
ThrowingMover(const ThrowingMover&) = delete;
|
||||
ThrowingMover& operator=(const ThrowingMover&) = delete;
|
||||
|
||||
// Define custom swap for ADL (must use the same non-noexcept status)
|
||||
friend void swap(ThrowingMover& lhs, ThrowingMover& rhs) noexcept(false) {
|
||||
std::swap(lhs.ptr, rhs.ptr);
|
||||
}
|
||||
};
|
||||
|
||||
// 2. Type that IS nothrow-swappable
|
||||
struct NoThrowMover {
|
||||
int* ptr;
|
||||
|
||||
// Move Constructor: IS noexcept
|
||||
NoThrowMover(NoThrowMover&& other) noexcept : ptr(std::exchange(other.ptr, nullptr)) {
|
||||
}
|
||||
|
||||
// Move Assignment: IS noexcept
|
||||
NoThrowMover& operator=(NoThrowMover&& other) noexcept {
|
||||
if (this != &other) {
|
||||
std::swap(ptr, other.ptr);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
NoThrowMover() : ptr(new int(42)) {
|
||||
}
|
||||
~NoThrowMover() {
|
||||
delete ptr;
|
||||
}
|
||||
NoThrowMover(const NoThrowMover&) = delete;
|
||||
NoThrowMover& operator=(const NoThrowMover&) = delete;
|
||||
|
||||
// Define custom swap for ADL (must be noexcept)
|
||||
friend void swap(NoThrowMover& lhs, NoThrowMover& rhs) noexcept {
|
||||
std::swap(lhs.ptr, rhs.ptr);
|
||||
}
|
||||
};
|
||||
|
||||
// --- The Static Assert Tests ---
|
||||
// We use basic static_asserts to verify the compiler's calculated noexcept status.
|
||||
|
||||
namespace NoexceptTests {
|
||||
using namespace everest::lib::util;
|
||||
template <typename T> using monitor = everest::lib::util::monitor<T>;
|
||||
using M_NT = monitor<NoThrowMover>; // Monitor protecting the safe type
|
||||
using M_T = monitor<ThrowingMover>; // Monitor protecting the unsafe type
|
||||
|
||||
// --- 1. Test against NoThrowMover (T is noexcept swappable) ---
|
||||
// All move and swap operations on the monitor should be noexcept(true).
|
||||
|
||||
static_assert(std::is_nothrow_swappable_v<NoThrowMover>,
|
||||
"Prerequisite 1 failed: NoThrowMover must be noexcept swappable.");
|
||||
|
||||
// Move Constructor: Should be noexcept(true)
|
||||
static_assert(std::is_nothrow_move_constructible_v<M_NT>, "NT Test 1 failed: Move Constructor must be noexcept.");
|
||||
|
||||
// Member Swap: Should be noexcept(true)
|
||||
static_assert(noexcept(std::declval<M_NT>().swap(std::declval<M_NT&>())),
|
||||
"NT Test 2 failed: Member Swap must be noexcept.");
|
||||
|
||||
// Move Assignment: Should be noexcept(true)
|
||||
static_assert(std::is_nothrow_move_assignable_v<M_NT>, "NT Test 3 failed: Move Assignment must be noexcept.");
|
||||
|
||||
// --- 2. Test against ThrowingMover (T is NOT noexcept swappable) ---
|
||||
// All move and swap operations on the monitor should be noexcept(false).
|
||||
|
||||
static_assert(!std::is_nothrow_swappable_v<ThrowingMover>,
|
||||
"Prerequisite 2 failed: ThrowingMover must NOT be noexcept swappable.");
|
||||
|
||||
// Move Constructor: Should be noexcept(false) (allows exceptions)
|
||||
static_assert(!std::is_nothrow_move_constructible_v<M_T>, "T Test 1 failed: Move Constructor must NOT be noexcept.");
|
||||
|
||||
// Member Swap: Should be noexcept(false) (allows exceptions)
|
||||
static_assert(!noexcept(std::declval<M_T>().swap(std::declval<M_T&>())),
|
||||
"T Test 2 failed: Member Swap must NOT be noexcept.");
|
||||
|
||||
// Move Assignment: Should be noexcept(false) (allows exceptions)
|
||||
static_assert(!std::is_nothrow_move_assignable_v<M_T>, "T Test 3 failed: Move Assignment must NOT be noexcept.");
|
||||
} // namespace NoexceptTests
|
||||
|
||||
using namespace everest::lib::util;
|
||||
|
||||
class MonitorTest : public ::testing::Test {
|
||||
protected:
|
||||
monitor<SharedData> simple_monitor_;
|
||||
monitor<std::unique_ptr<SharedData>> ptr_monitor_;
|
||||
// A timed mutex enabled monitor::handle(timeout)
|
||||
monitor<SharedData, std::timed_mutex> timed_mtx_monitor_;
|
||||
|
||||
// Time constants for tests
|
||||
const std::chrono::milliseconds BLOCK_TIME = std::chrono::milliseconds(200);
|
||||
const std::chrono::milliseconds SHORT_WAIT = std::chrono::milliseconds(10);
|
||||
const std::chrono::milliseconds LONG_WAIT = std::chrono::milliseconds(300);
|
||||
};
|
||||
|
||||
TEST_F(MonitorTest, SingleThreadedAccess) {
|
||||
// Block 1: Access and Modify (Lock acquired by handle, then released)
|
||||
{
|
||||
// Acquire the handle (locks the mutex)
|
||||
auto handle = simple_monitor_.handle();
|
||||
|
||||
// Access and modify the data using operator->
|
||||
handle->value = 100;
|
||||
handle->name = "updated";
|
||||
|
||||
// When 'handle' goes out of scope here, the lock is released (RAII).
|
||||
}
|
||||
|
||||
// Block 2: Verify changes (Lock acquired, then released)
|
||||
{
|
||||
// Now acquiring the lock succeeds because it was released above.
|
||||
auto handle_check = simple_monitor_.handle();
|
||||
|
||||
// Verify
|
||||
EXPECT_EQ(100, handle_check->value);
|
||||
EXPECT_EQ("updated", handle_check->name);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(MonitorTest, PointerLikeAccessChaining) {
|
||||
// Block 1: Initialization (Ensures the unique_ptr is created)
|
||||
{
|
||||
auto h = ptr_monitor_.handle();
|
||||
*h = std::make_unique<SharedData>();
|
||||
} // h is destroyed, lock released.
|
||||
|
||||
// Block 2: Access and Modify (Lock acquired by handle, then released)
|
||||
{
|
||||
// Acquire lock
|
||||
auto handle = ptr_monitor_.handle();
|
||||
|
||||
handle->value = 42;
|
||||
handle->name = "chained";
|
||||
} // handle is destroyed, lock released.
|
||||
|
||||
// Block 3: Verify (Lock acquired, then released)
|
||||
{
|
||||
// Acquire lock
|
||||
auto handle_check = ptr_monitor_.handle();
|
||||
|
||||
// Access via chaining
|
||||
EXPECT_EQ(42, handle_check->value);
|
||||
EXPECT_EQ("chained", handle_check->name);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(MonitorTest, ThreadSafeIncrement) {
|
||||
const int num_threads = 10;
|
||||
const int increments_per_thread = 1000;
|
||||
std::vector<std::thread> threads;
|
||||
|
||||
// Set initial value to 0
|
||||
simple_monitor_.handle()->value = 0;
|
||||
|
||||
for (int i = 0; i < num_threads; ++i) {
|
||||
threads.emplace_back([&] {
|
||||
for (int j = 0; j < increments_per_thread; ++j) {
|
||||
// Handle scope ensures RAII locking on every single increment
|
||||
auto handle = simple_monitor_.handle();
|
||||
handle->value++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (auto& t : threads) {
|
||||
t.join();
|
||||
}
|
||||
|
||||
// Verify the final value is correct
|
||||
auto final_handle = simple_monitor_.handle();
|
||||
EXPECT_EQ(num_threads * increments_per_thread, final_handle->value);
|
||||
}
|
||||
|
||||
TEST_F(MonitorTest, ConditionVariableWaitNotify) {
|
||||
bool done = false;
|
||||
|
||||
// Future/Promise pair 1: Waiter signals it is ready to wait
|
||||
std::promise<void> waiter_ready_promise;
|
||||
std::future<void> waiter_ready_future = waiter_ready_promise.get_future();
|
||||
|
||||
std::thread waiter([&] {
|
||||
// Acquire handle
|
||||
auto handle = simple_monitor_.handle();
|
||||
|
||||
// Signal that we are holding the lock and about to wait
|
||||
waiter_ready_promise.set_value();
|
||||
|
||||
// Wait until 'done' is true.
|
||||
handle.wait([&] { return done; });
|
||||
|
||||
EXPECT_EQ(99, handle->value);
|
||||
});
|
||||
|
||||
// Main thread waits until the waiter has acquired the lock and set the promise
|
||||
waiter_ready_future.get();
|
||||
|
||||
// Notifier thread operation (guaranteed to happen after waiter has locked/signaled)
|
||||
{
|
||||
// Acquire lock
|
||||
auto handle = simple_monitor_.handle();
|
||||
handle->value = 99;
|
||||
done = true;
|
||||
}
|
||||
|
||||
// Notify the waiting thread
|
||||
simple_monitor_.notify_one();
|
||||
|
||||
waiter.join();
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------
|
||||
|
||||
TEST_F(MonitorTest, TryLockHandleTimeout) {
|
||||
// Future/Promise pair 1: Blocker signals it has acquired the lock
|
||||
std::promise<void> blocker_locked_promise;
|
||||
std::future<void> blocker_locked_future = blocker_locked_promise.get_future();
|
||||
|
||||
// The shared object is used as the resource for the lock
|
||||
|
||||
std::thread blocker([&] {
|
||||
// Acquire the lock
|
||||
auto handle = timed_mtx_monitor_.handle(); // 🔒 Lock acquired
|
||||
|
||||
// Signal to the main thread that the lock is held
|
||||
blocker_locked_promise.set_value();
|
||||
|
||||
// Hold the lock for a specified duration
|
||||
std::this_thread::sleep_for(BLOCK_TIME);
|
||||
|
||||
// Lock released when handle goes out of scope 🔓
|
||||
});
|
||||
|
||||
// Main thread waits until the blocker explicitly confirms it is holding the lock
|
||||
blocker_locked_future.get();
|
||||
|
||||
// Test 1: Try to acquire the lock with a short timeout (Expected to FAIL)
|
||||
auto handle_opt = timed_mtx_monitor_.handle(SHORT_WAIT);
|
||||
EXPECT_FALSE(handle_opt.has_value());
|
||||
|
||||
// Test 2: Try to acquire the lock with a long timeout (Expected to SUCCEED eventually)
|
||||
// The total wait time will be slightly longer than BLOCK_TIME (200ms).
|
||||
auto start_success = std::chrono::steady_clock::now();
|
||||
auto handle_long_opt = timed_mtx_monitor_.handle(LONG_WAIT);
|
||||
|
||||
auto duration_success = std::chrono::steady_clock::now() - start_success;
|
||||
|
||||
EXPECT_TRUE(handle_long_opt.has_value());
|
||||
// FIX 2: Explicitly compare the count() to ensure stable comparison and output
|
||||
EXPECT_GE(duration_success.count(), BLOCK_TIME.count());
|
||||
|
||||
blocker.join();
|
||||
}
|
||||
|
||||
TEST_F(MonitorTest, TimedMutexLockAcquisition) {
|
||||
// This test ensures the complex timing logic for acquisition is sound.
|
||||
|
||||
// Synchronization barrier: Blocker signals it has acquired the lock
|
||||
std::promise<void> blocker_locked_promise;
|
||||
std::future<void> blocker_locked_future = blocker_locked_promise.get_future();
|
||||
|
||||
// THREAD A: The Blocker (Holds the lock on timed_mtx_monitor_)
|
||||
std::thread blocker([&] {
|
||||
// 1. Acquire lock
|
||||
auto handle = timed_mtx_monitor_.handle();
|
||||
blocker_locked_promise.set_value(); // Signal: Lock is now held
|
||||
|
||||
// 2. Hold the lock for the required duration
|
||||
std::this_thread::sleep_for(BLOCK_TIME); // 200ms
|
||||
|
||||
// Lock released when handle goes out of scope
|
||||
});
|
||||
|
||||
// 3. Main thread waits until the lock is actively held
|
||||
blocker_locked_future.get();
|
||||
|
||||
// --- Test 1: Fail Case (Wait is shorter than remaining lock time) ---
|
||||
auto fail_handle = timed_mtx_monitor_.handle(SHORT_WAIT);
|
||||
EXPECT_FALSE(fail_handle.has_value());
|
||||
|
||||
// --- Test 2: Success Case (Wait is longer than remaining lock time) ---
|
||||
auto start_success_timing = std::chrono::steady_clock::now();
|
||||
|
||||
// Acquire the lock with a sufficient timeout (300ms)
|
||||
auto success_handle = timed_mtx_monitor_.handle(LONG_WAIT);
|
||||
|
||||
auto duration_success = std::chrono::steady_clock::now() - start_success_timing;
|
||||
|
||||
// Must succeed acquisition
|
||||
EXPECT_TRUE(success_handle.has_value());
|
||||
|
||||
// FIX 3: Explicitly compare the count() to ensure stable comparison and output
|
||||
EXPECT_GE(duration_success.count(), BLOCK_TIME.count());
|
||||
|
||||
blocker.join();
|
||||
}
|
||||
|
||||
TEST_F(MonitorTest, ConditionVariableAtomicity) {
|
||||
bool notification_sent = false;
|
||||
// Use the SharedData member to track state
|
||||
simple_monitor_.handle()->value = 0;
|
||||
|
||||
// THREAD A: The Waiter
|
||||
std::thread waiter([&] {
|
||||
auto handle = simple_monitor_.handle();
|
||||
|
||||
// Waiter signals that it is holding the lock and about to enter the wait state
|
||||
// (This is implicitly tested by the notifier having to wait for the lock)
|
||||
|
||||
// Predicate check: Ensure 'notification_sent' is only true IF the lock is reacquired
|
||||
handle.wait([&] {
|
||||
// The predicate will be checked spuriously, but the critical check is on wake
|
||||
return notification_sent;
|
||||
});
|
||||
|
||||
// After waking up, the lock is held. Verify the resource state.
|
||||
// This checks that the state modification (value=1) happened while the lock was released.
|
||||
EXPECT_EQ(1, handle->value);
|
||||
});
|
||||
|
||||
// Give the waiter time to acquire the lock and block on the CV (Crucial setup time)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
|
||||
// THREAD B: The Notifier (This thread modifies the state and notifies)
|
||||
{
|
||||
// Must acquire the lock. This proves the waiter released it atomically inside wait().
|
||||
auto handle = simple_monitor_.handle();
|
||||
|
||||
// 1. Modify the resource while holding the lock
|
||||
handle->value = 1;
|
||||
|
||||
// 2. Set the wait condition *after* modification
|
||||
notification_sent = true;
|
||||
|
||||
// Lock is released here, which allows the waiter to potentially reacquire it.
|
||||
}
|
||||
|
||||
simple_monitor_.notify_one();
|
||||
|
||||
waiter.join();
|
||||
|
||||
// Final check that the state is 1, confirming the waiter successfully completed its EXPECT.
|
||||
EXPECT_EQ(1, simple_monitor_.handle()->value);
|
||||
}
|
||||
|
||||
TEST_F(MonitorTest, ThreadSafeMoveOperations) {
|
||||
// Setup: Monitor m1 (Source) starts with data, Monitor m2 (Destination) starts empty.
|
||||
monitor<SharedData> m1;
|
||||
m1.handle()->value = 10;
|
||||
auto m1_initial_id = m1.handle()->id; // Track resource identity
|
||||
|
||||
// Create m2 with different data
|
||||
monitor<SharedData> m2;
|
||||
m2.handle()->value = 99;
|
||||
auto m2_initial_id = m2.handle()->id;
|
||||
|
||||
std::promise<void> blocker_locked_promise;
|
||||
std::future<void> blocker_locked_future = blocker_locked_promise.get_future();
|
||||
|
||||
// THREAD A: The Blocker (Holds the lock on m1 to force the move operation to wait)
|
||||
std::thread blocker([&] {
|
||||
auto handle = m1.handle(); // Lock m1
|
||||
blocker_locked_promise.set_value();
|
||||
std::this_thread::sleep_for(BLOCK_TIME);
|
||||
});
|
||||
|
||||
// Main thread waits until m1 is locked by the blocker
|
||||
blocker_locked_future.get();
|
||||
|
||||
// --- Move Assignment Test: m2 = std::move(m1) ---
|
||||
|
||||
// Because the move assignment operator calls monitor::swap(m2, m1), and swap locks both,
|
||||
// it must wait for m1's lock (held by blocker thread) to be released.
|
||||
auto start_move = std::chrono::steady_clock::now();
|
||||
m2 = std::move(m1); // Should block here until blocker releases m1's lock
|
||||
auto duration_move = std::chrono::steady_clock::now() - start_move;
|
||||
|
||||
// Verify the move blocked until the blocker thread finished (duration > hold time)
|
||||
EXPECT_GE(duration_move, BLOCK_TIME);
|
||||
|
||||
// Verify data transfer (m2 now has m1's initial data)
|
||||
EXPECT_EQ(10, m2.handle()->value);
|
||||
EXPECT_EQ(m1_initial_id, m2.handle()->id); // m2 now owns m1's resource
|
||||
|
||||
// Verify source state (m1 now has m2's initial data)
|
||||
// The move assignment resulted in a SWAP.
|
||||
EXPECT_EQ(99, m1.handle()->value);
|
||||
EXPECT_EQ(m2_initial_id, m1.handle()->id); // m1 now owns m2's original resource
|
||||
|
||||
blocker.join();
|
||||
}
|
||||
@@ -0,0 +1,537 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <everest/util/async/thread_pool_scaling.hpp>
|
||||
#include <future>
|
||||
#include <vector>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
using namespace everest::lib::util;
|
||||
|
||||
// =================================================================
|
||||
// 1. Latency Scaling Tests
|
||||
// =================================================================
|
||||
|
||||
/**
|
||||
* @test ScalesOnLatencyThreshold
|
||||
* @brief Verifies that the pool spawns a new thread when a task waits too long.
|
||||
*/
|
||||
TEST(ThreadPoolScalingTest, ScalesOnLatencyThreshold) {
|
||||
thread_pool_scaling<LatencyScaling<10>> pool(1, 2, 1s);
|
||||
|
||||
std::promise<void> block_first_task;
|
||||
std::shared_future<void> block_future = block_first_task.get_future();
|
||||
|
||||
pool.run([block_future]() { block_future.wait(); });
|
||||
|
||||
std::this_thread::sleep_for(5ms);
|
||||
|
||||
std::atomic<bool> second_task_started{false};
|
||||
// This should spawn a new thread already.
|
||||
pool.run([&]() { second_task_started = true; });
|
||||
|
||||
ASSERT_FALSE(second_task_started.load());
|
||||
|
||||
std::this_thread::sleep_for(50ms);
|
||||
EXPECT_TRUE(second_task_started.load());
|
||||
|
||||
block_first_task.set_value();
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 2. Thread Retirement Tests
|
||||
// =================================================================
|
||||
|
||||
/**
|
||||
* @test SurplusThreadsRetireAfterTimeout
|
||||
* @brief Verifies that threads exceeding the minimum count retire when idle.
|
||||
*/
|
||||
TEST(ThreadPoolScalingTest, SurplusThreadsRetireAfterTimeout) {
|
||||
thread_pool_scaling<GreedyScaling> pool(1, 2, 100ms);
|
||||
|
||||
std::promise<void> p1, p2;
|
||||
auto f1 = p1.get_future().share();
|
||||
auto f2 = p2.get_future().share();
|
||||
|
||||
pool.run([f1]() { f1.wait(); });
|
||||
pool.run([f2]() { f2.wait(); });
|
||||
|
||||
p1.set_value();
|
||||
p2.set_value();
|
||||
|
||||
std::this_thread::sleep_for(300ms);
|
||||
|
||||
std::atomic<int> counter{0};
|
||||
for (int i = 0; i < 5; ++i)
|
||||
pool.run([&]() { counter++; });
|
||||
|
||||
std::this_thread::sleep_for(50ms);
|
||||
EXPECT_EQ(counter.load(), 5);
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 3. Backpressure Tests
|
||||
// =================================================================
|
||||
|
||||
/**
|
||||
* @test BackpressureBlocksProducer
|
||||
* @brief Ensures the calling thread blocks when the queue limit is reached.
|
||||
*/
|
||||
TEST(ThreadPoolScalingTest, BackpressureBlocksProducer) {
|
||||
thread_pool_scaling<GreedyScaling> pool(1, 1, 1s, 1);
|
||||
|
||||
std::promise<void> block;
|
||||
auto fut = block.get_future().share();
|
||||
|
||||
pool.run([fut]() { fut.wait(); });
|
||||
pool.run([]() {});
|
||||
|
||||
std::atomic<bool> producer_unblocked{false};
|
||||
std::thread producer([&]() {
|
||||
pool.run([]() {});
|
||||
producer_unblocked = true;
|
||||
});
|
||||
|
||||
std::this_thread::sleep_for(100ms);
|
||||
EXPECT_FALSE(producer_unblocked.load());
|
||||
|
||||
block.set_value();
|
||||
producer.join();
|
||||
EXPECT_TRUE(producer_unblocked.load());
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 4. Future Interface Tests
|
||||
// =================================================================
|
||||
|
||||
/**
|
||||
* @test OperatorReturnsValidFuture
|
||||
*/
|
||||
TEST(ThreadPoolScalingTest, OperatorReturnsValidFuture) {
|
||||
thread_pool_scaling<GreedyScaling> pool(1, 2, 1s);
|
||||
constexpr int lhs = 10;
|
||||
constexpr int rhs = 32;
|
||||
auto fut = pool([](int first, int second) { return first + second; }, lhs, rhs);
|
||||
EXPECT_EQ(fut.get(), 42);
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 5. Scaling and Retirement Stress Tests
|
||||
// =================================================================
|
||||
|
||||
/**
|
||||
* @test RapidScalingThrash
|
||||
* @brief Verifies stability during high-frequency fluctuations in workload.
|
||||
* @details Updated with more robust timing to handle OS scheduling jitter.
|
||||
*/
|
||||
TEST(ThreadPoolScalingStressTest, RapidScalingThrash) {
|
||||
constexpr std::size_t max_threads = 20;
|
||||
constexpr int burst_tasks = 40;
|
||||
constexpr int trickle_tasks = 5;
|
||||
constexpr int tasks_per_iteration = burst_tasks + trickle_tasks;
|
||||
thread_pool_scaling<GreedyScaling> pool(1, max_threads, 20ms);
|
||||
std::atomic<int> completed_tasks{0};
|
||||
const int iterations = 30;
|
||||
|
||||
for (int i = 0; i < iterations; ++i) {
|
||||
for (int j = 0; j < burst_tasks; ++j) {
|
||||
pool.run([&]() {
|
||||
std::this_thread::sleep_for(2ms);
|
||||
completed_tasks++;
|
||||
});
|
||||
}
|
||||
std::this_thread::sleep_for(30ms); // Allow some threads to start idling/retiring
|
||||
for (int j = 0; j < trickle_tasks; ++j) {
|
||||
pool.run([&]() { completed_tasks++; });
|
||||
}
|
||||
}
|
||||
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
while (completed_tasks < (iterations * tasks_per_iteration)) {
|
||||
std::this_thread::sleep_for(50ms);
|
||||
if (std::chrono::steady_clock::now() - start > 10s) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
EXPECT_EQ(completed_tasks.load(), iterations * tasks_per_iteration);
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 6. High Contention and Race Condition Tests
|
||||
// =================================================================
|
||||
|
||||
/**
|
||||
* @test HighContentionProducers
|
||||
*/
|
||||
TEST(ThreadPoolScalingStressTest, HighContentionProducers) {
|
||||
constexpr int num_producers = 8;
|
||||
constexpr int tasks_per_producer = 2000;
|
||||
constexpr std::size_t max_threads = 16;
|
||||
thread_pool_scaling<LatencyScaling<5>> pool(4, max_threads, 1s);
|
||||
|
||||
std::atomic<size_t> total_sum{0};
|
||||
std::vector<std::thread> producers;
|
||||
producers.reserve(static_cast<std::size_t>(num_producers));
|
||||
|
||||
for (int prod = 0; prod < num_producers; ++prod) {
|
||||
producers.emplace_back([&]() {
|
||||
for (int i = 0; i < tasks_per_producer; ++i) {
|
||||
pool.run([&total_sum]() { total_sum.fetch_add(1, std::memory_order_relaxed); });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (auto& thr : producers) {
|
||||
thr.join();
|
||||
}
|
||||
|
||||
const auto expected = static_cast<std::size_t>(num_producers) * static_cast<std::size_t>(tasks_per_producer);
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
while (total_sum.load() < expected) {
|
||||
std::this_thread::sleep_for(50ms);
|
||||
if (std::chrono::steady_clock::now() - start > 10s) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
EXPECT_EQ(total_sum.load(), expected);
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 7. Thread retirement versus pool destruction
|
||||
// =================================================================
|
||||
|
||||
/**
|
||||
* @test DestructorVsActiveScalingRace
|
||||
*/
|
||||
TEST(ThreadPoolScalingStressTest, DestructorVsActiveScalingRace) {
|
||||
constexpr int repetitions = 50;
|
||||
constexpr std::size_t max_threads = 10;
|
||||
constexpr int tasks_per_rep = 20;
|
||||
for (int i = 0; i < repetitions; ++i) {
|
||||
{
|
||||
thread_pool_scaling<GreedyScaling> pool(1, max_threads, 5ms);
|
||||
for (int j = 0; j < tasks_per_rep; ++j) {
|
||||
pool.run([]() { std::this_thread::sleep_for(1ms); });
|
||||
}
|
||||
std::this_thread::sleep_for(6ms);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 8. Edge Case: Full Idle Reset
|
||||
// =================================================================
|
||||
|
||||
/**
|
||||
* @test FullIdleResetToMinimum
|
||||
*/
|
||||
TEST(ThreadPoolScalingTest, FullIdleResetToMinimum) {
|
||||
const size_t min = 2;
|
||||
const size_t max = 5;
|
||||
const auto timeout = 50ms;
|
||||
thread_pool_scaling<GreedyScaling> pool(min, max, timeout);
|
||||
|
||||
std::vector<std::promise<void>> promises(max);
|
||||
for (int i = 0; i < max; ++i) {
|
||||
pool.run([&promises, i]() { promises[i].get_future().wait(); });
|
||||
}
|
||||
|
||||
for (auto& prom : promises) {
|
||||
prom.set_value();
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(timeout * 3);
|
||||
|
||||
std::atomic<bool> functional_check{false};
|
||||
pool.run([&]() { functional_check = true; });
|
||||
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
while (!functional_check.load() && std::chrono::steady_clock::now() - start < 1s) {
|
||||
std::this_thread::yield();
|
||||
}
|
||||
|
||||
ASSERT_TRUE(functional_check.load());
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 9. Re-entrancy and Policy Boundary Tests
|
||||
// =================================================================
|
||||
|
||||
/**
|
||||
* @test ReentrantScaling
|
||||
*/
|
||||
TEST(ThreadPoolScalingStressTest, ReentrantScaling) {
|
||||
constexpr int inner_tasks = 10;
|
||||
constexpr int total_tasks = inner_tasks + 1; // outer task + inner tasks
|
||||
thread_pool_scaling<LatencyScaling<10>> pool(1, 4, 1s);
|
||||
std::atomic<int> completed{0};
|
||||
|
||||
pool.run([&]() {
|
||||
for (int i = 0; i < inner_tasks; ++i) {
|
||||
pool.run([&]() { completed++; });
|
||||
}
|
||||
completed++;
|
||||
});
|
||||
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
while (completed < total_tasks && std::chrono::steady_clock::now() - start < 2s) {
|
||||
std::this_thread::sleep_for(10ms);
|
||||
}
|
||||
EXPECT_EQ(completed.load(), total_tasks);
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 10. Latency Boundary Check
|
||||
// =================================================================
|
||||
|
||||
/**
|
||||
* @test LatencyThresholdBoundary
|
||||
*/
|
||||
TEST(ThreadPoolScalingTest, LatencyThresholdBoundary) {
|
||||
// 100 ms threshold: tasks that wait less than 100 ms should NOT trigger scaling
|
||||
constexpr std::size_t threshold_ms = 100;
|
||||
thread_pool_scaling<LatencyScaling<threshold_ms>> pool(1, 2, 1s);
|
||||
|
||||
std::promise<void> block;
|
||||
auto fut = block.get_future().share();
|
||||
pool.run([fut]() { fut.wait(); });
|
||||
|
||||
std::atomic<bool> task2_started{false};
|
||||
pool.run([&]() { task2_started = true; });
|
||||
|
||||
std::this_thread::sleep_for(50ms);
|
||||
|
||||
EXPECT_FALSE(task2_started.load());
|
||||
|
||||
std::this_thread::sleep_for(100ms);
|
||||
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
while (!task2_started.load() && std::chrono::steady_clock::now() - start < 1s) {
|
||||
std::this_thread::sleep_for(10ms);
|
||||
}
|
||||
|
||||
EXPECT_TRUE(task2_started.load());
|
||||
block.set_value();
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 11. ConservativeScaling Policy Tests
|
||||
// =================================================================
|
||||
|
||||
/**
|
||||
* @test ConservativeScalingDoesNotGrowBelowThreshold
|
||||
* @brief With 1 worker, queue_size must exceed workers*2 (>2) to trigger growth.
|
||||
* At exactly 2 queued tasks the policy should NOT scale.
|
||||
*/
|
||||
TEST(ThreadPoolScalingTest, ConservativeScalingDoesNotGrowBelowThreshold) {
|
||||
// min=1, max=2 — second thread must NOT appear unless queue_size > 2
|
||||
thread_pool_scaling<ConservativeScaling> pool(1, 2, 1s);
|
||||
|
||||
std::promise<void> block;
|
||||
auto fut = block.get_future().share();
|
||||
|
||||
// Task 1: occupies the sole min thread
|
||||
pool.run([fut]() { fut.wait(); });
|
||||
|
||||
// Task 2: queue_size after push == 2, workers == 1, 2 > (1*2) is false → no growth
|
||||
std::atomic<bool> task2_ran{false};
|
||||
pool.run([&]() { task2_ran = true; });
|
||||
|
||||
std::this_thread::sleep_for(100ms);
|
||||
EXPECT_FALSE(task2_ran.load()); // still blocked behind task 1
|
||||
|
||||
block.set_value();
|
||||
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
while (!task2_ran.load() && std::chrono::steady_clock::now() - start < 2s) {
|
||||
std::this_thread::sleep_for(10ms);
|
||||
}
|
||||
EXPECT_TRUE(task2_ran.load());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test ConservativeScalingGrowsAboveThreshold
|
||||
* @brief With 1 worker, submitting 3 tasks (queue_size==3 > workers*2==2) must trigger growth.
|
||||
*/
|
||||
TEST(ThreadPoolScalingTest, ConservativeScalingGrowsAboveThreshold) {
|
||||
thread_pool_scaling<ConservativeScaling> pool(1, 3, 1s);
|
||||
|
||||
std::promise<void> block;
|
||||
auto fut = block.get_future().share();
|
||||
|
||||
// Task 1: pins the min thread
|
||||
pool.run([fut]() { fut.wait(); });
|
||||
|
||||
// Tasks 2 and 3: queue_size after task 3 == 3, workers == 1, 3 > 2 → growth
|
||||
std::atomic<int> ran{0};
|
||||
pool.run([&]() { ran++; });
|
||||
pool.run([&]() { ran++; });
|
||||
|
||||
block.set_value();
|
||||
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
while (ran.load() < 2 && std::chrono::steady_clock::now() - start < 2s) {
|
||||
std::this_thread::sleep_for(10ms);
|
||||
}
|
||||
EXPECT_EQ(ran.load(), 2);
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 12. FixedSizeScaling Policy Tests
|
||||
// =================================================================
|
||||
|
||||
/**
|
||||
* @test FixedSizeScalingDoesNotGrowBeforeLimit
|
||||
* @brief Pool must not scale when queue_size is below the fixed limit.
|
||||
*/
|
||||
TEST(ThreadPoolScalingTest, FixedSizeScalingDoesNotGrowBeforeLimit) {
|
||||
// Limit=3: grows only when queue_size >= 3
|
||||
thread_pool_scaling<FixedSizeScaling<3>> pool(1, 2, 1s);
|
||||
|
||||
std::promise<void> block;
|
||||
auto fut = block.get_future().share();
|
||||
|
||||
pool.run([fut]() { fut.wait(); });
|
||||
|
||||
// queue_size == 2 after this push, 2 < 3 → no growth
|
||||
std::atomic<bool> task2_ran{false};
|
||||
pool.run([&]() { task2_ran = true; });
|
||||
|
||||
std::this_thread::sleep_for(100ms);
|
||||
EXPECT_FALSE(task2_ran.load());
|
||||
|
||||
block.set_value();
|
||||
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
while (!task2_ran.load() && std::chrono::steady_clock::now() - start < 2s) {
|
||||
std::this_thread::sleep_for(10ms);
|
||||
}
|
||||
EXPECT_TRUE(task2_ran.load());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test FixedSizeScalingGrowsAtLimit
|
||||
* @brief Pool must spawn a new thread exactly when queue_size reaches the fixed limit.
|
||||
*/
|
||||
TEST(ThreadPoolScalingTest, FixedSizeScalingGrowsAtLimit) {
|
||||
// Limit=2: grows when queue_size >= 2
|
||||
thread_pool_scaling<FixedSizeScaling<2>> pool(1, 2, 1s);
|
||||
|
||||
std::promise<void> block;
|
||||
auto fut = block.get_future().share();
|
||||
|
||||
pool.run([fut]() { fut.wait(); });
|
||||
|
||||
// queue_size == 2 after this push, 2 >= 2 → growth
|
||||
std::atomic<bool> task2_ran{false};
|
||||
pool.run([&]() { task2_ran = true; });
|
||||
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
while (!task2_ran.load() && std::chrono::steady_clock::now() - start < 2s) {
|
||||
std::this_thread::sleep_for(10ms);
|
||||
}
|
||||
EXPECT_TRUE(task2_ran.load());
|
||||
|
||||
block.set_value();
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 13. min == max Degenerate Case
|
||||
// =================================================================
|
||||
|
||||
/**
|
||||
* @test FixedSizePoolNeverGrows
|
||||
* @brief When min == max the pool must never spawn additional threads regardless of backlog.
|
||||
*/
|
||||
TEST(ThreadPoolScalingTest, FixedSizePoolNeverGrows) {
|
||||
// min == max == 1: only ever one worker
|
||||
thread_pool_scaling<GreedyScaling> pool(1, 1, 1s);
|
||||
|
||||
std::promise<void> block;
|
||||
auto fut = block.get_future().share();
|
||||
|
||||
pool.run([fut]() { fut.wait(); });
|
||||
|
||||
// Queue up several tasks; none can run until the first finishes
|
||||
std::atomic<int> ran{0};
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
pool.run([&]() { ran++; });
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(100ms);
|
||||
EXPECT_EQ(ran.load(), 0); // no second thread spawned → all queued behind task 1
|
||||
|
||||
block.set_value();
|
||||
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
while (ran.load() < 4 && std::chrono::steady_clock::now() - start < 2s) {
|
||||
std::this_thread::sleep_for(10ms);
|
||||
}
|
||||
EXPECT_EQ(ran.load(), 4); // all tasks complete once the single worker is unblocked
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 14. Zombie reaping correctness
|
||||
// =================================================================
|
||||
|
||||
/**
|
||||
* @test ZombiesAreJoinedAfterRetirement
|
||||
* @brief Surplus threads that voluntarily retire must be fully joined — verified by
|
||||
* the pool destructor completing without hanging or calling std::terminate().
|
||||
*/
|
||||
TEST(ThreadPoolScalingTest, ZombiesAreJoinedAfterRetirement) {
|
||||
// Short idle timeout so surplus threads retire quickly
|
||||
thread_pool_scaling<GreedyScaling> pool(1, 4, 20ms);
|
||||
|
||||
std::promise<void> gate;
|
||||
auto fut = gate.get_future().share();
|
||||
|
||||
// Flood the pool to force scale-up to max
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
pool.run([fut]() { fut.wait(); });
|
||||
}
|
||||
gate.set_value();
|
||||
|
||||
// Let all surplus threads go idle and retire into the zombie deque
|
||||
std::this_thread::sleep_for(200ms);
|
||||
|
||||
// Destructor must complete cleanly: all zombies are joined before destruction
|
||||
}
|
||||
|
||||
/**
|
||||
* @test ZombiesReapedConcurrentlyWithTaskExecution
|
||||
* @brief Zombies created during task execution must be reaped correctly
|
||||
* by the worker loop while other tasks continue to execute.
|
||||
*/
|
||||
TEST(ThreadPoolScalingStressTest, ZombiesReapedConcurrentlyWithTaskExecution) {
|
||||
constexpr std::size_t max_threads = 8;
|
||||
constexpr int num_waves = 5;
|
||||
constexpr int tasks_per_wave = 10;
|
||||
constexpr int total_tasks = num_waves * tasks_per_wave;
|
||||
|
||||
// Short timeout forces retirement while tasks keep arriving
|
||||
thread_pool_scaling<GreedyScaling> pool(1, max_threads, 10ms);
|
||||
std::atomic<int> completed{0};
|
||||
|
||||
// Submit waves of tasks separated by the idle timeout to repeatedly
|
||||
// grow-then-shrink the pool, generating zombies during active execution
|
||||
for (int wave = 0; wave < num_waves; ++wave) {
|
||||
for (int i = 0; i < tasks_per_wave; ++i) {
|
||||
pool.run([&]() {
|
||||
std::this_thread::sleep_for(5ms);
|
||||
completed++;
|
||||
});
|
||||
}
|
||||
std::this_thread::sleep_for(15ms); // retire surplus threads between waves
|
||||
}
|
||||
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
while (completed.load() < total_tasks && std::chrono::steady_clock::now() - start < 5s) {
|
||||
std::this_thread::sleep_for(20ms);
|
||||
}
|
||||
EXPECT_EQ(completed.load(), total_tasks);
|
||||
// Destructor must complete cleanly with no unjoined zombie threads
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <everest/util/async/thread_pool.hpp>
|
||||
#include <iostream>
|
||||
#include <numeric>
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
using namespace everest::lib::util;
|
||||
|
||||
// --- Test Fixture ---
|
||||
// A fixture allows us to set up and tear down the thread pool easily.
|
||||
class ThreadPoolTest : public ::testing::Test {
|
||||
protected:
|
||||
// Pool size chosen small to encourage contention
|
||||
const unsigned int POOL_SIZE = 4;
|
||||
|
||||
// The thread_pool object will be initialized here and automatically
|
||||
// destroyed (and joined) when the test ends.
|
||||
thread_pool* pool;
|
||||
|
||||
void SetUp() override {
|
||||
pool = new thread_pool(POOL_SIZE);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
delete pool;
|
||||
pool = nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
// --- Helper Functions for Binding ---
|
||||
// Example function to test argument passing
|
||||
int add(int a, int b) {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
// Example function to test void return
|
||||
void do_nothing() {
|
||||
// This is run by a worker thread
|
||||
}
|
||||
|
||||
// --- Test Cases ---
|
||||
|
||||
// 1. Basic Correctness Tests
|
||||
// -------------------------
|
||||
|
||||
TEST_F(ThreadPoolTest, Test_Immediate_Execution_And_Contention) {
|
||||
std::atomic<int> counter{0};
|
||||
const int num_tasks = 1000;
|
||||
std::vector<std::future<void>> futures;
|
||||
|
||||
// Submit many more tasks than threads to test queue contention
|
||||
for (int i = 0; i < num_tasks; ++i) {
|
||||
// Use a lambda with no arguments (uses the specialized operator() if available)
|
||||
futures.push_back((*pool)([&counter]() { counter++; }));
|
||||
}
|
||||
|
||||
// Wait for all tasks to complete
|
||||
for (auto& f : futures) {
|
||||
f.get();
|
||||
}
|
||||
|
||||
// Check if all tasks ran correctly
|
||||
ASSERT_EQ(counter.load(), num_tasks);
|
||||
}
|
||||
|
||||
TEST_F(ThreadPoolTest, Test_Future_Return_Value_And_Arguments) {
|
||||
// Test passing arguments to a simple function
|
||||
std::future<int> f1 = (*pool)(add, 10, 20);
|
||||
|
||||
// Test a lambda with a return value and local arguments
|
||||
int multiplier = 5;
|
||||
std::future<double> f2 = (*pool)([multiplier](double val) { return val * multiplier; }, 10.0);
|
||||
|
||||
ASSERT_EQ(f1.get(), 30);
|
||||
ASSERT_DOUBLE_EQ(f2.get(), 50.0);
|
||||
}
|
||||
|
||||
TEST_F(ThreadPoolTest, Test_Run_Is_NonBlocking) {
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
|
||||
// Submit a task that takes 500ms
|
||||
pool->run([]() { std::this_thread::sleep_for(std::chrono::milliseconds(500)); });
|
||||
|
||||
auto end = std::chrono::steady_clock::now();
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
|
||||
|
||||
// It should return in basically 0ms (well under 50ms)
|
||||
EXPECT_LT(elapsed.count(), 50) << "run() blocked the caller!";
|
||||
}
|
||||
|
||||
TEST_F(ThreadPoolTest, Test_Recursive_Submission) {
|
||||
std::atomic<int> result{0};
|
||||
|
||||
std::future<void> f = (*pool)([this, &result]() {
|
||||
// Task A submits Task B
|
||||
std::future<int> f2 = (*pool)([]() { return 42; });
|
||||
result = f2.get();
|
||||
});
|
||||
|
||||
f.get();
|
||||
ASSERT_EQ(result.load(), 42);
|
||||
}
|
||||
|
||||
TEST_F(ThreadPoolTest, Test_Multi_Producer_Contention) {
|
||||
std::atomic<int> counter{0};
|
||||
const int num_producers = 4;
|
||||
const int tasks_per_producer = 250;
|
||||
std::vector<std::thread> producers;
|
||||
|
||||
for (int i = 0; i < num_producers; ++i) {
|
||||
producers.emplace_back([this, &counter, tasks_per_producer]() {
|
||||
for (int j = 0; j < tasks_per_producer; ++j) {
|
||||
pool->run([&counter]() { counter++; });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (auto& t : producers)
|
||||
t.join();
|
||||
|
||||
// Give workers a moment to finish the queue
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
ASSERT_EQ(counter.load(), num_producers * tasks_per_producer);
|
||||
}
|
||||
|
||||
// 2. Exception and Error Handling Tests
|
||||
// ------------------------------------
|
||||
|
||||
TEST_F(ThreadPoolTest, Test_Task_Exception_Transfer) {
|
||||
// Submit a task that throws an exception
|
||||
auto throwing_task = []() -> int {
|
||||
throw std::runtime_error("Task failed intentionally");
|
||||
return 42;
|
||||
};
|
||||
|
||||
std::future<int> f = (*pool)(throwing_task);
|
||||
|
||||
// future::get() should re-throw the exception from the worker thread
|
||||
ASSERT_THROW(f.get(), std::runtime_error);
|
||||
}
|
||||
|
||||
// 3. Critical Shutdown Tests
|
||||
// -------------------------
|
||||
|
||||
TEST_F(ThreadPoolTest, Test_Clean_Destruction_Blocked_Workers) {
|
||||
const int num_threads = POOL_SIZE;
|
||||
std::vector<std::future<void>> futures;
|
||||
std::atomic<int> started_count{0};
|
||||
|
||||
// Submit exactly POOL_SIZE tasks that sleep for a long time
|
||||
for (int i = 0; i < num_threads; ++i) {
|
||||
futures.push_back((*pool)([&started_count]() {
|
||||
started_count++;
|
||||
// Block the thread, forcing the destructor to wait
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
}));
|
||||
}
|
||||
|
||||
// Wait briefly to ensure all threads have started their tasks
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
|
||||
// The destruction of the 'pool' fixture happens automatically in TearDown.
|
||||
// TearDown calls 'delete pool', which calls '~thread_pool()'.
|
||||
// If TearDown completes without crashing, the test passes.
|
||||
|
||||
ASSERT_EQ(started_count.load(), num_threads) << "Not all threads started blocking tasks.";
|
||||
}
|
||||
|
||||
TEST_F(ThreadPoolTest, Test_Clean_Destruction_Full_Queue) {
|
||||
const int num_tasks_to_queue = POOL_SIZE * 5; // Overwhelm the pool
|
||||
std::vector<std::future<void>> futures;
|
||||
|
||||
// Submit many tasks, including long-running ones
|
||||
for (int i = 0; i < num_tasks_to_queue; ++i) {
|
||||
futures.push_back((*pool)([]() {
|
||||
// Mix of fast and slow tasks
|
||||
if (rand() % 10 == 0) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// Do NOT call f.get() here. Let the pool be destroyed immediately,
|
||||
// simulating a sudden program exit.
|
||||
// The destruction in TearDown must complete without a deadlock.
|
||||
|
||||
SUCCEED(); // If TearDown completes, the shutdown was clean.
|
||||
}
|
||||
|
||||
TEST_F(ThreadPoolTest, Test_Void_Return) {
|
||||
std::future<void> f = (*pool)(do_nothing);
|
||||
|
||||
// future::get() must be called to ensure the task finished without exception
|
||||
// It returns void, but checks for exceptions set on the promise.
|
||||
f.get();
|
||||
|
||||
SUCCEED();
|
||||
}
|
||||
|
||||
TEST_F(ThreadPoolTest, Test_Parallel_Execution_Proved) {
|
||||
// A single task duration that is long enough to measure accurately.
|
||||
const auto task_duration = std::chrono::milliseconds(100);
|
||||
|
||||
// Number of tasks equals the number of threads in the pool
|
||||
const unsigned int num_tasks = POOL_SIZE;
|
||||
|
||||
// Calculate the expected sequential time (N tasks * T duration)
|
||||
const auto expected_sequential_duration = task_duration * num_tasks;
|
||||
|
||||
// The expected parallel time should be slightly more than one task's duration
|
||||
// We use a large tolerance factor (e.g., 2.5x the single task duration)
|
||||
// to account for thread creation, scheduling, and I/O overhead.
|
||||
const auto expected_parallel_limit = task_duration * 2.5;
|
||||
|
||||
std::vector<std::future<void>> futures;
|
||||
|
||||
auto start_time = std::chrono::steady_clock::now();
|
||||
|
||||
// 1. Submit N blocking tasks (one for each thread)
|
||||
for (unsigned int i = 0; i < num_tasks; ++i) {
|
||||
futures.push_back((*pool)([task_duration]() { std::this_thread::sleep_for(task_duration); }));
|
||||
}
|
||||
|
||||
// 2. Wait for all tasks to complete
|
||||
for (auto& f : futures) {
|
||||
f.get();
|
||||
}
|
||||
|
||||
auto end_time = std::chrono::steady_clock::now();
|
||||
auto actual_duration = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
|
||||
|
||||
// 3. Assert parallelism
|
||||
|
||||
// Log results for manual inspection if the test fails
|
||||
// std::cout << "\n[ PARALLELISM TEST ]" << std::endl;
|
||||
// std::cout << " Pool Size: " << POOL_SIZE << " threads" << std::endl;
|
||||
// std::cout << " Single Task Time: " << task_duration.count() << "ms" << std::endl;
|
||||
// std::cout << " Sequential Expected: " << expected_sequential_duration.count() << "ms" << std::endl;
|
||||
// std::cout << " Parallel Limit: " << expected_parallel_limit.count() << "ms" << std::endl;
|
||||
// std::cout << " Actual Time Taken: " << actual_duration.count() << "ms" << std::endl;
|
||||
|
||||
// CRITICAL ASSERTION: The actual time must be much less than the sequential time.
|
||||
// Use EXPECT_LT (less than) against the safe parallel limit.
|
||||
EXPECT_LT(actual_duration.count(), expected_parallel_limit.count())
|
||||
<< "The total time taken (" << actual_duration.count() << "ms) suggests tasks ran sequentially."
|
||||
<< "Expected time less than " << expected_parallel_limit.count() << "ms for parallel execution.";
|
||||
}
|
||||
|
||||
TEST(ThreadPoolStress, Test_Rapid_Lifecycle) {
|
||||
for (int i = 0; i < 50; ++i) {
|
||||
thread_pool temporary_pool(2);
|
||||
for (int j = 0; j < 10; ++j) {
|
||||
temporary_pool.run([]() { std::this_thread::yield(); });
|
||||
}
|
||||
// Destruction happens immediately
|
||||
}
|
||||
SUCCEED();
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include <everest/util/async/async_wrapper.hpp>
|
||||
#include <exception>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
struct Counter {
|
||||
Counter(int v) : value(v) {
|
||||
}
|
||||
int value = 0;
|
||||
std::thread::id worker_id;
|
||||
|
||||
// Mutator
|
||||
void add(int v) {
|
||||
value += v;
|
||||
worker_id = std::this_thread::get_id();
|
||||
}
|
||||
// Accessor
|
||||
int get() const {
|
||||
return value;
|
||||
}
|
||||
// Thrower
|
||||
int throw_if_equal(int v) {
|
||||
if (value == v) {
|
||||
throw std::runtime_error("User-defined fatal error.");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
using namespace everest::lib::util;
|
||||
|
||||
// --- GTEST FIXTURE ---
|
||||
namespace everest::lib::util::testing_interface {
|
||||
class AsyncWrapperTest : public ::testing::Test {
|
||||
public:
|
||||
template <class T> std::shared_ptr<std::promise<void>> const& get_global_promise_for_test(T& wrapper) const {
|
||||
return wrapper.m_global_promise;
|
||||
}
|
||||
|
||||
template <class T> auto& get_queue_for_test(T& wrapper) {
|
||||
return wrapper.m_queue;
|
||||
}
|
||||
};
|
||||
} // namespace everest::lib::util::testing_interface
|
||||
|
||||
using namespace everest::lib::util::testing_interface;
|
||||
|
||||
template <class T> class TestQueue : public thread_safe_queue<T> {
|
||||
public:
|
||||
using ThisT = thread_safe_queue<T>;
|
||||
void push(T item) {
|
||||
ThisT::push(item);
|
||||
}
|
||||
|
||||
T pop() {
|
||||
if (m_throw_on_next_pop) {
|
||||
throw std::runtime_error("oh no");
|
||||
}
|
||||
|
||||
auto tmp = ThisT::pop();
|
||||
|
||||
return tmp;
|
||||
}
|
||||
|
||||
void force_throw_on_next_pop() {
|
||||
m_throw_on_next_pop = true;
|
||||
push([]() {});
|
||||
}
|
||||
|
||||
private:
|
||||
bool m_throw_on_next_pop{false};
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
using async_guarded_testqueue = async_wrapper_impl<T, GlobalFailurePolicy, WaitToFinishPolicy, TestQueue>;
|
||||
|
||||
// Test 1: Basic functionality and thread serialization (Background/WaitToFinish)
|
||||
TEST_F(AsyncWrapperTest, CoreFunctionality) {
|
||||
async_wrapper_wait<Counter> wrapper(0);
|
||||
|
||||
// 1. Check asynchronous execution and result retrieval
|
||||
auto fut1 = wrapper([](Counter& c) {
|
||||
c.add(5);
|
||||
return c.get();
|
||||
});
|
||||
auto fut2 = wrapper([](Counter& c) {
|
||||
c.add(10);
|
||||
return c.get();
|
||||
});
|
||||
|
||||
EXPECT_EQ(fut1.get(), 5);
|
||||
EXPECT_EQ(fut2.get(), 15);
|
||||
|
||||
// 2. Check side effect on resource
|
||||
auto fut3 = wrapper([](Counter& c) { return c.get(); });
|
||||
EXPECT_EQ(fut3.get(), 15);
|
||||
|
||||
// 3. Check 'run' (fire-and-forget)
|
||||
wrapper.run([](Counter& c) { c.add(5); });
|
||||
|
||||
auto fut4 = wrapper([](Counter& c) { return c.get(); });
|
||||
EXPECT_EQ(fut4.get(), 20);
|
||||
|
||||
// 4. Check thread ID
|
||||
std::thread::id main_thread_id = std::this_thread::get_id();
|
||||
auto fut_id = wrapper([](Counter& c) { return c.worker_id; });
|
||||
|
||||
EXPECT_NE(fut_id.get(), main_thread_id);
|
||||
}
|
||||
|
||||
// Test 2: LocalFailurePolicy (Background) - User Exception is Isolated
|
||||
TEST_F(AsyncWrapperTest, LocalPolicy_UserExceptionIsContained) {
|
||||
async_wrapper_wait<Counter> wrapper(5);
|
||||
|
||||
auto fut_a = wrapper([](Counter& c) { return c.throw_if_equal(5); });
|
||||
auto fut_b = wrapper([](Counter& c) {
|
||||
c.add(10);
|
||||
return c.get();
|
||||
});
|
||||
|
||||
fut_a.wait();
|
||||
fut_b.wait();
|
||||
|
||||
ASSERT_THROW(fut_a.get(), std::runtime_error);
|
||||
|
||||
int received_value = 0;
|
||||
EXPECT_NO_THROW(received_value = fut_b.get());
|
||||
EXPECT_EQ(received_value, 15);
|
||||
|
||||
auto fut_c = wrapper([](Counter& c) { return c.get(); });
|
||||
fut_c.wait();
|
||||
EXPECT_EQ(fut_c.get(), 15);
|
||||
}
|
||||
|
||||
// Test 3: GlobalFailurePolicy (Guarded) - User Exception Shuts Down Instance
|
||||
TEST_F(AsyncWrapperTest, GlobalPolicy_UserExceptionCausesShutdown) {
|
||||
async_wrapper_guarded_wait<Counter> wrapper(5);
|
||||
|
||||
auto fut_a = wrapper([](Counter& c) { return c.throw_if_equal(5); });
|
||||
auto fut_b = wrapper([](Counter& c) {
|
||||
c.add(10);
|
||||
return c.get();
|
||||
});
|
||||
|
||||
fut_a.wait();
|
||||
fut_b.wait();
|
||||
|
||||
ASSERT_THROW(fut_a.get(), std::runtime_error);
|
||||
ASSERT_THROW(fut_b.get(), std::runtime_error);
|
||||
|
||||
auto fut_c = wrapper([](Counter& c) { return c.get(); });
|
||||
fut_c.wait();
|
||||
ASSERT_THROW(fut_c.get(), std::runtime_error);
|
||||
}
|
||||
|
||||
// Test 4: GlobalFailureSignal_BlocksNewTasks (Tests signal effect)
|
||||
TEST_F(AsyncWrapperTest, GlobalFailureSignal_BlocksNewTasks) {
|
||||
async_wrapper_guarded_wait<Counter> wrapper(0);
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
|
||||
// 1. Manually set the Global Promise (simulating infrastructure or user failure)
|
||||
try {
|
||||
throw std::runtime_error("Simulated Global Signal Set.");
|
||||
} catch (...) {
|
||||
get_global_promise_for_test(wrapper)->set_exception(std::current_exception());
|
||||
}
|
||||
|
||||
// 2. Submit a new task (Task B)
|
||||
auto fut_b = wrapper([](Counter& c) {
|
||||
c.add(50);
|
||||
return c.get();
|
||||
});
|
||||
|
||||
fut_b.wait();
|
||||
|
||||
// 3. Task B must immediately fail because the infrastructure flag is set
|
||||
ASSERT_THROW(fut_b.get(), std::runtime_error);
|
||||
}
|
||||
|
||||
// Test 5: Destructor Behavior - WaitToFinishPolicy vs FastQuitPolicy
|
||||
TEST_F(AsyncWrapperTest, DestructorShutdownPolicies) {
|
||||
// Setup 1: Test WaitToFinishPolicy (Guaranteed execution of queued task)
|
||||
int wait_result = 0;
|
||||
{
|
||||
async_wrapper_guarded_wait<Counter> wrapper(0);
|
||||
auto fut = wrapper([&wait_result](Counter& c) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
wait_result = 1;
|
||||
return c.get();
|
||||
});
|
||||
// Destructor runs here (WaitToFinishPolicy::shutdown), MUST wait 50ms.
|
||||
}
|
||||
// Result confirms the destructor waited for the task to finish.
|
||||
EXPECT_EQ(wait_result, 1);
|
||||
|
||||
// Setup 2: Test FastQuitPolicy (Drops queued task, joins quickly)
|
||||
int fast_result = 0;
|
||||
{
|
||||
async_wrapper_guarded_fast<Counter> wrapper(0);
|
||||
|
||||
// Push a task that runs briefly. If the task starts, the destructor must wait.
|
||||
// We rely on the race condition being won by the destructor for EXPECT_EQ(0) to pass.
|
||||
wrapper.run([&fast_result](Counter& c) {
|
||||
std::this_thread::sleep_for(std::chrono::microseconds(100)); // Very fast sleep
|
||||
fast_result = 2; // Should not reach here if the task is aborted while queued
|
||||
});
|
||||
|
||||
// Destructor runs here (FastQuitPolicy::shutdown), should join quickly.
|
||||
}
|
||||
// If fast_result == 0, the task was aborted while queued.
|
||||
// If fast_result == 2, the task started and the destructor waited for it to finish.
|
||||
EXPECT_EQ(fast_result, 0);
|
||||
}
|
||||
|
||||
// Test 6: Verify Worker's internal catch block works correctly
|
||||
TEST_F(AsyncWrapperTest, InfrastructureFailure_WorkerSetsSignalAndShutsDown) {
|
||||
async_guarded_testqueue<Counter> wrapper(0); // Use the specialized TestQueue type
|
||||
|
||||
// 1. Ensure worker thread is blocked on pop()
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
|
||||
// 2. Force the queue to throw an exception, waking up the worker thread
|
||||
get_queue_for_test(wrapper).force_throw_on_next_pop();
|
||||
|
||||
// 3. Give the worker time to execute the catch block and shut down.
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
|
||||
// 4. Submit a new task (Task A). This hits the Synchronous Gatekeeper check.
|
||||
auto fut_a = wrapper([](Counter& c) {
|
||||
c.add(99);
|
||||
return c.get();
|
||||
});
|
||||
|
||||
fut_a.wait();
|
||||
|
||||
// 5. Task A must fail with the infrastructure exception
|
||||
bool active_runtime_error = false;
|
||||
std::string message = "";
|
||||
try {
|
||||
fut_a.get();
|
||||
} catch (const std::runtime_error& e) {
|
||||
message = e.what();
|
||||
active_runtime_error = true;
|
||||
}
|
||||
EXPECT_TRUE(active_runtime_error);
|
||||
|
||||
EXPECT_EQ(message, "Async worker infrastructure failure.");
|
||||
}
|
||||
|
||||
// Test 7: Run method must not block the main thread (optimized fire-and-forget)
|
||||
TEST_F(AsyncWrapperTest, RunMethodDoesNotBlock) {
|
||||
// We use the WaitToFinish policy so we know the task will complete before the test ends.
|
||||
async_wrapper_wait<Counter> wrapper(0);
|
||||
const int SLOW_TASK_MS = 50;
|
||||
|
||||
auto slow_task = [SLOW_TASK_MS](Counter& c) {
|
||||
// Task takes significant time to execute
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(SLOW_TASK_MS));
|
||||
c.add(1);
|
||||
};
|
||||
|
||||
auto start_time = std::chrono::steady_clock::now();
|
||||
|
||||
// Submit the slow task using the optimized run() method
|
||||
wrapper.run(slow_task);
|
||||
|
||||
auto end_time = std::chrono::steady_clock::now();
|
||||
|
||||
// The main thread should not have blocked for the duration of the task.
|
||||
// The elapsed time should be much less than the task execution time (10ms tolerance).
|
||||
auto elapsed_ms = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time).count();
|
||||
|
||||
// 1. Assertion on timing: Proves non-blocking submission
|
||||
EXPECT_LT(elapsed_ms, 10);
|
||||
|
||||
// 2. Assert task eventually ran (rely on WaitToFinishPolicy destructor)
|
||||
auto fut_check = wrapper([](Counter& c) { return c.get(); });
|
||||
EXPECT_EQ(fut_check.get(), 1);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <everest/util/enum/EnumFlags.hpp>
|
||||
|
||||
enum class ErrorHandlingFlags : std::uint8_t {
|
||||
prevent_charging,
|
||||
prevent_charging_welded,
|
||||
all_errors_cleared,
|
||||
last = all_errors_cleared
|
||||
};
|
||||
|
||||
enum class BspErrors : std::uint8_t {
|
||||
DiodeFault,
|
||||
VentilationNotAvailable,
|
||||
BrownOut,
|
||||
EnergyManagement,
|
||||
PermanentFault,
|
||||
MREC2GroundFailure,
|
||||
MREC4OverCurrentFailure,
|
||||
MREC5OverVoltage,
|
||||
MREC6UnderVoltage,
|
||||
MREC8EmergencyStop,
|
||||
MREC10InvalidVehicleMode,
|
||||
MREC14PilotFault,
|
||||
MREC15PowerLoss,
|
||||
MREC17EVSEContactorFault,
|
||||
MREC19CableOverTempStop,
|
||||
MREC20PartialInsertion,
|
||||
MREC23ProximityFault,
|
||||
MREC24ConnectorVoltageHigh,
|
||||
MREC25BrokenLatch,
|
||||
MREC26CutCable,
|
||||
VendorError,
|
||||
last = VendorError
|
||||
};
|
||||
|
||||
using namespace everest::lib::util;
|
||||
|
||||
TEST(AtomicEnumFlagsTest, init) {
|
||||
AtomicEnumFlags<ErrorHandlingFlags> flags;
|
||||
EXPECT_TRUE(flags.all_reset());
|
||||
}
|
||||
|
||||
TEST(AtomicEnumFlagsTest, init_large) {
|
||||
AtomicEnumFlags<BspErrors> flags;
|
||||
EXPECT_TRUE(flags.all_reset());
|
||||
}
|
||||
|
||||
TEST(AtomicEnumFlagsTest, set_reset_one) {
|
||||
AtomicEnumFlags<ErrorHandlingFlags> flags;
|
||||
EXPECT_TRUE(flags.all_reset());
|
||||
|
||||
flags.set(ErrorHandlingFlags::all_errors_cleared);
|
||||
EXPECT_FALSE(flags.all_reset());
|
||||
flags.reset(ErrorHandlingFlags::all_errors_cleared);
|
||||
EXPECT_TRUE(flags.all_reset());
|
||||
}
|
||||
|
||||
TEST(AtomicEnumFlagsTest, set_reset_two) {
|
||||
AtomicEnumFlags<ErrorHandlingFlags> flags;
|
||||
EXPECT_TRUE(flags.all_reset());
|
||||
|
||||
flags.set(ErrorHandlingFlags::all_errors_cleared);
|
||||
EXPECT_FALSE(flags.all_reset());
|
||||
flags.set(ErrorHandlingFlags::prevent_charging);
|
||||
EXPECT_FALSE(flags.all_reset());
|
||||
flags.reset(ErrorHandlingFlags::all_errors_cleared);
|
||||
EXPECT_FALSE(flags.all_reset());
|
||||
flags.reset(ErrorHandlingFlags::prevent_charging);
|
||||
EXPECT_TRUE(flags.all_reset());
|
||||
}
|
||||
|
||||
TEST(AtomicEnumFlagsTest, set_reset_three) {
|
||||
AtomicEnumFlags<ErrorHandlingFlags> flags;
|
||||
EXPECT_TRUE(flags.all_reset());
|
||||
|
||||
flags.set(ErrorHandlingFlags::all_errors_cleared);
|
||||
EXPECT_FALSE(flags.all_reset());
|
||||
flags.set(ErrorHandlingFlags::prevent_charging);
|
||||
EXPECT_FALSE(flags.all_reset());
|
||||
flags.set(ErrorHandlingFlags::prevent_charging_welded);
|
||||
EXPECT_FALSE(flags.all_reset());
|
||||
flags.reset();
|
||||
EXPECT_TRUE(flags.all_reset());
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <everest/util/enum/EnumFlags.hpp>
|
||||
|
||||
namespace {
|
||||
using namespace everest::lib::util;
|
||||
|
||||
// needs an 8-bit value
|
||||
enum class small : std::uint8_t {
|
||||
one,
|
||||
two,
|
||||
three,
|
||||
four,
|
||||
five,
|
||||
six,
|
||||
seven,
|
||||
last = seven,
|
||||
};
|
||||
|
||||
// needs an 8-bit value
|
||||
enum class full : std::uint8_t {
|
||||
one,
|
||||
two,
|
||||
three,
|
||||
four,
|
||||
five,
|
||||
six,
|
||||
seven,
|
||||
eight,
|
||||
last = eight,
|
||||
};
|
||||
|
||||
// needs an 16-bit value
|
||||
enum class large : std::uint8_t {
|
||||
zero,
|
||||
one,
|
||||
two,
|
||||
three,
|
||||
four,
|
||||
five,
|
||||
six,
|
||||
seven,
|
||||
eight,
|
||||
last = eight,
|
||||
};
|
||||
|
||||
static_assert(sizeof(full) == sizeof(std::uint8_t));
|
||||
static_assert(sizeof(SelectedUInt<full>) == sizeof(std::uint8_t));
|
||||
|
||||
static_assert(sizeof(large) == sizeof(std::uint8_t));
|
||||
static_assert(sizeof(SelectedUInt<large>) == sizeof(std::uint16_t));
|
||||
|
||||
TEST(EnumFlags, InitFull) {
|
||||
EnumFlags<full> flags;
|
||||
|
||||
EXPECT_EQ(flags.get(), 0);
|
||||
EXPECT_TRUE(flags.all_reset());
|
||||
EXPECT_FALSE(flags.any_set());
|
||||
EXPECT_FALSE(flags.all_set());
|
||||
|
||||
EXPECT_FALSE(flags.is_set(full::one));
|
||||
EXPECT_FALSE(flags.is_set(full::two));
|
||||
EXPECT_FALSE(flags.is_set(full::three));
|
||||
EXPECT_FALSE(flags.is_set(full::four));
|
||||
EXPECT_FALSE(flags.is_set(full::five));
|
||||
EXPECT_FALSE(flags.is_set(full::six));
|
||||
EXPECT_FALSE(flags.is_set(full::seven));
|
||||
EXPECT_FALSE(flags.is_set(full::eight));
|
||||
|
||||
EXPECT_TRUE(flags.is_reset(full::one));
|
||||
EXPECT_TRUE(flags.is_reset(full::two));
|
||||
EXPECT_TRUE(flags.is_reset(full::three));
|
||||
EXPECT_TRUE(flags.is_reset(full::four));
|
||||
EXPECT_TRUE(flags.is_reset(full::five));
|
||||
EXPECT_TRUE(flags.is_reset(full::six));
|
||||
EXPECT_TRUE(flags.is_reset(full::seven));
|
||||
EXPECT_TRUE(flags.is_reset(full::eight));
|
||||
|
||||
flags.set(full::one);
|
||||
EXPECT_EQ(flags.get(), 1);
|
||||
EXPECT_FALSE(flags.all_reset());
|
||||
EXPECT_TRUE(flags.any_set());
|
||||
EXPECT_FALSE(flags.all_set());
|
||||
|
||||
EXPECT_TRUE(flags.is_set(full::one));
|
||||
EXPECT_FALSE(flags.is_set(full::two));
|
||||
EXPECT_FALSE(flags.is_set(full::three));
|
||||
EXPECT_FALSE(flags.is_set(full::four));
|
||||
EXPECT_FALSE(flags.is_set(full::five));
|
||||
EXPECT_FALSE(flags.is_set(full::six));
|
||||
EXPECT_FALSE(flags.is_set(full::seven));
|
||||
EXPECT_FALSE(flags.is_set(full::eight));
|
||||
|
||||
EXPECT_FALSE(flags.is_reset(full::one));
|
||||
EXPECT_TRUE(flags.is_reset(full::two));
|
||||
EXPECT_TRUE(flags.is_reset(full::three));
|
||||
EXPECT_TRUE(flags.is_reset(full::four));
|
||||
EXPECT_TRUE(flags.is_reset(full::five));
|
||||
EXPECT_TRUE(flags.is_reset(full::six));
|
||||
EXPECT_TRUE(flags.is_reset(full::seven));
|
||||
EXPECT_TRUE(flags.is_reset(full::eight));
|
||||
|
||||
flags.set(full::two, full::three, full::four);
|
||||
EXPECT_EQ(flags.get(), 0b1111);
|
||||
EXPECT_FALSE(flags.all_reset());
|
||||
EXPECT_TRUE(flags.any_set());
|
||||
EXPECT_FALSE(flags.all_set());
|
||||
|
||||
EXPECT_TRUE(flags.is_set(full::one));
|
||||
EXPECT_TRUE(flags.is_set(full::two));
|
||||
EXPECT_TRUE(flags.is_set(full::three));
|
||||
EXPECT_TRUE(flags.is_set(full::four));
|
||||
EXPECT_FALSE(flags.is_set(full::five));
|
||||
EXPECT_FALSE(flags.is_set(full::six));
|
||||
EXPECT_FALSE(flags.is_set(full::seven));
|
||||
EXPECT_FALSE(flags.is_set(full::eight));
|
||||
|
||||
EXPECT_FALSE(flags.is_reset(full::one));
|
||||
EXPECT_FALSE(flags.is_reset(full::two));
|
||||
EXPECT_FALSE(flags.is_reset(full::three));
|
||||
EXPECT_FALSE(flags.is_reset(full::four));
|
||||
EXPECT_TRUE(flags.is_reset(full::five));
|
||||
EXPECT_TRUE(flags.is_reset(full::six));
|
||||
EXPECT_TRUE(flags.is_reset(full::seven));
|
||||
EXPECT_TRUE(flags.is_reset(full::eight));
|
||||
|
||||
flags.set(full::five, full::six, full::seven, full::eight);
|
||||
EXPECT_EQ(flags.get(), 0xff);
|
||||
EXPECT_FALSE(flags.all_reset());
|
||||
EXPECT_TRUE(flags.any_set());
|
||||
EXPECT_TRUE(flags.all_set());
|
||||
|
||||
EXPECT_TRUE(flags.is_set(full::one));
|
||||
EXPECT_TRUE(flags.is_set(full::two));
|
||||
EXPECT_TRUE(flags.is_set(full::three));
|
||||
EXPECT_TRUE(flags.is_set(full::four));
|
||||
EXPECT_TRUE(flags.is_set(full::five));
|
||||
EXPECT_TRUE(flags.is_set(full::six));
|
||||
EXPECT_TRUE(flags.is_set(full::seven));
|
||||
EXPECT_TRUE(flags.is_set(full::eight));
|
||||
|
||||
EXPECT_FALSE(flags.is_reset(full::one));
|
||||
EXPECT_FALSE(flags.is_reset(full::two));
|
||||
EXPECT_FALSE(flags.is_reset(full::three));
|
||||
EXPECT_FALSE(flags.is_reset(full::four));
|
||||
EXPECT_FALSE(flags.is_reset(full::five));
|
||||
EXPECT_FALSE(flags.is_reset(full::six));
|
||||
EXPECT_FALSE(flags.is_reset(full::seven));
|
||||
EXPECT_FALSE(flags.is_reset(full::eight));
|
||||
|
||||
flags.reset(full::one, full::eight);
|
||||
EXPECT_EQ(flags.get(), 0b01111110);
|
||||
EXPECT_FALSE(flags.all_reset());
|
||||
EXPECT_TRUE(flags.any_set());
|
||||
EXPECT_FALSE(flags.all_set());
|
||||
EXPECT_FALSE(flags.is_set(full::one, full::eight));
|
||||
EXPECT_FALSE(flags.is_set(full::one, full::eight, full::five));
|
||||
EXPECT_TRUE(flags.is_set(full::two, full::five, full::seven));
|
||||
|
||||
flags.set(0xfe);
|
||||
EXPECT_EQ(flags.get(), 0b11111110);
|
||||
EXPECT_FALSE(flags.all_reset());
|
||||
EXPECT_TRUE(flags.any_set());
|
||||
EXPECT_FALSE(flags.all_set());
|
||||
|
||||
flags.set(full::one);
|
||||
EXPECT_EQ(flags.get(), 0b11111111);
|
||||
EXPECT_FALSE(flags.all_reset());
|
||||
EXPECT_TRUE(flags.any_set());
|
||||
EXPECT_TRUE(flags.all_set());
|
||||
}
|
||||
|
||||
TEST(EnumFlags, Set) {
|
||||
EnumFlags<small> sflags;
|
||||
EXPECT_TRUE(sflags.all_reset());
|
||||
sflags.set();
|
||||
EXPECT_TRUE(sflags.all_set());
|
||||
EXPECT_EQ(sflags.get(), 0b01111111);
|
||||
|
||||
EnumFlags<full> flags;
|
||||
EXPECT_TRUE(flags.all_reset());
|
||||
EXPECT_FALSE(flags.any_set());
|
||||
|
||||
flags.set(full::one);
|
||||
EXPECT_EQ(flags.get(), 0b1);
|
||||
|
||||
flags.reset();
|
||||
flags.set(full::one, full::two);
|
||||
EXPECT_EQ(flags.get(), 0b11);
|
||||
|
||||
flags.reset();
|
||||
flags.set(full::one, full::two, full::three);
|
||||
EXPECT_EQ(flags.get(), 0b111);
|
||||
|
||||
flags.reset();
|
||||
flags.set(full::one, full::two, full::three, full::four);
|
||||
EXPECT_EQ(flags.get(), 0b1111);
|
||||
}
|
||||
|
||||
TEST(EnumFlags, Reset) {
|
||||
EnumFlags<full> flags;
|
||||
flags.set();
|
||||
EXPECT_TRUE(flags.all_set());
|
||||
EXPECT_FALSE(flags.any_reset());
|
||||
|
||||
flags.reset(full::one);
|
||||
EXPECT_EQ(flags.get(), 0b11111110);
|
||||
|
||||
flags.set();
|
||||
flags.reset(full::one, full::two);
|
||||
EXPECT_EQ(flags.get(), 0b11111100);
|
||||
|
||||
flags.set();
|
||||
flags.reset(full::one, full::two, full::three);
|
||||
EXPECT_EQ(flags.get(), 0b11111000);
|
||||
|
||||
flags.set();
|
||||
flags.reset(full::one, full::two, full::three, full::four);
|
||||
EXPECT_EQ(flags.get(), 0b11110000);
|
||||
}
|
||||
|
||||
TEST(EnumFlags, AnySet) {
|
||||
EnumFlags<full> flags;
|
||||
flags.set(0x7e);
|
||||
EXPECT_EQ(flags.get(), 0b01111110);
|
||||
|
||||
EXPECT_TRUE(flags.is_set(full::two));
|
||||
EXPECT_FALSE(flags.is_set(full::one, full::two));
|
||||
EXPECT_FALSE(flags.is_set(full::two, full::one));
|
||||
EXPECT_FALSE(flags.is_set(full::one, full::two, full::three));
|
||||
EXPECT_FALSE(flags.is_set(full::three, full::two, full::one));
|
||||
|
||||
EXPECT_TRUE(flags.is_any_set(full::one, full::two));
|
||||
EXPECT_TRUE(flags.is_any_set(full::two, full::one));
|
||||
EXPECT_TRUE(flags.is_any_set(full::one, full::two, full::three));
|
||||
EXPECT_TRUE(flags.is_any_set(full::one, full::three, full::two));
|
||||
EXPECT_TRUE(flags.is_any_set(full::three, full::two, full::one));
|
||||
|
||||
EXPECT_FALSE(flags.is_any_set(full::one, full::eight));
|
||||
EXPECT_TRUE(flags.is_any_set(full::eight, full::two, full::one));
|
||||
}
|
||||
|
||||
TEST(EnumFlags, AnyReSet) {
|
||||
EnumFlags<full> flags;
|
||||
flags.set(0x7e);
|
||||
EXPECT_EQ(flags.get(), 0b01111110);
|
||||
|
||||
EXPECT_TRUE(flags.is_reset(full::one));
|
||||
EXPECT_TRUE(flags.is_set(full::two));
|
||||
EXPECT_FALSE(flags.is_reset(full::one, full::two));
|
||||
EXPECT_FALSE(flags.is_reset(full::two, full::one));
|
||||
EXPECT_FALSE(flags.is_reset(full::one, full::two, full::three));
|
||||
EXPECT_FALSE(flags.is_reset(full::three, full::two, full::one));
|
||||
|
||||
EXPECT_TRUE(flags.is_any_reset(full::one, full::two));
|
||||
EXPECT_TRUE(flags.is_any_reset(full::two, full::one));
|
||||
EXPECT_TRUE(flags.is_any_reset(full::one, full::two, full::three));
|
||||
EXPECT_TRUE(flags.is_any_reset(full::three, full::two, full::one));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
435
tools/EVerest-main/lib/everest/util/tests/fsm/fsm_tests.cpp
Normal file
435
tools/EVerest-main/lib/everest/util/tests/fsm/fsm_tests.cpp
Normal file
@@ -0,0 +1,435 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <everest/util/fsm/fsm.hpp>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// --- Minimal test state infrastructure ---
|
||||
|
||||
enum class StateID {
|
||||
A,
|
||||
B,
|
||||
C,
|
||||
};
|
||||
|
||||
enum class Event {
|
||||
GoToB,
|
||||
GoToC,
|
||||
Stay,
|
||||
Unknown,
|
||||
};
|
||||
|
||||
struct TestState;
|
||||
using TestStatePtr = std::unique_ptr<TestState>;
|
||||
|
||||
struct FeedResult {
|
||||
FeedResult() = default;
|
||||
FeedResult(bool handled) : unhandled(!handled) {
|
||||
}
|
||||
FeedResult(TestStatePtr result_state) : unhandled(false), new_state(std::move(result_state)) {
|
||||
}
|
||||
|
||||
bool unhandled{true};
|
||||
TestStatePtr new_state{nullptr};
|
||||
};
|
||||
|
||||
struct TestState {
|
||||
using ContainerType = TestStatePtr;
|
||||
using EventType = Event;
|
||||
|
||||
TestState(StateID id, std::vector<std::string>& log) : m_id(id), m_log(log) {
|
||||
}
|
||||
|
||||
virtual ~TestState() = default;
|
||||
|
||||
StateID get_id() const {
|
||||
return m_id;
|
||||
}
|
||||
|
||||
virtual void enter() {
|
||||
m_log.push_back("enter:" + state_name());
|
||||
}
|
||||
|
||||
virtual FeedResult feed(Event ev) = 0;
|
||||
|
||||
virtual void leave() {
|
||||
m_log.push_back("leave:" + state_name());
|
||||
}
|
||||
|
||||
std::string state_name() const {
|
||||
switch (m_id) {
|
||||
case StateID::A:
|
||||
return "A";
|
||||
case StateID::B:
|
||||
return "B";
|
||||
case StateID::C:
|
||||
return "C";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
protected:
|
||||
StateID m_id;
|
||||
std::vector<std::string>& m_log;
|
||||
};
|
||||
|
||||
struct StateA : TestState {
|
||||
StateA(std::vector<std::string>& log) : TestState(StateID::A, log) {
|
||||
}
|
||||
|
||||
FeedResult feed(Event ev) override;
|
||||
};
|
||||
|
||||
struct StateB : TestState {
|
||||
StateB(std::vector<std::string>& log) : TestState(StateID::B, log) {
|
||||
}
|
||||
|
||||
FeedResult feed(Event ev) override {
|
||||
if (ev == Event::Stay) {
|
||||
return FeedResult(true);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
FeedResult StateA::feed(Event ev) {
|
||||
if (ev == Event::GoToB) {
|
||||
return FeedResult(std::make_unique<StateB>(m_log));
|
||||
}
|
||||
if (ev == Event::Stay) {
|
||||
return FeedResult(true);
|
||||
}
|
||||
return {}; // unhandled
|
||||
}
|
||||
|
||||
// --- FSM Tests ---
|
||||
|
||||
TEST(FsmV2Test, ConstructionCallsEnter) {
|
||||
std::vector<std::string> log;
|
||||
{
|
||||
fsm::v2::FSM<TestState> fsm(std::make_unique<StateA>(log));
|
||||
EXPECT_EQ(log.size(), 1u);
|
||||
EXPECT_EQ(log[0], "enter:A");
|
||||
}
|
||||
}
|
||||
|
||||
TEST(FsmV2Test, DestructionCallsLeave) {
|
||||
std::vector<std::string> log;
|
||||
{
|
||||
fsm::v2::FSM<TestState> fsm(std::make_unique<StateA>(log));
|
||||
log.clear();
|
||||
}
|
||||
EXPECT_EQ(log.size(), 1u);
|
||||
EXPECT_EQ(log[0], "leave:A");
|
||||
}
|
||||
|
||||
TEST(FsmV2Test, FeedNoTransition) {
|
||||
std::vector<std::string> log;
|
||||
fsm::v2::FSM<TestState> fsm(std::make_unique<StateA>(log));
|
||||
|
||||
auto result = fsm.feed(Event::Stay);
|
||||
EXPECT_TRUE(result);
|
||||
EXPECT_FALSE(result.transitioned());
|
||||
EXPECT_EQ(fsm.get_current_state_id(), StateID::A);
|
||||
}
|
||||
|
||||
TEST(FsmV2Test, FeedWithTransition) {
|
||||
std::vector<std::string> log;
|
||||
fsm::v2::FSM<TestState> fsm(std::make_unique<StateA>(log));
|
||||
log.clear();
|
||||
|
||||
auto result = fsm.feed(Event::GoToB);
|
||||
EXPECT_TRUE(result);
|
||||
EXPECT_TRUE(result.transitioned());
|
||||
EXPECT_EQ(fsm.get_current_state_id(), StateID::B);
|
||||
|
||||
ASSERT_EQ(log.size(), 2u);
|
||||
EXPECT_EQ(log[0], "leave:A");
|
||||
EXPECT_EQ(log[1], "enter:B");
|
||||
}
|
||||
|
||||
TEST(FsmV2Test, UnhandledEvent) {
|
||||
std::vector<std::string> log;
|
||||
fsm::v2::FSM<TestState> fsm(std::make_unique<StateA>(log));
|
||||
|
||||
auto result = fsm.feed(Event::Unknown);
|
||||
EXPECT_FALSE(result); // FeedResult is falsy for unhandled
|
||||
EXPECT_FALSE(result.transitioned());
|
||||
EXPECT_EQ(fsm.get_current_state_id(), StateID::A);
|
||||
}
|
||||
|
||||
TEST(FsmV2Test, GetCurrentStateId) {
|
||||
std::vector<std::string> log;
|
||||
fsm::v2::FSM<TestState> fsm(std::make_unique<StateA>(log));
|
||||
EXPECT_EQ(fsm.get_current_state_id(), StateID::A);
|
||||
|
||||
fsm.feed(Event::GoToB);
|
||||
EXPECT_EQ(fsm.get_current_state_id(), StateID::B);
|
||||
}
|
||||
|
||||
// --- FeedResult with output ---
|
||||
|
||||
struct OutputState;
|
||||
using OutputStatePtr = std::unique_ptr<OutputState>;
|
||||
|
||||
struct OutputFeedResult {
|
||||
OutputFeedResult() = default;
|
||||
OutputFeedResult(bool handled) : unhandled(!handled) {
|
||||
}
|
||||
OutputFeedResult(int output_value, bool handled) : unhandled(!handled), output(output_value) {
|
||||
}
|
||||
OutputFeedResult(OutputStatePtr result_state) : unhandled(false), new_state(std::move(result_state)) {
|
||||
}
|
||||
OutputFeedResult(OutputStatePtr result_state, int output_value) :
|
||||
unhandled(false), new_state(std::move(result_state)), output(output_value) {
|
||||
}
|
||||
|
||||
bool unhandled{true};
|
||||
OutputStatePtr new_state{nullptr};
|
||||
int output{0};
|
||||
};
|
||||
|
||||
struct OutputState {
|
||||
using ContainerType = OutputStatePtr;
|
||||
using EventType = Event;
|
||||
|
||||
OutputState(StateID id) : m_id(id) {
|
||||
}
|
||||
|
||||
virtual ~OutputState() = default;
|
||||
|
||||
StateID get_id() const {
|
||||
return m_id;
|
||||
}
|
||||
|
||||
virtual void enter() {
|
||||
}
|
||||
|
||||
virtual OutputFeedResult feed(Event ev) = 0;
|
||||
|
||||
virtual void leave() {
|
||||
}
|
||||
|
||||
protected:
|
||||
StateID m_id;
|
||||
};
|
||||
|
||||
struct OutputStateA : OutputState {
|
||||
OutputStateA() : OutputState(StateID::A) {
|
||||
}
|
||||
|
||||
OutputFeedResult feed(Event ev) override {
|
||||
if (ev == Event::Stay) {
|
||||
return OutputFeedResult(42, true);
|
||||
}
|
||||
if (ev == Event::GoToB) {
|
||||
return OutputFeedResult(std::make_unique<OutputStateA>(), 99);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
TEST(FsmV2Test, FeedResultWithOutput) {
|
||||
fsm::v2::FSM<OutputState> fsm(std::make_unique<OutputStateA>());
|
||||
|
||||
auto result = fsm.feed(Event::Stay);
|
||||
EXPECT_TRUE(result);
|
||||
EXPECT_FALSE(result.transitioned());
|
||||
EXPECT_EQ(result.output, 42);
|
||||
}
|
||||
|
||||
TEST(FsmV2Test, FeedResultWithOutputOnTransition) {
|
||||
fsm::v2::FSM<OutputState> fsm(std::make_unique<OutputStateA>());
|
||||
|
||||
auto result = fsm.feed(Event::GoToB);
|
||||
EXPECT_TRUE(result);
|
||||
EXPECT_TRUE(result.transitioned());
|
||||
EXPECT_EQ(result.output, 99);
|
||||
}
|
||||
|
||||
TEST(FsmV2Test, FeedResultVoidUnhandled) {
|
||||
std::vector<std::string> log;
|
||||
fsm::v2::FSM<TestState> fsm(std::make_unique<StateA>(log));
|
||||
|
||||
auto result = fsm.feed(Event::Unknown);
|
||||
// FeedResult<void> — no .output member, just check bool and transitioned
|
||||
EXPECT_FALSE(result);
|
||||
EXPECT_FALSE(result.transitioned());
|
||||
}
|
||||
|
||||
// --- NestedFSM tests ---
|
||||
|
||||
struct NestedState;
|
||||
using NestedStatePtr = std::unique_ptr<NestedState>;
|
||||
|
||||
struct NestedFeedResult {
|
||||
NestedFeedResult() = default;
|
||||
NestedFeedResult(bool handled) : unhandled(!handled) {
|
||||
}
|
||||
NestedFeedResult(NestedStatePtr result_state) : unhandled(false), new_state(std::move(result_state)) {
|
||||
}
|
||||
|
||||
bool unhandled{true};
|
||||
NestedStatePtr new_state{nullptr};
|
||||
};
|
||||
|
||||
struct NestedState {
|
||||
using ContainerType = NestedStatePtr;
|
||||
using EventType = Event;
|
||||
|
||||
NestedState(StateID id, std::vector<std::string>& log) : m_id(id), m_log(log) {
|
||||
}
|
||||
|
||||
virtual ~NestedState() = default;
|
||||
|
||||
StateID get_id() const {
|
||||
return m_id;
|
||||
}
|
||||
|
||||
virtual void enter() {
|
||||
m_log.push_back("enter:" + state_name());
|
||||
}
|
||||
|
||||
virtual NestedFeedResult feed(Event ev) = 0;
|
||||
|
||||
virtual void leave() {
|
||||
m_log.push_back("leave:" + state_name());
|
||||
}
|
||||
|
||||
virtual NestedStatePtr get_initial() {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::string state_name() const {
|
||||
switch (m_id) {
|
||||
case StateID::A:
|
||||
return "A";
|
||||
case StateID::B:
|
||||
return "B";
|
||||
case StateID::C:
|
||||
return "C";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
protected:
|
||||
StateID m_id;
|
||||
std::vector<std::string>& m_log;
|
||||
};
|
||||
|
||||
// ChildB is a leaf child of ParentA
|
||||
struct ChildB : NestedState {
|
||||
ChildB(std::vector<std::string>& log) : NestedState(StateID::B, log) {
|
||||
}
|
||||
|
||||
NestedFeedResult feed(Event ev) override {
|
||||
if (ev == Event::Stay) {
|
||||
return NestedFeedResult(true);
|
||||
}
|
||||
return {}; // bubble up to parent
|
||||
}
|
||||
};
|
||||
|
||||
// ParentA has ChildB as initial child state
|
||||
struct ParentA : NestedState {
|
||||
ParentA(std::vector<std::string>& log) : NestedState(StateID::A, log) {
|
||||
}
|
||||
|
||||
NestedFeedResult feed(Event ev) override {
|
||||
if (ev == Event::GoToC) {
|
||||
// Transition to a new state C (leaf, no children)
|
||||
return NestedFeedResult(std::make_unique<LeafC>(m_log));
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
NestedStatePtr get_initial() override {
|
||||
return std::make_unique<ChildB>(m_log);
|
||||
}
|
||||
|
||||
struct LeafC : NestedState {
|
||||
LeafC(std::vector<std::string>& log) : NestedState(StateID::C, log) {
|
||||
}
|
||||
|
||||
NestedFeedResult feed(Event) override {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
TEST(NestedFsmV2Test, ConstructionUnrollsChildren) {
|
||||
std::vector<std::string> log;
|
||||
{
|
||||
fsm::v2::NestedFSM<NestedState> fsm(std::make_unique<ParentA>(log));
|
||||
// Should enter ParentA, then enter ChildB
|
||||
ASSERT_EQ(log.size(), 2u);
|
||||
EXPECT_EQ(log[0], "enter:A");
|
||||
EXPECT_EQ(log[1], "enter:B");
|
||||
}
|
||||
}
|
||||
|
||||
TEST(NestedFsmV2Test, DestructionLeavesAllStates) {
|
||||
std::vector<std::string> log;
|
||||
{
|
||||
fsm::v2::NestedFSM<NestedState> fsm(std::make_unique<ParentA>(log));
|
||||
log.clear();
|
||||
}
|
||||
// Should leave ChildB then ParentA
|
||||
ASSERT_EQ(log.size(), 2u);
|
||||
EXPECT_EQ(log[0], "leave:B");
|
||||
EXPECT_EQ(log[1], "leave:A");
|
||||
}
|
||||
|
||||
TEST(NestedFsmV2Test, LeafHandlesEvent) {
|
||||
std::vector<std::string> log;
|
||||
fsm::v2::NestedFSM<NestedState> fsm(std::make_unique<ParentA>(log));
|
||||
|
||||
auto result = fsm.feed(Event::Stay);
|
||||
EXPECT_TRUE(result);
|
||||
EXPECT_FALSE(result.transitioned());
|
||||
}
|
||||
|
||||
TEST(NestedFsmV2Test, EventBubblesToParent) {
|
||||
std::vector<std::string> log;
|
||||
fsm::v2::NestedFSM<NestedState> fsm(std::make_unique<ParentA>(log));
|
||||
log.clear();
|
||||
|
||||
// GoToC is unhandled by ChildB, bubbles to ParentA which transitions
|
||||
auto result = fsm.feed(Event::GoToC);
|
||||
EXPECT_TRUE(result);
|
||||
EXPECT_TRUE(result.transitioned());
|
||||
|
||||
// ChildB leave, ParentA leave (popped off stack), then new LeafC enter
|
||||
ASSERT_EQ(log.size(), 3u);
|
||||
EXPECT_EQ(log[0], "leave:B");
|
||||
EXPECT_EQ(log[1], "leave:A");
|
||||
EXPECT_EQ(log[2], "enter:C");
|
||||
}
|
||||
|
||||
TEST(NestedFsmV2Test, UnhandledByAll) {
|
||||
std::vector<std::string> log;
|
||||
fsm::v2::NestedFSM<NestedState> fsm(std::make_unique<ParentA>(log));
|
||||
|
||||
auto result = fsm.feed(Event::Unknown);
|
||||
EXPECT_FALSE(result);
|
||||
}
|
||||
|
||||
TEST(NestedFsmV2Test, GetCurrentStateIdReturnsFullPath) {
|
||||
std::vector<std::string> log;
|
||||
fsm::v2::NestedFSM<NestedState> fsm(std::make_unique<ParentA>(log));
|
||||
|
||||
auto ids = fsm.get_current_state_id();
|
||||
ASSERT_EQ(ids.size(), 2u);
|
||||
EXPECT_EQ(ids[0], StateID::A);
|
||||
EXPECT_EQ(ids[1], StateID::B);
|
||||
|
||||
fsm.feed(Event::GoToC);
|
||||
ids = fsm.get_current_state_id();
|
||||
ASSERT_EQ(ids.size(), 1u);
|
||||
EXPECT_EQ(ids[0], StateID::C);
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
#include <everest/util/math/comparison.hpp>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
namespace everest::lib::util {
|
||||
|
||||
class ComparisonTest : public ::testing::Test {};
|
||||
|
||||
// --- Floating Point & almost_eq Tests ---
|
||||
|
||||
TEST_F(ComparisonTest, RangeLimit) {
|
||||
EXPECT_NEAR(range_limit<double>(1), 0.1, 1e-9);
|
||||
EXPECT_NEAR(range_limit<double>(3), 0.001, 1e-9);
|
||||
EXPECT_EQ(range_limit<double>(0), 1.0);
|
||||
EXPECT_DOUBLE_EQ(range_limit<double>(-1), 10.0);
|
||||
EXPECT_DOUBLE_EQ(range_limit<double>(-2), 100.0);
|
||||
EXPECT_DOUBLE_EQ(range_limit<double>(-3), 1000.0);
|
||||
}
|
||||
|
||||
TEST_F(ComparisonTest, AlmostEqBasic) {
|
||||
// 3 digits of precision = 0.001 threshold
|
||||
EXPECT_TRUE((almost_eq<3>(1.0001, 1.0002)));
|
||||
EXPECT_FALSE((almost_eq<3>(1.0, 1.002)));
|
||||
}
|
||||
|
||||
TEST_F(ComparisonTest, AlmostEqNegativePrecision) {
|
||||
// -2 digits of precision = 100.0 threshold
|
||||
EXPECT_TRUE((almost_eq<-2>(207.0, 250.0)));
|
||||
EXPECT_FALSE((almost_eq<-2>(100.0, 250.0)));
|
||||
|
||||
// -1 digit of precision = 10.0 threshold
|
||||
EXPECT_TRUE((almost_eq<-1>(15.0, 22.0))); // diff 7 < 10
|
||||
EXPECT_FALSE((almost_eq<-1>(15.0, 28.0))); // diff 13 > 10
|
||||
}
|
||||
|
||||
TEST_F(ComparisonTest, AlmostEqOptional) {
|
||||
std::optional<double> a = 1.0001;
|
||||
std::optional<double> b = 1.0002;
|
||||
std::optional<double> empty;
|
||||
|
||||
EXPECT_TRUE(almost_eq<3>(a, b));
|
||||
EXPECT_TRUE(almost_eq<3>(empty, empty));
|
||||
EXPECT_FALSE(almost_eq<3>(a, empty));
|
||||
}
|
||||
|
||||
// --- Min/Max Optional Tests ---
|
||||
|
||||
TEST_F(ComparisonTest, MinOptional) {
|
||||
std::optional<float> low = 10.0f;
|
||||
std::optional<float> high = 20.0f;
|
||||
std::optional<float> empty;
|
||||
|
||||
// Optional & Optional
|
||||
EXPECT_EQ(min_optional(low, high).value(), 10.0f);
|
||||
EXPECT_EQ(min_optional(low, empty).value(), 10.0f);
|
||||
EXPECT_FALSE(min_optional(empty, empty).has_value());
|
||||
|
||||
// Value & Optional
|
||||
EXPECT_EQ(min_optional(15.0f, high), 15.0f);
|
||||
EXPECT_EQ(min_optional(25.0f, high), 20.0f);
|
||||
EXPECT_EQ(min_optional(25.0f, empty), 25.0f);
|
||||
}
|
||||
|
||||
TEST_F(ComparisonTest, MaxOptional) {
|
||||
std::optional<float> low = 10.0f;
|
||||
std::optional<float> empty;
|
||||
|
||||
EXPECT_EQ(max_optional(low, 5.0f), 10.0f);
|
||||
EXPECT_EQ(max_optional(low, 15.0f), 15.0f);
|
||||
EXPECT_EQ(max_optional(empty, 15.0f), 15.0f);
|
||||
}
|
||||
|
||||
// --- Clamping Tests ---
|
||||
|
||||
TEST_F(ComparisonTest, ClampOptional) {
|
||||
std::optional<double> min_limit = 10.0;
|
||||
std::optional<double> max_limit = 20.0;
|
||||
std::optional<double> no_limit;
|
||||
|
||||
// Inside range
|
||||
EXPECT_DOUBLE_EQ(clamp_optional(15.0, min_limit, max_limit), 15.0);
|
||||
|
||||
// Underflow
|
||||
EXPECT_DOUBLE_EQ(clamp_optional(5.0, min_limit, max_limit), 10.0);
|
||||
|
||||
// Overflow
|
||||
EXPECT_DOUBLE_EQ(clamp_optional(25.0, min_limit, max_limit), 20.0);
|
||||
|
||||
// One-sided clamping
|
||||
EXPECT_DOUBLE_EQ(clamp_optional(5.0, no_limit, max_limit), 5.0);
|
||||
EXPECT_DOUBLE_EQ(clamp_optional(25.0, min_limit, no_limit), 25.0);
|
||||
|
||||
// No limits
|
||||
EXPECT_DOUBLE_EQ(clamp_optional(100.0, no_limit, no_limit), 100.0);
|
||||
}
|
||||
|
||||
// --- Noise Range Tests ---
|
||||
|
||||
TEST_F(ComparisonTest, InNoiseRange) {
|
||||
EXPECT_TRUE(in_noise_range(10.0, 10.05, 0.1));
|
||||
EXPECT_FALSE(in_noise_range(10.0, 10.11, 0.1));
|
||||
// Exact boundary
|
||||
EXPECT_TRUE(in_noise_range(10.0, 10.1, 0.1));
|
||||
}
|
||||
|
||||
} // namespace everest::lib::util
|
||||
@@ -0,0 +1,172 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include <everest/util/queue/simple_queue.hpp>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
using namespace everest::lib::util;
|
||||
|
||||
// =================================================================
|
||||
// 2. Test Fixture Setup
|
||||
// =================================================================
|
||||
|
||||
template <typename T> class SimpleQueueTest : public ::testing::Test {
|
||||
protected:
|
||||
simple_queue<T> queue;
|
||||
};
|
||||
|
||||
// Typed Test Suite for standard types
|
||||
using QueueTypes = ::testing::Types<int, std::string>;
|
||||
TYPED_TEST_SUITE(SimpleQueueTest, QueueTypes);
|
||||
|
||||
// =================================================================
|
||||
// A. Basic Functionality Tests (FIFO & Empty Checks)
|
||||
// =================================================================
|
||||
|
||||
TYPED_TEST(SimpleQueueTest, InitialStateIsEmpty) {
|
||||
ASSERT_TRUE(this->queue.empty());
|
||||
ASSERT_EQ(this->queue.size(), 0);
|
||||
ASSERT_FALSE(this->queue.pop().has_value());
|
||||
}
|
||||
|
||||
TYPED_TEST(SimpleQueueTest, PushAndEmptyCheck) {
|
||||
TypeParam value;
|
||||
|
||||
// Use if constexpr to initialize value correctly
|
||||
if constexpr (std::is_same_v<TypeParam, int>) {
|
||||
value = 10;
|
||||
} else if constexpr (std::is_same_v<TypeParam, std::string>) {
|
||||
value = "Test_10";
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
this->queue.push(value);
|
||||
|
||||
ASSERT_FALSE(this->queue.empty());
|
||||
ASSERT_EQ(this->queue.size(), 1);
|
||||
}
|
||||
|
||||
TYPED_TEST(SimpleQueueTest, PushPopAndEmptyCheck) {
|
||||
TypeParam expected_value;
|
||||
|
||||
// Use if constexpr to initialize value correctly
|
||||
if constexpr (std::is_same_v<TypeParam, int>) {
|
||||
expected_value = 42;
|
||||
} else if constexpr (std::is_same_v<TypeParam, std::string>) {
|
||||
expected_value = "Test_42";
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
this->queue.push(expected_value);
|
||||
|
||||
std::optional<TypeParam> result = this->queue.pop();
|
||||
|
||||
ASSERT_TRUE(result.has_value());
|
||||
ASSERT_EQ(result.value(), expected_value);
|
||||
ASSERT_TRUE(this->queue.empty());
|
||||
ASSERT_EQ(this->queue.size(), 0);
|
||||
}
|
||||
|
||||
TYPED_TEST(SimpleQueueTest, MultiplePushAndPopOrder) {
|
||||
const int count = 3;
|
||||
|
||||
// Push elements (0, 1, 2)
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if constexpr (std::is_same_v<TypeParam, int>) {
|
||||
this->queue.push(i);
|
||||
} else {
|
||||
this->queue.push(std::to_string(i));
|
||||
}
|
||||
}
|
||||
|
||||
// Pop elements and verify FIFO order (0, 1, 2)
|
||||
for (int i = 0; i < count; ++i) {
|
||||
std::optional<TypeParam> result = this->queue.pop();
|
||||
TypeParam expected_value;
|
||||
if constexpr (std::is_same_v<TypeParam, int>) {
|
||||
expected_value = i;
|
||||
} else {
|
||||
expected_value = std::to_string(i);
|
||||
}
|
||||
|
||||
ASSERT_TRUE(result.has_value());
|
||||
ASSERT_EQ(result.value(), expected_value) << "Element popped out of FIFO order.";
|
||||
}
|
||||
|
||||
ASSERT_TRUE(this->queue.empty());
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// B. Reference Tests (front() and back())
|
||||
// =================================================================
|
||||
|
||||
TYPED_TEST(SimpleQueueTest, FrontAndBackReferences) {
|
||||
TypeParam val1, val2;
|
||||
|
||||
if constexpr (std::is_same_v<TypeParam, int>) {
|
||||
val1 = 100;
|
||||
val2 = 200;
|
||||
} else if constexpr (std::is_same_v<TypeParam, std::string>) {
|
||||
val1 = "Front";
|
||||
val2 = "Back";
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
this->queue.push(val1);
|
||||
this->queue.push(val2);
|
||||
|
||||
// Verify front()
|
||||
ASSERT_EQ(this->queue.front(), val1);
|
||||
|
||||
// Verify back()
|
||||
ASSERT_EQ(this->queue.back(), val2);
|
||||
|
||||
// After pop, front should change
|
||||
this->queue.pop();
|
||||
ASSERT_EQ(this->queue.front(), val2);
|
||||
ASSERT_EQ(this->queue.back(), val2);
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// C. Move-Only Type Compatibility Test (Verifying the pop() fix)
|
||||
// =================================================================
|
||||
|
||||
// Test suite for std::unique_ptr<int> (a move-only type)
|
||||
class SimpleQueueMoveOnlyTest : public ::testing::Test {
|
||||
protected:
|
||||
simple_queue<std::unique_ptr<int>> queue;
|
||||
};
|
||||
|
||||
TEST_F(SimpleQueueMoveOnlyTest, PushAndPopMoveOnlyType) {
|
||||
const int value1 = 10;
|
||||
const int value2 = 20;
|
||||
|
||||
// Push: Requires the r-value push overload
|
||||
this->queue.push(std::make_unique<int>(value1));
|
||||
this->queue.push(std::make_unique<int>(value2));
|
||||
|
||||
ASSERT_EQ(this->queue.size(), 2);
|
||||
|
||||
// Pop: Requires the fixed move-based pop()
|
||||
std::optional<std::unique_ptr<int>> opt_result1 = this->queue.pop();
|
||||
|
||||
// Verify the value was retrieved
|
||||
ASSERT_TRUE(opt_result1.has_value());
|
||||
ASSERT_NE(opt_result1.value(), nullptr);
|
||||
ASSERT_EQ(*opt_result1.value(), value1);
|
||||
|
||||
// Pop the second item
|
||||
std::optional<std::unique_ptr<int>> opt_result2 = this->queue.pop();
|
||||
ASSERT_TRUE(opt_result2.has_value());
|
||||
ASSERT_EQ(*opt_result2.value(), value2);
|
||||
|
||||
ASSERT_TRUE(this->queue.empty());
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <everest/util/queue/thread_safe_bounded_queue.hpp>
|
||||
#include <optional>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
using namespace everest::lib::util;
|
||||
|
||||
/**
|
||||
* @brief Helper struct to mimic the TrackedAction used in the thread pool,
|
||||
* as the queue now expects a type with an .arrival member.
|
||||
*/
|
||||
struct TestTask {
|
||||
int value;
|
||||
std::chrono::steady_clock::time_point arrival;
|
||||
|
||||
explicit TestTask(int v = 0) : value(v), arrival(std::chrono::steady_clock::now()) {
|
||||
}
|
||||
};
|
||||
|
||||
// =================================================================
|
||||
// 1. Bounded Functionality Tests (Backpressure)
|
||||
// =================================================================
|
||||
|
||||
TEST(ThreadSafeBoundedQueueTest, PushBlocksWhenFull) {
|
||||
const size_t limit = 2;
|
||||
thread_safe_bounded_queue<TestTask> queue(limit);
|
||||
|
||||
// Fill the queue to the limit
|
||||
queue.push(TestTask(1));
|
||||
queue.push(TestTask(2));
|
||||
|
||||
std::atomic<bool> push_completed{false};
|
||||
std::thread producer([&] {
|
||||
// This should block until a consumer pops an item
|
||||
queue.push(TestTask(3));
|
||||
push_completed = true;
|
||||
});
|
||||
|
||||
// Give the thread a moment to start and block
|
||||
std::this_thread::sleep_for(50ms);
|
||||
ASSERT_FALSE(push_completed.load());
|
||||
|
||||
// Pop an item, which should unblock the producer
|
||||
auto popped = queue.try_pop(100ms);
|
||||
ASSERT_TRUE(popped.has_value());
|
||||
ASSERT_EQ(popped->value, 1);
|
||||
|
||||
producer.join();
|
||||
ASSERT_TRUE(push_completed.load());
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 2. Latency Interface Tests
|
||||
// =================================================================
|
||||
|
||||
TEST(ThreadSafeBoundedQueueTest, OldestArrivalTracking) {
|
||||
thread_safe_bounded_queue<TestTask> queue(10);
|
||||
|
||||
auto t1 = std::chrono::steady_clock::now();
|
||||
queue.push(TestTask(100));
|
||||
std::this_thread::sleep_for(10ms);
|
||||
|
||||
auto t2 = std::chrono::steady_clock::now();
|
||||
queue.push(TestTask(200));
|
||||
|
||||
auto oldest = queue.oldest_arrival();
|
||||
|
||||
// The oldest arrival should be close to t1, certainly before t2
|
||||
ASSERT_GE(oldest, t1);
|
||||
ASSERT_LT(oldest, t2);
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 3. Stop and Signaling Tests
|
||||
// =================================================================
|
||||
|
||||
TEST(ThreadSafeBoundedQueueTest, StopUnblocksBlockedProducers) {
|
||||
thread_safe_bounded_queue<TestTask> queue(1);
|
||||
queue.push(TestTask(1)); // Fill it
|
||||
|
||||
std::atomic<bool> producer_exited{false};
|
||||
std::thread producer([&] {
|
||||
// This blocks because queue is full
|
||||
size_t result = queue.push(TestTask(2));
|
||||
// result should be 0 because the queue was stopped
|
||||
if (result == 0) {
|
||||
producer_exited = true;
|
||||
}
|
||||
});
|
||||
|
||||
std::this_thread::sleep_for(50ms);
|
||||
queue.stop(); // This should wake the producer up
|
||||
|
||||
producer.join();
|
||||
ASSERT_TRUE(producer_exited.load());
|
||||
}
|
||||
|
||||
TEST(ThreadSafeBoundedQueueTest, StopReturnsNullOptToConsumers) {
|
||||
thread_safe_bounded_queue<TestTask> queue(5);
|
||||
|
||||
std::thread consumer([&] {
|
||||
auto result = queue.try_pop(1s);
|
||||
ASSERT_FALSE(result.has_value());
|
||||
});
|
||||
|
||||
std::this_thread::sleep_for(20ms);
|
||||
queue.stop();
|
||||
consumer.join();
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 4. Stress Tests (Concurrent Producers and Consumers)
|
||||
// =================================================================
|
||||
|
||||
TEST(ThreadSafeBoundedQueueTest, HighContentionStressTest) {
|
||||
const int num_producers = 4;
|
||||
const int num_consumers = 4;
|
||||
const int items_per_producer = 1000;
|
||||
const size_t queue_limit = 10;
|
||||
|
||||
thread_safe_bounded_queue<TestTask> queue(queue_limit);
|
||||
std::atomic<int> total_popped{0};
|
||||
std::atomic<int> sum_popped{0};
|
||||
|
||||
std::vector<std::thread> workers;
|
||||
|
||||
// Consumers
|
||||
for (int i = 0; i < num_consumers; ++i) {
|
||||
workers.emplace_back([&] {
|
||||
while (total_popped < (num_producers * items_per_producer)) {
|
||||
auto val = queue.try_pop(10ms);
|
||||
if (val) {
|
||||
sum_popped += val->value;
|
||||
total_popped++;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Producers
|
||||
for (int i = 0; i < num_producers; ++i) {
|
||||
workers.emplace_back([&] {
|
||||
for (int j = 0; j < items_per_producer; ++j) {
|
||||
queue.push(TestTask(1));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (auto& w : workers)
|
||||
w.join();
|
||||
|
||||
ASSERT_EQ(total_popped.load(), num_producers * items_per_producer);
|
||||
ASSERT_EQ(sum_popped.load(), num_producers * items_per_producer);
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <everest/util/queue/thread_safe_queue.hpp>
|
||||
#include <memory> // For std::unique_ptr
|
||||
#include <numeric>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <thread>
|
||||
#include <type_traits> // For std::is_same_v
|
||||
#include <vector>
|
||||
|
||||
// Note: Add includes for your simple_queue and thread_safe_queue here
|
||||
// #include "simple_queue.h"
|
||||
// #include "thread_safe_queue.h"
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
using namespace everest::lib::util;
|
||||
|
||||
// =================================================================
|
||||
// 1. Test Fixture Setup
|
||||
// =================================================================
|
||||
|
||||
// Helper to initialize TypeParam correctly in typed tests
|
||||
template <typename T> T initialize_value(int id) {
|
||||
if constexpr (std::is_same_v<T, int>) {
|
||||
return id;
|
||||
} else if constexpr (std::is_same_v<T, std::string>) {
|
||||
return "Value_" + std::to_string(id);
|
||||
} else {
|
||||
// Fallback for other non-tested types
|
||||
return T{};
|
||||
}
|
||||
}
|
||||
|
||||
// Test Fixture
|
||||
template <typename T> class ThreadSafeQueueTest : public ::testing::Test {
|
||||
protected:
|
||||
thread_safe_queue<T> queue;
|
||||
};
|
||||
|
||||
// Typed Test Suite for int and std::string
|
||||
using QueueTypes = ::testing::Types<int, std::string>;
|
||||
TYPED_TEST_SUITE(ThreadSafeQueueTest, QueueTypes);
|
||||
|
||||
// Define a test suite specifically for concurrency checks (using int)
|
||||
using ThreadSafeQueueIntTest = ThreadSafeQueueTest<int>;
|
||||
|
||||
// =================================================================
|
||||
// 2. Basic Functionality Tests (Single Thread)
|
||||
// =================================================================
|
||||
|
||||
TYPED_TEST(ThreadSafeQueueTest, PushAndPopSimple) {
|
||||
TypeParam expected_value = initialize_value<TypeParam>(42);
|
||||
|
||||
this->queue.push(expected_value);
|
||||
|
||||
// Test the non-blocking pop
|
||||
std::optional<TypeParam> result = this->queue.try_pop();
|
||||
|
||||
ASSERT_TRUE(result.has_value());
|
||||
ASSERT_EQ(result.value(), expected_value);
|
||||
ASSERT_FALSE(this->queue.try_pop().has_value());
|
||||
}
|
||||
|
||||
TYPED_TEST(ThreadSafeQueueTest, MultiplePushAndPopOrder) {
|
||||
const int count = 5;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
this->queue.push(initialize_value<TypeParam>(i));
|
||||
}
|
||||
|
||||
for (int i = 0; i < count; ++i) {
|
||||
TypeParam expected_value = initialize_value<TypeParam>(i);
|
||||
std::optional<TypeParam> result = this->queue.try_pop();
|
||||
|
||||
ASSERT_TRUE(result.has_value());
|
||||
ASSERT_EQ(result.value(), expected_value);
|
||||
}
|
||||
ASSERT_FALSE(this->queue.try_pop().has_value());
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 3. Time-Based Functionality Tests
|
||||
// =================================================================
|
||||
|
||||
TYPED_TEST(ThreadSafeQueueTest, TryPopWithTimeout_Timeout) {
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
|
||||
// Try to pop with a short timeout
|
||||
std::optional<TypeParam> result = this->queue.try_pop(10ms);
|
||||
|
||||
auto end = std::chrono::steady_clock::now();
|
||||
|
||||
ASSERT_FALSE(result.has_value());
|
||||
|
||||
auto elapsed = end - start;
|
||||
ASSERT_GE(elapsed, 9ms);
|
||||
ASSERT_LE(elapsed, 50ms);
|
||||
}
|
||||
|
||||
TYPED_TEST(ThreadSafeQueueTest, TryPopWithTimeout_ImmediateSuccess) {
|
||||
TypeParam value = initialize_value<TypeParam>(101);
|
||||
this->queue.push(value);
|
||||
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
std::optional<TypeParam> result = this->queue.try_pop(10s);
|
||||
auto end = std::chrono::steady_clock::now();
|
||||
|
||||
ASSERT_TRUE(result.has_value());
|
||||
ASSERT_EQ(result.value(), value);
|
||||
|
||||
ASSERT_LT(end - start, 5ms);
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 4. Synchronization and Blocking Tests
|
||||
// =================================================================
|
||||
|
||||
TEST_F(ThreadSafeQueueIntTest, BlockingPopUnblocksOnPush) {
|
||||
const int expected_value = 123;
|
||||
std::atomic<int> result = 0;
|
||||
|
||||
std::thread consumer([this, &result] {
|
||||
// Blocking pop() call
|
||||
result = this->queue.pop();
|
||||
});
|
||||
|
||||
std::this_thread::sleep_for(100ms);
|
||||
|
||||
this->queue.push(expected_value);
|
||||
|
||||
consumer.join();
|
||||
ASSERT_EQ(result.load(), expected_value);
|
||||
}
|
||||
|
||||
TEST_F(ThreadSafeQueueIntTest, MultipleWaitersUnblockedSequentially) {
|
||||
const int num_waiters = 5;
|
||||
std::vector<std::thread> consumers;
|
||||
std::atomic<int> pops_received = 0;
|
||||
|
||||
for (int i = 0; i < num_waiters; ++i) {
|
||||
consumers.emplace_back([this, &pops_received] {
|
||||
this->queue.pop();
|
||||
pops_received++;
|
||||
});
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(100ms);
|
||||
|
||||
// Push exactly the number of waiters—only one waiter should be released per push
|
||||
for (int i = 0; i < num_waiters; ++i) {
|
||||
this->queue.push(i);
|
||||
}
|
||||
|
||||
for (auto& t : consumers) {
|
||||
t.join();
|
||||
}
|
||||
|
||||
ASSERT_EQ(pops_received.load(), num_waiters);
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 5. Stress and Race Condition Tests
|
||||
// =================================================================
|
||||
|
||||
TEST_F(ThreadSafeQueueIntTest, ConcurrentPushConsistency) {
|
||||
const int num_producers = 10;
|
||||
const int items_per_producer = 1000;
|
||||
const int total_items = num_producers * items_per_producer;
|
||||
|
||||
std::vector<std::thread> producers;
|
||||
std::set<int> expected_values;
|
||||
|
||||
for (int i = 0; i < num_producers; ++i) {
|
||||
producers.emplace_back([this, i, items_per_producer] {
|
||||
int start_value = i * items_per_producer;
|
||||
for (int j = 0; j < items_per_producer; ++j) {
|
||||
this->queue.push(start_value + j);
|
||||
}
|
||||
});
|
||||
int start_value = i * items_per_producer;
|
||||
for (int j = 0; j < items_per_producer; ++j) {
|
||||
expected_values.insert(start_value + j);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& t : producers) {
|
||||
t.join();
|
||||
}
|
||||
|
||||
// Drain the queue and check for consistency
|
||||
std::set<int> retrieved_values;
|
||||
for (int i = 0; i < total_items; ++i) {
|
||||
auto val = this->queue.pop();
|
||||
retrieved_values.insert(val);
|
||||
}
|
||||
|
||||
ASSERT_EQ(retrieved_values.size(), total_items);
|
||||
ASSERT_EQ(retrieved_values, expected_values);
|
||||
}
|
||||
|
||||
TEST_F(ThreadSafeQueueIntTest, ConcurrentPopNoDuplicate) {
|
||||
const int total_items = 10000;
|
||||
const int num_consumers = 10;
|
||||
|
||||
// Producer pushes all items
|
||||
for (int i = 0; i < total_items; ++i) {
|
||||
this->queue.push(i);
|
||||
}
|
||||
|
||||
// Consumers pop concurrently
|
||||
std::vector<std::thread> consumers;
|
||||
std::mutex result_mtx;
|
||||
std::set<int> retrieved_values;
|
||||
std::atomic<int> pop_count = 0;
|
||||
|
||||
for (int i = 0; i < num_consumers; ++i) {
|
||||
consumers.emplace_back([this, &result_mtx, &retrieved_values, &pop_count, total_items] {
|
||||
while (pop_count.load() < total_items) {
|
||||
// Use try_pop so threads don't block indefinitely waiting for a push
|
||||
// that won't come until the other threads finish.
|
||||
if (auto val = this->queue.try_pop(); val.has_value()) {
|
||||
std::lock_guard lock(result_mtx);
|
||||
retrieved_values.insert(val.value());
|
||||
pop_count++;
|
||||
}
|
||||
std::this_thread::yield();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (auto& t : consumers) {
|
||||
t.join();
|
||||
}
|
||||
|
||||
// Check consistency
|
||||
ASSERT_EQ(pop_count.load(), total_items) << "Total pops do not match total pushed items.";
|
||||
ASSERT_EQ(retrieved_values.size(), total_items) << "Duplicate items were retrieved.";
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 6. Move-Only Type Compatibility Test (Verifying the push/pop fix)
|
||||
// =================================================================
|
||||
|
||||
// Test fixture for std::unique_ptr<int> (a move-only type)
|
||||
class ThreadSafeQueueMoveOnlyTest : public ::testing::Test {
|
||||
protected:
|
||||
thread_safe_queue<std::unique_ptr<int>> queue;
|
||||
};
|
||||
|
||||
TEST_F(ThreadSafeQueueMoveOnlyTest, HandlesConcurrentMoveOnlyTypes) {
|
||||
const int total_items = 1000;
|
||||
const int num_threads = 5;
|
||||
|
||||
std::vector<std::thread> threads;
|
||||
std::atomic<int> pop_count = 0;
|
||||
|
||||
// Producer/Consumer set for unique ownership verification
|
||||
std::set<int> retrieved_values;
|
||||
std::mutex result_mtx;
|
||||
|
||||
// Start 5 threads: 3 producers, 2 consumers
|
||||
for (int i = 0; i < num_threads; ++i) {
|
||||
if (i < 3) { // Producers
|
||||
threads.emplace_back([this, i, total_items] {
|
||||
int start_value = i * total_items;
|
||||
for (int j = 0; j < total_items; ++j) {
|
||||
// Requires thread_safe_queue::push(T&&)
|
||||
this->queue.push(std::make_unique<int>(start_value + j));
|
||||
}
|
||||
});
|
||||
} else { // Consumers
|
||||
threads.emplace_back([this, &pop_count, &result_mtx, &retrieved_values, total_items] {
|
||||
int pops = 0;
|
||||
while (pops < total_items * 3 / 2) { // Try to pop 1500 times
|
||||
// Requires thread_safe_queue::try_pop()
|
||||
if (auto opt_ptr = this->queue.try_pop(); opt_ptr.has_value()) {
|
||||
std::lock_guard lock(result_mtx);
|
||||
retrieved_values.insert(*opt_ptr.value());
|
||||
pop_count++;
|
||||
pops++;
|
||||
}
|
||||
std::this_thread::yield();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& t : threads) {
|
||||
if (t.joinable()) {
|
||||
t.join();
|
||||
}
|
||||
}
|
||||
|
||||
// Drain any remaining items in the main thread (should be few/none)
|
||||
while (auto opt_ptr = this->queue.try_pop()) {
|
||||
std::lock_guard lock(result_mtx);
|
||||
retrieved_values.insert(*opt_ptr.value());
|
||||
pop_count++;
|
||||
}
|
||||
|
||||
const int total_expected = total_items * 3; // 3 producers * 1000 items
|
||||
|
||||
ASSERT_EQ(pop_count.load(), total_expected) << "Total items popped does not match total pushed.";
|
||||
ASSERT_EQ(retrieved_values.size(), total_expected)
|
||||
<< "Duplicate pointers/values were retrieved, indicating a race condition failure or a failed move.";
|
||||
}
|
||||
@@ -0,0 +1,647 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <everest/util/vector/fixed_vector.hpp>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
using namespace everest::lib::util;
|
||||
|
||||
TEST(FixedVectorTest, BasicInt) {
|
||||
fixed_vector<int, 10> vec;
|
||||
EXPECT_TRUE(vec.empty());
|
||||
EXPECT_EQ(vec.size(), 0);
|
||||
|
||||
vec.push_back(1);
|
||||
vec.push_back(2);
|
||||
vec.push_back(3);
|
||||
|
||||
EXPECT_FALSE(vec.empty());
|
||||
EXPECT_EQ(vec.size(), 3);
|
||||
EXPECT_EQ(vec[0], 1);
|
||||
EXPECT_EQ(vec[1], 2);
|
||||
EXPECT_EQ(vec[2], 3);
|
||||
EXPECT_EQ(vec.at(1), 2);
|
||||
|
||||
int count = 1;
|
||||
for (const auto& val : vec) {
|
||||
EXPECT_EQ(val, count++);
|
||||
}
|
||||
|
||||
vec.clear();
|
||||
EXPECT_TRUE(vec.empty());
|
||||
EXPECT_EQ(vec.size(), 0);
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, StringAndMove) {
|
||||
fixed_vector<std::string, 5> vec;
|
||||
vec.push_back("hello");
|
||||
std::string s = "world";
|
||||
vec.push_back(std::move(s));
|
||||
|
||||
EXPECT_EQ(vec.size(), 2);
|
||||
EXPECT_EQ(vec[0], "hello");
|
||||
EXPECT_EQ(vec[1], "world");
|
||||
// Note: The state of a moved-from string is valid but unspecified.
|
||||
// In many implementations it is empty, but we shouldn't rely on it.
|
||||
// EXPECT_TRUE(s.empty());
|
||||
|
||||
vec.emplace_back(5, 'c');
|
||||
EXPECT_EQ(vec.size(), 3);
|
||||
EXPECT_EQ(vec[2], "ccccc");
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, Capacity) {
|
||||
fixed_vector<int, 3> vec;
|
||||
vec.push_back(1);
|
||||
vec.push_back(2);
|
||||
vec.push_back(3);
|
||||
EXPECT_EQ(vec.size(), 3);
|
||||
EXPECT_THROW(vec.push_back(4), std::length_error);
|
||||
EXPECT_THROW(vec.emplace_back(5), std::length_error);
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, AtThrows) {
|
||||
fixed_vector<int, 5> vec;
|
||||
vec.push_back(1);
|
||||
EXPECT_THROW(vec.at(1), std::out_of_range);
|
||||
const auto& cvec = vec;
|
||||
EXPECT_THROW(cvec.at(1), std::out_of_range);
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, Erase) {
|
||||
fixed_vector<int, 10> vec;
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
vec.push_back(i);
|
||||
}
|
||||
|
||||
// erase first
|
||||
auto it = vec.erase(vec.begin());
|
||||
EXPECT_EQ(*it, 1);
|
||||
EXPECT_EQ(vec.size(), 9);
|
||||
EXPECT_EQ(vec[0], 1);
|
||||
EXPECT_EQ(vec[8], 9);
|
||||
|
||||
// erase last
|
||||
it = vec.erase(vec.end() - 1);
|
||||
EXPECT_EQ(it, vec.end());
|
||||
EXPECT_EQ(vec.size(), 8);
|
||||
EXPECT_EQ(vec[7], 8);
|
||||
|
||||
// erase middle
|
||||
it = vec.erase(vec.begin() + 3); // erase '4' from {1,2,3,4,5,6,7,8}
|
||||
EXPECT_EQ(*it, 5);
|
||||
EXPECT_EQ(vec.size(), 7);
|
||||
EXPECT_EQ(vec[0], 1);
|
||||
EXPECT_EQ(vec[1], 2);
|
||||
EXPECT_EQ(vec[2], 3);
|
||||
EXPECT_EQ(vec[3], 5);
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, EraseRange) {
|
||||
fixed_vector<int, 10> vec;
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
vec.push_back(i);
|
||||
}
|
||||
|
||||
// erase range in the middle
|
||||
auto it = vec.erase(vec.begin() + 2, vec.begin() + 5); // erase 2, 3, 4
|
||||
EXPECT_EQ(*it, 5);
|
||||
EXPECT_EQ(vec.size(), 7);
|
||||
EXPECT_EQ(vec[0], 0);
|
||||
EXPECT_EQ(vec[1], 1);
|
||||
EXPECT_EQ(vec[2], 5);
|
||||
EXPECT_EQ(vec[3], 6);
|
||||
EXPECT_EQ(vec[6], 9);
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, MoveOnlyType) {
|
||||
fixed_vector<std::unique_ptr<int>, 5> vec;
|
||||
vec.emplace_back(std::make_unique<int>(1));
|
||||
vec.push_back(std::make_unique<int>(2));
|
||||
EXPECT_EQ(vec.size(), 2);
|
||||
EXPECT_EQ(*vec[0], 1);
|
||||
EXPECT_EQ(*vec[1], 2);
|
||||
|
||||
vec.erase(vec.begin());
|
||||
EXPECT_EQ(vec.size(), 1);
|
||||
EXPECT_EQ(*vec[0], 2);
|
||||
}
|
||||
|
||||
struct DestructorCheck {
|
||||
static int destructor_calls;
|
||||
DestructorCheck() = default;
|
||||
~DestructorCheck() {
|
||||
destructor_calls++;
|
||||
}
|
||||
};
|
||||
int DestructorCheck::destructor_calls = 0;
|
||||
|
||||
TEST(FixedVectorTest, DestructorCalledOnClear) {
|
||||
DestructorCheck::destructor_calls = 0;
|
||||
fixed_vector<DestructorCheck, 5> vec;
|
||||
vec.emplace_back();
|
||||
vec.emplace_back();
|
||||
vec.emplace_back();
|
||||
EXPECT_EQ(DestructorCheck::destructor_calls, 0);
|
||||
vec.clear();
|
||||
EXPECT_EQ(DestructorCheck::destructor_calls, 3);
|
||||
vec.clear();
|
||||
EXPECT_EQ(DestructorCheck::destructor_calls, 3);
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, DestructorCalledOnErase) {
|
||||
DestructorCheck::destructor_calls = 0;
|
||||
fixed_vector<DestructorCheck, 5> vec;
|
||||
vec.emplace_back();
|
||||
vec.emplace_back();
|
||||
vec.emplace_back();
|
||||
EXPECT_EQ(DestructorCheck::destructor_calls, 0);
|
||||
vec.erase(vec.begin());
|
||||
EXPECT_EQ(DestructorCheck::destructor_calls, 1);
|
||||
vec.erase(vec.begin(), vec.end());
|
||||
EXPECT_EQ(DestructorCheck::destructor_calls, 3);
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, DestructorCalledOnDestruction) {
|
||||
DestructorCheck::destructor_calls = 0;
|
||||
{
|
||||
fixed_vector<DestructorCheck, 5> vec;
|
||||
vec.emplace_back();
|
||||
vec.emplace_back();
|
||||
EXPECT_EQ(DestructorCheck::destructor_calls, 0);
|
||||
}
|
||||
EXPECT_EQ(DestructorCheck::destructor_calls, 2);
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, CopyConstructor) {
|
||||
fixed_vector<int, 5> original;
|
||||
original.push_back(1);
|
||||
original.push_back(2);
|
||||
|
||||
fixed_vector<int, 5> copy = original;
|
||||
EXPECT_EQ(copy.size(), 2);
|
||||
EXPECT_EQ(copy[0], 1);
|
||||
EXPECT_EQ(copy[1], 2);
|
||||
|
||||
// Ensure original is untouched
|
||||
EXPECT_EQ(original.size(), 2);
|
||||
EXPECT_EQ(original[0], 1);
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, CopyAssignment) {
|
||||
fixed_vector<int, 5> original;
|
||||
original.push_back(1);
|
||||
original.push_back(2);
|
||||
|
||||
fixed_vector<int, 5> copy;
|
||||
copy.push_back(99);
|
||||
copy = original;
|
||||
|
||||
EXPECT_EQ(copy.size(), 2);
|
||||
EXPECT_EQ(copy[0], 1);
|
||||
EXPECT_EQ(copy[1], 2);
|
||||
|
||||
// Ensure original is untouched
|
||||
EXPECT_EQ(original.size(), 2);
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, CopyAssignmentEdgeCases) {
|
||||
// Case 1: Destination is larger than source
|
||||
fixed_vector<DestructorCheck, 5> dest1;
|
||||
dest1.emplace_back();
|
||||
dest1.emplace_back();
|
||||
dest1.emplace_back();
|
||||
|
||||
fixed_vector<DestructorCheck, 5> source1;
|
||||
source1.emplace_back();
|
||||
source1.emplace_back();
|
||||
|
||||
DestructorCheck::destructor_calls = 0;
|
||||
dest1 = source1;
|
||||
EXPECT_EQ(dest1.size(), 2);
|
||||
EXPECT_EQ(DestructorCheck::destructor_calls, 1); // One surplus element should have been destroyed
|
||||
|
||||
// Case 2: Destination is smaller than source
|
||||
fixed_vector<int, 5> dest2;
|
||||
dest2.push_back(1);
|
||||
|
||||
fixed_vector<int, 5> source2;
|
||||
source2.push_back(10);
|
||||
source2.push_back(20);
|
||||
|
||||
dest2 = source2;
|
||||
EXPECT_EQ(dest2.size(), 2);
|
||||
EXPECT_EQ(dest2[0], 10);
|
||||
EXPECT_EQ(dest2[1], 20);
|
||||
|
||||
// Case 3: Sizes are equal
|
||||
fixed_vector<int, 5> dest3;
|
||||
dest3.push_back(1);
|
||||
dest3.push_back(2);
|
||||
|
||||
fixed_vector<int, 5> source3;
|
||||
source3.push_back(10);
|
||||
source3.push_back(20);
|
||||
|
||||
dest3 = source3;
|
||||
EXPECT_EQ(dest3.size(), 2);
|
||||
EXPECT_EQ(dest3[0], 10);
|
||||
EXPECT_EQ(dest3[1], 20);
|
||||
|
||||
// Case 4: Self-assignment
|
||||
fixed_vector<int, 5> self_assign;
|
||||
self_assign.push_back(123);
|
||||
self_assign = self_assign;
|
||||
EXPECT_EQ(self_assign.size(), 1);
|
||||
EXPECT_EQ(self_assign[0], 123);
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, MoveConstructor) {
|
||||
fixed_vector<std::string, 5> original;
|
||||
original.push_back("a");
|
||||
original.push_back("b");
|
||||
|
||||
fixed_vector<std::string, 5> moved = std::move(original);
|
||||
EXPECT_EQ(moved.size(), 2);
|
||||
EXPECT_EQ(moved[0], "a");
|
||||
|
||||
EXPECT_TRUE(original.empty());
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, MoveAssignment) {
|
||||
fixed_vector<std::string, 5> original;
|
||||
original.push_back("a");
|
||||
original.push_back("b");
|
||||
|
||||
fixed_vector<std::string, 5> moved;
|
||||
moved.push_back("c");
|
||||
moved = std::move(original);
|
||||
|
||||
EXPECT_EQ(moved.size(), 2);
|
||||
EXPECT_EQ(moved[0], "a");
|
||||
|
||||
EXPECT_TRUE(original.empty());
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, ZeroCapacity) {
|
||||
fixed_vector<int, 0> vec;
|
||||
EXPECT_TRUE(vec.empty());
|
||||
EXPECT_EQ(vec.size(), 0);
|
||||
EXPECT_EQ(vec.capacity(), 0);
|
||||
EXPECT_THROW(vec.push_back(1), std::length_error);
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, FrontAndBack) {
|
||||
fixed_vector<int, 3> vec;
|
||||
vec.push_back(10);
|
||||
vec.push_back(20);
|
||||
EXPECT_EQ(vec.front(), 10);
|
||||
EXPECT_EQ(vec.back(), 20);
|
||||
vec.front() = 11;
|
||||
EXPECT_EQ(vec[0], 11);
|
||||
|
||||
const auto& cvec = vec;
|
||||
EXPECT_EQ(cvec.front(), 11);
|
||||
EXPECT_EQ(cvec.back(), 20);
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, ReverseIteration) {
|
||||
fixed_vector<int, 3> vec;
|
||||
vec.push_back(1);
|
||||
vec.push_back(2);
|
||||
vec.push_back(3);
|
||||
|
||||
int expected = 3;
|
||||
for (auto it = vec.rbegin(); it != vec.rend(); ++it) {
|
||||
EXPECT_EQ(*it, expected--);
|
||||
}
|
||||
|
||||
const auto& cvec = vec;
|
||||
expected = 3;
|
||||
for (auto it = cvec.rbegin(); it != cvec.rend(); ++it) {
|
||||
EXPECT_EQ(*it, expected--);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, EraseEdgeCases) {
|
||||
fixed_vector<int, 5> vec;
|
||||
vec.push_back(1);
|
||||
vec.push_back(2);
|
||||
vec.push_back(3);
|
||||
vec.push_back(4);
|
||||
|
||||
// Erase empty range
|
||||
auto it = vec.erase(vec.begin() + 1, vec.begin() + 1);
|
||||
EXPECT_EQ(vec.size(), 4);
|
||||
EXPECT_EQ(*it, 2);
|
||||
|
||||
// Erase to the end
|
||||
it = vec.erase(vec.begin() + 2, vec.end());
|
||||
EXPECT_EQ(vec.size(), 2);
|
||||
EXPECT_EQ(it, vec.end());
|
||||
EXPECT_EQ(vec[0], 1);
|
||||
EXPECT_EQ(vec[1], 2);
|
||||
|
||||
// Erase everything
|
||||
it = vec.erase(vec.begin(), vec.end());
|
||||
EXPECT_TRUE(vec.empty());
|
||||
EXPECT_EQ(it, vec.end());
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, TryEmplaceBack) {
|
||||
fixed_vector<int, 3> vec;
|
||||
auto* elem1 = vec.try_emplace_back(10);
|
||||
ASSERT_NE(elem1, nullptr);
|
||||
EXPECT_EQ(*elem1, 10);
|
||||
EXPECT_EQ(vec.size(), 1);
|
||||
EXPECT_EQ(vec[0], 10);
|
||||
|
||||
vec.try_emplace_back(20);
|
||||
vec.try_emplace_back(30);
|
||||
|
||||
EXPECT_EQ(vec.size(), 3);
|
||||
|
||||
// Vector is full
|
||||
auto* elem4 = vec.try_emplace_back(40);
|
||||
EXPECT_EQ(elem4, nullptr);
|
||||
EXPECT_EQ(vec.size(), 3);
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, PopBack) {
|
||||
fixed_vector<int, 5> vec;
|
||||
vec.push_back(1);
|
||||
vec.push_back(2);
|
||||
vec.push_back(3);
|
||||
|
||||
EXPECT_EQ(vec.size(), 3);
|
||||
EXPECT_EQ(vec.back(), 3);
|
||||
|
||||
vec.pop_back();
|
||||
EXPECT_EQ(vec.size(), 2);
|
||||
EXPECT_EQ(vec.back(), 2);
|
||||
|
||||
vec.pop_back();
|
||||
EXPECT_EQ(vec.size(), 1);
|
||||
EXPECT_EQ(vec.back(), 1);
|
||||
|
||||
vec.pop_back();
|
||||
EXPECT_TRUE(vec.empty());
|
||||
EXPECT_EQ(vec.size(), 0);
|
||||
|
||||
// Popping from an empty vector should be a no-op
|
||||
vec.pop_back();
|
||||
EXPECT_TRUE(vec.empty());
|
||||
EXPECT_EQ(vec.size(), 0);
|
||||
|
||||
// Test with DestructorCheck
|
||||
DestructorCheck::destructor_calls = 0;
|
||||
fixed_vector<DestructorCheck, 5> vec_dc;
|
||||
vec_dc.emplace_back();
|
||||
vec_dc.emplace_back();
|
||||
vec_dc.emplace_back();
|
||||
EXPECT_EQ(DestructorCheck::destructor_calls, 0);
|
||||
|
||||
vec_dc.pop_back();
|
||||
EXPECT_EQ(DestructorCheck::destructor_calls, 1);
|
||||
vec_dc.pop_back();
|
||||
EXPECT_EQ(DestructorCheck::destructor_calls, 2);
|
||||
vec_dc.pop_back();
|
||||
EXPECT_EQ(DestructorCheck::destructor_calls, 3);
|
||||
EXPECT_TRUE(vec_dc.empty());
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, CapacityAndMaxSize) {
|
||||
fixed_vector<int, 5> vec;
|
||||
EXPECT_EQ(vec.capacity(), 5);
|
||||
EXPECT_EQ(vec.max_size(), 5);
|
||||
|
||||
const fixed_vector<int, 0> zero_vec;
|
||||
EXPECT_EQ(zero_vec.capacity(), 0);
|
||||
EXPECT_EQ(zero_vec.max_size(), 0);
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, ConstIteratorMethods) {
|
||||
fixed_vector<int, 5> vec;
|
||||
vec.push_back(1);
|
||||
vec.push_back(2);
|
||||
vec.push_back(3);
|
||||
|
||||
const auto& cvec = vec;
|
||||
|
||||
// Test cbegin() and cend()
|
||||
int expected_val = 1;
|
||||
for (auto it = cvec.cbegin(); it != cvec.cend(); ++it) {
|
||||
EXPECT_EQ(*it, expected_val++);
|
||||
}
|
||||
EXPECT_EQ(expected_val, 4); // Should have iterated 1, 2, 3
|
||||
|
||||
// Test crbegin() and crend()
|
||||
expected_val = 3;
|
||||
for (auto it = cvec.crbegin(); it != cvec.crend(); ++it) {
|
||||
EXPECT_EQ(*it, expected_val--);
|
||||
}
|
||||
EXPECT_EQ(expected_val, 0); // Should have iterated 3, 2, 1
|
||||
|
||||
// Test on empty vector
|
||||
fixed_vector<int, 5> empty_vec;
|
||||
const auto& cempty_vec = empty_vec;
|
||||
EXPECT_EQ(cempty_vec.cbegin(), cempty_vec.cend());
|
||||
EXPECT_EQ(cempty_vec.crbegin(), cempty_vec.crend());
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, EraseInvalidRange) {
|
||||
fixed_vector<int, 5> vec;
|
||||
vec.push_back(1);
|
||||
vec.push_back(2);
|
||||
vec.push_back(3);
|
||||
vec.push_back(4);
|
||||
|
||||
// Test with first > last, which should do nothing
|
||||
auto original_size = vec.size();
|
||||
auto it = vec.erase(vec.begin() + 2, vec.begin() + 1);
|
||||
|
||||
// Verify nothing happened
|
||||
EXPECT_EQ(vec.size(), original_size);
|
||||
EXPECT_EQ(vec[0], 1);
|
||||
EXPECT_EQ(vec[1], 2);
|
||||
EXPECT_EQ(vec[2], 3);
|
||||
EXPECT_EQ(vec[3], 4);
|
||||
|
||||
// The returned iterator should be the 'first' iterator passed in
|
||||
EXPECT_EQ(it, vec.begin() + 2);
|
||||
EXPECT_EQ(*it, 3);
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, ComparisonOperators) {
|
||||
fixed_vector<int, 5> vec1;
|
||||
vec1.push_back(1);
|
||||
vec1.push_back(2);
|
||||
|
||||
fixed_vector<int, 5> vec2;
|
||||
vec2.push_back(1);
|
||||
vec2.push_back(2);
|
||||
|
||||
fixed_vector<int, 5> vec3;
|
||||
vec3.push_back(1);
|
||||
vec3.push_back(99);
|
||||
|
||||
fixed_vector<int, 5> vec4;
|
||||
vec4.push_back(1);
|
||||
|
||||
fixed_vector<int, 5> empty1;
|
||||
fixed_vector<int, 5> empty2;
|
||||
|
||||
EXPECT_TRUE(vec1 == vec2);
|
||||
EXPECT_FALSE(vec1 != vec2);
|
||||
|
||||
EXPECT_FALSE(vec1 == vec3);
|
||||
EXPECT_TRUE(vec1 != vec3);
|
||||
|
||||
EXPECT_FALSE(vec1 == vec4);
|
||||
EXPECT_TRUE(vec1 != vec4);
|
||||
|
||||
EXPECT_TRUE(empty1 == empty2);
|
||||
EXPECT_FALSE(empty1 != empty2);
|
||||
|
||||
EXPECT_FALSE(vec1 == empty1);
|
||||
EXPECT_TRUE(vec1 != empty1);
|
||||
}
|
||||
|
||||
// Verify that fixed_vector enforces nothrow move requirements at compile time.
|
||||
// Types with throwing move constructors/assignments are rejected by static_assert.
|
||||
struct NothrowMovable {
|
||||
NothrowMovable() = default;
|
||||
NothrowMovable(NothrowMovable&&) noexcept = default;
|
||||
NothrowMovable& operator=(NothrowMovable&&) noexcept = default;
|
||||
NothrowMovable(const NothrowMovable&) = default;
|
||||
NothrowMovable& operator=(const NothrowMovable&) = default;
|
||||
};
|
||||
|
||||
TEST(FixedVectorTest, NothrowMoveConstraint) {
|
||||
// Verify that fixed_vector works with nothrow-movable types
|
||||
fixed_vector<NothrowMovable, 5> vec;
|
||||
vec.emplace_back();
|
||||
EXPECT_EQ(vec.size(), 1);
|
||||
|
||||
// Move construction should be noexcept
|
||||
static_assert(std::is_nothrow_move_constructible_v<fixed_vector<NothrowMovable, 5>>);
|
||||
static_assert(std::is_nothrow_move_assignable_v<fixed_vector<NothrowMovable, 5>>);
|
||||
static_assert(std::is_nothrow_move_constructible_v<fixed_vector<int, 5>>);
|
||||
static_assert(std::is_nothrow_move_assignable_v<fixed_vector<int, 5>>);
|
||||
static_assert(std::is_nothrow_move_constructible_v<fixed_vector<std::string, 5>>);
|
||||
static_assert(std::is_nothrow_move_assignable_v<fixed_vector<std::string, 5>>);
|
||||
}
|
||||
|
||||
// Types with throwing move operations — used only in compile-time rejection checks below.
|
||||
struct ThrowingMoveConstructor {
|
||||
ThrowingMoveConstructor() = default;
|
||||
ThrowingMoveConstructor(ThrowingMoveConstructor&&) noexcept(false) {
|
||||
}
|
||||
ThrowingMoveConstructor& operator=(ThrowingMoveConstructor&&) noexcept = default;
|
||||
};
|
||||
|
||||
struct ThrowingMoveAssignment {
|
||||
ThrowingMoveAssignment() = default;
|
||||
ThrowingMoveAssignment(ThrowingMoveAssignment&&) noexcept = default;
|
||||
ThrowingMoveAssignment& operator=(ThrowingMoveAssignment&&) noexcept(false) {
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
|
||||
struct ThrowingBothMoveOps {
|
||||
ThrowingBothMoveOps() = default;
|
||||
ThrowingBothMoveOps(ThrowingBothMoveOps&&) noexcept(false) {
|
||||
}
|
||||
ThrowingBothMoveOps& operator=(ThrowingBothMoveOps&&) noexcept(false) {
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
|
||||
// Verify at compile time that fixed_vector rejects types whose move operations can throw.
|
||||
// The static_asserts inside fixed_vector prevent instantiation of these types.
|
||||
// We verify this indirectly: if fixed_vector's constraint is working, these types must not
|
||||
// satisfy the nothrow move requirements.
|
||||
TEST(FixedVectorTest, ThrowingMoveTypesAreRejected) {
|
||||
// Confirm the types themselves have throwing move operations
|
||||
static_assert(!std::is_nothrow_move_constructible_v<ThrowingMoveConstructor>,
|
||||
"ThrowingMoveConstructor should not be nothrow move constructible");
|
||||
static_assert(!std::is_nothrow_move_constructible_v<ThrowingBothMoveOps>,
|
||||
"ThrowingBothMoveOps should not be nothrow move constructible");
|
||||
static_assert(!std::is_nothrow_move_assignable_v<ThrowingMoveAssignment>,
|
||||
"ThrowingMoveAssignment should not be nothrow move assignable");
|
||||
static_assert(!std::is_nothrow_move_assignable_v<ThrowingBothMoveOps>,
|
||||
"ThrowingBothMoveOps should not be nothrow move assignable");
|
||||
|
||||
// fixed_vector<ThrowingMoveConstructor, 5> would fail to compile due to static_assert.
|
||||
// fixed_vector<ThrowingMoveAssignment, 5> would fail to compile due to static_assert.
|
||||
// fixed_vector<ThrowingBothMoveOps, 5> would fail to compile due to static_assert.
|
||||
//
|
||||
// These cannot be tested at runtime since instantiation itself is a compile error.
|
||||
// The static_asserts above confirm the trait checks that fixed_vector relies on.
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, InitializerListConstructor) {
|
||||
// Basic construction
|
||||
fixed_vector<int, 5> vec1 = {1, 2, 3};
|
||||
EXPECT_EQ(vec1.size(), 3);
|
||||
EXPECT_EQ(vec1[0], 1);
|
||||
EXPECT_EQ(vec1[1], 2);
|
||||
EXPECT_EQ(vec1[2], 3);
|
||||
|
||||
// Empty list
|
||||
fixed_vector<int, 5> vec2 = {};
|
||||
EXPECT_TRUE(vec2.empty());
|
||||
EXPECT_EQ(vec2.size(), 0);
|
||||
|
||||
// Full capacity
|
||||
fixed_vector<int, 3> vec3 = {10, 20, 30};
|
||||
EXPECT_EQ(vec3.size(), 3);
|
||||
EXPECT_EQ(vec3[2], 30);
|
||||
|
||||
// Exceeding capacity - use a lambda to avoid comma issues with the macro
|
||||
EXPECT_THROW(([] { fixed_vector<int, 2> vec4 = {1, 2, 3}; }()), std::length_error);
|
||||
|
||||
// With strings
|
||||
fixed_vector<std::string, 4> vec5 = {"hello", "world"};
|
||||
EXPECT_EQ(vec5.size(), 2);
|
||||
EXPECT_EQ(vec5[0], "hello");
|
||||
EXPECT_EQ(vec5[1], "world");
|
||||
}
|
||||
|
||||
TEST(FixedVectorTest, StdVectorConstructor) {
|
||||
// Test case 1: Construct from an empty std::vector
|
||||
std::vector<int> empty_std_vec = {};
|
||||
fixed_vector<int, 5> vec_from_empty(empty_std_vec);
|
||||
EXPECT_TRUE(vec_from_empty.empty());
|
||||
EXPECT_EQ(vec_from_empty.size(), 0);
|
||||
|
||||
// Test case 2: Construct from a std::vector with elements (within capacity)
|
||||
std::vector<int> small_std_vec = {1, 2, 3};
|
||||
fixed_vector<int, 5> vec_from_small(small_std_vec);
|
||||
EXPECT_EQ(vec_from_small.size(), 3);
|
||||
EXPECT_EQ(vec_from_small[0], 1);
|
||||
EXPECT_EQ(vec_from_small[1], 2);
|
||||
EXPECT_EQ(vec_from_small[2], 3);
|
||||
|
||||
// Test case 3: Construct from a std::vector with elements (exactly capacity)
|
||||
std::vector<int> full_std_vec = {10, 20, 30, 40, 50};
|
||||
fixed_vector<int, 5> vec_from_full(full_std_vec);
|
||||
EXPECT_EQ(vec_from_full.size(), 5);
|
||||
EXPECT_EQ(vec_from_full[0], 10);
|
||||
EXPECT_EQ(vec_from_full[4], 50);
|
||||
|
||||
// Test case 4: Construct from a std::vector with elements exceeding capacity
|
||||
std::vector<int> large_std_vec = {1, 2, 3, 4, 5, 6};
|
||||
EXPECT_THROW(([large_std_vec] { fixed_vector<int, 5> vec_from_large(large_std_vec); }()), std::length_error);
|
||||
|
||||
// Test case 5: Construct with std::string elements
|
||||
std::vector<std::string> string_std_vec = {"apple", "banana"};
|
||||
fixed_vector<std::string, 3> vec_from_strings(string_std_vec);
|
||||
EXPECT_EQ(vec_from_strings.size(), 2);
|
||||
EXPECT_EQ(vec_from_strings[0], "apple");
|
||||
EXPECT_EQ(vec_from_strings[1], "banana");
|
||||
}
|
||||
Reference in New Issue
Block a user