Files
weaseldb/tests/test_arena_allocator.cpp

759 lines
22 KiB
C++

#include "arena_allocator.hpp"
#include "format.hpp"
#include <cstring>
#include <doctest/doctest.h>
#include <string>
#include <vector>
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<uintptr_t>(ptr) % alignof(std::max_align_t) == 0);
}
SUBCASE("custom alignment") {
void *ptr8 = arena.allocate_raw(1, 8);
CHECK(reinterpret_cast<uintptr_t>(ptr8) % 8 == 0);
void *ptr16 = arena.allocate_raw(1, 16);
CHECK(reinterpret_cast<uintptr_t>(ptr16) % 16 == 0);
void *ptr32 = arena.allocate_raw(1, 32);
CHECK(reinterpret_cast<uintptr_t>(ptr32) % 32 == 0);
}
SUBCASE("alignment with larger allocations") {
ArenaAllocator fresh_arena;
void *ptr = fresh_arena.allocate_raw(100, 64);
CHECK(reinterpret_cast<uintptr_t>(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<int>(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<FixedString>("hello world");
CHECK(ptr != nullptr);
CHECK(std::strcmp(ptr->data, "hello world") == 0);
}
SUBCASE("construct multiple objects") {
int *ptr1 = arena.construct<int>(10);
int *ptr2 = arena.construct<int>(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<IntPair>(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<void *> 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<TestPOD>(42, "first");
TestPOD *obj2 = arena.construct<TestPOD>(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<uintptr_t>(ptr2) % 8 == 0);
}
SUBCASE("large alignment requirements") {
ArenaAllocator fresh_arena;
void *ptr = fresh_arena.allocate_raw(1, 128);
CHECK(ptr != nullptr);
CHECK(reinterpret_cast<uintptr_t>(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<char *>(extended_ptr);
for (size_t i = 0; i < 100; ++i) {
CHECK(data[i] == static_cast<char>(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<char *>(shrunk_ptr);
for (size_t i = 0; i < 100; ++i) {
CHECK(data[i] == static_cast<char>(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<char *>(realloc_ptr1);
for (size_t i = 0; i < 60; ++i) {
CHECK(data[i] == static_cast<char>(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<char *>(realloc_ptr2);
for (size_t i = 0; i < 30; ++i) {
CHECK(data2[i] == static_cast<char>(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<char *>(extended_ptr);
for (size_t i = 0; i < 90; ++i) {
CHECK(data[i] == static_cast<char>(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<uintptr_t>(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<uintptr_t>(extended_ptr) % 16 == 0);
// Check data preservation
char *data = static_cast<char *>(extended_ptr);
for (size_t i = 0; i < 50; ++i) {
CHECK(data[i] == static_cast<char>(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<char *>(ptr)[i] = static_cast<char>(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<char *>(new_ptr)[i] == static_cast<char>(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<char>(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<char>(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<char>(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<TrivialObject>(42);
static_assert(std::is_same_v<decltype(ptr), TrivialObject *>,
"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<TestObject>(42);
static_assert(
std::is_same_v<decltype(ptr), ArenaAllocator::Ptr<TestObject>>,
"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<TestObject>(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<TestObject>(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<TestObject>(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<bool>(ptr) == true);
}
SUBCASE("ArenaAllocator::Ptr reset functionality") {
ArenaAllocator arena;
auto ptr = arena.construct<TestObject>(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<TestObject>(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<TestObject>(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<TestObject>(42);
auto ptr2 = arena.construct<TestObject>(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
}
}