From 01f1611c9ffe6883c19cf091c1c2e6d58c391144 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 21 Jan 2026 15:30:22 -0800 Subject: [PATCH] tripwire: change backing store from ArrayHashMap to EnumMap This eliminates all allocation from Tripwire. --- src/font/Atlas.zig | 6 ++-- src/font/SharedGrid.zig | 4 +-- src/terminal/PageList.zig | 6 ++-- src/terminal/Screen.zig | 2 +- src/terminal/Tabstops.zig | 2 +- src/terminal/search/screen.zig | 4 +-- src/tripwire.zig | 57 +++++++++++----------------------- 7 files changed, 30 insertions(+), 51 deletions(-) diff --git a/src/font/Atlas.zig b/src/font/Atlas.zig index 4af9cb439..d12064576 100644 --- a/src/font/Atlas.zig +++ b/src/font/Atlas.zig @@ -829,7 +829,7 @@ test "init error" { for (std.meta.tags(init_tw.FailPoint)) |tag| { const tw = init_tw; defer tw.end(.reset) catch unreachable; - try tw.errorAlways(tag, error.OutOfMemory); + tw.errorAlways(tag, error.OutOfMemory); try testing.expectError( error.OutOfMemory, init(testing.allocator, 32, .grayscale), @@ -847,7 +847,7 @@ test "reserve error" { var atlas = try init(testing.allocator, 32, .grayscale); defer atlas.deinit(testing.allocator); - try tw.errorAlways(tag, error.OutOfMemory); + tw.errorAlways(tag, error.OutOfMemory); try testing.expectError( error.OutOfMemory, atlas.reserve(testing.allocator, 2, 2), @@ -872,7 +872,7 @@ test "grow error" { const old_modified = atlas.modified.load(.monotonic); const old_resized = atlas.resized.load(.monotonic); - try tw.errorAlways(tag, error.OutOfMemory); + tw.errorAlways(tag, error.OutOfMemory); try testing.expectError( error.OutOfMemory, atlas.grow(testing.allocator, atlas.size + 1), diff --git a/src/font/SharedGrid.zig b/src/font/SharedGrid.zig index df98398f2..5fd729b30 100644 --- a/src/font/SharedGrid.zig +++ b/src/font/SharedGrid.zig @@ -484,7 +484,7 @@ test "renderGlyph error after cache insert rolls back cache entry" { // We use OutOfMemory as it's a valid error in the renderGlyph error set. const tw = renderGlyph_tw; defer tw.end(.reset) catch {}; - try tw.errorAlways(.get_presentation, error.OutOfMemory); + tw.errorAlways(.get_presentation, error.OutOfMemory); // This should fail due to the tripwire try testing.expectError( @@ -510,7 +510,7 @@ test "init error" { for (std.meta.tags(init_tw.FailPoint)) |tag| { const tw = init_tw; defer tw.end(.reset) catch unreachable; - try tw.errorAlways(tag, error.OutOfMemory); + tw.errorAlways(tag, error.OutOfMemory); // Create a resolver for testing - we need to set up a minimal one. // The caller is responsible for cleaning up the resolver if init fails. diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 7fe515818..f7d3c735f 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -5170,7 +5170,7 @@ test "PageList init error" { for (std.meta.tags(init_tw.FailPoint)) |tag| { const tw = init_tw; defer tw.end(.reset) catch unreachable; - try tw.errorAlways(tag, error.OutOfMemory); + tw.errorAlways(tag, error.OutOfMemory); try std.testing.expectError( error.OutOfMemory, init( @@ -5187,7 +5187,7 @@ test "PageList init error" { for (std.meta.tags(initPages_tw.FailPoint)) |tag| { const tw = initPages_tw; defer tw.end(.reset) catch unreachable; - try tw.errorAlways(tag, error.OutOfMemory); + tw.errorAlways(tag, error.OutOfMemory); const cols: size.CellCountInt = if (tag == .page_buf_std) 80 else std_capacity.maxCols().? + 1; try std.testing.expectError( @@ -5207,7 +5207,7 @@ test "PageList init error" { }) |tag| { const tw = initPages_tw; defer tw.end(.reset) catch unreachable; - try tw.errorAfter(tag, error.OutOfMemory, 1); + tw.errorAfter(tag, error.OutOfMemory, 1); try std.testing.expectError( error.OutOfMemory, init( diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index fe158c0a3..45fe9dfc6 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -9493,7 +9493,7 @@ test "selectionString map allocation failure cleanup" { // Trigger allocation failure on toOwnedSlice var map: StringMap = undefined; - try selectionString_tw.errorAlways(.copy_map, error.OutOfMemory); + selectionString_tw.errorAlways(.copy_map, error.OutOfMemory); const result = s.selectionString(alloc, .{ .sel = sel, .map = &map, diff --git a/src/terminal/Tabstops.zig b/src/terminal/Tabstops.zig index 5e784e6f2..68138cbf8 100644 --- a/src/terminal/Tabstops.zig +++ b/src/terminal/Tabstops.zig @@ -261,7 +261,7 @@ test "Tabstops: resize alloc failure preserves state" { const original_cols = t.cols; // Trigger allocation failure when resizing beyond prealloc - try resize_tw.errorAlways(.dynamic_alloc, error.OutOfMemory); + resize_tw.errorAlways(.dynamic_alloc, error.OutOfMemory); const result = t.resize(testing.allocator, prealloc_columns * 2); try testing.expectError(error.OutOfMemory, result); try resize_tw.end(.reset); diff --git a/src/terminal/search/screen.zig b/src/terminal/search/screen.zig index c3f48b422..74828d879 100644 --- a/src/terminal/search/screen.zig +++ b/src/terminal/search/screen.zig @@ -1460,7 +1460,7 @@ test "reloadActive partial history cleanup on appendSlice error" { // that need cleanup. const tw = reloadActive_tw; defer tw.end(.reset) catch unreachable; - try tw.errorAlways(.history_append_existing, error.OutOfMemory); + tw.errorAlways(.history_append_existing, error.OutOfMemory); // reloadActive is called by select(), which should trigger the error path. // If the bug exists, testing.allocator will report a memory leak @@ -1507,7 +1507,7 @@ test "reloadActive partial history cleanup on loop append error" { // that needs cleanup. const tw = reloadActive_tw; defer tw.end(.reset) catch unreachable; - try tw.errorAfter(.history_append_new, error.OutOfMemory, 1); + tw.errorAfter(.history_append_new, error.OutOfMemory, 1); // reloadActive is called by select(), which should trigger the error path. // If the bug exists, testing.allocator will report a memory leak diff --git a/src/tripwire.zig b/src/tripwire.zig index f8aaead14..225674b33 100644 --- a/src/tripwire.zig +++ b/src/tripwire.zig @@ -43,7 +43,7 @@ //! } //! //! test "myFunction fails on alloc" { -//! try tw.errorAlways(.alloc_buf, error.OutOfMemory); +//! tw.errorAlways(.alloc_buf, error.OutOfMemory); //! try std.testing.expectError(error.OutOfMemory, myFunction()); //! try tw.end(.reset); //! } @@ -67,7 +67,6 @@ const std = @import("std"); const builtin = @import("builtin"); const assert = std.debug.assert; const testing = std.testing; -const Allocator = std.mem.Allocator; const log = std.log.scoped(.tripwire); @@ -134,8 +133,8 @@ pub fn module( } /// The configured tripwires for this module. - var tripwires: TripwireMap = .empty; - const TripwireMap = std.AutoArrayHashMapUnmanaged(FailPoint, Tripwire); + var tripwires: TripwireMap = .{}; + const TripwireMap = std.EnumMap(FailPoint, Tripwire); const Tripwire = struct { /// Error to return when tripped err: Error, @@ -155,14 +154,6 @@ pub fn module( tripped: bool = false, }; - /// For all allocations we use an allocator that can leak memory - /// without reporting it, since this is only used in tests. We don't - /// want to use a testing allocator here because that would report - /// leaks. Users are welcome to call `deinit` on the module to - /// free all memory. - const LeakyAllocator = std.heap.DebugAllocator(.{}); - var alloc_state: LeakyAllocator = .init; - /// Check for a failure at the given failure point. These should /// be placed directly before the `try` operation that may fail. pub fn check(point: FailPoint) callconv(callingConvention()) Error!void { @@ -187,42 +178,31 @@ pub fn module( } /// Mark a failure point to always trip with the given error. - pub fn errorAlways( - point: FailPoint, - err: Error, - ) Allocator.Error!void { - try errorAfter(point, err, 0); + pub fn errorAlways(point: FailPoint, err: Error) void { + errorAfter(point, err, 0); } /// Mark a failure point to trip with the given error after /// the failure point is reached at least `min` times. A value of /// zero is equivalent to `errorAlways`. - pub fn errorAfter( - point: FailPoint, - err: Error, - min: usize, - ) Allocator.Error!void { - try tripwires.put( - alloc_state.allocator(), - point, - .{ .err = err, .min = min }, - ); + pub fn errorAfter(point: FailPoint, err: Error, min: usize) void { + tripwires.put(point, .{ .err = err, .min = min }); } /// Ends the tripwire session. This will raise an error if there /// were untripped error expectations. The reset mode specifies - /// whether memory is reset too. Memory is always reset, even if - /// this returns an error. + /// whether expectations are reset too. Expectations are always reset, + /// even if this returns an error. pub fn end(reset_mode: enum { reset, retain }) error{UntrippedError}!void { var untripped: bool = false; - for (tripwires.keys(), tripwires.values()) |key, entry| { - if (!entry.tripped) { - log.warn("untripped point={t}", .{key}); + var iter = tripwires.iterator(); + while (iter.next()) |entry| { + if (!entry.value.tripped) { + log.warn("untripped point={s}", .{@tagName(entry.key)}); untripped = true; } } - // We always reset memory before failing switch (reset_mode) { .reset => reset(), .retain => {}, @@ -231,10 +211,9 @@ pub fn module( if (untripped) return error.UntrippedError; } - /// Unset all the tripwires and free all allocated memory. You - /// should usually call `end` instead. + /// Unset all the tripwires. You should usually call `end` instead. pub fn reset() void { - tripwires.clearAndFree(alloc_state.allocator()); + tripwires = .{}; } /// Our calling convention is inline if our tripwire module is @@ -258,7 +237,7 @@ test { try io.check(.read); // Always trip - try io.errorAlways(.read, error.OutOfMemory); + io.errorAlways(.read, error.OutOfMemory); try testing.expectError( error.OutOfMemory, io.check(.read), @@ -283,7 +262,7 @@ test "module as error set" { test "errorAfter" { const io = module(enum { read, write }, anyerror); // Trip after 2 calls (on the 3rd call) - try io.errorAfter(.read, error.OutOfMemory, 2); + io.errorAfter(.read, error.OutOfMemory, 2); // First two calls succeed try io.check(.read); @@ -298,7 +277,7 @@ test "errorAfter" { test "errorAfter untripped error if min not reached" { const io = module(enum { read }, anyerror); - try io.errorAfter(.read, error.OutOfMemory, 2); + io.errorAfter(.read, error.OutOfMemory, 2); // Only call once, not enough to trip try io.check(.read); // end should fail because tripwire was set but never tripped