authorgravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2022-01-13 00:35:50-08:00
committergravatar for git@vexu.euVeikka Tuominen <git@vexu.eu> 2022-04-04 15:32:43+03:00
log6d04ab6d5be1eaa47a920aaa3989bd3515fec5d6
treeab350433d9238a240c4e7a59664edb94b557adf7
parent91eb1af9177d774158d888484023e3bf0be65412

Add `std.testing.checkAllAllocationFailures`

Adds a function that allows checking for memory leaks (and other problems) by taking advantage of the FailingAllocator and inducing failure at every allocation point within the provided `test_fn` (based on the strategy employed in the Zig parser tests, which can now use this function).

2 files changed, 161 insertions(+), 45 deletions(-)

lib/std/testing.zig+144
......@@ -574,6 +574,150 @@ test {
574574 try expectEqualStrings("foo", "foo");
575575}
576576
577/// Exhaustively check that allocation failures within `test_fn` are handled without
578/// introducing memory leaks. If used with the `testing.allocator` as the `backing_allocator`,
579/// it will also be able to detect double frees, etc (when runtime safety is enabled).
580///
581/// The provided `test_fn` must have a `std.mem.Allocator` as its first argument,
582/// and must have a return type of `!void`. Any extra arguments of `test_fn` can
583/// be provided via the `extra_args` tuple.
584///
585/// Any relevant state shared between runs of `test_fn` *must* be reset within `test_fn`.
586///
587/// Expects that the `test_fn` has a deterministic number of memory allocations
588/// (an error will be returned if non-deterministic allocations are detected).
589///
590/// The strategy employed is to:
591/// - Run the test function once to get the total number of allocations.
592/// - Then, iterate and run the function X more times, incrementing
593/// the failing index each iteration (where X is the total number of
594/// allocations determined previously)
595///
596/// ---
597///
598/// Here's an example of using a simple test case that will cause a leak when the
599/// allocation of `bar` fails (but will pass normally):
600///
601/// ```zig
602/// test {
603/// const length: usize = 10;
604/// const allocator = std.testing.allocator;
605/// var foo = try allocator.alloc(u8, length);
606/// var bar = try allocator.alloc(u8, length);
607///
608/// allocator.free(foo);
609/// allocator.free(bar);
610/// }
611/// ```
612///
613/// The test case can be converted to something that this function can use by
614/// doing:
615///
616/// ```zig
617/// fn testImpl(allocator: std.mem.Allocator, length: usize) !void {
618/// var foo = try allocator.alloc(u8, length);
619/// var bar = try allocator.alloc(u8, length);
620///
621/// allocator.free(foo);
622/// allocator.free(bar);
623/// }
624///
625/// test {
626/// const length: usize = 10;
627/// const allocator = std.testing.allocator;
628/// try std.testing.checkAllAllocationFailures(allocator, testImpl, .{length});
629/// }
630/// ```
631///
632/// Running this test will show that `foo` is leaked when the allocation of
633/// `bar` fails. The simplest fix, in this case, would be to use defer like so:
634///
635/// ```zig
636/// fn testImpl(allocator: std.mem.Allocator, length: usize) !void {
637/// var foo = try allocator.alloc(u8, length);
638/// defer allocator.free(foo);
639/// var bar = try allocator.alloc(u8, length);
640/// defer allocator.free(bar);
641/// }
642/// ```
643pub fn checkAllAllocationFailures(backing_allocator: std.mem.Allocator, comptime test_fn: anytype, extra_args: anytype) !void {
644 switch (@typeInfo(@typeInfo(@TypeOf(test_fn)).Fn.return_type.?)) {
645 .ErrorUnion => |info| {
646 if (info.payload != void) {
647 @compileError("Return type must be !void");
648 }
649 },
650 else => @compileError("Return type must be !void"),
651 }
652 if (@typeInfo(@TypeOf(extra_args)) != .Struct) {
653 @compileError("Expected tuple or struct argument, found " ++ @typeName(@TypeOf(extra_args)));
654 }
655
656 const ArgsTuple = std.meta.ArgsTuple(@TypeOf(test_fn));
657 const fn_args_fields = @typeInfo(ArgsTuple).Struct.fields;
658 if (fn_args_fields.len == 0 or fn_args_fields[0].field_type != std.mem.Allocator) {
659 @compileError("The provided function must have an " ++ @typeName(std.mem.Allocator) ++ " as its first argument");
660 }
661 const expected_args_tuple_len = fn_args_fields.len - 1;
662 if (extra_args.len != expected_args_tuple_len) {
663 @compileError("The provided function expects " ++ (comptime std.fmt.comptimePrint("{d}", .{expected_args_tuple_len})) ++ " extra arguments, but the provided tuple contains " ++ (comptime std.fmt.comptimePrint("{d}", .{extra_args.len})));
664 }
665
666 // Setup the tuple that will actually be used with @call (we'll need to insert
667 // the failing allocator in field @"0" before each @call)
668 var args: ArgsTuple = undefined;
669 inline for (@typeInfo(@TypeOf(extra_args)).Struct.fields) |field, i| {
670 const expected_type = fn_args_fields[i + 1].field_type;
671 if (expected_type != field.field_type) {
672 @compileError("Unexpected type for extra argument at index " ++ (comptime std.fmt.comptimePrint("{d}", .{i})) ++ ": expected " ++ @typeName(expected_type) ++ ", found " ++ @typeName(field.field_type));
673 }
674 const arg_i_str = comptime str: {
675 var str_buf: [100]u8 = undefined;
676 const args_i = i + 1;
677 const str_len = std.fmt.formatIntBuf(&str_buf, args_i, 10, .lower, .{});
678 break :str str_buf[0..str_len];
679 };
680 @field(args, arg_i_str) = @field(extra_args, field.name);
681 }
682
683 // Try it once with unlimited memory, make sure it works
684 const needed_alloc_count = x: {
685 var failing_allocator_inst = std.testing.FailingAllocator.init(backing_allocator, std.math.maxInt(usize));
686 args.@"0" = failing_allocator_inst.allocator();
687
688 try @call(.{}, test_fn, args);
689 break :x failing_allocator_inst.index;
690 };
691
692 var fail_index: usize = 0;
693 while (fail_index < needed_alloc_count) : (fail_index += 1) {
694 var failing_allocator_inst = std.testing.FailingAllocator.init(backing_allocator, fail_index);
695 args.@"0" = failing_allocator_inst.allocator();
696
697 if (@call(.{}, test_fn, args)) |_| {
698 return error.NondeterministicMemoryUsage;
699 } else |err| switch (err) {
700 error.OutOfMemory => {
701 if (failing_allocator_inst.allocated_bytes != failing_allocator_inst.freed_bytes) {
702 print(
703 "\nfail_index: {d}/{d}\nallocated bytes: {d}\nfreed bytes: {d}\nallocations: {d}\ndeallocations: {d}\n",
704 .{
705 fail_index,
706 needed_alloc_count,
707 failing_allocator_inst.allocated_bytes,
708 failing_allocator_inst.freed_bytes,
709 failing_allocator_inst.allocations,
710 failing_allocator_inst.deallocations,
711 },
712 );
713 return error.MemoryLeakDetected;
714 }
715 },
716 else => return err,
717 }
718 }
719}
720
577721/// Given a type, reference all the declarations inside, so that the semantic analyzer sees them.
578722pub fn refAllDecls(comptime T: type) void {
579723 if (!builtin.is_test) return;
lib/std/zig/parser_test.zig+17-45
......@@ -5459,52 +5459,24 @@ fn testParse(source: [:0]const u8, allocator: mem.Allocator, anything_changed: *
54595459 anything_changed.* = !mem.eql(u8, formatted, source);
54605460 return formatted;
54615461}
5462fn testTransform(source: [:0]const u8, expected_source: []const u8) !void {
5463 const needed_alloc_count = x: {
5464 // Try it once with unlimited memory, make sure it works
5465 var fixed_allocator = std.heap.FixedBufferAllocator.init(fixed_buffer_mem[0..]);
5466 var failing_allocator = std.testing.FailingAllocator.init(fixed_allocator.allocator(), maxInt(usize));
5467 const allocator = failing_allocator.allocator();
5468 var anything_changed: bool = undefined;
5469 const result_source = try testParse(source, allocator, &anything_changed);
5470 try std.testing.expectEqualStrings(expected_source, result_source);
5471 const changes_expected = source.ptr != expected_source.ptr;
5472 if (anything_changed != changes_expected) {
5473 print("std.zig.render returned {} instead of {}\n", .{ anything_changed, changes_expected });
5474 return error.TestFailed;
5475 }
5476 try std.testing.expect(anything_changed == changes_expected);
5477 allocator.free(result_source);
5478 break :x failing_allocator.index;
5479 };
5480
5481 var fail_index: usize = 0;
5482 while (fail_index < needed_alloc_count) : (fail_index += 1) {
5483 var fixed_allocator = std.heap.FixedBufferAllocator.init(fixed_buffer_mem[0..]);
5484 var failing_allocator = std.testing.FailingAllocator.init(fixed_allocator.allocator(), fail_index);
5485 var anything_changed: bool = undefined;
5486 if (testParse(source, failing_allocator.allocator(), &anything_changed)) |_| {
5487 return error.NondeterministicMemoryUsage;
5488 } else |err| switch (err) {
5489 error.OutOfMemory => {
5490 if (failing_allocator.allocated_bytes != failing_allocator.freed_bytes) {
5491 print(
5492 "\nfail_index: {d}/{d}\nallocated bytes: {d}\nfreed bytes: {d}\nallocations: {d}\ndeallocations: {d}\n",
5493 .{
5494 fail_index,
5495 needed_alloc_count,
5496 failing_allocator.allocated_bytes,
5497 failing_allocator.freed_bytes,
5498 failing_allocator.allocations,
5499 failing_allocator.deallocations,
5500 },
5501 );
5502 return error.MemoryLeakDetected;
5503 }
5504 },
5505 else => return err,
5506 }
5462fn testTransformImpl(allocator: mem.Allocator, fba: *std.heap.FixedBufferAllocator, source: [:0]const u8, expected_source: []const u8) !void {
5463 // reset the fixed buffer allocator each run so that it can be re-used for each
5464 // iteration of the failing index
5465 fba.reset();
5466 var anything_changed: bool = undefined;
5467 const result_source = try testParse(source, allocator, &anything_changed);
5468 try std.testing.expectEqualStrings(expected_source, result_source);
5469 const changes_expected = source.ptr != expected_source.ptr;
5470 if (anything_changed != changes_expected) {
5471 print("std.zig.render returned {} instead of {}\n", .{ anything_changed, changes_expected });
5472 return error.TestFailed;
55075473 }
5474 try std.testing.expect(anything_changed == changes_expected);
5475 allocator.free(result_source);
5476}
5477fn testTransform(source: [:0]const u8, expected_source: []const u8) !void {
5478 var fixed_allocator = std.heap.FixedBufferAllocator.init(fixed_buffer_mem[0..]);
5479 return std.testing.checkAllAllocationFailures(fixed_allocator.allocator(), testTransformImpl, .{ &fixed_allocator, source, expected_source });
55085480}
55095481fn testCanonical(source: [:0]const u8) !void {
55105482 return testTransform(source, source);