//! An allocator that never grows or shrinks a block in place, so every
//! growth is a fresh allocation an allocation-failure walk can count.
//!
//! `std.testing.checkAllAllocationFailures` counts only `alloc` calls and
//! needs the same count on every run. List, map and writer growth, `toOwnedSlice`
//! and arenas first ask the heap to resize or remap in place and allocate only
//! when it says no, and the heap's answer depends on what earlier runs left in
//! its buckets and, for large blocks on Linux, on `mremap`. So a walk over
//! anything that grows or shrinks a block counts differently from one heap to
//! another. Every walk goes through `checkAllAllocationFailures` below, which
//! puts one of these between the walk and its backing allocator.
const std = @import("std");
const NoGrowAllocator = @This();
inner: std.mem.Allocator,
pub fn init(inner: std.mem.Allocator) NoGrowAllocator {
return .{ .inner = inner };
}
pub fn allocator(self: *NoGrowAllocator) std.mem.Allocator {
return .{ .ptr = self, .vtable = &.{ .alloc = alloc, .resize = resize, .remap = remap, .free = free } };
}
/// `std.testing.checkAllAllocationFailures` over a `NoGrowAllocator`, so the
/// number of allocations it walks is the same on every heap and platform.
pub fn checkAllAllocationFailures(
backing: std.mem.Allocator,
comptime test_fn: anytype,
extra_args: ExtraArgs(@TypeOf(test_fn)),
) !void {
var no_grow = init(backing);
return std.testing.checkAllAllocationFailures(no_grow.allocator(), test_fn, extra_args);
}
/// The arguments `test_fn` takes after its allocator, so a caller's tuple
/// literal gets its result type here as it would from std.
fn ExtraArgs(comptime TestFn: type) type {
const fields = @typeInfo(std.meta.ArgsTuple(TestFn)).@"struct".field_types;
return @Tuple(fields[1..]);
}
fn alloc(ctx: *anyopaque, len: usize, alignment: std.mem.Alignment, ret_addr: usize) ?[*]u8 {
const self: *NoGrowAllocator = @ptrCast(@alignCast(ctx));
return self.inner.rawAlloc(len, alignment, ret_addr);
}
fn resize(_: *anyopaque, _: []u8, _: std.mem.Alignment, _: usize, _: usize) bool {
return false;
}
fn remap(_: *anyopaque, _: []u8, _: std.mem.Alignment, _: usize, _: usize) ?[*]u8 {
return null;
}
fn free(ctx: *anyopaque, memory: []u8, alignment: std.mem.Alignment, ret_addr: usize) void {
const self: *NoGrowAllocator = @ptrCast(@alignCast(ctx));
self.inner.rawFree(memory, alignment, ret_addr);
}
test "every growth is a fresh allocation" {
var no_grow = NoGrowAllocator.init(std.testing.allocator);
const gpa = no_grow.allocator();
var list: std.ArrayList(u8) = .empty;
defer list.deinit(gpa);
try list.appendSlice(gpa, "abc");
const before = list.items.ptr;
try list.ensureTotalCapacity(gpa, 4096);
try std.testing.expect(list.items.ptr != before);
try std.testing.expectEqualStrings("abc", list.items);
}