From cab9da35bd65c81e8efbac3a6c970ce17627e4c7 Mon Sep 17 00:00:00 2001 From: Gregory Anders Date: Mon, 28 Aug 2023 20:25:05 -0500 Subject: [PATCH] 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. --- lib/std/array_list.zig | 22 +++-------- lib/std/heap/general_purpose_allocator.zig | 2 +- lib/std/heap/memory_pool.zig | 4 +- lib/std/json/scanner_test.zig | 2 +- lib/std/json/static_test.zig | 3 +- lib/std/testing.zig | 8 ++-- lib/std/testing/failing_allocator.zig | 45 ++++++++++++++-------- 7 files changed, 44 insertions(+), 42 deletions(-) diff --git a/lib/std/array_list.zig b/lib/std/array_list.zig index ec38fad50b245a448d76a6ce7bcf4b22448a3f48..a258475517953fa1abfca08d20e1b6af5900f016 100644 --- a/lib/std/array_list.zig +++ b/lib/std/array_list.zig @@ -1587,13 +1587,8 @@ test "std.ArrayListUnmanaged(u8) implements writer" { } test "shrink still sets length when resizing is disabled" { - // Use the testing allocator but with resize disabled. - var a = testing.allocator; - a.vtable = &.{ - .alloc = a.vtable.alloc, - .resize = Allocator.noResize, - .free = a.vtable.free, - }; + var failing_allocator = testing.FailingAllocator.init(testing.allocator, .{ .resize_fail_index = 0 }); + const a = failing_allocator.allocator(); { var list = ArrayList(i32).init(a); @@ -1620,13 +1615,9 @@ test "shrink still sets length when resizing is disabled" { } test "shrinkAndFree with a copy" { - // Use the testing allocator but with resize disabled. - var a = testing.allocator; - a.vtable = &.{ - .alloc = a.vtable.alloc, - .resize = Allocator.noResize, - .free = a.vtable.free, - }; + var failing_allocator = testing.FailingAllocator.init(testing.allocator, .{ .resize_fail_index = 0 }); + const a = failing_allocator.allocator(); + var list = ArrayList(i32).init(a); defer list.deinit(); @@ -1748,8 +1739,7 @@ test "ArrayListAligned/ArrayListAlignedUnmanaged accepts unaligned slices" { test "std.ArrayList(u0)" { // An ArrayList on zero-sized types should not need to allocate - var failing_allocator = testing.FailingAllocator.init(testing.allocator, 0); - const a = failing_allocator.allocator(); + const a = testing.failing_allocator; var list = ArrayList(u0).init(a); defer list.deinit(); diff --git a/lib/std/heap/general_purpose_allocator.zig b/lib/std/heap/general_purpose_allocator.zig index ccc4b6a8b242bd77a1d5a96d3687a22384d4d755..7bdcf9ca2d8da99bbd81965728e4ba04a804e8a0 100644 --- a/lib/std/heap/general_purpose_allocator.zig +++ b/lib/std/heap/general_purpose_allocator.zig @@ -1305,7 +1305,7 @@ test "realloc large object to larger alignment" { } test "large object shrinks to small but allocation fails during shrink" { - var failing_allocator = std.testing.FailingAllocator.init(std.heap.page_allocator, 3); + var failing_allocator = std.testing.FailingAllocator.init(std.heap.page_allocator, .{ .fail_index = 3 }); var gpa = GeneralPurposeAllocator(.{}){ .backing_allocator = failing_allocator.allocator() }; defer std.testing.expect(gpa.deinit() == .ok) catch @panic("leak"); const allocator = gpa.allocator(); diff --git a/lib/std/heap/memory_pool.zig b/lib/std/heap/memory_pool.zig index b56a15d006ad21d7c2fc9a7c00dc6d4b66ea511f..764198d36d710a4b4691ea336107290b6ed57dfe 100644 --- a/lib/std/heap/memory_pool.zig +++ b/lib/std/heap/memory_pool.zig @@ -164,8 +164,8 @@ test "memory pool: preheating (success)" { } test "memory pool: preheating (failure)" { - var failer = std.testing.FailingAllocator.init(std.testing.allocator, 0); - try std.testing.expectError(error.OutOfMemory, MemoryPool(u32).initPreheated(failer.allocator(), 5)); + var failer = std.testing.failing_allocator; + try std.testing.expectError(error.OutOfMemory, MemoryPool(u32).initPreheated(failer, 5)); } test "memory pool: growable" { diff --git a/lib/std/json/scanner_test.zig b/lib/std/json/scanner_test.zig index acc4fc740453c15c423d4eedf5266caa001b2583..8ab7f5e7153e371e03aa3f41ed9754439df1a3d8 100644 --- a/lib/std/json/scanner_test.zig +++ b/lib/std/json/scanner_test.zig @@ -397,7 +397,7 @@ test "skipValue" { } fn testEnsureStackCapacity(do_ensure: bool) !void { - var fail_alloc = std.testing.FailingAllocator.init(std.testing.allocator, 1); + var fail_alloc = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = 1 }); const failing_allocator = fail_alloc.allocator(); const nestings = 999; // intentionally not a power of 2. diff --git a/lib/std/json/static_test.zig b/lib/std/json/static_test.zig index bbfdc6c40998d9518b4555a237218dca32bc47ba..82b0d89044939e69cb05ec52c62c7cc1f1689434 100644 --- a/lib/std/json/static_test.zig +++ b/lib/std/json/static_test.zig @@ -461,8 +461,7 @@ test "parse into tagged union errors" { try testing.expectError(error.UnexpectedToken, parseFromSliceLeaky(T, arena.allocator(), "{\"nothing\":{\"no\":0}}", .{})); // Allocator failure - var fail_alloc = testing.FailingAllocator.init(testing.allocator, 0); - try testing.expectError(error.OutOfMemory, parseFromSlice(T, fail_alloc.allocator(), "{\"string\"\"foo\"}", .{})); + try testing.expectError(error.OutOfMemory, parseFromSlice(T, testing.failing_allocator, "{\"string\"\"foo\"}", .{})); } test "parse into struct with no fields" { diff --git a/lib/std/testing.zig b/lib/std/testing.zig index 40a95a450bedd7c4083e25c4d0de2754081d74f3..17ac0d4b04f212def4d49befd1f229069d1a4bae 100644 --- a/lib/std/testing.zig +++ b/lib/std/testing.zig @@ -14,7 +14,7 @@ pub var allocator_instance = b: { }; pub const failing_allocator = failing_allocator_instance.allocator(); -pub var failing_allocator_instance = FailingAllocator.init(base_allocator_instance.allocator(), 0); +pub var failing_allocator_instance = FailingAllocator.init(base_allocator_instance.allocator(), .{ .fail_index = 0 }); pub var base_allocator_instance = std.heap.FixedBufferAllocator.init(""); @@ -1081,16 +1081,16 @@ pub fn checkAllAllocationFailures(backing_allocator: std.mem.Allocator, comptime // Try it once with unlimited memory, make sure it works const needed_alloc_count = x: { - var failing_allocator_inst = std.testing.FailingAllocator.init(backing_allocator, std.math.maxInt(usize)); + var failing_allocator_inst = std.testing.FailingAllocator.init(backing_allocator, .{}); args.@"0" = failing_allocator_inst.allocator(); try @call(.auto, test_fn, args); - break :x failing_allocator_inst.index; + break :x failing_allocator_inst.alloc_index; }; var fail_index: usize = 0; while (fail_index < needed_alloc_count) : (fail_index += 1) { - var failing_allocator_inst = std.testing.FailingAllocator.init(backing_allocator, fail_index); + var failing_allocator_inst = std.testing.FailingAllocator.init(backing_allocator, .{ .fail_index = fail_index }); args.@"0" = failing_allocator_inst.allocator(); if (@call(.auto, test_fn, args)) |_| { diff --git a/lib/std/testing/failing_allocator.zig b/lib/std/testing/failing_allocator.zig index 313af987ab5bc079b4b1088550a90622db9089fc..3a83f313bf093c66b987261c453666134e8b96b0 100644 --- a/lib/std/testing/failing_allocator.zig +++ b/lib/std/testing/failing_allocator.zig @@ -1,19 +1,33 @@ const std = @import("../std.zig"); const mem = std.mem; +pub const Config = struct { + /// The number of successful allocations you can expect from this allocator. + /// The next allocation will fail. For example, with `fail_index` equal to + /// 2, the following test will pass: + /// + /// var a = try failing_alloc.create(i32); + /// var b = try failing_alloc.create(i32); + /// testing.expectError(error.OutOfMemory, failing_alloc.create(i32)); + fail_index: usize = std.math.maxInt(usize), + + /// Number of successful resizes to expect from this allocator. The next resize will fail. + resize_fail_index: usize = std.math.maxInt(usize), +}; + /// Allocator that fails after N allocations, useful for making sure out of /// memory conditions are handled correctly. /// /// To use this, first initialize it and get an allocator with /// /// `const failing_allocator = &FailingAllocator.init(, -/// ).allocator;` +/// ).allocator;` /// /// Then use `failing_allocator` anywhere you would have used a /// different allocator. pub const FailingAllocator = struct { - index: usize, - fail_index: usize, + alloc_index: usize, + resize_index: usize, internal_allocator: mem.Allocator, allocated_bytes: usize, freed_bytes: usize, @@ -21,28 +35,24 @@ pub const FailingAllocator = struct { deallocations: usize, stack_addresses: [num_stack_frames]usize, has_induced_failure: bool, + fail_index: usize, + resize_fail_index: usize, const num_stack_frames = if (std.debug.sys_can_stack_trace) 16 else 0; - /// `fail_index` is the number of successful allocations you can - /// expect from this allocator. The next allocation will fail. - /// For example, if this is called with `fail_index` equal to 2, - /// the following test will pass: - /// - /// var a = try failing_alloc.create(i32); - /// var b = try failing_alloc.create(i32); - /// testing.expectError(error.OutOfMemory, failing_alloc.create(i32)); - pub fn init(internal_allocator: mem.Allocator, fail_index: usize) FailingAllocator { + pub fn init(internal_allocator: mem.Allocator, config: Config) FailingAllocator { return FailingAllocator{ .internal_allocator = internal_allocator, - .fail_index = fail_index, - .index = 0, + .alloc_index = 0, + .resize_index = 0, .allocated_bytes = 0, .freed_bytes = 0, .allocations = 0, .deallocations = 0, .stack_addresses = undefined, .has_induced_failure = false, + .fail_index = config.fail_index, + .resize_fail_index = config.resize_fail_index, }; } @@ -64,7 +74,7 @@ pub const FailingAllocator = struct { return_address: usize, ) ?[*]u8 { const self: *FailingAllocator = @ptrCast(@alignCast(ctx)); - if (self.index == self.fail_index) { + if (self.alloc_index == self.fail_index) { if (!self.has_induced_failure) { @memset(&self.stack_addresses, 0); var stack_trace = std.builtin.StackTrace{ @@ -80,7 +90,7 @@ pub const FailingAllocator = struct { return null; self.allocated_bytes += len; self.allocations += 1; - self.index += 1; + self.alloc_index += 1; return result; } @@ -92,6 +102,8 @@ pub const FailingAllocator = struct { ra: usize, ) bool { const self: *FailingAllocator = @ptrCast(@alignCast(ctx)); + if (self.resize_index == self.resize_fail_index) + return false; if (!self.internal_allocator.rawResize(old_mem, log2_old_align, new_len, ra)) return false; if (new_len < old_mem.len) { @@ -99,6 +111,7 @@ pub const FailingAllocator = struct { } else { self.allocated_bytes += new_len - old_mem.len; } + self.resize_index += 1; return true; } -- 2.54.0