#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN #include "arena_allocator.hpp" #include "format.hpp" #include #include #include #include TEST_CASE("ArenaAllocator basic construction") { ArenaAllocator arena; CHECK(arena.num_blocks() == 0); CHECK(arena.used_bytes() == 0); CHECK(arena.total_allocated() == 0); CHECK(arena.available_in_current_block() == 0); } TEST_CASE("ArenaAllocator custom initial size") { ArenaAllocator arena(2048); CHECK(arena.num_blocks() == 0); CHECK(arena.total_allocated() == 0); CHECK(arena.available_in_current_block() == 0); } TEST_CASE("ArenaAllocator basic allocation") { ArenaAllocator arena; SUBCASE("allocate zero bytes returns nullptr") { void *ptr = arena.allocate_raw(0); CHECK(ptr == nullptr); CHECK(arena.used_bytes() == 0); } SUBCASE("allocate single byte") { void *ptr = arena.allocate_raw(1); CHECK(ptr != nullptr); CHECK(arena.used_bytes() >= 1); } SUBCASE("allocate multiple bytes") { void *ptr1 = arena.allocate_raw(100); void *ptr2 = arena.allocate_raw(200); CHECK(ptr1 != nullptr); CHECK(ptr2 != nullptr); CHECK(ptr1 != ptr2); CHECK(arena.used_bytes() >= 300); } } TEST_CASE("ArenaAllocator alignment") { ArenaAllocator arena; SUBCASE("default alignment") { void *ptr = arena.allocate_raw(1); CHECK(reinterpret_cast(ptr) % alignof(std::max_align_t) == 0); } SUBCASE("custom alignment") { void *ptr8 = arena.allocate_raw(1, 8); CHECK(reinterpret_cast(ptr8) % 8 == 0); void *ptr16 = arena.allocate_raw(1, 16); CHECK(reinterpret_cast(ptr16) % 16 == 0); void *ptr32 = arena.allocate_raw(1, 32); CHECK(reinterpret_cast(ptr32) % 32 == 0); } SUBCASE("alignment with larger allocations") { ArenaAllocator fresh_arena; void *ptr = fresh_arena.allocate_raw(100, 64); CHECK(reinterpret_cast(ptr) % 64 == 0); } } TEST_CASE("ArenaAllocator block management") { ArenaAllocator arena(128); SUBCASE("single block allocation") { void *ptr = arena.allocate_raw(64); CHECK(ptr != nullptr); CHECK(arena.num_blocks() == 1); CHECK(arena.used_bytes() == 64); } SUBCASE("multiple blocks when size exceeded") { void *ptr1 = arena.allocate_raw(100); CHECK(arena.num_blocks() == 1); void *ptr2 = arena.allocate_raw(50); CHECK(arena.num_blocks() == 2); CHECK(ptr1 != ptr2); } SUBCASE("allocation larger than block size grows arena") { void *ptr = arena.allocate_raw(200); CHECK(ptr != nullptr); CHECK(arena.num_blocks() == 1); } } TEST_CASE("ArenaAllocator construct template") { ArenaAllocator arena; SUBCASE("construct int") { int *ptr = arena.construct(42); CHECK(ptr != nullptr); CHECK(*ptr == 42); } SUBCASE("construct array") { struct FixedString { char data[16]; FixedString(const char *str) { std::strncpy(data, str, sizeof(data) - 1); data[sizeof(data) - 1] = '\0'; } }; FixedString *ptr = arena.construct("hello world"); CHECK(ptr != nullptr); CHECK(std::strcmp(ptr->data, "hello world") == 0); } SUBCASE("construct multiple objects") { int *ptr1 = arena.construct(10); int *ptr2 = arena.construct(20); CHECK(ptr1 != ptr2); CHECK(*ptr1 == 10); CHECK(*ptr2 == 20); } SUBCASE("construct with multiple arguments") { struct IntPair { int first; int second; IntPair(int a, int b) : first(a), second(b) {} }; auto *ptr = arena.construct(42, 24); CHECK(ptr != nullptr); CHECK(ptr->first == 42); CHECK(ptr->second == 24); } } TEST_CASE("ArenaAllocator reset functionality") { ArenaAllocator arena; arena.allocate_raw(100); arena.allocate_raw(200); size_t used_before = arena.used_bytes(); CHECK(used_before > 0); arena.reset(); CHECK(arena.used_bytes() == 0); CHECK(arena.num_blocks() >= 1); void *ptr = arena.allocate_raw(50); CHECK(ptr != nullptr); CHECK(arena.used_bytes() == 50); } TEST_CASE("ArenaAllocator reset memory leak test") { ArenaAllocator arena(32); // Smaller initial size // Force multiple blocks arena.allocate_raw(30); // First block (32 bytes) CHECK(arena.num_blocks() == 1); arena.allocate_raw( 30); // Should create second block (64 bytes due to doubling) CHECK(arena.num_blocks() == 2); arena.allocate_raw(100); // Should create third block (128 bytes due to // doubling, or larger for 100) CHECK(arena.num_blocks() == 3); size_t total_before = arena.total_allocated(); CHECK(total_before > 32); arena.reset(); // After reset, only first block should remain (others freed to prevent memory // leak) CHECK(arena.num_blocks() == 1); CHECK(arena.total_allocated() == 32); // Only first block size CHECK(arena.used_bytes() == 0); // Should be able to use the first block again void *ptr = arena.allocate_raw(20); CHECK(ptr != nullptr); CHECK(arena.used_bytes() == 20); } TEST_CASE("ArenaAllocator memory tracking") { ArenaAllocator arena(512); CHECK(arena.total_allocated() == 0); CHECK(arena.used_bytes() == 0); CHECK(arena.available_in_current_block() == 0); arena.allocate_raw(100); CHECK(arena.used_bytes() >= 100); CHECK(arena.available_in_current_block() <= 412); arena.allocate_raw(400); CHECK(arena.num_blocks() == 1); arena.allocate_raw(50); CHECK(arena.num_blocks() == 2); CHECK(arena.total_allocated() >= 1024); } TEST_CASE("ArenaAllocator stress test") { ArenaAllocator arena(1024); SUBCASE("many small allocations") { std::vector ptrs; for (int i = 0; i < 1000; ++i) { void *ptr = arena.allocate_raw(8); CHECK(ptr != nullptr); ptrs.push_back(ptr); } for (size_t i = 1; i < ptrs.size(); ++i) { CHECK(ptrs[i] != ptrs[i - 1]); } } SUBCASE("alternating small and large allocations") { for (int i = 0; i < 50; ++i) { void *small_ptr = arena.allocate_raw(16); void *large_ptr = arena.allocate_raw(256); CHECK(small_ptr != nullptr); CHECK(large_ptr != nullptr); CHECK(small_ptr != large_ptr); } } } TEST_CASE("ArenaAllocator move semantics") { ArenaAllocator arena1(512); arena1.allocate_raw(100); size_t used_bytes = arena1.used_bytes(); size_t num_blocks = arena1.num_blocks(); ArenaAllocator arena2 = std::move(arena1); CHECK(arena2.used_bytes() == used_bytes); CHECK(arena2.num_blocks() == num_blocks); void *ptr = arena2.allocate_raw(50); CHECK(ptr != nullptr); } TEST_CASE("ArenaAllocator edge cases") { SUBCASE("very small block size") { ArenaAllocator arena(16); void *ptr = arena.allocate_raw(8); CHECK(ptr != nullptr); CHECK(arena.num_blocks() == 1); } SUBCASE("allocation exactly block size") { ArenaAllocator arena(64); void *ptr = arena.allocate_raw(64); CHECK(ptr != nullptr); CHECK(arena.num_blocks() == 1); void *ptr2 = arena.allocate_raw(1); CHECK(ptr2 != nullptr); CHECK(arena.num_blocks() == 2); } SUBCASE("multiple resets") { ArenaAllocator arena; for (int i = 0; i < 10; ++i) { arena.allocate_raw(100); arena.reset(); CHECK(arena.used_bytes() == 0); } } } struct TestPOD { int value; char name[16]; TestPOD(int v, const char *n) : value(v) { std::strncpy(name, n, sizeof(name) - 1); name[sizeof(name) - 1] = '\0'; } }; TEST_CASE("ArenaAllocator with custom objects") { ArenaAllocator arena; TestPOD *obj1 = arena.construct(42, "first"); TestPOD *obj2 = arena.construct(84, "second"); CHECK(obj1 != nullptr); CHECK(obj2 != nullptr); CHECK(obj1 != obj2); CHECK(obj1->value == 42); CHECK(std::strcmp(obj1->name, "first") == 0); CHECK(obj2->value == 84); CHECK(std::strcmp(obj2->name, "second") == 0); } TEST_CASE("ArenaAllocator geometric growth policy") { ArenaAllocator arena(64); SUBCASE("normal geometric growth doubles size") { arena.allocate_raw(60); // Fill first block size_t initial_total = arena.total_allocated(); arena.allocate_raw(10); // Force new block CHECK(arena.num_blocks() == 2); CHECK(arena.total_allocated() == initial_total + 128); // 64 * 2 = 128 } SUBCASE("large allocation creates appropriately sized block") { arena.allocate_raw(60); // Fill first block size_t initial_total = arena.total_allocated(); arena.allocate_raw(200); // Force large block CHECK(arena.num_blocks() == 2); CHECK(arena.total_allocated() >= initial_total + 200); // At least 200 bytes } SUBCASE("multiple growths maintain O(log n) blocks") { size_t allocation_size = 32; for (int i = 0; i < 10; ++i) { arena.allocate_raw(allocation_size); } // Should have grown logarithmically, not linearly CHECK(arena.num_blocks() < 6); // Much less than 10 } } TEST_CASE("ArenaAllocator alignment edge cases") { ArenaAllocator arena; SUBCASE("unaligned then aligned allocation") { void *ptr1 = arena.allocate_raw(1, 1); void *ptr2 = arena.allocate_raw(8, 8); CHECK(ptr1 != nullptr); CHECK(ptr2 != nullptr); CHECK(reinterpret_cast(ptr2) % 8 == 0); } SUBCASE("large alignment requirements") { ArenaAllocator fresh_arena; void *ptr = fresh_arena.allocate_raw(1, 128); CHECK(ptr != nullptr); CHECK(reinterpret_cast(ptr) % 128 == 0); } } TEST_CASE("ArenaAllocator realloc functionality") { ArenaAllocator arena; SUBCASE("realloc edge cases") { // realloc with new_size == 0 returns nullptr and reclaims memory if it's // the last allocation ArenaAllocator fresh_arena(256); void *ptr = fresh_arena.allocate_raw(100); size_t used_before = fresh_arena.used_bytes(); CHECK(used_before == 100); void *result = fresh_arena.realloc_raw(ptr, 100, 0); CHECK(result == nullptr); CHECK(fresh_arena.used_bytes() == 0); // Memory should be reclaimed // Test case where it's NOT the last allocation - memory cannot be reclaimed ArenaAllocator arena2(256); void *ptr1 = arena2.allocate_raw(50); (void)arena2.allocate_raw(50); size_t used_before2 = arena2.used_bytes(); CHECK(used_before2 >= 100); // At least 100 bytes due to potential alignment void *result2 = arena2.realloc_raw(ptr1, 50, 0); // ptr1 is not the last allocation CHECK(result2 == nullptr); CHECK(arena2.used_bytes() == used_before2); // Memory should NOT be reclaimed // realloc with nullptr behaves like allocate void *new_ptr = arena.realloc_raw(nullptr, 0, 150); CHECK(new_ptr != nullptr); CHECK(arena.used_bytes() >= 150); // realloc with same size returns same pointer void *same_ptr = arena.realloc_raw(new_ptr, 150, 150); CHECK(same_ptr == new_ptr); } SUBCASE("in-place extension - growing") { ArenaAllocator fresh_arena(1024); void *ptr = fresh_arena.allocate_raw(100); CHECK(ptr != nullptr); // Fill the allocation with test data std::memset(ptr, 0xAB, 100); // Should extend in place since it's the last allocation void *extended_ptr = fresh_arena.realloc_raw(ptr, 100, 200); CHECK(extended_ptr == ptr); // Should be same pointer (in-place) // Check that original data is preserved char *data = static_cast(extended_ptr); for (size_t i = 0; i < 100; ++i) { CHECK(data[i] == static_cast(0xAB)); } CHECK(fresh_arena.used_bytes() == 200); } SUBCASE("in-place shrinking") { ArenaAllocator fresh_arena(1024); void *ptr = fresh_arena.allocate_raw(200); std::memset(ptr, 0xCD, 200); // Should shrink in place void *shrunk_ptr = fresh_arena.realloc_raw(ptr, 200, 100); CHECK(shrunk_ptr == ptr); // Same pointer CHECK(fresh_arena.used_bytes() == 100); // Check that remaining data is preserved char *data = static_cast(shrunk_ptr); for (size_t i = 0; i < 100; ++i) { CHECK(data[i] == static_cast(0xCD)); } } SUBCASE("copy when can't extend in place") { ArenaAllocator fresh_arena(256); // Larger block to avoid edge cases // Allocate first chunk void *ptr1 = fresh_arena.allocate_raw(60); std::memset(ptr1, 0x11, 60); // Allocate second chunk (this prevents in-place extension of ptr1) void *ptr2 = fresh_arena.allocate_raw(30); std::memset(ptr2, 0x22, 30); // Try to reallocate ptr1 - should copy since ptr2 is now the last // allocation void *realloc_ptr1 = fresh_arena.realloc_raw(ptr1, 60, 120); CHECK(realloc_ptr1 != ptr1); // Should be different pointer (copy occurred) // Check that data was copied correctly char *data = static_cast(realloc_ptr1); for (size_t i = 0; i < 60; ++i) { CHECK(data[i] == static_cast(0x11)); } // Try to reallocate ptr2 (last allocation) - should extend in place if // space allows void *realloc_ptr2 = fresh_arena.realloc_raw(ptr2, 30, 50); // Note: this might not be in-place if the realloc of ptr1 used up space in // the block Let's just check the result is valid and data is preserved CHECK(realloc_ptr2 != nullptr); char *data2 = static_cast(realloc_ptr2); for (size_t i = 0; i < 30; ++i) { CHECK(data2[i] == static_cast(0x22)); } } SUBCASE("copy when insufficient space for extension") { ArenaAllocator fresh_arena(100); // Allocate almost all space void *ptr = fresh_arena.allocate_raw(90); std::memset(ptr, 0x33, 90); // Try to extend beyond block size - should copy to new block void *extended_ptr = fresh_arena.realloc_raw(ptr, 90, 150); CHECK(extended_ptr != ptr); // Should be different (new block) CHECK(fresh_arena.num_blocks() == 2); // Should have created new block // Check data preservation char *data = static_cast(extended_ptr); for (size_t i = 0; i < 90; ++i) { CHECK(data[i] == static_cast(0x33)); } } SUBCASE("realloc with custom alignment") { ArenaAllocator fresh_arena(1024); // Allocate with specific alignment void *ptr = fresh_arena.allocate_raw(50, 16); CHECK(reinterpret_cast(ptr) % 16 == 0); std::memset(ptr, 0x44, 50); // Realloc with same alignment - should extend in place void *extended_ptr = fresh_arena.realloc_raw(ptr, 50, 100, 16); CHECK(extended_ptr == ptr); // In place CHECK(reinterpret_cast(extended_ptr) % 16 == 0); // Check data preservation char *data = static_cast(extended_ptr); for (size_t i = 0; i < 50; ++i) { CHECK(data[i] == static_cast(0x44)); } } SUBCASE("realloc stress test") { ArenaAllocator fresh_arena(512); void *ptr = fresh_arena.allocate_raw(50); size_t current_size = 50; // Fill with pattern for (size_t i = 0; i < 50; ++i) { static_cast(ptr)[i] = static_cast(i & 0xFF); } // Grow in multiple steps for (size_t new_size = 100; new_size <= 500; new_size += 100) { void *new_ptr = fresh_arena.realloc_raw(ptr, current_size, new_size); CHECK(new_ptr != nullptr); // Verify original data is still there for (size_t i = 0; i < 50; ++i) { CHECK(static_cast(new_ptr)[i] == static_cast(i & 0xFF)); } ptr = new_ptr; current_size = new_size; } } } TEST_CASE("format function fallback codepath") { SUBCASE("single-pass optimization success") { ArenaAllocator arena(128); auto result = format(arena, "Hello %s! Number: %d", "World", 42); CHECK(result == "Hello World! Number: 42"); CHECK(result.length() == 23); } SUBCASE("fallback when speculative formatting fails") { // Create arena with limited space to force fallback ArenaAllocator arena(16); // Consume most space to leave insufficient room for speculative formatting arena.allocate(10); CHECK(arena.available_in_current_block() == 6); // Format string larger than available space - should trigger fallback std::string long_string = "This is a very long string that won't fit"; auto result = format(arena, "Prefix: %s with %d", long_string.c_str(), 123); std::string expected = "Prefix: This is a very long string that won't fit with 123"; CHECK(result == expected); CHECK(result.length() == expected.length()); } SUBCASE("edge case - exactly available space") { ArenaAllocator arena(32); arena.allocate(20); // Leave 12 bytes CHECK(arena.available_in_current_block() == 12); // Format that needs exactly available space (should still use fallback due // to null terminator) auto result = format(arena, "Test%d", 123); // "Test123" = 7 chars CHECK(result == "Test123"); CHECK(result.length() == 7); } SUBCASE("allocate_remaining_space postcondition") { // Test empty arena ArenaAllocator empty_arena(64); auto space1 = empty_arena.allocate_remaining_space(); CHECK(space1.allocated_bytes >= 1); CHECK(space1.allocated_bytes == 64); // Test full arena (should create new block) ArenaAllocator full_arena(32); full_arena.allocate(32); // Fill completely auto space2 = full_arena.allocate_remaining_space(); CHECK(space2.allocated_bytes >= 1); CHECK(space2.allocated_bytes == 32); // New block created } SUBCASE("format error handling") { ArenaAllocator arena(64); // Test with invalid format (should return empty string_view) // Note: This is hard to trigger reliably across platforms, // so we focus on successful cases in the other subcases auto result = format(arena, "Valid format: %d", 42); CHECK(result == "Valid format: 42"); } } // Test object with non-trivial destructor for ArenaAllocator::Ptr testing class TestObject { public: static int destructor_count; static int constructor_count; int value; TestObject(int v) : value(v) { constructor_count++; } ~TestObject() { destructor_count++; } static void reset_counters() { constructor_count = 0; destructor_count = 0; } }; int TestObject::destructor_count = 0; int TestObject::constructor_count = 0; // Test struct with trivial destructor struct TrivialObject { int value; TrivialObject(int v) : value(v) {} }; TEST_CASE("ArenaAllocator::Ptr smart pointer functionality") { TestObject::reset_counters(); SUBCASE("construct returns raw pointer for trivially destructible types") { ArenaAllocator arena; auto ptr = arena.construct(42); static_assert(std::is_same_v, "construct() should return raw pointer for trivially " "destructible types"); CHECK(ptr != nullptr); CHECK(ptr->value == 42); } SUBCASE("construct returns ArenaAllocator::Ptr for non-trivially " "destructible types") { ArenaAllocator arena; auto ptr = arena.construct(42); static_assert( std::is_same_v>, "construct() should return ArenaAllocator::Ptr for non-trivially " "destructible types"); CHECK(ptr); CHECK(ptr->value == 42); CHECK(TestObject::constructor_count == 1); CHECK(TestObject::destructor_count == 0); } SUBCASE("ArenaAllocator::Ptr calls destructor on destruction") { ArenaAllocator arena; { auto ptr = arena.construct(42); CHECK(TestObject::constructor_count == 1); CHECK(TestObject::destructor_count == 0); } // ptr goes out of scope CHECK(TestObject::destructor_count == 1); } SUBCASE("ArenaAllocator::Ptr move semantics") { ArenaAllocator arena; auto ptr1 = arena.construct(42); CHECK(TestObject::constructor_count == 1); auto ptr2 = std::move(ptr1); CHECK(!ptr1); // ptr1 should be null after move CHECK(ptr2); CHECK(ptr2->value == 42); CHECK(TestObject::destructor_count == 0); // No destruction yet ptr2.reset(); CHECK(TestObject::destructor_count == 1); // Destructor called } SUBCASE("ArenaAllocator::Ptr access operators") { ArenaAllocator arena; auto ptr = arena.construct(123); // Test operator-> CHECK(ptr->value == 123); // Test operator* CHECK((*ptr).value == 123); // Test get() TestObject *raw_ptr = ptr.get(); CHECK(raw_ptr != nullptr); CHECK(raw_ptr->value == 123); // Test bool conversion CHECK(ptr); CHECK(static_cast(ptr) == true); } SUBCASE("ArenaAllocator::Ptr reset functionality") { ArenaAllocator arena; auto ptr = arena.construct(42); CHECK(TestObject::constructor_count == 1); CHECK(TestObject::destructor_count == 0); ptr.reset(); CHECK(!ptr); CHECK(TestObject::destructor_count == 1); // Reset with new object TestObject *raw_obj = arena.construct(84).release(); ptr.reset(raw_obj); CHECK(ptr); CHECK(ptr->value == 84); CHECK(TestObject::constructor_count == 2); CHECK(TestObject::destructor_count == 1); } SUBCASE("ArenaAllocator::Ptr release functionality") { ArenaAllocator arena; auto ptr = arena.construct(42); TestObject *raw_ptr = ptr.release(); CHECK(!ptr); // ptr should be null after release CHECK(raw_ptr != nullptr); CHECK(raw_ptr->value == 42); CHECK(TestObject::destructor_count == 0); // No destructor called // Manually call destructor (since we released ownership) raw_ptr->~TestObject(); CHECK(TestObject::destructor_count == 1); } SUBCASE("ArenaAllocator::Ptr move assignment") { ArenaAllocator arena; auto ptr1 = arena.construct(42); auto ptr2 = arena.construct(84); CHECK(TestObject::constructor_count == 2); CHECK(TestObject::destructor_count == 0); ptr1 = std::move(ptr2); // Should destroy first object, move second CHECK(!ptr2); // ptr2 should be null CHECK(ptr1); CHECK(ptr1->value == 84); CHECK(TestObject::destructor_count == 1); // First object destroyed } }