513 lines
14 KiB
C++
513 lines
14 KiB
C++
#include <barrier>
|
|
#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) {}
|
|
};
|
|
|
|
// Classes for polymorphism testing
|
|
struct Base {
|
|
int base_value;
|
|
explicit Base(int v) : base_value(v) {}
|
|
virtual ~Base() = default;
|
|
virtual int get_value() const { return base_value; }
|
|
};
|
|
|
|
struct Derived : public Base {
|
|
int derived_value;
|
|
explicit Derived(int base_v, int derived_v)
|
|
: Base(base_v), derived_value(derived_v) {}
|
|
int get_value() const override { return base_value + derived_value; }
|
|
};
|
|
|
|
struct AnotherDerived : public Base {
|
|
int another_value;
|
|
explicit AnotherDerived(int base_v, int another_v)
|
|
: Base(base_v), another_value(another_v) {}
|
|
int get_value() const override { return base_value * another_value; }
|
|
};
|
|
|
|
// Classes to test polymorphic pointer address changes
|
|
struct Interface1 {
|
|
int interface1_data = 1;
|
|
virtual ~Interface1() = default;
|
|
virtual int get_interface1() const { return interface1_data; }
|
|
};
|
|
|
|
struct Interface2 {
|
|
int interface2_data = 2;
|
|
virtual ~Interface2() = default;
|
|
virtual int get_interface2() const { return interface2_data; }
|
|
};
|
|
|
|
// Multiple inheritance - this will cause pointer address changes
|
|
struct MultipleInheritance : public Interface1, public Interface2 {
|
|
int own_data;
|
|
explicit MultipleInheritance(int data) : own_data(data) {}
|
|
int get_own_data() const { return own_data; }
|
|
};
|
|
} // 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("explicit copy increments reference count") {
|
|
auto ref1 = make_ref<TestObject>(123);
|
|
auto ref2 = ref1.copy();
|
|
|
|
CHECK(ref1);
|
|
CHECK(ref2);
|
|
CHECK(ref1.get() == ref2.get());
|
|
CHECK(ref1->value == 123);
|
|
CHECK(ref2->value == 123);
|
|
}
|
|
|
|
SUBCASE("explicit copy assignment works correctly") {
|
|
auto ref1 = make_ref<TestObject>(100);
|
|
auto ref2 = make_ref<TestObject>(200);
|
|
|
|
ref2 = ref1.copy();
|
|
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.as_weak();
|
|
|
|
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.as_weak();
|
|
}
|
|
// 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.as_weak();
|
|
WeakRef<TestObject> weak2 = weak1.copy(); // explicit 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.copy();
|
|
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("Control block cleanup race condition test") {
|
|
// This test specifically targets the race condition where both
|
|
// the last strong reference and last weak reference are destroyed
|
|
// simultaneously, potentially causing double-free of control block
|
|
|
|
const int test_iterations = 10000;
|
|
|
|
// Shared state for passing references between threads
|
|
Ref<TestObject> ptr1;
|
|
WeakRef<TestObject> ptr2;
|
|
auto setup = [&]() {
|
|
ptr1 = make_ref<TestObject>(0);
|
|
ptr2 = ptr1.as_weak();
|
|
};
|
|
|
|
// Barrier for synchronization - 2 participants (main thread + worker thread)
|
|
std::barrier sync_barrier{2};
|
|
|
|
std::thread worker_thread([&]() {
|
|
for (int iter = 0; iter < test_iterations; ++iter) {
|
|
// Wait for main thread to create the references
|
|
sync_barrier.arrive_and_wait();
|
|
|
|
// Worker thread destroys the weak reference simultaneously with main
|
|
// thread
|
|
ptr2.reset();
|
|
|
|
// Wait for next iteration
|
|
sync_barrier.arrive_and_wait();
|
|
}
|
|
});
|
|
|
|
for (int iter = 0; iter < test_iterations; ++iter) {
|
|
// Create references
|
|
setup();
|
|
|
|
// Both threads are ready - synchronize for simultaneous destruction
|
|
sync_barrier.arrive_and_wait();
|
|
|
|
// Main thread destroys the strong reference at the same time
|
|
// as worker thread destroys the weak reference
|
|
ptr1.reset();
|
|
|
|
// Wait for both destructions to complete
|
|
sync_barrier.arrive_and_wait();
|
|
|
|
// Clean up for next iteration
|
|
ptr1.reset();
|
|
ptr2.reset();
|
|
}
|
|
|
|
worker_thread.join();
|
|
|
|
// If we reach here without segfault/double-free, the test passes
|
|
// The bug would manifest as a crash or memory corruption
|
|
}
|
|
|
|
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.as_weak();
|
|
|
|
// 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.copy(); // Strong reference: parent → child
|
|
child->parent = parent.as_weak(); // 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());
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Polymorphic Ref conversions") {
|
|
SUBCASE("copy construction from derived to base") {
|
|
auto derived_ref = make_ref<Derived>(10, 20);
|
|
CHECK(derived_ref->get_value() == 30); // 10 + 20
|
|
|
|
// Convert Ref<Derived> to Ref<Base>
|
|
Ref<Base> base_ref = derived_ref.copy();
|
|
CHECK(base_ref);
|
|
CHECK(base_ref->get_value() == 30); // Virtual dispatch works
|
|
CHECK(base_ref->base_value == 10);
|
|
|
|
// Both should point to same object
|
|
CHECK(base_ref.get() == derived_ref.get());
|
|
}
|
|
|
|
SUBCASE("copy assignment from derived to base") {
|
|
auto derived_ref = make_ref<Derived>(5, 15);
|
|
auto base_ref = make_ref<Base>(100);
|
|
|
|
// Before assignment
|
|
CHECK(base_ref->get_value() == 100);
|
|
|
|
// Assign derived to base
|
|
base_ref = derived_ref.copy();
|
|
CHECK(base_ref->get_value() == 20); // 5 + 15
|
|
CHECK(base_ref.get() == derived_ref.get());
|
|
}
|
|
|
|
SUBCASE("move construction from derived to base") {
|
|
auto derived_ref = make_ref<Derived>(7, 3);
|
|
Base *original_ptr = derived_ref.get();
|
|
|
|
// Move construct base from derived
|
|
Ref<Base> base_ref = std::move(derived_ref);
|
|
CHECK(base_ref);
|
|
CHECK(base_ref->get_value() == 10); // 7 + 3
|
|
CHECK(base_ref.get() == original_ptr);
|
|
CHECK(!derived_ref); // Original should be empty after move
|
|
}
|
|
|
|
SUBCASE("move assignment from derived to base") {
|
|
auto derived_ref = make_ref<Derived>(8, 12);
|
|
auto base_ref = make_ref<Base>(200);
|
|
Base *derived_ptr = derived_ref.get();
|
|
|
|
// Move assign
|
|
base_ref = std::move(derived_ref);
|
|
CHECK(base_ref);
|
|
CHECK(base_ref->get_value() == 20); // 8 + 12
|
|
CHECK(base_ref.get() == derived_ptr);
|
|
CHECK(!derived_ref); // Should be empty after move
|
|
}
|
|
|
|
SUBCASE("multiple inheritance levels") {
|
|
auto another_derived = make_ref<AnotherDerived>(6, 4);
|
|
CHECK(another_derived->get_value() == 24); // 6 * 4
|
|
|
|
// Convert to base
|
|
Ref<Base> base_ref = another_derived.copy();
|
|
CHECK(base_ref->get_value() == 24); // Virtual dispatch
|
|
CHECK(base_ref.get() == another_derived.get());
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Polymorphic WeakRef conversions") {
|
|
SUBCASE("WeakRef copy construction from derived to base") {
|
|
auto derived_ref = make_ref<Derived>(3, 7);
|
|
|
|
// Create WeakRef<Derived>
|
|
WeakRef<Derived> weak_derived = derived_ref.as_weak();
|
|
|
|
// Convert to WeakRef<Base>
|
|
WeakRef<Base> weak_base = weak_derived.copy();
|
|
|
|
// Both should lock to same object
|
|
auto locked_derived = weak_derived.lock();
|
|
auto locked_base = weak_base.lock();
|
|
|
|
CHECK(locked_derived);
|
|
CHECK(locked_base);
|
|
CHECK(locked_derived.get() == locked_base.get());
|
|
CHECK(locked_base->get_value() == 10); // 3 + 7
|
|
}
|
|
|
|
SUBCASE("WeakRef copy assignment from derived to base") {
|
|
auto derived_ref = make_ref<Derived>(4, 6);
|
|
auto base_ref = make_ref<Base>(999);
|
|
|
|
WeakRef<Derived> weak_derived = derived_ref.as_weak();
|
|
WeakRef<Base> weak_base = base_ref.as_weak();
|
|
|
|
// Assign derived weak ref to base weak ref
|
|
weak_base = weak_derived.copy();
|
|
|
|
auto locked = weak_base.lock();
|
|
CHECK(locked);
|
|
CHECK(locked->get_value() == 10); // 4 + 6
|
|
CHECK(locked.get() == derived_ref.get());
|
|
}
|
|
|
|
SUBCASE("WeakRef from Ref<Derived> to WeakRef<Base>") {
|
|
auto derived_ref = make_ref<Derived>(2, 8);
|
|
|
|
// Create WeakRef<Base> directly from Ref<Derived>
|
|
WeakRef<Base> weak_base = derived_ref.as_weak();
|
|
|
|
auto locked = weak_base.lock();
|
|
CHECK(locked);
|
|
CHECK(locked->get_value() == 10); // 2 + 8
|
|
CHECK(locked.get() == derived_ref.get());
|
|
}
|
|
|
|
SUBCASE("WeakRef move operations") {
|
|
auto derived_ref = make_ref<Derived>(1, 9);
|
|
WeakRef<Derived> weak_derived = derived_ref.as_weak();
|
|
|
|
// Move construct
|
|
WeakRef<Base> weak_base = std::move(weak_derived);
|
|
|
|
// Original should be empty, new should work
|
|
CHECK(!weak_derived.lock());
|
|
|
|
auto locked = weak_base.lock();
|
|
CHECK(locked);
|
|
CHECK(locked->get_value() == 10); // 1 + 9
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Polymorphic edge cases") {
|
|
SUBCASE("empty Ref conversions") {
|
|
Ref<Derived> empty_derived;
|
|
CHECK(!empty_derived);
|
|
|
|
// Convert empty derived to base
|
|
Ref<Base> empty_base = empty_derived.copy();
|
|
CHECK(!empty_base);
|
|
|
|
// Move empty derived to base
|
|
Ref<Base> moved_base = std::move(empty_derived);
|
|
CHECK(!moved_base);
|
|
}
|
|
|
|
SUBCASE("empty WeakRef conversions") {
|
|
WeakRef<Derived> empty_weak_derived;
|
|
CHECK(!empty_weak_derived.lock());
|
|
|
|
// Convert empty weak derived to weak base
|
|
WeakRef<Base> empty_weak_base = empty_weak_derived.copy();
|
|
CHECK(!empty_weak_base.lock());
|
|
}
|
|
|
|
SUBCASE("mixed Ref and WeakRef conversions") {
|
|
auto derived_ref = make_ref<Derived>(5, 5);
|
|
|
|
// Ref<Derived> → WeakRef<Base>
|
|
WeakRef<Base> weak_base_from_ref = derived_ref.as_weak();
|
|
|
|
// WeakRef<Base> → Ref<Base> via lock
|
|
auto base_ref_from_weak = weak_base_from_ref.lock();
|
|
|
|
CHECK(base_ref_from_weak);
|
|
CHECK(base_ref_from_weak->get_value() == 10); // 5 + 5
|
|
CHECK(base_ref_from_weak.get() == derived_ref.get());
|
|
}
|
|
|
|
SUBCASE("multiple inheritance pointer address bug test") {
|
|
auto multi_ref = make_ref<MultipleInheritance>(42);
|
|
|
|
// Get pointers to different base classes - these will have different
|
|
// addresses
|
|
Interface1 *interface1_ptr = multi_ref.get();
|
|
Interface2 *interface2_ptr = multi_ref.get();
|
|
MultipleInheritance *multi_ptr = multi_ref.get();
|
|
|
|
// Verify that pointers are indeed different (demonstrating the issue)
|
|
CHECK(static_cast<void *>(interface1_ptr) !=
|
|
static_cast<void *>(interface2_ptr));
|
|
|
|
// Create WeakRef to Interface2 (which has a different pointer address)
|
|
WeakRef<Interface2> weak_interface2 = multi_ref.as_weak();
|
|
|
|
// Lock should return the correct Interface2 pointer, not miscalculated one
|
|
auto locked_interface2 = weak_interface2.lock();
|
|
CHECK(locked_interface2);
|
|
CHECK(locked_interface2.get() ==
|
|
interface2_ptr); // This might fail due to the bug!
|
|
CHECK(locked_interface2->get_interface2() == 2);
|
|
|
|
// Also test Interface1
|
|
WeakRef<Interface1> weak_interface1 = multi_ref.as_weak();
|
|
auto locked_interface1 = weak_interface1.lock();
|
|
CHECK(locked_interface1);
|
|
CHECK(locked_interface1.get() == interface1_ptr); // This might also fail!
|
|
CHECK(locked_interface1->get_interface1() == 1);
|
|
}
|
|
}
|
|
|
|
// Should be run with asan or valgrind
|
|
TEST_CASE("Self-referencing WeakRef pattern") {
|
|
struct AmIAlive {
|
|
volatile int x;
|
|
~AmIAlive() { x = 0; }
|
|
};
|
|
struct SelfReferencing {
|
|
AmIAlive am;
|
|
WeakRef<SelfReferencing> self_;
|
|
};
|
|
auto x = make_ref<SelfReferencing>();
|
|
x->self_ = x.as_weak();
|
|
}
|