lib/alloc/fixed/src/test.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const observe = @import("alloc_observe");
  3 const fixed = @import("root.zig");
  4 
  5 const Recorder = struct {
  6     events: [32]observe.Event = undefined,
  7     count: usize = 0,
  8 
  9     fn sink(self: *Recorder) observe.Sink {
 10         return .{ .context = self, .record = record };
 11     }
 12 
 13     fn record(context: *anyopaque, event: observe.Event) void {
 14         const self: *Recorder = @ptrCast(@alignCast(context));
 15         std.debug.assert(self.count < self.events.len);
 16         self.events[self.count] = event;
 17         self.count += 1;
 18     }
 19 };
 20 
 21 test "owned fixed buffer preserves standard behavior" {
 22     var storage: [512]u8 align(64) = undefined;
 23     var owner = fixed.FixedBuffer.init(&storage);
 24     const allocator = owner.allocator();
 25     const first = try allocator.alloc(u8, 37);
 26     const second = try allocator.alignedAlloc(u32, .@"16", 11);
 27     @memset(first, 0x51);
 28     @memset(second, 0x52525252);
 29     try std.testing.expect(fixed.used(&owner) >= first.len + @sizeOf(u32) * second.len);
 30     allocator.free(second);
 31     allocator.free(first);
 32     owner.reset();
 33     try std.testing.expectEqual(@as(usize, 0), fixed.used(&owner));
 34 }
 35 
 36 test "tracked fixed storage reports exact capacity and exhaustion" {
 37     var bytes: [64]u8 align(16) = undefined;
 38     var tracked = fixed.Tracked.init(&bytes);
 39     const allocator = tracked.allocator();
 40     const memory = try allocator.alloc(u8, bytes.len);
 41 
 42     try std.testing.expectEqual(bytes.len, tracked.status().used_bytes);
 43     try std.testing.expectEqual(bytes.len, tracked.status().peak_bytes);
 44     try std.testing.expectEqual(bytes.len, tracked.status().high_water_bytes);
 45     try std.testing.expectError(error.OutOfMemory, allocator.alloc(u8, 1));
 46     try std.testing.expect(tracked.status().exhausted);
 47 
 48     allocator.free(memory);
 49     tracked.reset();
 50     try std.testing.expectEqual(@as(usize, 0), tracked.status().used_bytes);
 51     try std.testing.expectEqual(@as(usize, 0), tracked.status().peak_bytes);
 52     try std.testing.expectEqual(bytes.len, tracked.status().high_water_bytes);
 53     try std.testing.expect(!tracked.status().exhausted);
 54     _ = try tracked.allocator().alloc(u8, bytes.len);
 55 }
 56 
 57 test "chunked fixed buffer retains committed storage" {
 58     var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
 59     defer arena.deinit();
 60     var counting = std.testing.FailingAllocator.init(arena.allocator(), .{});
 61     var owner = fixed.Chunks.init(counting.allocator(), 64, 256);
 62     const allocator = owner.allocator();
 63     const first = try allocator.alloc(u8, 37);
 64     const second = try allocator.alloc(u8, 41);
 65     @memset(first, 0x51);
 66     @memset(second, 0x52);
 67     try std.testing.expectEqual(@as(usize, 2), counting.alloc_index);
 68     owner.retain();
 69     for (first) |byte| try std.testing.expectEqual(@as(u8, 0x51), byte);
 70     for (second) |byte| try std.testing.expectEqual(@as(u8, 0x52), byte);
 71 }
 72 
 73 test "chunked fixed buffer discards uncommitted storage" {
 74     var counting = std.testing.FailingAllocator.init(std.testing.allocator, .{});
 75     var owner = fixed.Chunks.init(counting.allocator(), 64, 256);
 76     _ = try owner.allocator().alloc(u8, 37);
 77     _ = try owner.allocator().alloc(u8, 41);
 78     owner.discard();
 79     try std.testing.expectEqual(@as(usize, 2), counting.alloc_index);
 80 }
 81 
 82 test "chunked fixed buffer caps scheduled growth" {
 83     var backing_storage: [496]u8 align(16) = undefined;
 84     var backing = fixed.FixedBuffer.init(&backing_storage);
 85     var counting = std.testing.FailingAllocator.init(backing.allocator(), .{});
 86     var owner = fixed.Chunks.init(counting.allocator(), 64, 128);
 87     const allocator = owner.allocator();
 88     for (0..4) |_| _ = try allocator.alloc(u8, 100);
 89     try std.testing.expectEqual(@as(usize, 4), counting.alloc_index);
 90     try std.testing.expectEqual(backing_storage.len, fixed.used(&backing));
 91 }
 92 
 93 test "chunked fixed buffer bounds rollback metadata" {
 94     var owner = fixed.Chunks.init(std.testing.allocator, 16, 16);
 95     defer owner.discard();
 96     const allocator = owner.allocator();
 97     for (0..fixed.Chunks.maximum_chunk_count) |_| {
 98         _ = try allocator.alloc(u8, 16);
 99     }
100     try std.testing.expectError(error.OutOfMemory, allocator.alloc(u8, 16));
101 }
102 
103 test "recycling chunks stay distinct from retained output chunks" {
104     var retained = fixed.Chunks.init(std.testing.allocator, 64, 256);
105     defer retained.discard();
106     var recycling = fixed.Recycling.init(std.testing.allocator, 64, 256);
107     defer recycling.discard();
108 
109     try std.testing.expect(fixed.Chunks.isAllocator(retained.allocator()));
110     try std.testing.expect(!fixed.Chunks.isAllocator(recycling.allocator()));
111 }
112 
113 test "recycling chunks rewind retained storage without backing growth" {
114     var storage_bytes: [448]u8 align(16) = undefined;
115     var storage_owner = fixed.Tracked.init(&storage_bytes);
116     var owner = fixed.Recycling.init(storage_owner.allocator(), 64, 256);
117     defer owner.discard();
118     const allocator = owner.allocator();
119 
120     const small = try allocator.alloc(u8, 48);
121     const large = try allocator.alloc(u8, 96);
122     @memset(small, 0x51);
123     @memset(large, 0x52);
124     try std.testing.expectEqual(@as(usize, 192), storage_owner.status().used_bytes);
125 
126     owner.reset();
127     const reused_large = try allocator.alloc(u8, large.len);
128     const reused_small = try allocator.alloc(u8, small.len);
129     try std.testing.expectEqual(@intFromPtr(large.ptr), @intFromPtr(reused_large.ptr));
130     try std.testing.expectEqual(@intFromPtr(small.ptr), @intFromPtr(reused_small.ptr));
131     _ = try allocator.alloc(u8, 129);
132     try std.testing.expectEqual(storage_bytes.len, storage_owner.status().used_bytes);
133     try std.testing.expectEqual(storage_bytes.len, storage_owner.status().high_water_bytes);
134 }
135 
136 test "recycling chunks preserve a live sibling while alternating" {
137     var storage_bytes: [192]u8 align(16) = undefined;
138     var storage_owner = fixed.Tracked.init(&storage_bytes);
139     var owners = [2]fixed.Recycling{
140         fixed.Recycling.init(storage_owner.allocator(), 96, 256),
141         fixed.Recycling.init(storage_owner.allocator(), 96, 256),
142     };
143     defer owners[1].discard();
144     defer owners[0].discard();
145 
146     const first = try owners[0].allocator().alloc(u8, 96);
147     @memset(first, 0x51);
148     const second = try owners[1].allocator().alloc(u8, 96);
149     @memset(second, 0x52);
150     const warmed_high_water = storage_owner.status().high_water_bytes;
151 
152     owners[0].reset();
153     const reused_first = try owners[0].allocator().alloc(u8, 96);
154     try std.testing.expectEqual(@intFromPtr(first.ptr), @intFromPtr(reused_first.ptr));
155     for (second) |byte| try std.testing.expectEqual(@as(u8, 0x52), byte);
156     @memset(reused_first, 0x53);
157     try std.testing.expectEqual(warmed_high_water, storage_owner.status().high_water_bytes);
158 
159     owners[1].reset();
160     const reused_second = try owners[1].allocator().alloc(u8, 96);
161     try std.testing.expectEqual(@intFromPtr(second.ptr), @intFromPtr(reused_second.ptr));
162     for (reused_first) |byte| try std.testing.expectEqual(@as(u8, 0x53), byte);
163     try std.testing.expectEqual(warmed_high_water, storage_owner.status().high_water_bytes);
164 }
165 
166 test "recycling chunks retain aligned storage after first acquisition" {
167     var storage_bytes: [256]u8 align(128) = undefined;
168     var storage_owner = fixed.Tracked.init(&storage_bytes);
169     var owner = fixed.Recycling.init(storage_owner.allocator(), 64, 256);
170     defer owner.discard();
171     const allocator = owner.allocator();
172 
173     _ = try allocator.alloc(u8, 64);
174     owner.reset();
175     const alignment = comptime std.mem.Alignment.fromByteUnits(128);
176     const aligned = try allocator.alignedAlloc(u8, alignment, 64);
177     try std.testing.expectEqual(@as(usize, 0), @intFromPtr(aligned.ptr) % 128);
178     const warmed_high_water = storage_owner.status().high_water_bytes;
179 
180     owner.reset();
181     const reused = try allocator.alignedAlloc(u8, alignment, 64);
182     try std.testing.expectEqual(@intFromPtr(aligned.ptr), @intFromPtr(reused.ptr));
183     try std.testing.expectEqual(warmed_high_water, storage_owner.status().high_water_bytes);
184 }
185 
186 test "recycling chunks leave maximum plus one to the backing owner" {
187     var storage_bytes: [128]u8 align(16) = undefined;
188     var storage_owner = fixed.Tracked.init(&storage_bytes);
189     var owner = fixed.Recycling.init(storage_owner.allocator(), 128, 128);
190     const allocator = owner.allocator();
191 
192     _ = try allocator.alloc(u8, storage_bytes.len);
193     try std.testing.expectEqual(storage_bytes.len, storage_owner.status().high_water_bytes);
194     owner.reset();
195     try std.testing.expectError(
196         error.OutOfMemory,
197         allocator.alloc(u8, storage_bytes.len + 1),
198     );
199     try std.testing.expect(storage_owner.status().exhausted);
200     try std.testing.expect(!owner.metadataExhausted());
201     try std.testing.expectEqual(storage_bytes.len, storage_owner.status().high_water_bytes);
202 
203     owner.discard();
204     storage_owner.reset();
205     var retry = fixed.Recycling.init(storage_owner.allocator(), 128, 128);
206     defer retry.discard();
207     _ = try retry.allocator().alloc(u8, storage_bytes.len);
208 }
209 
210 test "recycling chunks distinguish metadata exhaustion" {
211     const chunk_bytes = 16;
212     const storage_size = (fixed.Recycling.maximum_chunk_count + 1) * chunk_bytes;
213     var storage_bytes: [storage_size]u8 align(16) = undefined;
214     var storage_owner = fixed.Tracked.init(&storage_bytes);
215     var owner = fixed.Recycling.init(
216         storage_owner.allocator(),
217         chunk_bytes,
218         chunk_bytes,
219     );
220     defer owner.discard();
221     const allocator = owner.allocator();
222 
223     for (0..fixed.Recycling.maximum_chunk_count) |_| {
224         _ = try allocator.alloc(u8, chunk_bytes);
225     }
226     try std.testing.expectError(
227         error.OutOfMemory,
228         allocator.alloc(u8, chunk_bytes),
229     );
230     try std.testing.expect(owner.metadataExhausted());
231     try std.testing.expect(!storage_owner.status().exhausted);
232     try std.testing.expectEqual(
233         storage_size - chunk_bytes,
234         storage_owner.status().used_bytes,
235     );
236 
237     owner.reset();
238     try std.testing.expect(!owner.metadataExhausted());
239     _ = try allocator.alloc(u8, chunk_bytes);
240     try std.testing.expectEqual(
241         storage_size - chunk_bytes,
242         storage_owner.status().high_water_bytes,
243     );
244 }
245 
246 test "recycling chunks unwind a failed nested chunk transaction" {
247     var storage_bytes: [640]u8 align(16) = undefined;
248     var storage_owner = fixed.Tracked.init(&storage_bytes);
249     var failing = std.testing.FailingAllocator.init(
250         storage_owner.allocator(),
251         .{ .fail_index = 2 },
252     );
253     var owner = fixed.Recycling.init(failing.allocator(), 128, 256);
254 
255     var failed = fixed.Chunks.init(owner.allocator(), 64, 256);
256     _ = try failed.allocator().alloc(u8, 48);
257     _ = try failed.allocator().alloc(u8, 96);
258     try std.testing.expectError(
259         error.OutOfMemory,
260         failed.allocator().alloc(u8, 192),
261     );
262     failed.discard();
263 
264     failing.fail_index = std.math.maxInt(usize);
265     var retry = fixed.Chunks.init(owner.allocator(), 64, 256);
266     _ = try retry.allocator().alloc(u8, 48);
267     _ = try retry.allocator().alloc(u8, 96);
268     _ = try retry.allocator().alloc(u8, 192);
269     retry.retain();
270     try std.testing.expectEqual(@as(usize, 3), failing.alloc_index);
271     try std.testing.expectEqual(storage_bytes.len, storage_owner.status().used_bytes);
272 
273     owner.discard();
274     try std.testing.expectEqual(@as(usize, 0), storage_owner.status().used_bytes);
275 }
276 
277 test "recycling chunks rewind alignment padding across chunks" {
278     var storage_bytes: [64]u8 align(16) = undefined;
279     var storage_owner = fixed.Tracked.init(&storage_bytes);
280     var owner = fixed.Recycling.init(storage_owner.allocator(), 32, 32);
281     defer owner.discard();
282     const allocator = owner.allocator();
283 
284     const first = try allocator.alloc(u8, 1);
285     const padded = try allocator.alignedAlloc(u8, .@"16", 1);
286     const overflow = try allocator.alloc(u8, 16);
287     try std.testing.expectEqual(
288         @as(usize, 15),
289         @intFromPtr(padded.ptr) - @intFromPtr(first.ptr) - first.len,
290     );
291     try std.testing.expectEqual(storage_bytes.len, storage_owner.status().used_bytes);
292 
293     allocator.free(overflow);
294     allocator.free(padded);
295     allocator.free(first);
296     const reused_first = try allocator.alloc(u8, 32);
297     const reused_second = try allocator.alloc(u8, 32);
298     try std.testing.expectEqual(@intFromPtr(first.ptr), @intFromPtr(reused_first.ptr));
299     try std.testing.expectEqual(@intFromPtr(overflow.ptr), @intFromPtr(reused_second.ptr));
300     try std.testing.expectEqual(storage_bytes.len, storage_owner.status().high_water_bytes);
301 }
302 
303 test "recycling chunks release relocated allocations" {
304     var storage_bytes: [72]u8 align(16) = undefined;
305     var storage_owner = fixed.Tracked.init(&storage_bytes);
306     var owner = fixed.Recycling.init(storage_owner.allocator(), 32, 40);
307     defer owner.discard();
308     const allocator = owner.allocator();
309 
310     const initial = try allocator.alloc(u8, 24);
311     @memset(initial, 0x51);
312     const relocated = try allocator.realloc(initial, 40);
313     try std.testing.expect(initial.ptr != relocated.ptr);
314     for (relocated[0..initial.len]) |byte| {
315         try std.testing.expectEqual(@as(u8, 0x51), byte);
316     }
317     allocator.free(relocated);
318 
319     const reused_first = try allocator.alloc(u8, 32);
320     const reused_second = try allocator.alloc(u8, 40);
321     try std.testing.expectEqual(@intFromPtr(initial.ptr), @intFromPtr(reused_first.ptr));
322     try std.testing.expectEqual(@intFromPtr(relocated.ptr), @intFromPtr(reused_second.ptr));
323     try std.testing.expectEqual(storage_bytes.len, storage_owner.status().high_water_bytes);
324 }
325 
326 test "monotonic fixed chunks preserve tails around oversized allocations" {
327     var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
328     defer arena.deinit();
329     var counting = std.testing.FailingAllocator.init(arena.allocator(), .{});
330     var owner = fixed.Monotonic.init(counting.allocator(), 64);
331     const allocator = owner.allocator();
332     const first = try allocator.alloc(u8, 37);
333     const second = try allocator.alloc(u8, 41);
334     const oversized = try allocator.alloc(u8, 100);
335     const tail = try allocator.alloc(u8, 20);
336     @memset(first, 0x51);
337     @memset(second, 0x52);
338     @memset(oversized, 0x53);
339     @memset(tail, 0x54);
340     try std.testing.expectEqual(@as(usize, 3), counting.alloc_index);
341     for (first) |byte| try std.testing.expectEqual(@as(u8, 0x51), byte);
342     for (second) |byte| try std.testing.expectEqual(@as(u8, 0x52), byte);
343     for (oversized) |byte| try std.testing.expectEqual(@as(u8, 0x53), byte);
344     for (tail) |byte| try std.testing.expectEqual(@as(u8, 0x54), byte);
345 }
346 
347 test "monotonic fixed chunks grow and release the current allocation" {
348     var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
349     defer arena.deinit();
350     var counting = std.testing.FailingAllocator.init(arena.allocator(), .{});
351     var owner = fixed.Monotonic.init(counting.allocator(), 64);
352     const allocator = owner.allocator();
353     const initial = try allocator.alloc(u8, 16);
354     @memset(initial, 0x51);
355     const grown = try allocator.realloc(initial, 32);
356     try std.testing.expect(grown.ptr == initial.ptr);
357     for (grown[0..initial.len]) |byte| {
358         try std.testing.expectEqual(@as(u8, 0x51), byte);
359     }
360     allocator.free(grown);
361     _ = try allocator.alloc(u8, 64);
362     try std.testing.expectEqual(@as(usize, 1), counting.alloc_index);
363 }
364 
365 test "fixed fallback routes only excess storage to its backing allocator" {
366     var storage: [64]u8 align(16) = undefined;
367     var counting = std.testing.FailingAllocator.init(std.testing.allocator, .{});
368     var owner = fixed.Fallback.init(&storage, counting.allocator());
369     const allocator = owner.allocator();
370     const fixed_bytes = try allocator.alloc(u8, 48);
371     const spilled = try allocator.alloc(u8, 32);
372     @memset(fixed_bytes, 0x51);
373     @memset(spilled, 0x52);
374     try std.testing.expectEqual(@as(usize, 1), counting.alloc_index);
375     allocator.free(spilled);
376     allocator.free(fixed_bytes);
377 }
378 
379 test "instrumented fixed buffer records logical operations and failures" {
380     if (!observe.enabled) return error.SkipZigTest;
381     var recorder = Recorder{};
382     var sink = recorder.sink();
383     var session = try observe.install(&sink);
384     defer session.deinit();
385 
386     var storage: [64]u8 align(16) = undefined;
387     var owner = fixed.FixedBuffer.init(&storage);
388     const allocator = owner.allocator();
389     const first = try allocator.alloc(u8, 32);
390     const second = try allocator.alloc(u8, 8);
391     try std.testing.expectError(error.OutOfMemory, allocator.alloc(u8, 64));
392     allocator.free(second);
393     allocator.free(first);
394 
395     try std.testing.expectEqual(@as(usize, 5), recorder.count);
396     try std.testing.expectEqual(observe.Operation.alloc, recorder.events[0].operation);
397     try std.testing.expectEqual(observe.Operation.alloc, recorder.events[1].operation);
398     try std.testing.expectEqual(observe.Operation.alloc, recorder.events[2].operation);
399     try std.testing.expect(!recorder.events[2].succeeded);
400     try std.testing.expectEqual(observe.Operation.free, recorder.events[3].operation);
401     try std.testing.expectEqual(observe.Operation.free, recorder.events[4].operation);
402     const producer_id = recorder.events[0].producer_id;
403     try std.testing.expect(producer_id != 0);
404     for (recorder.events[0..recorder.count]) |event| {
405         try std.testing.expectEqual(observe.Producer.fixed_buffer, event.producer);
406         try std.testing.expectEqual(producer_id, event.producer_id);
407         try std.testing.expect(event.return_address != 0);
408     }
409 }
410 
411 test "thread-safe fixed buffer uses the observed vtable" {
412     if (!observe.enabled) return error.SkipZigTest;
413     var recorder = Recorder{};
414     var sink = recorder.sink();
415     var session = try observe.install(&sink);
416     defer session.deinit();
417 
418     var storage: [64]u8 align(16) = undefined;
419     var owner = fixed.FixedBuffer.init(&storage);
420     const allocator = owner.threadSafeAllocator();
421     const bytes = try allocator.alloc(u8, 8);
422     allocator.free(bytes);
423     try std.testing.expectEqual(@as(usize, 2), recorder.count);
424 }
425 
426 test "owned fixed-buffer package namespace" {
427     std.testing.refAllDecls(fixed);
428 }