Add test_reference.cpp
This commit is contained in:
@@ -268,3 +268,15 @@ add_executable(test_api_url_parser tests/test_api_url_parser.cpp)
|
||||
target_link_libraries(test_api_url_parser doctest_impl weaseldb_sources_debug)
|
||||
target_compile_options(test_api_url_parser PRIVATE -UNDEBUG)
|
||||
add_test(NAME api_url_parser_tests COMMAND test_api_url_parser)
|
||||
|
||||
# Reference counting tests and benchmarks
|
||||
add_executable(test_reference tests/test_reference.cpp)
|
||||
target_link_libraries(test_reference doctest_impl)
|
||||
target_include_directories(test_reference PRIVATE src)
|
||||
target_compile_options(test_reference PRIVATE -UNDEBUG)
|
||||
add_test(NAME reference_tests COMMAND test_reference)
|
||||
|
||||
add_executable(bench_reference benchmarks/bench_reference.cpp)
|
||||
target_link_libraries(bench_reference nanobench_impl Threads::Threads)
|
||||
target_include_directories(bench_reference PRIVATE src)
|
||||
add_test(NAME reference_benchmarks COMMAND bench_reference)
|
||||
|
||||
@@ -385,9 +385,14 @@ template <typename T, typename... Args> Ref<T> make_ref(Args &&...args) {
|
||||
constexpr size_t padded_cb_size =
|
||||
(cb_size + alignment - 1) & ~(alignment - 1);
|
||||
|
||||
constexpr size_t total_alignment =
|
||||
std::max(alignof(detail::ControlBlock), alignment);
|
||||
constexpr size_t total_size = padded_cb_size + sizeof(T);
|
||||
constexpr size_t aligned_total_size =
|
||||
(total_size + total_alignment - 1) & ~(total_alignment - 1);
|
||||
|
||||
char *buf = reinterpret_cast<char *>(
|
||||
std::aligned_alloc(std::max(alignof(detail::ControlBlock), alignment),
|
||||
padded_cb_size + sizeof(T)));
|
||||
std::aligned_alloc(total_alignment, aligned_total_size));
|
||||
if (!buf) {
|
||||
std::fprintf(stderr, "Out of memory\n");
|
||||
std::abort();
|
||||
|
||||
201
tests/test_reference.cpp
Normal file
201
tests/test_reference.cpp
Normal file
@@ -0,0 +1,201 @@
|
||||
#include <doctest/doctest.h>
|
||||
#include <latch>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "reference.hpp"
|
||||
|
||||
namespace {
|
||||
struct TestObject {
|
||||
int value;
|
||||
explicit TestObject(int v) : value(v) {}
|
||||
};
|
||||
|
||||
struct Node {
|
||||
int data;
|
||||
Ref<Node> next;
|
||||
WeakRef<Node> parent;
|
||||
|
||||
explicit Node(int d) : data(d) {}
|
||||
};
|
||||
} // anonymous namespace
|
||||
|
||||
TEST_CASE("Ref basic functionality") {
|
||||
SUBCASE("make_ref creates valid Ref") {
|
||||
auto ref = make_ref<TestObject>(42);
|
||||
CHECK(ref);
|
||||
CHECK(ref.get() != nullptr);
|
||||
CHECK(ref->value == 42);
|
||||
CHECK((*ref).value == 42);
|
||||
}
|
||||
|
||||
SUBCASE("copy construction increments reference count") {
|
||||
auto ref1 = make_ref<TestObject>(123);
|
||||
auto ref2 = ref1;
|
||||
|
||||
CHECK(ref1);
|
||||
CHECK(ref2);
|
||||
CHECK(ref1.get() == ref2.get());
|
||||
CHECK(ref1->value == 123);
|
||||
CHECK(ref2->value == 123);
|
||||
}
|
||||
|
||||
SUBCASE("copy assignment works correctly") {
|
||||
auto ref1 = make_ref<TestObject>(100);
|
||||
auto ref2 = make_ref<TestObject>(200);
|
||||
|
||||
ref2 = ref1;
|
||||
CHECK(ref1.get() == ref2.get());
|
||||
CHECK(ref1->value == 100);
|
||||
CHECK(ref2->value == 100);
|
||||
}
|
||||
|
||||
SUBCASE("move construction transfers ownership") {
|
||||
auto ref1 = make_ref<TestObject>(456);
|
||||
auto *ptr = ref1.get();
|
||||
auto ref2 = std::move(ref1);
|
||||
|
||||
CHECK(!ref1);
|
||||
CHECK(ref2);
|
||||
CHECK(ref2.get() == ptr);
|
||||
CHECK(ref2->value == 456);
|
||||
}
|
||||
|
||||
SUBCASE("move assignment transfers ownership") {
|
||||
auto ref1 = make_ref<TestObject>(789);
|
||||
auto ref2 = make_ref<TestObject>(999);
|
||||
auto *ptr = ref1.get();
|
||||
|
||||
ref2 = std::move(ref1);
|
||||
CHECK(!ref1);
|
||||
CHECK(ref2);
|
||||
CHECK(ref2.get() == ptr);
|
||||
CHECK(ref2->value == 789);
|
||||
}
|
||||
|
||||
SUBCASE("reset clears reference") {
|
||||
auto ref = make_ref<TestObject>(111);
|
||||
CHECK(ref);
|
||||
|
||||
ref.reset();
|
||||
CHECK(!ref);
|
||||
CHECK(ref.get() == nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("WeakRef basic functionality") {
|
||||
SUBCASE("construction from Ref") {
|
||||
auto ref = make_ref<TestObject>(333);
|
||||
WeakRef<TestObject> weak_ref = ref;
|
||||
|
||||
auto locked = weak_ref.lock();
|
||||
CHECK(locked);
|
||||
CHECK(locked.get() == ref.get());
|
||||
CHECK(locked->value == 333);
|
||||
}
|
||||
|
||||
SUBCASE("lock() returns empty when object destroyed") {
|
||||
WeakRef<TestObject> weak_ref;
|
||||
{
|
||||
auto ref = make_ref<TestObject>(444);
|
||||
weak_ref = ref;
|
||||
}
|
||||
// ref goes out of scope, object should be destroyed
|
||||
|
||||
auto locked = weak_ref.lock();
|
||||
CHECK(!locked);
|
||||
}
|
||||
|
||||
SUBCASE("copy and move semantics") {
|
||||
auto ref = make_ref<TestObject>(666);
|
||||
WeakRef<TestObject> weak1 = ref;
|
||||
WeakRef<TestObject> weak2 = weak1; // copy
|
||||
WeakRef<TestObject> weak3 = std::move(weak1); // move
|
||||
|
||||
auto locked2 = weak2.lock();
|
||||
auto locked3 = weak3.lock();
|
||||
CHECK(locked2);
|
||||
CHECK(locked3);
|
||||
CHECK(locked2->value == 666);
|
||||
CHECK(locked3->value == 666);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Ref thread safety") {
|
||||
SUBCASE("concurrent copying") {
|
||||
const int num_threads = 4;
|
||||
const int copies_per_thread = 100;
|
||||
const int test_iterations = 1000;
|
||||
|
||||
for (int iter = 0; iter < test_iterations; ++iter) {
|
||||
auto ref = make_ref<TestObject>(777);
|
||||
std::vector<std::thread> threads;
|
||||
std::latch start_latch{num_threads + 1};
|
||||
|
||||
for (int i = 0; i < num_threads; ++i) {
|
||||
threads.emplace_back([&]() {
|
||||
start_latch.arrive_and_wait();
|
||||
|
||||
for (int j = 0; j < copies_per_thread; ++j) {
|
||||
auto copy = ref;
|
||||
CHECK(copy);
|
||||
CHECK(copy->value == 777);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
start_latch.arrive_and_wait();
|
||||
|
||||
for (auto &t : threads) {
|
||||
t.join();
|
||||
}
|
||||
|
||||
CHECK(ref);
|
||||
CHECK(ref->value == 777);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("WeakRef prevents circular references") {
|
||||
SUBCASE("simple weak reference lifecycle") {
|
||||
WeakRef<TestObject> weak_ref;
|
||||
|
||||
// Create object and weak reference
|
||||
{
|
||||
auto ref = make_ref<TestObject>(123);
|
||||
weak_ref = ref;
|
||||
|
||||
// Should be able to lock while object exists
|
||||
auto locked = weak_ref.lock();
|
||||
CHECK(locked);
|
||||
CHECK(locked->value == 123);
|
||||
}
|
||||
// Object destroyed when ref goes out of scope
|
||||
|
||||
// Should not be able to lock after object destroyed
|
||||
auto locked = weak_ref.lock();
|
||||
CHECK(!locked);
|
||||
}
|
||||
|
||||
SUBCASE("parent-child cycle with WeakRef breaks cycle") {
|
||||
auto parent = make_ref<Node>(1);
|
||||
auto child = make_ref<Node>(2);
|
||||
|
||||
// Create potential cycle
|
||||
parent->next = child; // Strong reference: parent → child
|
||||
child->parent = parent; // WeakRef: child ⇝ parent (breaks cycle)
|
||||
|
||||
CHECK(parent->data == 1);
|
||||
CHECK(child->data == 2);
|
||||
CHECK(parent->next == child);
|
||||
|
||||
// Verify weak reference works while parent exists
|
||||
CHECK(child->parent.lock() == parent);
|
||||
|
||||
// Clear the only strong reference to parent
|
||||
parent.reset(); // This should destroy the parent object
|
||||
|
||||
// Now child's weak reference should fail to lock since parent is destroyed
|
||||
CHECK(!child->parent.lock());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user