// g++ --std=c++11 test.cpp #include "test.h" #include "fl/ui.h" #include "fl/variant.h" #include "fl/optional.h" #include "fl/str.h" #include "fl/shared_ptr.h" #include "fl/function.h" using namespace fl; // Test object that tracks construction/destruction for move semantics testing struct TrackedObject { static int construction_count; static int destruction_count; static int move_construction_count; static int copy_construction_count; int value; bool moved_from; TrackedObject(int v = 0) : value(v), moved_from(false) { construction_count++; } TrackedObject(const TrackedObject& other) : value(other.value), moved_from(false) { copy_construction_count++; } TrackedObject(TrackedObject&& other) noexcept : value(other.value), moved_from(false) { other.moved_from = true; move_construction_count++; } TrackedObject& operator=(const TrackedObject& other) { if (this != &other) { value = other.value; moved_from = false; } return *this; } TrackedObject& operator=(TrackedObject&& other) noexcept { if (this != &other) { value = other.value; moved_from = false; other.moved_from = true; } return *this; } ~TrackedObject() { destruction_count++; } static void reset_counters() { construction_count = 0; destruction_count = 0; move_construction_count = 0; copy_construction_count = 0; } }; // Static member definitions int TrackedObject::construction_count = 0; int TrackedObject::destruction_count = 0; int TrackedObject::move_construction_count = 0; int TrackedObject::copy_construction_count = 0; TEST_CASE("Variant move semantics and RAII") { // Test the core issue: moved-from variants should be empty and not destroy moved-from objects TrackedObject::reset_counters(); // Test 1: Verify moved-from variant is empty { Variant source(TrackedObject(42)); CHECK(source.is()); // Move construct - this is where the bug was Variant destination(fl::move(source)); // Critical test: source should be empty after move CHECK(source.empty()); CHECK(!source.is()); CHECK(!source.is()); // destination should have the object CHECK(destination.is()); CHECK_EQ(destination.ptr()->value, 42); } TrackedObject::reset_counters(); // Test 2: Verify moved-from variant via assignment is empty { Variant source(TrackedObject(100)); Variant destination; CHECK(source.is()); CHECK(destination.empty()); // Move assign - this is where the bug was destination = fl::move(source); // Critical test: source should be empty after move CHECK(source.empty()); CHECK(!source.is()); CHECK(!source.is()); // destination should have the object CHECK(destination.is()); CHECK_EQ(destination.ptr()->value, 100); } TrackedObject::reset_counters(); // Test 3: Simulate the original fetch callback scenario // The key issue was that function objects containing shared_ptr were being destroyed // after being moved, causing use-after-free in the shared_ptr reference counting { using MockCallback = fl::function; auto shared_resource = fl::make_shared(999); // Create callback that captures shared_ptr (like fetch callbacks did) MockCallback callback = [shared_resource]() { // Use the resource FL_WARN("Using resource with value: " << shared_resource->value); }; // Store in variant (like WasmFetchCallbackManager did) Variant callback_variant(fl::move(callback)); CHECK(callback_variant.is()); // Extract via move (like takeCallback did) - this was causing heap-use-after-free Variant extracted_callback(fl::move(callback_variant)); // Original variant should be empty - this is the key fix CHECK(callback_variant.empty()); CHECK(!callback_variant.is()); // Extracted callback should work and shared_ptr should be valid CHECK(extracted_callback.is()); CHECK_EQ(shared_resource.use_count(), 2); // One in extracted callback, one local // Call the extracted callback - should not crash if (auto* cb = extracted_callback.ptr()) { (*cb)(); } // Shared resource should still be valid CHECK_EQ(shared_resource.use_count(), 2); } } TEST_CASE("HashMap iterator-based erase") { fl::hash_map map; // Fill the map with some data map[1] = "one"; map[2] = "two"; map[3] = "three"; map[4] = "four"; map[5] = "five"; CHECK_EQ(map.size(), 5); // Test iterator-based erase auto it = map.find(3); CHECK(it != map.end()); CHECK_EQ(it->second, "three"); // Erase using iterator - should return iterator to next element auto next_it = map.erase(it); CHECK_EQ(map.size(), 4); CHECK(map.find(3) == map.end()); // Element should be gone // Verify all other elements are still there CHECK(map.find(1) != map.end()); CHECK(map.find(2) != map.end()); CHECK(map.find(4) != map.end()); CHECK(map.find(5) != map.end()); // Test erasing at end auto end_it = map.find(999); // Non-existent key CHECK(end_it == map.end()); auto result_it = map.erase(end_it); // Should handle gracefully CHECK(result_it == map.end()); CHECK_EQ(map.size(), 4); // Size should be unchanged // Test erasing all remaining elements using iterators while (!map.empty()) { auto first = map.begin(); map.erase(first); } CHECK_EQ(map.size(), 0); CHECK(map.empty()); } // Test the original test cases TEST_CASE("Variant tests") { // 1) Default is empty Variant v; REQUIRE(v.empty()); REQUIRE(!v.is()); REQUIRE(!v.is()); // 2) Emplace a T v = 123; REQUIRE(v.is()); REQUIRE(!v.is()); REQUIRE_EQ(*v.ptr(), 123); // 3) Reset back to empty v.reset(); REQUIRE(v.empty()); // 4) Emplace a U v = fl::string("hello"); REQUIRE(v.is()); REQUIRE(!v.is()); REQUIRE(v.equals(fl::string("hello"))); // 5) Copy‐construct preserves the U Variant v2(v); REQUIRE(v2.is()); fl::string* str_ptr = v2.ptr(); REQUIRE_NE(str_ptr, nullptr); REQUIRE_EQ(*str_ptr, fl::string("hello")); const bool is_str = v2.is(); const bool is_int = v2.is(); CHECK(is_str); CHECK(!is_int); #if 0 // 6) Move‐construct leaves source empty Variant v3(std::move(v2)); REQUIRE(v3.is()); REQUIRE_EQ(v3.getU(), fl::string("hello")); REQUIRE(v2.isEmpty()); // 7) Copy‐assign Variant v4; v4 = v3; REQUIRE(v4.is()); REQUIRE_EQ(v4.getU(), fl::string("hello")); // 8) Swap two variants v4.emplaceT(7); v3.swap(v4); REQUIRE(v3.is()); REQUIRE_EQ(v3.getT(), 7); REQUIRE(v4.is()); REQUIRE_EQ(v4.getU(), fl::string("hello")); #endif } TEST_CASE("Variant") { // 1) Default is empty Variant v; REQUIRE(v.empty()); REQUIRE(!v.is()); REQUIRE(!v.is()); REQUIRE(!v.is()); // 2) Construct with a value Variant v1(123); REQUIRE(v1.is()); REQUIRE(!v1.is()); REQUIRE(!v1.is()); REQUIRE_EQ(*v1.ptr(), 123); // 3) Construct with a different type Variant v2(fl::string("hello")); REQUIRE(!v2.is()); REQUIRE(v2.is()); REQUIRE(!v2.is()); REQUIRE_EQ(*v2.ptr(), fl::string("hello")); // 4) Copy construction Variant v3(v2); REQUIRE(v3.is()); REQUIRE(v3.equals(fl::string("hello"))); // 5) Assignment v = v1; REQUIRE(v.is()); REQUIRE_EQ(*v.ptr(), 123); // 6) Reset v.reset(); REQUIRE(v.empty()); // 7) Assignment of a value v = 3.14; REQUIRE(v.is()); // REQUIRE_EQ(v.get(), 3.14); REQUIRE_EQ(*v.ptr(), 3.14); // 8) Visitor pattern struct TestVisitor { int result = 0; void accept(int value) { result = value; } void accept(const fl::string& value) { result = value.length(); } void accept(double value) { result = static_cast(value); } }; TestVisitor visitor; v.visit(visitor); REQUIRE_EQ(visitor.result, 3); // 3.14 truncated to 3 v = fl::string("hello world"); v.visit(visitor); REQUIRE_EQ(visitor.result, 11); // length of "hello world" v = 42; v.visit(visitor); REQUIRE_EQ(visitor.result, 42); } // TEST_CASE("Optional") { // Optional opt; // REQUIRE(opt.empty()); // opt = 42; // REQUIRE(!opt.empty()); // REQUIRE_EQ(*opt.ptr(), 42); // Optional opt2 = opt; // REQUIRE(!opt2.empty()); // REQUIRE_EQ(*opt2.ptr(), 42); // opt2 = 100; // REQUIRE_EQ(*opt2.ptr(), 100); // }