authorgravatar for greg@gpanders.comGregory Anders <greg@gpanders.com> 2023-08-28 20:25:05-05:00
committergravatar for git@vexu.euVeikka Tuominen <git@vexu.eu> 2023-09-06 19:06:32+03:00
logcab9da35bd65c81e8efbac3a6c970ce17627e4c7
tree6ffbf71d4d44490e554c355cc853fa0671dd3a2e
parent8976ad7ecbd6a0c70749e3bef9c508d0bfde02d2

std: enable FailingAllocator to fail on resize

Now that allocator.resize() is allowed to fail, programs may wish to test code paths that handle resize() failure. The simplest way to do this now is to replace the vtable of the testing allocator with one that uses Allocator.noResize for the 'resize' function pointer. An alternative way to support this testing capability is to augment the FailingAllocator (which is already useful for testing allocation failure scenarios) to intentionally fail on calls to resize(). To do this, add a 'resize_fail_index' parameter to the FailingAllocator that causes resize() to fail after the given number of calls.

7 files changed, 44 insertions(+), 42 deletions(-)

lib/std/array_list.zig+6-16
...@@ -1587,13 +1587,8 @@ test "std.ArrayListUnmanaged(u8) implements writer" {...@@ -1587,13 +1587,8 @@ test "std.ArrayListUnmanaged(u8) implements writer" {
1587}1587}
15881588
1589test "shrink still sets length when resizing is disabled" {1589test "shrink still sets length when resizing is disabled" {
1590 // Use the testing allocator but with resize disabled.1590 var failing_allocator = testing.FailingAllocator.init(testing.allocator, .{ .resize_fail_index = 0 });
1591 var a = testing.allocator;1591 const a = failing_allocator.allocator();
1592 a.vtable = &.{
1593 .alloc = a.vtable.alloc,
1594 .resize = Allocator.noResize,
1595 .free = a.vtable.free,
1596 };
15971592
1598 {1593 {
1599 var list = ArrayList(i32).init(a);1594 var list = ArrayList(i32).init(a);
...@@ -1620,13 +1615,9 @@ test "shrink still sets length when resizing is disabled" {...@@ -1620,13 +1615,9 @@ test "shrink still sets length when resizing is disabled" {
1620}1615}
16211616
1622test "shrinkAndFree with a copy" {1617test "shrinkAndFree with a copy" {
1623 // Use the testing allocator but with resize disabled.1618 var failing_allocator = testing.FailingAllocator.init(testing.allocator, .{ .resize_fail_index = 0 });
1624 var a = testing.allocator;1619 const a = failing_allocator.allocator();
1625 a.vtable = &.{1620
1626 .alloc = a.vtable.alloc,
1627 .resize = Allocator.noResize,
1628 .free = a.vtable.free,
1629 };
1630 var list = ArrayList(i32).init(a);1621 var list = ArrayList(i32).init(a);
1631 defer list.deinit();1622 defer list.deinit();
16321623
...@@ -1748,8 +1739,7 @@ test "ArrayListAligned/ArrayListAlignedUnmanaged accepts unaligned slices" {...@@ -1748,8 +1739,7 @@ test "ArrayListAligned/ArrayListAlignedUnmanaged accepts unaligned slices" {
17481739
1749test "std.ArrayList(u0)" {1740test "std.ArrayList(u0)" {
1750 // An ArrayList on zero-sized types should not need to allocate1741 // An ArrayList on zero-sized types should not need to allocate
1751 var failing_allocator = testing.FailingAllocator.init(testing.allocator, 0);1742 const a = testing.failing_allocator;
1752 const a = failing_allocator.allocator();
17531743
1754 var list = ArrayList(u0).init(a);1744 var list = ArrayList(u0).init(a);
1755 defer list.deinit();1745 defer list.deinit();
lib/std/heap/general_purpose_allocator.zig+1-1
...@@ -1305,7 +1305,7 @@ test "realloc large object to larger alignment" {...@@ -1305,7 +1305,7 @@ test "realloc large object to larger alignment" {
1305}1305}
13061306
1307test "large object shrinks to small but allocation fails during shrink" {1307test "large object shrinks to small but allocation fails during shrink" {
1308 var failing_allocator = std.testing.FailingAllocator.init(std.heap.page_allocator, 3);1308 var failing_allocator = std.testing.FailingAllocator.init(std.heap.page_allocator, .{ .fail_index = 3 });
1309 var gpa = GeneralPurposeAllocator(.{}){ .backing_allocator = failing_allocator.allocator() };1309 var gpa = GeneralPurposeAllocator(.{}){ .backing_allocator = failing_allocator.allocator() };
1310 defer std.testing.expect(gpa.deinit() == .ok) catch @panic("leak");1310 defer std.testing.expect(gpa.deinit() == .ok) catch @panic("leak");
1311 const allocator = gpa.allocator();1311 const allocator = gpa.allocator();
lib/std/heap/memory_pool.zig+2-2
...@@ -164,8 +164,8 @@ test "memory pool: preheating (success)" {...@@ -164,8 +164,8 @@ test "memory pool: preheating (success)" {
164}164}
165165
166test "memory pool: preheating (failure)" {166test "memory pool: preheating (failure)" {
167 var failer = std.testing.FailingAllocator.init(std.testing.allocator, 0);167 var failer = std.testing.failing_allocator;
168 try std.testing.expectError(error.OutOfMemory, MemoryPool(u32).initPreheated(failer.allocator(), 5));168 try std.testing.expectError(error.OutOfMemory, MemoryPool(u32).initPreheated(failer, 5));
169}169}
170170
171test "memory pool: growable" {171test "memory pool: growable" {
lib/std/json/scanner_test.zig+1-1
...@@ -397,7 +397,7 @@ test "skipValue" {...@@ -397,7 +397,7 @@ test "skipValue" {
397}397}
398398
399fn testEnsureStackCapacity(do_ensure: bool) !void {399fn testEnsureStackCapacity(do_ensure: bool) !void {
400 var fail_alloc = std.testing.FailingAllocator.init(std.testing.allocator, 1);400 var fail_alloc = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = 1 });
401 const failing_allocator = fail_alloc.allocator();401 const failing_allocator = fail_alloc.allocator();
402402
403 const nestings = 999; // intentionally not a power of 2.403 const nestings = 999; // intentionally not a power of 2.
lib/std/json/static_test.zig+1-2
...@@ -461,8 +461,7 @@ test "parse into tagged union errors" {...@@ -461,8 +461,7 @@ test "parse into tagged union errors" {
461 try testing.expectError(error.UnexpectedToken, parseFromSliceLeaky(T, arena.allocator(), "{\"nothing\":{\"no\":0}}", .{}));461 try testing.expectError(error.UnexpectedToken, parseFromSliceLeaky(T, arena.allocator(), "{\"nothing\":{\"no\":0}}", .{}));
462462
463 // Allocator failure463 // Allocator failure
464 var fail_alloc = testing.FailingAllocator.init(testing.allocator, 0);464 try testing.expectError(error.OutOfMemory, parseFromSlice(T, testing.failing_allocator, "{\"string\"\"foo\"}", .{}));
465 try testing.expectError(error.OutOfMemory, parseFromSlice(T, fail_alloc.allocator(), "{\"string\"\"foo\"}", .{}));
466}465}
467466
468test "parse into struct with no fields" {467test "parse into struct with no fields" {
lib/std/testing.zig+4-4
...@@ -14,7 +14,7 @@ pub var allocator_instance = b: {...@@ -14,7 +14,7 @@ pub var allocator_instance = b: {
14};14};
1515
16pub const failing_allocator = failing_allocator_instance.allocator();16pub const failing_allocator = failing_allocator_instance.allocator();
17pub var failing_allocator_instance = FailingAllocator.init(base_allocator_instance.allocator(), 0);17pub var failing_allocator_instance = FailingAllocator.init(base_allocator_instance.allocator(), .{ .fail_index = 0 });
1818
19pub var base_allocator_instance = std.heap.FixedBufferAllocator.init("");19pub var base_allocator_instance = std.heap.FixedBufferAllocator.init("");
2020
...@@ -1081,16 +1081,16 @@ pub fn checkAllAllocationFailures(backing_allocator: std.mem.Allocator, comptime...@@ -1081,16 +1081,16 @@ pub fn checkAllAllocationFailures(backing_allocator: std.mem.Allocator, comptime
10811081
1082 // Try it once with unlimited memory, make sure it works1082 // Try it once with unlimited memory, make sure it works
1083 const needed_alloc_count = x: {1083 const needed_alloc_count = x: {
1084 var failing_allocator_inst = std.testing.FailingAllocator.init(backing_allocator, std.math.maxInt(usize));1084 var failing_allocator_inst = std.testing.FailingAllocator.init(backing_allocator, .{});
1085 args.@"0" = failing_allocator_inst.allocator();1085 args.@"0" = failing_allocator_inst.allocator();
10861086
1087 try @call(.auto, test_fn, args);1087 try @call(.auto, test_fn, args);
1088 break :x failing_allocator_inst.index;1088 break :x failing_allocator_inst.alloc_index;
1089 };1089 };
10901090
1091 var fail_index: usize = 0;1091 var fail_index: usize = 0;
1092 while (fail_index < needed_alloc_count) : (fail_index += 1) {1092 while (fail_index < needed_alloc_count) : (fail_index += 1) {
1093 var failing_allocator_inst = std.testing.FailingAllocator.init(backing_allocator, fail_index);1093 var failing_allocator_inst = std.testing.FailingAllocator.init(backing_allocator, .{ .fail_index = fail_index });
1094 args.@"0" = failing_allocator_inst.allocator();1094 args.@"0" = failing_allocator_inst.allocator();
10951095
1096 if (@call(.auto, test_fn, args)) |_| {1096 if (@call(.auto, test_fn, args)) |_| {
lib/std/testing/failing_allocator.zig+29-16
...@@ -1,19 +1,33 @@...@@ -1,19 +1,33 @@
1const std = @import("../std.zig");1const std = @import("../std.zig");
2const mem = std.mem;2const mem = std.mem;
33
4pub const Config = struct {
5 /// The number of successful allocations you can expect from this allocator.
6 /// The next allocation will fail. For example, with `fail_index` equal to
7 /// 2, the following test will pass:
8 ///
9 /// var a = try failing_alloc.create(i32);
10 /// var b = try failing_alloc.create(i32);
11 /// testing.expectError(error.OutOfMemory, failing_alloc.create(i32));
12 fail_index: usize = std.math.maxInt(usize),
13
14 /// Number of successful resizes to expect from this allocator. The next resize will fail.
15 resize_fail_index: usize = std.math.maxInt(usize),
16};
17
4/// Allocator that fails after N allocations, useful for making sure out of18/// Allocator that fails after N allocations, useful for making sure out of
5/// memory conditions are handled correctly.19/// memory conditions are handled correctly.
6///20///
7/// To use this, first initialize it and get an allocator with21/// To use this, first initialize it and get an allocator with
8///22///
9/// `const failing_allocator = &FailingAllocator.init(<allocator>,23/// `const failing_allocator = &FailingAllocator.init(<allocator>,
10/// <fail_index>).allocator;`24/// <config>).allocator;`
11///25///
12/// Then use `failing_allocator` anywhere you would have used a26/// Then use `failing_allocator` anywhere you would have used a
13/// different allocator.27/// different allocator.
14pub const FailingAllocator = struct {28pub const FailingAllocator = struct {
15 index: usize,29 alloc_index: usize,
16 fail_index: usize,30 resize_index: usize,
17 internal_allocator: mem.Allocator,31 internal_allocator: mem.Allocator,
18 allocated_bytes: usize,32 allocated_bytes: usize,
19 freed_bytes: usize,33 freed_bytes: usize,
...@@ -21,28 +35,24 @@ pub const FailingAllocator = struct {...@@ -21,28 +35,24 @@ pub const FailingAllocator = struct {
21 deallocations: usize,35 deallocations: usize,
22 stack_addresses: [num_stack_frames]usize,36 stack_addresses: [num_stack_frames]usize,
23 has_induced_failure: bool,37 has_induced_failure: bool,
38 fail_index: usize,
39 resize_fail_index: usize,
2440
25 const num_stack_frames = if (std.debug.sys_can_stack_trace) 16 else 0;41 const num_stack_frames = if (std.debug.sys_can_stack_trace) 16 else 0;
2642
27 /// `fail_index` is the number of successful allocations you can43 pub fn init(internal_allocator: mem.Allocator, config: Config) FailingAllocator {
28 /// expect from this allocator. The next allocation will fail.
29 /// For example, if this is called with `fail_index` equal to 2,
30 /// the following test will pass:
31 ///
32 /// var a = try failing_alloc.create(i32);
33 /// var b = try failing_alloc.create(i32);
34 /// testing.expectError(error.OutOfMemory, failing_alloc.create(i32));
35 pub fn init(internal_allocator: mem.Allocator, fail_index: usize) FailingAllocator {
36 return FailingAllocator{44 return FailingAllocator{
37 .internal_allocator = internal_allocator,45 .internal_allocator = internal_allocator,
38 .fail_index = fail_index,46 .alloc_index = 0,
39 .index = 0,47 .resize_index = 0,
40 .allocated_bytes = 0,48 .allocated_bytes = 0,
41 .freed_bytes = 0,49 .freed_bytes = 0,
42 .allocations = 0,50 .allocations = 0,
43 .deallocations = 0,51 .deallocations = 0,
44 .stack_addresses = undefined,52 .stack_addresses = undefined,
45 .has_induced_failure = false,53 .has_induced_failure = false,
54 .fail_index = config.fail_index,
55 .resize_fail_index = config.resize_fail_index,
46 };56 };
47 }57 }
4858
...@@ -64,7 +74,7 @@ pub const FailingAllocator = struct {...@@ -64,7 +74,7 @@ pub const FailingAllocator = struct {
64 return_address: usize,74 return_address: usize,
65 ) ?[*]u8 {75 ) ?[*]u8 {
66 const self: *FailingAllocator = @ptrCast(@alignCast(ctx));76 const self: *FailingAllocator = @ptrCast(@alignCast(ctx));
67 if (self.index == self.fail_index) {77 if (self.alloc_index == self.fail_index) {
68 if (!self.has_induced_failure) {78 if (!self.has_induced_failure) {
69 @memset(&self.stack_addresses, 0);79 @memset(&self.stack_addresses, 0);
70 var stack_trace = std.builtin.StackTrace{80 var stack_trace = std.builtin.StackTrace{
...@@ -80,7 +90,7 @@ pub const FailingAllocator = struct {...@@ -80,7 +90,7 @@ pub const FailingAllocator = struct {
80 return null;90 return null;
81 self.allocated_bytes += len;91 self.allocated_bytes += len;
82 self.allocations += 1;92 self.allocations += 1;
83 self.index += 1;93 self.alloc_index += 1;
84 return result;94 return result;
85 }95 }
8696
...@@ -92,6 +102,8 @@ pub const FailingAllocator = struct {...@@ -92,6 +102,8 @@ pub const FailingAllocator = struct {
92 ra: usize,102 ra: usize,
93 ) bool {103 ) bool {
94 const self: *FailingAllocator = @ptrCast(@alignCast(ctx));104 const self: *FailingAllocator = @ptrCast(@alignCast(ctx));
105 if (self.resize_index == self.resize_fail_index)
106 return false;
95 if (!self.internal_allocator.rawResize(old_mem, log2_old_align, new_len, ra))107 if (!self.internal_allocator.rawResize(old_mem, log2_old_align, new_len, ra))
96 return false;108 return false;
97 if (new_len < old_mem.len) {109 if (new_len < old_mem.len) {
...@@ -99,6 +111,7 @@ pub const FailingAllocator = struct {...@@ -99,6 +111,7 @@ pub const FailingAllocator = struct {
99 } else {111 } else {
100 self.allocated_bytes += new_len - old_mem.len;112 self.allocated_bytes += new_len - old_mem.len;
101 }113 }
114 self.resize_index += 1;
102 return true;115 return true;
103 }116 }
104117