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 }