lib/trace/src/store/test.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const sys = @import("sys");
3 const subject = @import("root.zig");
4 const event = @import("../root.zig").event;
5 const format = @import("format/root.zig");
6 const database = format.database;
7 pub const reader_test = @import("reader/test.zig");
8 pub const writer_test = @import("writer/test.zig");
9
10 const Limits = subject.Limits;
11 const Manifest = subject.Manifest;
12 const Reader = subject.Reader;
13 const ReaderLimits = subject.ReaderLimits;
14 const ReaderStorage = subject.ReaderStorage;
15 const VerifyResult = subject.VerifyResult;
16 const Writer = subject.Writer;
17 const WriterLimits = subject.WriterLimits;
18 const WriterStorage = subject.WriterStorage;
19 const verify = subject.verify;
20
21 fn verifyFixture(
22 allocator: std.mem.Allocator,
23 root_path: []const u8,
24 trace_limits: Limits,
25 ) !VerifyResult {
26 var reader_storage = try ReaderStorage.init(
27 allocator,
28 ReaderLimits.fromOpen(root_path, trace_limits),
29 );
30 defer reader_storage.deinit(allocator);
31 reader_storage.activate();
32 return try verify(&reader_storage, root_path);
33 }
34
35 fn tracePath(allocator: std.mem.Allocator, tmp: *std.testing.TmpDir, name: []const u8) ![]u8 {
36 const root = try tmp.parent_dir.realPathFileAlloc(sys.fs.debugIo(), tmp.sub_path[0..], allocator);
37 defer allocator.free(root);
38 return try sys.path.join(allocator, &.{ root, name });
39 }
40
41 fn writeFixture(allocator: std.mem.Allocator, path: []const u8, limits: Limits, count: usize) !void {
42 const manifest = Manifest{ .build_id = "store-test" };
43 var writer_storage: WriterStorage = undefined;
44 var trace_writer: Writer = .{};
45 try openWriter(&writer_storage, &trace_writer, allocator, path, manifest, limits);
46 defer writer_storage.deinit(allocator);
47 defer trace_writer.deinit();
48 for (0..count) |index| {
49 try trace_writer.append(event.Event.user(
50 .{ .thread_id = @intCast(index % 3 + 1), .seq = index + 1 },
51 if (index % 2 == 0) "alpha" else "beta",
52 "production-shaped-payload-0123456789abcdef",
53 ));
54 }
55 try trace_writer.finish();
56 }
57
58 fn openWriter(
59 writer_storage: *WriterStorage,
60 trace_writer: *Writer,
61 allocator: std.mem.Allocator,
62 path: []const u8,
63 manifest: Manifest,
64 limits: Limits,
65 ) !void {
66 writer_storage.* = try WriterStorage.init(allocator, WriterLimits.fromOpen(path, manifest, limits));
67 errdefer writer_storage.deinit(allocator);
68 writer_storage.activate();
69 try trace_writer.open(writer_storage, path, manifest, limits);
70 }
71
72 test "SQL trace round trips across logical rollovers" {
73 comptime {
74 @stardustClaim(
75 @import("alloc_phase").capacity.witness(@import("./reader/root.zig").TraceReaderStorage, "trace_trace_reader_rollover"),
76 null,
77 null,
78 null,
79 null,
80 null,
81 null,
82 );
83 }
84 comptime {
85 @stardustClaim(
86 @import("alloc_phase").capacity.witness(@import("./writer/root.zig").Storage, "trace_trace_writer_rollover"),
87 null,
88 null,
89 null,
90 null,
91 null,
92 null,
93 );
94 }
95
96 const allocator = std.testing.allocator;
97 var tmp = std.testing.tmpDir(.{});
98 defer tmp.cleanup();
99 const path = try tracePath(allocator, &tmp, "rollover.trace");
100 defer allocator.free(path);
101 const limits = Limits{ .max_event_bytes = 512, .max_chunk_bytes = 512 };
102 try writeFixture(allocator, path, limits, 40);
103
104 var reader_storage = try ReaderStorage.init(allocator, ReaderLimits.fromOpen(path, limits));
105 defer reader_storage.deinit(allocator);
106 reader_storage.activate();
107 var trace_reader: Reader = .{};
108 try trace_reader.open(&reader_storage, path);
109 defer trace_reader.deinit();
110 var source = trace_reader.source();
111 var count: usize = 0;
112 while (try source.peek()) |item| {
113 try std.testing.expectEqual(@as(u64, count + 1), item.timepoint.seq);
114 try std.testing.expectEqualStrings(
115 if (count % 2 == 0) "alpha" else "beta",
116 item.label.?,
117 );
118 source.advance();
119 count += 1;
120 }
121 try std.testing.expectEqual(@as(usize, 40), count);
122 const result = try trace_reader.verify();
123 try std.testing.expectEqual(@as(u64, 40), result.event_count);
124 try std.testing.expect(result.chunk_count > 1);
125 try std.testing.expect(result.event_bytes > limits.max_chunk_bytes);
126 const database_path = try sys.path.join(allocator, &.{ path, database.file_name });
127 defer allocator.free(database_path);
128 const wal_path = try sys.path.join(allocator, &.{ path, database.wal_file_name });
129 defer allocator.free(wal_path);
130 const legacy_index_path = try sys.path.join(allocator, &.{ path, "index/chunks.jsonl" });
131 defer allocator.free(legacy_index_path);
132 const legacy_events_path = try sys.path.join(allocator, &.{ path, "events" });
133 defer allocator.free(legacy_events_path);
134 try std.testing.expect(sys.fs.exists(database_path));
135 try std.testing.expect(sys.fs.exists(wal_path));
136 try std.testing.expect(!sys.fs.exists(legacy_index_path));
137 try std.testing.expect(!sys.fs.exists(legacy_events_path));
138 }
139
140 test "SQL trace groups events into bounded binary blocks" {
141 const allocator = std.testing.allocator;
142 var tmp = std.testing.tmpDir(.{});
143 defer tmp.cleanup();
144 const path = try tracePath(allocator, &tmp, "blocks.trace");
145 defer allocator.free(path);
146 try writeFixture(allocator, path, .{}, 1_000);
147
148 var reader_storage = try ReaderStorage.init(allocator, ReaderLimits.fromOpen(path, .{}));
149 defer reader_storage.deinit(allocator);
150 reader_storage.activate();
151 var trace_reader: Reader = .{};
152 try trace_reader.open(&reader_storage, path);
153 defer trace_reader.deinit();
154 try std.testing.expectEqual(@as(u64, 1), trace_reader.header.block_count);
155 try std.testing.expect(trace_reader.header.block_count < trace_reader.header.event_count);
156 try std.testing.expect(trace_reader.header.block_bytes > trace_reader.header.event_bytes);
157 const result = try trace_reader.verify();
158 try std.testing.expectEqual(@as(u64, 1_000), result.event_count);
159 }
160
161 test "writer reports caller-selected manifest I/O capacity" {
162 comptime {
163 @stardustClaim(
164 @import("alloc_phase").capacity.witness(@import("./writer/root.zig").Storage, "trace_trace_writer_tuning"),
165 null,
166 null,
167 null,
168 null,
169 null,
170 null,
171 );
172 }
173
174 const allocator = std.testing.allocator;
175 var tmp = std.testing.tmpDir(.{});
176 defer tmp.cleanup();
177 const path = try tracePath(allocator, &tmp, "tiny-io.trace");
178 defer allocator.free(path);
179 const manifest = Manifest{ .build_id = "tiny-io" };
180 const limits = Limits{ .max_event_bytes = 512, .max_chunk_bytes = 512 };
181 var storage_limits = WriterLimits.fromOpen(path, manifest, limits);
182 storage_limits.manifest_io_bytes = 1;
183 var writer_storage = try WriterStorage.init(allocator, storage_limits);
184 defer writer_storage.deinit(allocator);
185 writer_storage.activate();
186 var trace_writer: Writer = .{};
187 try trace_writer.open(&writer_storage, path, manifest, limits);
188 defer trace_writer.deinit();
189 for (0..20) |index| {
190 try trace_writer.append(event.Event.user(
191 .{ .thread_id = 1, .seq = index + 1 },
192 "tiny-io",
193 "payload",
194 ));
195 }
196 try trace_writer.finish();
197 const status = writer_storage.status();
198 try std.testing.expectEqual(@as(usize, 1), status.manifest_io_bytes);
199 try std.testing.expect(status.block_bytes >= limits.max_event_bytes);
200 try std.testing.expect(status.sql_bytes > 0);
201 const result = try verifyFixture(allocator, path, limits);
202 try std.testing.expectEqual(@as(u64, 20), result.event_count);
203 try std.testing.expect(result.chunk_count > 1);
204 }
205
206 test "writer rejects an unreadable event without poisoning the trace" {
207 const allocator = std.testing.allocator;
208 var tmp = std.testing.tmpDir(.{});
209 defer tmp.cleanup();
210 const path = try tracePath(allocator, &tmp, "oversize.trace");
211 defer allocator.free(path);
212 const manifest = Manifest{ .build_id = "oversize" };
213 const limits = Limits{ .max_event_bytes = 256, .max_chunk_bytes = 512 };
214 var writer_storage: WriterStorage = undefined;
215 var trace_writer: Writer = .{};
216 try openWriter(&writer_storage, &trace_writer, allocator, path, manifest, limits);
217 defer writer_storage.deinit(allocator);
218 defer trace_writer.deinit();
219 var payload: [256]u8 = undefined;
220 @memset(&payload, 'x');
221 try std.testing.expectError(
222 error.EventTooLarge,
223 trace_writer.append(event.Event.user(.{ .thread_id = 1, .seq = 1 }, "oversize", &payload)),
224 );
225 try trace_writer.append(event.Event.sessionStart(.{ .thread_id = 1, .seq = 2 }, "small"));
226 try trace_writer.finish();
227 const result = try verifyFixture(allocator, path, limits);
228 try std.testing.expectEqual(@as(u64, 1), result.event_count);
229 }
230
231 test "recording append has bounded allocation-free steady state" {
232 var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{});
233 const allocator = failing.allocator();
234 var tmp = std.testing.tmpDir(.{});
235 defer tmp.cleanup();
236 const path = try tracePath(allocator, &tmp, "steady.trace");
237 defer allocator.free(path);
238 const manifest = Manifest{ .build_id = "steady" };
239 const limits = Limits{ .max_event_bytes = 1024, .max_chunk_bytes = 64 * 1024 };
240 var writer_storage: WriterStorage = undefined;
241 var trace_writer: Writer = .{};
242 try openWriter(&writer_storage, &trace_writer, allocator, path, manifest, limits);
243 defer writer_storage.deinit(allocator);
244 defer trace_writer.deinit();
245 try trace_writer.append(event.Event.user(.{ .thread_id = 1, .seq = 1 }, "warm", "payload"));
246 failing.fail_index = failing.alloc_index;
247 for (0..100) |index| {
248 try trace_writer.append(event.Event.user(
249 .{ .thread_id = 1, .seq = index + 2 },
250 "steady",
251 "payload",
252 ));
253 }
254 try std.testing.expect(!failing.has_induced_failure);
255 failing.fail_index = std.math.maxInt(usize);
256 try trace_writer.finish();
257 }
258
259 test "reader reports caller-selected admission and SQL capacities" {
260 comptime {
261 @stardustClaim(
262 @import("alloc_phase").capacity.witness(@import("./reader/root.zig").TraceReaderStorage, "trace_trace_reader_tuning"),
263 null,
264 null,
265 null,
266 null,
267 null,
268 null,
269 );
270 }
271
272 const allocator = std.testing.allocator;
273 var tmp = std.testing.tmpDir(.{});
274 defer tmp.cleanup();
275 const path = try tracePath(allocator, &tmp, "reader-tuning.trace");
276 defer allocator.free(path);
277 const limits = Limits{ .max_event_bytes = 1024, .max_chunk_bytes = 64 * 1024 };
278 try writeFixture(allocator, path, limits, 101);
279
280 var storage_limits = ReaderLimits.fromOpen(path, limits);
281 storage_limits.json_nesting = 9;
282 var reader_storage = try ReaderStorage.init(allocator, storage_limits);
283 defer reader_storage.deinit(allocator);
284 reader_storage.activate();
285 var trace_reader: Reader = .{};
286 try trace_reader.open(&reader_storage, path);
287 defer trace_reader.deinit();
288 const result = try trace_reader.verify();
289 try std.testing.expectEqual(@as(u64, 101), result.event_count);
290 const status = reader_storage.status();
291 try std.testing.expectEqual(@as(usize, 1024), status.event_bytes);
292 try std.testing.expectEqual(@as(usize, 64 * 1024), status.chunk_bytes);
293 try std.testing.expect(status.block_bytes >= status.event_bytes);
294 try std.testing.expectEqual(@as(usize, 9), status.json_nesting);
295 try std.testing.expect(status.sql_bytes > 0);
296 }
297
298 test "trace reader capacity rejection releases storage without publishing state" {
299 comptime {
300 @stardustClaim(
301 @import("alloc_phase").capacity.witness(@import("./reader/root.zig").TraceReaderStorage, "trace_trace_reader_open_atomic"),
302 null,
303 null,
304 null,
305 null,
306 null,
307 null,
308 );
309 }
310
311 const allocator = std.testing.allocator;
312 var tmp = std.testing.tmpDir(.{});
313 defer tmp.cleanup();
314 const path = try tracePath(allocator, &tmp, "reader-admission.trace");
315 defer allocator.free(path);
316 const limits = Limits{ .max_event_bytes = 512, .max_chunk_bytes = 512 };
317 try writeFixture(allocator, path, limits, 3);
318 var storage_limits = ReaderLimits.fromOpen(path, limits);
319 storage_limits.event_bytes -= 1;
320 var reader_storage = try ReaderStorage.init(allocator, storage_limits);
321 defer reader_storage.deinit(allocator);
322 reader_storage.activate();
323 var trace_reader: Reader = .{};
324 try std.testing.expectError(error.EventCapacityExceeded, trace_reader.open(&reader_storage, path));
325 try std.testing.expect(!reader_storage.status().in_use);
326 try std.testing.expect(!trace_reader.initialized);
327 }
328
329 test "verification rejects SQL event corruption" {
330 const allocator = std.testing.allocator;
331 var tmp = std.testing.tmpDir(.{});
332 defer tmp.cleanup();
333 const path = try tracePath(allocator, &tmp, "corrupt-chunk.trace");
334 defer allocator.free(path);
335 try writeFixture(allocator, path, .{}, 3);
336 const database_path = try sys.path.join(allocator, &.{ path, database.file_name });
337 defer allocator.free(database_path);
338 const bytes = try sys.fs.readFileAlloc(allocator, database_path, 64 * 1024 * 1024);
339 defer allocator.free(bytes);
340 const label_index = std.mem.indexOf(u8, bytes, "alpha") orelse return error.TestExpectedResult;
341 bytes[label_index] = 'z';
342 try sys.fs.writeFile(database_path, bytes);
343 try std.testing.expectError(error.TraceChecksumMismatch, verifyFixture(allocator, path, .{}));
344 }
345
346 test "verification rejects a truncated SQL database" {
347 const allocator = std.testing.allocator;
348 var tmp = std.testing.tmpDir(.{});
349 defer tmp.cleanup();
350 const path = try tracePath(allocator, &tmp, "corrupt-index.trace");
351 defer allocator.free(path);
352 try writeFixture(allocator, path, .{}, 3);
353 const database_path = try sys.path.join(allocator, &.{ path, database.file_name });
354 defer allocator.free(database_path);
355 const bytes = try sys.fs.readFileAlloc(allocator, database_path, 64 * 1024 * 1024);
356 defer allocator.free(bytes);
357 try std.testing.expect(bytes.len > 1);
358 try sys.fs.writeFile(database_path, bytes[0 .. bytes.len - 1]);
359 try std.testing.expectError(error.InvalidDatabaseFile, verifyFixture(allocator, path, .{}));
360 }
361
362 test "reader refuses an unfinished recording" {
363 const allocator = std.testing.allocator;
364 var tmp = std.testing.tmpDir(.{});
365 defer tmp.cleanup();
366 const path = try tracePath(allocator, &tmp, "unfinished.trace");
367 defer allocator.free(path);
368 const manifest = Manifest{ .build_id = "unfinished" };
369 var writer_storage: WriterStorage = undefined;
370 var trace_writer: Writer = .{};
371 try openWriter(&writer_storage, &trace_writer, allocator, path, manifest, .{});
372 defer writer_storage.deinit(allocator);
373 defer trace_writer.deinit();
374 try trace_writer.append(event.Event.sessionStart(.{ .thread_id = 1, .seq = 1 }, "open"));
375
376 var reader_storage = try ReaderStorage.init(allocator, ReaderLimits.fromOpen(path, .{}));
377 defer reader_storage.deinit(allocator);
378 reader_storage.activate();
379 var trace_reader: Reader = .{};
380 try std.testing.expectError(error.TraceNotClosed, trace_reader.open(&reader_storage, path));
381 }
382
383 test "reader rejects the removed single-file trace format" {
384 const allocator = std.testing.allocator;
385 var tmp = std.testing.tmpDir(.{});
386 defer tmp.cleanup();
387 const path = try tracePath(allocator, &tmp, "legacy.trace");
388 defer allocator.free(path);
389 try sys.fs.createDirPath(path);
390 const manifest_path = try sys.path.join(allocator, &.{ path, format.manifest_path });
391 defer allocator.free(manifest_path);
392 try sys.fs.writeFile(manifest_path, "{\"format_version\":1}\n");
393 var reader_storage = try ReaderStorage.init(allocator, ReaderLimits.fromOpen(path, .{}));
394 defer reader_storage.deinit(allocator);
395 reader_storage.activate();
396 var trace_reader: Reader = .{};
397 try std.testing.expectError(error.UnsupportedTraceVersion, trace_reader.open(&reader_storage, path));
398 }