| ... | @@ -574,6 +574,150 @@ test { | ... | @@ -574,6 +574,150 @@ test { |
| 574 | try expectEqualStrings("foo", "foo"); | 574 | try expectEqualStrings("foo", "foo"); |
| 575 | } | 575 | } |
| 576 | | 576 | |
| | 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 | /// ``` |
| | 643 | pub 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 | |
| 577 | /// Given a type, reference all the declarations inside, so that the semantic analyzer sees them. | 721 | /// Given a type, reference all the declarations inside, so that the semantic analyzer sees them. |
| 578 | pub fn refAllDecls(comptime T: type) void { | 722 | pub fn refAllDecls(comptime T: type) void { |
| 579 | if (!builtin.is_test) return; | 723 | if (!builtin.is_test) return; |