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 }