Exhaustively check that allocation failures within test_fn are handled without
introducing memory leaks. If used with the testing.allocator as the backing_allocator,
it will also be able to detect double frees, etc (when runtime safety is enabled).
The provided test_fn must have a std.mem.Allocator as its first argument,
and must have a return type of !void. Any extra arguments of test_fn can
be provided via the extra_args tuple.
Any relevant state shared between runs of test_fn must be reset within test_fn.
The strategy employed is to:
Expects that test_fn has a deterministic number of memory allocations:
test_fn, but test_fn
didn't return error.OutOfMemory, then error.SwallowedOutOfMemoryError
is returned from checkAllAllocationFailures. You may want to ignore this
depending on whether or not the code you're testing includes some strategies
for recovering from error.OutOfMemory.test_fn with an expected allocation failure executes without
an allocation failure being induced, then error.NondeterministicMemoryUsage
is returned. This error means that there are allocation points that won't be
tested by the strategy this function employs (that is, there are sometimes more
points of allocation than the initial run of test_fn detects).Here's an example using a simple test case that will cause a leak when the
allocation of bar fails (but will pass normally):
test {
const length: usize = 10;
const allocator = std.testing.allocator;
var foo = try allocator.alloc(u8, length);
var bar = try allocator.alloc(u8, length);
allocator.free(foo);
allocator.free(bar);
}
The test case can be converted to something that this function can use by doing:
fn testImpl(allocator: std.mem.Allocator, length: usize) !void {
var foo = try allocator.alloc(u8, length);
var bar = try allocator.alloc(u8, length);
allocator.free(foo);
allocator.free(bar);
}
test {
const length: usize = 10;
const allocator = std.testing.allocator;
try std.testing.checkAllAllocationFailures(allocator, testImpl, .{length});
}
Running this test will show that foo is leaked when the allocation of
bar fails. The simplest fix, in this case, would be to use defer like so:
fn testImpl(allocator: std.mem.Allocator, length: usize) !void {
var foo = try allocator.alloc(u8, length);
defer allocator.free(foo);
var bar = try allocator.alloc(u8, length);
defer allocator.free(bar);
}
pub fn checkAllAllocationFailures(
backing_allocator: std.mem.Allocator,
comptime test_fn: anytype,
extra_args: CheckAllAllocationFailuresExtraArgs(@TypeOf(test_fn)),
) !void
pub fn checkAllAllocationFailures(
backing_allocator: std.mem.Allocator,
comptime test_fn: anytype,
extra_args: CheckAllAllocationFailuresExtraArgs(@TypeOf(test_fn)),
) !void {
// Try it once with unlimited memory, make sure it works
const needed_alloc_count = x: {
var failing_allocator_inst = std.testing.FailingAllocator.init(backing_allocator, .{});
try @call(.auto, test_fn, .{failing_allocator_inst.allocator()} ++ extra_args);
break :x failing_allocator_inst.alloc_index;
};
for (0..needed_alloc_count) |fail_index| {
var failing_allocator_inst = std.testing.FailingAllocator.init(backing_allocator, .{
.fail_index = fail_index,
});
if (@call(.auto, test_fn, .{failing_allocator_inst.allocator()} ++ extra_args)) |_| {
if (failing_allocator_inst.has_induced_failure) {
return error.SwallowedOutOfMemoryError;
} else {
return error.NondeterministicMemoryUsage;
}
} else |err| switch (err) {
error.OutOfMemory => {
if (failing_allocator_inst.allocated_bytes != failing_allocator_inst.freed_bytes) {
print(
"\nfail_index: {d}/{d}\nallocated bytes: {d}\nfreed bytes: {d}\nallocations: {d}\ndeallocations: {d}\nallocation that was made to fail: {f}",
.{
fail_index,
needed_alloc_count,
failing_allocator_inst.allocated_bytes,
failing_allocator_inst.freed_bytes,
failing_allocator_inst.allocations,
failing_allocator_inst.deallocations,
std.debug.FormatStackTrace{
.stack_trace = failing_allocator_inst.getStackTrace(),
},
},
);
return error.MemoryLeakDetected;
}
},
else => |e| return e,
}
}
}