font/Atlas: add test for OOM behavior of grow

Similar tests should be added throughout the codebase for any function
that's supposed to gracefully handle OOM conditions. This one was added
because grow previously had a use-after-free bug under OOM, which this
would have caught.
This commit is contained in:
Qwerasd
2025-08-15 12:49:09 -06:00
parent 37ebf212d5
commit 0d4e673366

View File

@@ -86,6 +86,11 @@ pub const Region = extern struct {
height: u32,
};
/// Number of nodes to preallocate in the list on init.
///
/// TODO: figure out optimal prealloc based on real world usage
const node_prealloc: usize = 64;
pub fn init(alloc: Allocator, size: u32, format: Format) Allocator.Error!Atlas {
var result = Atlas{
.data = try alloc.alloc(u8, size * size * format.depth()),
@@ -95,8 +100,8 @@ pub fn init(alloc: Allocator, size: u32, format: Format) Allocator.Error!Atlas {
};
errdefer result.deinit(alloc);
// TODO: figure out optimal prealloc based on real world usage
try result.nodes.ensureUnusedCapacity(alloc, 64);
// Prealloc some nodes.
result.nodes = try .initCapacity(alloc, node_prealloc);
// This sets up our initial state
result.clear();
@@ -744,3 +749,49 @@ test "grow BGR" {
_ = try atlas.reserve(alloc, 2, 1);
try testing.expectError(Error.AtlasFull, atlas.reserve(alloc, 1, 1));
}
test "grow OOM" {
// We use a fixed buffer allocator so that we can consistently hit OOM.
//
// We calculate the size to exactly fit the 4x4 pixels and node list.
var buf: [
4 * 4 * 1 // 4x4 pixels, each 1 byte.
+ node_prealloc * @sizeOf(Node) // preallocated nodes.
]u8 = undefined;
var fba: std.heap.FixedBufferAllocator = .init(&buf);
const alloc = fba.allocator();
var atlas = try init(alloc, 4, .grayscale); // +2 for 1px border
defer atlas.deinit(alloc);
const reg = try atlas.reserve(alloc, 2, 2);
try testing.expectError(
Error.AtlasFull,
atlas.reserve(alloc, 1, 1),
);
// Write some data so we can verify that attempted growing doesn't mess it up.
atlas.set(reg, &[_]u8{ 1, 2, 3, 4 });
try testing.expectEqual(@as(u8, 1), atlas.data[5]);
try testing.expectEqual(@as(u8, 2), atlas.data[6]);
try testing.expectEqual(@as(u8, 3), atlas.data[9]);
try testing.expectEqual(@as(u8, 4), atlas.data[10]);
// Expand by 1, should give OOM, modified and resized should be unchanged.
const old_modified = atlas.modified.load(.monotonic);
const old_resized = atlas.resized.load(.monotonic);
try testing.expectError(
Allocator.Error.OutOfMemory,
atlas.grow(alloc, atlas.size + 1),
);
const new_modified = atlas.modified.load(.monotonic);
const new_resized = atlas.resized.load(.monotonic);
try testing.expectEqual(old_modified, new_modified);
try testing.expectEqual(old_resized, new_resized);
// Ensure our data is still set.
try testing.expectEqual(@as(u8, 1), atlas.data[5]);
try testing.expectEqual(@as(u8, 2), atlas.data[6]);
try testing.expectEqual(@as(u8, 3), atlas.data[9]);
try testing.expectEqual(@as(u8, 4), atlas.data[10]);
}