#include "test.h" #include "fl/shared_ptr.h" #include "fl/weak_ptr.h" #include "fl/memory.h" #include "fl/compiler_control.h" #include "fl/vector.h" // Test class that does NOT inherit from fl::Referent (non-intrusive) class TestClass { public: TestClass() : value_(0), destructor_called_(nullptr) {} TestClass(int value) : value_(value), destructor_called_(nullptr) {} // Constructor that allows tracking destructor calls TestClass(int value, bool* destructor_flag) : value_(value), destructor_called_(destructor_flag) {} ~TestClass() { if (destructor_called_) { *destructor_called_ = true; } } int getValue() const { return value_; } void setValue(int value) { value_ = value; } private: int value_; bool* destructor_called_; }; TEST_CASE("fl::weak_ptr default construction") { fl::weak_ptr weak; CHECK_EQ(weak.use_count(), 0); CHECK(weak.expired()); auto shared = weak.lock(); CHECK(!shared); CHECK_EQ(shared.get(), nullptr); } TEST_CASE("fl::weak_ptr construction from shared_ptr") { fl::shared_ptr shared = fl::make_shared(42); CHECK_EQ(shared.use_count(), 1); fl::weak_ptr weak(shared); CHECK_EQ(weak.use_count(), 1); CHECK_EQ(shared.use_count(), 1); // weak_ptr doesn't increase shared count CHECK(!weak.expired()); auto locked = weak.lock(); CHECK(locked); CHECK_EQ(locked.use_count(), 2); // lock() creates a shared_ptr CHECK_EQ(locked->getValue(), 42); } TEST_CASE("fl::weak_ptr copy construction") { fl::shared_ptr shared = fl::make_shared(42); fl::weak_ptr weak1(shared); fl::weak_ptr weak2(weak1); CHECK_EQ(weak1.use_count(), 1); CHECK_EQ(weak2.use_count(), 1); CHECK(!weak1.expired()); CHECK(!weak2.expired()); auto locked1 = weak1.lock(); auto locked2 = weak2.lock(); CHECK_EQ(locked1.get(), locked2.get()); CHECK_EQ(locked1->getValue(), 42); } TEST_CASE("fl::weak_ptr move construction") { fl::shared_ptr shared = fl::make_shared(42); fl::weak_ptr weak1(shared); fl::weak_ptr weak2(fl::move(weak1)); CHECK_EQ(weak1.use_count(), 0); CHECK(weak1.expired()); CHECK_EQ(weak2.use_count(), 1); CHECK(!weak2.expired()); auto locked = weak2.lock(); CHECK(locked); CHECK_EQ(locked->getValue(), 42); } TEST_CASE("fl::weak_ptr assignment from shared_ptr") { fl::shared_ptr shared1 = fl::make_shared(42); fl::shared_ptr shared2 = fl::make_shared(100); fl::weak_ptr weak(shared1); CHECK_EQ(weak.lock()->getValue(), 42); weak = shared2; CHECK_EQ(weak.lock()->getValue(), 100); } TEST_CASE("fl::weak_ptr assignment from weak_ptr") { fl::shared_ptr shared1 = fl::make_shared(42); fl::shared_ptr shared2 = fl::make_shared(100); fl::weak_ptr weak1(shared1); fl::weak_ptr weak2(shared2); CHECK_EQ(weak1.lock()->getValue(), 42); CHECK_EQ(weak2.lock()->getValue(), 100); weak1 = weak2; CHECK_EQ(weak1.lock()->getValue(), 100); CHECK_EQ(weak2.lock()->getValue(), 100); } TEST_CASE("fl::weak_ptr move assignment") { fl::shared_ptr shared1 = fl::make_shared(42); fl::shared_ptr shared2 = fl::make_shared(100); fl::weak_ptr weak1(shared1); fl::weak_ptr weak2(shared2); weak1 = fl::move(weak2); CHECK_EQ(weak1.lock()->getValue(), 100); CHECK(weak2.expired()); } TEST_CASE("fl::weak_ptr expiration when shared_ptr destroyed") { bool destructor_called = false; fl::weak_ptr weak; { fl::shared_ptr shared = fl::make_shared(42, &destructor_called); weak = shared; CHECK(!weak.expired()); CHECK_EQ(weak.use_count(), 1); CHECK(!destructor_called); auto locked = weak.lock(); CHECK(locked); CHECK_EQ(locked->getValue(), 42); } // shared_ptr is destroyed, object should be destroyed CHECK(destructor_called); CHECK(weak.expired()); CHECK_EQ(weak.use_count(), 0); auto locked = weak.lock(); CHECK(!locked); CHECK_EQ(locked.get(), nullptr); } TEST_CASE("fl::weak_ptr with multiple shared_ptr references") { fl::shared_ptr shared1 = fl::make_shared(42); fl::shared_ptr shared2 = shared1; fl::weak_ptr weak(shared1); CHECK_EQ(shared1.use_count(), 2); CHECK_EQ(weak.use_count(), 2); CHECK(!weak.expired()); shared1.reset(); CHECK_EQ(shared2.use_count(), 1); CHECK_EQ(weak.use_count(), 1); CHECK(!weak.expired()); shared2.reset(); CHECK(weak.expired()); CHECK_EQ(weak.use_count(), 0); } TEST_CASE("fl::weak_ptr reset functionality") { fl::shared_ptr shared = fl::make_shared(42); fl::weak_ptr weak(shared); CHECK(!weak.expired()); CHECK_EQ(weak.use_count(), 1); weak.reset(); CHECK(weak.expired()); CHECK_EQ(weak.use_count(), 0); auto locked = weak.lock(); CHECK(!locked); } TEST_CASE("fl::weak_ptr swap functionality") { fl::shared_ptr shared1 = fl::make_shared(42); fl::shared_ptr shared2 = fl::make_shared(100); fl::weak_ptr weak1(shared1); fl::weak_ptr weak2(shared2); CHECK_EQ(weak1.lock()->getValue(), 42); CHECK_EQ(weak2.lock()->getValue(), 100); weak1.swap(weak2); CHECK_EQ(weak1.lock()->getValue(), 100); CHECK_EQ(weak2.lock()->getValue(), 42); } TEST_CASE("fl::weak_ptr owner_before functionality") { fl::shared_ptr shared1 = fl::make_shared(42); fl::shared_ptr shared2 = fl::make_shared(100); fl::weak_ptr weak1(shared1); fl::weak_ptr weak2(shared2); // owner_before should provide a strict weak ordering bool order1 = weak1.owner_before(weak2); bool order2 = weak2.owner_before(weak1); // One should be true and the other false (strict ordering) CHECK(order1 != order2); // Test owner_before with shared_ptr bool order3 = weak1.owner_before(shared2); bool order4 = weak2.owner_before(shared1); CHECK(order3 != order4); } TEST_CASE("fl::weak_ptr conversion to shared_ptr") { fl::shared_ptr shared = fl::make_shared(42); fl::weak_ptr weak(shared); // Test construction from weak_ptr fl::shared_ptr converted(weak); CHECK(converted); CHECK_EQ(converted.use_count(), 2); CHECK_EQ(shared.use_count(), 2); CHECK_EQ(converted->getValue(), 42); CHECK_EQ(converted.get(), shared.get()); } TEST_CASE("fl::weak_ptr conversion from expired weak_ptr") { fl::weak_ptr weak; { fl::shared_ptr shared = fl::make_shared(42); weak = shared; CHECK(!weak.expired()); } CHECK(weak.expired()); // Converting expired weak_ptr should result in empty shared_ptr fl::shared_ptr converted(weak); CHECK(!converted); CHECK_EQ(converted.get(), nullptr); CHECK_EQ(converted.use_count(), 0); } TEST_CASE("fl::weak_ptr multiple weak references") { fl::shared_ptr shared = fl::make_shared(42); fl::weak_ptr weak1(shared); fl::weak_ptr weak2(shared); fl::weak_ptr weak3(weak1); CHECK_EQ(shared.use_count(), 1); CHECK_EQ(weak1.use_count(), 1); CHECK_EQ(weak2.use_count(), 1); CHECK_EQ(weak3.use_count(), 1); shared.reset(); CHECK(weak1.expired()); CHECK(weak2.expired()); CHECK(weak3.expired()); CHECK_EQ(weak1.use_count(), 0); CHECK_EQ(weak2.use_count(), 0); CHECK_EQ(weak3.use_count(), 0); } TEST_CASE("fl::weak_ptr self-assignment safety") { fl::shared_ptr shared = fl::make_shared(42); fl::weak_ptr weak(shared); CHECK_EQ(weak.use_count(), 1); CHECK(!weak.expired()); // Self-assignment should not change anything FL_DISABLE_WARNING_PUSH FL_DISABLE_WARNING_SELF_ASSIGN_OVERLOADED weak = weak; // Test self-assignment FL_DISABLE_WARNING_POP CHECK_EQ(weak.use_count(), 1); CHECK(!weak.expired()); CHECK_EQ(weak.lock()->getValue(), 42); // Self-move assignment should not break anything weak = fl::move(weak); CHECK_EQ(weak.use_count(), 1); CHECK(!weak.expired()); CHECK_EQ(weak.lock()->getValue(), 42); } // Node class for testing circular references and self-assignment scenarios class Node { public: Node(int value) : value_(value), destructor_called_(nullptr) {} Node(int value, bool* destructor_flag) : value_(value), destructor_called_(destructor_flag) {} ~Node() { if (destructor_called_) { *destructor_called_ = true; } } int getValue() const { return value_; } void setValue(int value) { value_ = value; } void setNext(fl::shared_ptr next) { next_ = next; } fl::shared_ptr getNext() const { return next_; } void setWeakNext(fl::weak_ptr next) { weak_next_ = next; } fl::weak_ptr getWeakNext() const { return weak_next_; } private: int value_; bool* destructor_called_; fl::shared_ptr next_; fl::weak_ptr weak_next_; }; TEST_CASE("fl::weak_ptr dead memory safety - basic scenario") { bool destructor_called = false; fl::weak_ptr weak; // Create shared_ptr and weak_ptr in a scope { auto shared = fl::make_shared(42); //shared = fl::shared_ptr(new TestClass(100, &destructor_called)); shared = fl::make_shared(100, &destructor_called); weak = shared; CHECK(!weak.expired()); CHECK_EQ(weak.use_count(), 1); CHECK(!destructor_called); // Verify we can still access the object auto locked = weak.lock(); CHECK(locked); CHECK_EQ(locked->getValue(), 100); } // shared_ptr goes out of scope here // Object should be destroyed, weak_ptr should be expired CHECK(destructor_called); CHECK(weak.expired()); CHECK_EQ(weak.use_count(), 0); // Attempting to lock should return empty shared_ptr - no segfault! auto locked = weak.lock(); CHECK(!locked); CHECK_EQ(locked.get(), nullptr); } TEST_CASE("fl::weak_ptr dead memory safety - multiple weak_ptrs") { bool destructor_called = false; fl::weak_ptr weak1, weak2, weak3; { //auto shared = fl::shared_ptr(new TestClass(42, &destructor_called)); auto shared = fl::make_shared(42, &destructor_called); weak1 = shared; weak2 = weak1; weak3 = shared; CHECK_EQ(weak1.use_count(), 1); CHECK_EQ(weak2.use_count(), 1); CHECK_EQ(weak3.use_count(), 1); CHECK(!destructor_called); } // shared_ptr destroyed // All weak_ptrs should be expired CHECK(destructor_called); CHECK(weak1.expired()); CHECK(weak2.expired()); CHECK(weak3.expired()); // None should be able to lock CHECK(!weak1.lock()); CHECK(!weak2.lock()); CHECK(!weak3.lock()); } TEST_CASE("fl::weak_ptr dead memory safety - repeated lock attempts") { bool destructor_called = false; fl::weak_ptr weak; { //auto shared = fl::shared_ptr(new TestClass(42, &destructor_called)); auto shared = fl::make_shared(42, &destructor_called); weak = shared; } CHECK(destructor_called); CHECK(weak.expired()); // Try locking multiple times - should never segfault for (int i = 0; i < 10; ++i) { auto locked = weak.lock(); CHECK(!locked); } } TEST_CASE("fl::weak_ptr circular reference - basic linked list") { bool nodeA_destroyed = false; bool nodeB_destroyed = false; { //auto nodeA = fl::shared_ptr(new Node(1, &nodeA_destroyed)); auto nodeA = fl::make_shared(1, &nodeA_destroyed); //auto nodeB = fl::shared_ptr(new Node(2, &nodeB_destroyed)); auto nodeB = fl::make_shared(2, &nodeB_destroyed); // Create circular reference: A -> B -> A nodeA->setNext(nodeB); nodeB->setNext(nodeA); CHECK_EQ(nodeA.use_count(), 2); // nodeA and nodeB->next_ CHECK_EQ(nodeB.use_count(), 2); // nodeB and nodeA->next_ CHECK(!nodeA_destroyed); CHECK(!nodeB_destroyed); } // nodeA and nodeB local variables destroyed // Due to circular reference, objects are NOT destroyed yet // This is expected behavior - circular references prevent cleanup // In real code, you'd use weak_ptr to break cycles } TEST_CASE("fl::weak_ptr circular reference - broken with weak_ptr") { bool nodeA_destroyed = false; bool nodeB_destroyed = false; { //auto nodeA = fl::shared_ptr(new Node(1, &nodeA_destroyed)); auto nodeA = fl::make_shared(1, &nodeA_destroyed); //auto nodeB = fl::shared_ptr(new Node(2, &nodeB_destroyed)); auto nodeB = fl::make_shared(2, &nodeB_destroyed); // Create non-circular reference: A -> B, A <- weak B nodeA->setNext(nodeB); nodeB->setWeakNext(nodeA); // Use weak_ptr to break cycle CHECK_EQ(nodeA.use_count(), 1); // Only nodeA variable CHECK_EQ(nodeB.use_count(), 2); // nodeB variable and nodeA->next_ CHECK(!nodeA_destroyed); CHECK(!nodeB_destroyed); } // nodeA and nodeB local variables destroyed // Objects should be properly destroyed since cycle is broken CHECK(nodeA_destroyed); CHECK(nodeB_destroyed); } TEST_CASE("fl::weak_ptr self-assignment safety - a = b scenario") { bool nodeA_destroyed = false; bool nodeB_destroyed = false; //auto nodeA = fl::shared_ptr(new Node(1, &nodeA_destroyed)); auto nodeA = fl::make_shared(1, &nodeA_destroyed); //auto nodeB = fl::shared_ptr(new Node(2, &nodeB_destroyed)); auto nodeB = fl::make_shared(2, &nodeB_destroyed); // Test the scenario: a -> b, and we have a, and a = b nodeA->setNext(nodeB); // Verify initial state CHECK_EQ(nodeA->getValue(), 1); CHECK_EQ(nodeB->getValue(), 2); CHECK_EQ(nodeA->getNext().get(), nodeB.get()); CHECK_EQ(nodeA.use_count(), 1); // Only nodeA variable CHECK_EQ(nodeB.use_count(), 2); // nodeB variable + nodeA->next_ CHECK(!nodeA_destroyed); CHECK(!nodeB_destroyed); // Get a reference to A before the dangerous assignment auto aRef = nodeA; CHECK_EQ(aRef.get(), nodeA.get()); CHECK_EQ(nodeA.use_count(), 2); // nodeA + aRef CHECK_EQ(nodeB.use_count(), 2); // nodeB + nodeA->next_ // Now do the dangerous assignment: a = b (while a is referenced through aRef) // This could cause issues if a gets destroyed while setting itself to b nodeA = nodeB; // a = b (dangerous assignment) // Verify no segfault occurred and state is consistent CHECK_EQ(nodeA.get(), nodeB.get()); // nodeA should now point to nodeB CHECK_EQ(nodeA->getValue(), 2); // Should have nodeB's value CHECK_EQ(nodeB->getValue(), 2); // nodeB unchanged CHECK(!nodeA_destroyed); // Original nodeA object should still exist CHECK(!nodeB_destroyed); // aRef should still be valid (original nodeA should still exist) CHECK(aRef); CHECK_EQ(aRef->getValue(), 1); // Original nodeA value CHECK_EQ(aRef.use_count(), 1); // Only aRef now points to original nodeA // nodeB should now have increased reference count CHECK_EQ(nodeB.use_count(), 3); // nodeB + nodeA + nodeA->next_ (which points to nodeB) // Clean up - clear the circular reference in the original node aRef->setNext(nullptr); CHECK_EQ(nodeB.use_count(), 2); // nodeB + nodeA CHECK(!nodeA_destroyed); // Original nodeA still referenced by aRef CHECK(!nodeB_destroyed); // Clear the reference to original nodeA aRef.reset(); CHECK(nodeA_destroyed); // Now original nodeA should be destroyed CHECK(!nodeB_destroyed); // nodeB still referenced by nodeA // Clear final reference nodeA.reset(); nodeB.reset(); CHECK(nodeB_destroyed); // Now nodeB should be destroyed } TEST_CASE("fl::weak_ptr complex circular scenario with weak references") { bool nodeA_destroyed = false; bool nodeB_destroyed = false; bool nodeC_destroyed = false; fl::weak_ptr weakA, weakB, weakC; { //auto nodeA = fl::shared_ptr(new Node(1, &nodeA_destroyed)); auto nodeA = fl::make_shared(1, &nodeA_destroyed); //auto nodeB = fl::shared_ptr(new Node(2, &nodeB_destroyed)); auto nodeB = fl::make_shared(2, &nodeB_destroyed); //auto nodeC = fl::shared_ptr(new Node(3, &nodeC_destroyed)); auto nodeC = fl::make_shared(3, &nodeC_destroyed); // Create complex references: A -> B -> C, with weak back-references nodeA->setNext(nodeB); nodeB->setNext(nodeC); nodeC->setWeakNext(nodeA); // Weak reference back to A weakA = nodeA; weakB = nodeB; weakC = nodeC; CHECK(!weakA.expired()); CHECK(!weakB.expired()); CHECK(!weakC.expired()); CHECK(!nodeA_destroyed); CHECK(!nodeB_destroyed); CHECK(!nodeC_destroyed); } // All shared_ptr variables destroyed // All objects should be destroyed since no circular strong references exist CHECK(nodeA_destroyed); CHECK(nodeB_destroyed); CHECK(nodeC_destroyed); CHECK(weakA.expired()); CHECK(weakB.expired()); CHECK(weakC.expired()); } TEST_CASE("fl::weak_ptr stress test - rapid creation and destruction") { fl::vector> weak_ptrs; weak_ptrs.reserve(100); // Create many shared_ptrs and weak_ptrs rapidly for (int i = 0; i < 100; ++i) { auto shared = fl::make_shared(i); weak_ptrs.push_back(fl::weak_ptr(shared)); // Shared goes out of scope immediately - weak_ptr should handle this } // All weak_ptrs should be expired for (auto& weak : weak_ptrs) { CHECK(weak.expired()); CHECK(!weak.lock()); } }