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" {
15871587}
15881588
15891589test "shrink still sets length when resizing is disabled" {
1590 // Use the testing allocator but with resize disabled.
1591 var a = testing.allocator;
1592 a.vtable = &.{
1593 .alloc = a.vtable.alloc,
1594 .resize = Allocator.noResize,
1595 .free = a.vtable.free,
1596 };
1590 var failing_allocator = testing.FailingAllocator.init(testing.allocator, .{ .resize_fail_index = 0 });
1591 const a = failing_allocator.allocator();
15971592
15981593 {
15991594 var list = ArrayList(i32).init(a);
......@@ -1620,13 +1615,9 @@ test "shrink still sets length when resizing is disabled" {
16201615}
16211616
16221617test "shrinkAndFree with a copy" {
1623 // Use the testing allocator but with resize disabled.
1624 var a = testing.allocator;
1625 a.vtable = &.{
1626 .alloc = a.vtable.alloc,
1627 .resize = Allocator.noResize,
1628 .free = a.vtable.free,
1629 };
1618 var failing_allocator = testing.FailingAllocator.init(testing.allocator, .{ .resize_fail_index = 0 });
1619 const a = failing_allocator.allocator();
1620
16301621 var list = ArrayList(i32).init(a);
16311622 defer list.deinit();
16321623
......@@ -1748,8 +1739,7 @@ test "ArrayListAligned/ArrayListAlignedUnmanaged accepts unaligned slices" {
17481739
17491740test "std.ArrayList(u0)" {
17501741 // An ArrayList on zero-sized types should not need to allocate
1751 var failing_allocator = testing.FailingAllocator.init(testing.allocator, 0);
1752 const a = failing_allocator.allocator();
1742 const a = testing.failing_allocator;
17531743
17541744 var list = ArrayList(u0).init(a);
17551745 defer list.deinit();
lib/std/heap/general_purpose_allocator.zig+1-1
......@@ -1305,7 +1305,7 @@ test "realloc large object to larger alignment" {
13051305}
13061306
13071307test "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 });
13091309 var gpa = GeneralPurposeAllocator(.{}){ .backing_allocator = failing_allocator.allocator() };
13101310 defer std.testing.expect(gpa.deinit() == .ok) catch @panic("leak");
13111311 const allocator = gpa.allocator();
lib/std/heap/memory_pool.zig+2-2
......@@ -164,8 +164,8 @@ test "memory pool: preheating (success)" {
164164}
165165
166166test "memory pool: preheating (failure)" {
167 var failer = std.testing.FailingAllocator.init(std.testing.allocator, 0);
168 try std.testing.expectError(error.OutOfMemory, MemoryPool(u32).initPreheated(failer.allocator(), 5));
167 var failer = std.testing.failing_allocator;
168 try std.testing.expectError(error.OutOfMemory, MemoryPool(u32).initPreheated(failer, 5));
169169}
170170
171171test "memory pool: growable" {
lib/std/json/scanner_test.zig+1-1
......@@ -397,7 +397,7 @@ test "skipValue" {
397397}
398398
399399fn 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 });
401401 const failing_allocator = fail_alloc.allocator();
402402
403403 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" {
461461 try testing.expectError(error.UnexpectedToken, parseFromSliceLeaky(T, arena.allocator(), "{\"nothing\":{\"no\":0}}", .{}));
462462
463463 // Allocator failure
464 var fail_alloc = testing.FailingAllocator.init(testing.allocator, 0);
465 try testing.expectError(error.OutOfMemory, parseFromSlice(T, fail_alloc.allocator(), "{\"string\"\"foo\"}", .{}));
464 try testing.expectError(error.OutOfMemory, parseFromSlice(T, testing.failing_allocator, "{\"string\"\"foo\"}", .{}));
466465}
467466
468467test "parse into struct with no fields" {
lib/std/testing.zig+4-4
......@@ -14,7 +14,7 @@ pub var allocator_instance = b: {
1414};
1515
1616pub 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
1919pub var base_allocator_instance = std.heap.FixedBufferAllocator.init("");
2020
......@@ -1081,16 +1081,16 @@ pub fn checkAllAllocationFailures(backing_allocator: std.mem.Allocator, comptime
10811081
10821082 // Try it once with unlimited memory, make sure it works
10831083 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, .{});
10851085 args.@"0" = failing_allocator_inst.allocator();
10861086
10871087 try @call(.auto, test_fn, args);
1088 break :x failing_allocator_inst.index;
1088 break :x failing_allocator_inst.alloc_index;
10891089 };
10901090
10911091 var fail_index: usize = 0;
10921092 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 });
10941094 args.@"0" = failing_allocator_inst.allocator();
10951095
10961096 if (@call(.auto, test_fn, args)) |_| {
lib/std/testing/failing_allocator.zig+29-16
......@@ -1,19 +1,33 @@
11const std = @import("../std.zig");
22const 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
418/// Allocator that fails after N allocations, useful for making sure out of
519/// memory conditions are handled correctly.
620///
721/// To use this, first initialize it and get an allocator with
822///
923/// `const failing_allocator = &FailingAllocator.init(<allocator>,
10/// <fail_index>).allocator;`
24/// <config>).allocator;`
1125///
1226/// Then use `failing_allocator` anywhere you would have used a
1327/// different allocator.
1428pub const FailingAllocator = struct {
15 index: usize,
16 fail_index: usize,
29 alloc_index: usize,
30 resize_index: usize,
1731 internal_allocator: mem.Allocator,
1832 allocated_bytes: usize,
1933 freed_bytes: usize,
......@@ -21,28 +35,24 @@ pub const FailingAllocator = struct {
2135 deallocations: usize,
2236 stack_addresses: [num_stack_frames]usize,
2337 has_induced_failure: bool,
38 fail_index: usize,
39 resize_fail_index: usize,
2440
2541 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 can
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 {
43 pub fn init(internal_allocator: mem.Allocator, config: Config) FailingAllocator {
3644 return FailingAllocator{
3745 .internal_allocator = internal_allocator,
38 .fail_index = fail_index,
39 .index = 0,
46 .alloc_index = 0,
47 .resize_index = 0,
4048 .allocated_bytes = 0,
4149 .freed_bytes = 0,
4250 .allocations = 0,
4351 .deallocations = 0,
4452 .stack_addresses = undefined,
4553 .has_induced_failure = false,
54 .fail_index = config.fail_index,
55 .resize_fail_index = config.resize_fail_index,
4656 };
4757 }
4858
......@@ -64,7 +74,7 @@ pub const FailingAllocator = struct {
6474 return_address: usize,
6575 ) ?[*]u8 {
6676 const self: *FailingAllocator = @ptrCast(@alignCast(ctx));
67 if (self.index == self.fail_index) {
77 if (self.alloc_index == self.fail_index) {
6878 if (!self.has_induced_failure) {
6979 @memset(&self.stack_addresses, 0);
7080 var stack_trace = std.builtin.StackTrace{
......@@ -80,7 +90,7 @@ pub const FailingAllocator = struct {
8090 return null;
8191 self.allocated_bytes += len;
8292 self.allocations += 1;
83 self.index += 1;
93 self.alloc_index += 1;
8494 return result;
8595 }
8696
......@@ -92,6 +102,8 @@ pub const FailingAllocator = struct {
92102 ra: usize,
93103 ) bool {
94104 const self: *FailingAllocator = @ptrCast(@alignCast(ctx));
105 if (self.resize_index == self.resize_fail_index)
106 return false;
95107 if (!self.internal_allocator.rawResize(old_mem, log2_old_align, new_len, ra))
96108 return false;
97109 if (new_len < old_mem.len) {
......@@ -99,6 +111,7 @@ pub const FailingAllocator = struct {
99111 } else {
100112 self.allocated_bytes += new_len - old_mem.len;
101113 }
114 self.resize_index += 1;
102115 return true;
103116 }
104117