From b9106a0d3c7dcf82954e0a56e79d968246ef5df0 Mon Sep 17 00:00:00 2001 From: Andrew Noyes Date: Wed, 10 Sep 2025 22:05:31 -0400 Subject: [PATCH] Add test_reference.cpp --- CMakeLists.txt | 12 +++ src/reference.hpp | 9 +- tests/test_reference.cpp | 201 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 220 insertions(+), 2 deletions(-) create mode 100644 tests/test_reference.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index fab3be7..c4bbd21 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/src/reference.hpp b/src/reference.hpp index 62129bd..15ff539 100644 --- a/src/reference.hpp +++ b/src/reference.hpp @@ -385,9 +385,14 @@ template Ref 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( - 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(); diff --git a/tests/test_reference.cpp b/tests/test_reference.cpp new file mode 100644 index 0000000..1236772 --- /dev/null +++ b/tests/test_reference.cpp @@ -0,0 +1,201 @@ +#include +#include +#include +#include + +#include "reference.hpp" + +namespace { +struct TestObject { + int value; + explicit TestObject(int v) : value(v) {} +}; + +struct Node { + int data; + Ref next; + WeakRef parent; + + explicit Node(int d) : data(d) {} +}; +} // anonymous namespace + +TEST_CASE("Ref basic functionality") { + SUBCASE("make_ref creates valid Ref") { + auto ref = make_ref(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(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(100); + auto ref2 = make_ref(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(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(789); + auto ref2 = make_ref(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(111); + CHECK(ref); + + ref.reset(); + CHECK(!ref); + CHECK(ref.get() == nullptr); + } +} + +TEST_CASE("WeakRef basic functionality") { + SUBCASE("construction from Ref") { + auto ref = make_ref(333); + WeakRef 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 weak_ref; + { + auto ref = make_ref(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(666); + WeakRef weak1 = ref; + WeakRef weak2 = weak1; // copy + WeakRef 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(777); + std::vector 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 weak_ref; + + // Create object and weak reference + { + auto ref = make_ref(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(1); + auto child = make_ref(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()); + } +}