759 lines
22 KiB
C++
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
|
|
}
|
|
}
|