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