lib/trace/src/properties/store.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const hypothesis = @import("hypothesis");
3 const sys = @import("sys");
4 const trace = @import("trace");
5
6 const Allocator = std.mem.Allocator;
7 const event = trace.event;
8 const TraceReader = trace.TraceReader;
9 const TraceReaderLimits = trace.TraceReaderLimits;
10 const TraceReaderStorage = trace.TraceReaderStorage;
11 const TraceWriter = trace.TraceWriter;
12 const TraceWriterLimits = trace.TraceWriterLimits;
13 const TraceWriterStorage = trace.TraceWriterStorage;
14
15 const text_values = [_][]const u8{
16 "",
17 "run",
18 "env.TEST",
19 "checkpoint",
20 "quote\"value",
21 "slash\\path",
22 "line\nbreak",
23 "tab\tvalue",
24 "\x00control",
25 };
26
27 const payload_values = [_][]const u8{
28 "",
29 "value",
30 "tiny\x00trace",
31 "\x00\x01\x02\xfe\xff",
32 "{\"protocol\":\"tiny.trace/v1\",\"elapsed_ns\":7}",
33 };
34
35 const u64_values = [_]u64{
36 0,
37 1,
38 2,
39 7,
40 42,
41 std.math.maxInt(i64),
42 @as(u64, @intCast(std.math.maxInt(i64))) + 1,
43 std.math.maxInt(u64),
44 };
45
46 const i64_values = [_]i64{
47 std.math.minInt(i64),
48 -1,
49 0,
50 1,
51 42,
52 std.math.maxInt(i64),
53 };
54
55 const alignment_values = [_]u32{
56 0,
57 1,
58 2,
59 8,
60 16,
61 4096,
62 std.math.maxInt(u32),
63 };
64
65 pub fn settings() hypothesis.Settings {
66 return hypothesis.Settings.quick()
67 .withSeed(0x5452_4143_4550_4254)
68 .withDatabase("zig-out/hypothesis-failures/trace");
69 }
70
71 fn drawUsize(
72 conjecture: *hypothesis.ConjectureData,
73 min: usize,
74 max: usize,
75 shrink_towards: usize,
76 ) !usize {
77 return @intCast(try conjecture.drawInteger(
78 @intCast(min),
79 @intCast(max),
80 @intCast(shrink_towards),
81 ));
82 }
83
84 fn drawU64(conjecture: *hypothesis.ConjectureData) !u64 {
85 return u64_values[try drawUsize(conjecture, 0, u64_values.len - 1, 0)];
86 }
87
88 fn drawI64(conjecture: *hypothesis.ConjectureData) !i64 {
89 return i64_values[try drawUsize(conjecture, 0, i64_values.len - 1, 2)];
90 }
91
92 fn drawText(conjecture: *hypothesis.ConjectureData) ![]const u8 {
93 return text_values[try drawUsize(conjecture, 0, text_values.len - 1, 1)];
94 }
95
96 fn drawPayload(conjecture: *hypothesis.ConjectureData) ![]const u8 {
97 return payload_values[try drawUsize(conjecture, 0, payload_values.len - 1, 1)];
98 }
99
100 fn drawTimepoint(
101 conjecture: *hypothesis.ConjectureData,
102 fallback_seq: usize,
103 ) !event.Timepoint {
104 return .{
105 .epoch = try drawU64(conjecture),
106 .thread_id = @intCast(try drawUsize(conjecture, 0, 4, 1)),
107 .seq = if (try conjecture.drawBoolean())
108 @intCast(fallback_seq + 1)
109 else
110 try drawU64(conjecture),
111 };
112 }
113
114 fn drawSafepoint(conjecture: *hypothesis.ConjectureData) !event.Safepoint {
115 return .{
116 .function_id = try drawU64(conjecture),
117 .site_id = try drawU64(conjecture),
118 .stack_map_id = try drawU64(conjecture),
119 };
120 }
121
122 fn drawEvent(
123 conjecture: *hypothesis.ConjectureData,
124 index: usize,
125 ) !event.Event {
126 const timepoint = try drawTimepoint(conjecture, index);
127 return switch (try drawUsize(conjecture, 0, 10, 4)) {
128 0 => event.Event.sessionStart(timepoint, try drawText(conjecture)),
129 1 => event.Event.sessionEnd(timepoint, try drawI64(conjecture)),
130 2 => event.Event.functionEnter(timepoint, try drawSafepoint(conjecture)),
131 3 => event.Event.functionExit(timepoint, try drawSafepoint(conjecture)),
132 4 => event.Event.safepointReached(timepoint, try drawSafepoint(conjecture)),
133 5 => event.Event.boundaryBytes(timepoint, try drawText(conjecture), try drawPayload(conjecture)),
134 6 => event.Event.allocation(
135 timepoint,
136 try drawU64(conjecture),
137 try drawU64(conjecture),
138 alignment_values[try drawUsize(conjecture, 0, alignment_values.len - 1, 3)],
139 if (try conjecture.drawBoolean()) try drawText(conjecture) else null,
140 ),
141 7 => event.Event.free(timepoint, try drawU64(conjecture)),
142 8 => event.Event.checkpoint(timepoint, try drawText(conjecture), try drawPayload(conjecture)),
143 9 => event.Event.checkpointRestore(timepoint, try drawText(conjecture)),
144 else => event.Event.user(timepoint, try drawText(conjecture), try drawPayload(conjecture)),
145 };
146 }
147
148 fn drawEvents(
149 conjecture: *hypothesis.ConjectureData,
150 property_allocator: Allocator,
151 ) ![]event.Event {
152 const count = try drawUsize(conjecture, 0, 24, 4);
153 const events = try property_allocator.alloc(event.Event, count);
154 for (events, 0..) |*item, index| {
155 item.* = try drawEvent(conjecture, index);
156 }
157 return events;
158 }
159
160 fn expectJsonRoundtrip(
161 allocator: Allocator,
162 original: event.Event,
163 ) !void {
164 var out = std.Io.Writer.Allocating.init(allocator);
165 defer out.deinit();
166 try original.writeJsonLine(&out.writer);
167 const bytes = try out.toOwnedSlice();
168 defer allocator.free(bytes);
169
170 var parsed = try event.Event.fromJsonLine(allocator, std.mem.trimEnd(u8, bytes, "\n"));
171 defer parsed.deinit(allocator);
172
173 try std.testing.expect(original.eqlForReplay(parsed));
174 }
175
176 pub const TracePersistenceProperty = struct {
177 pub fn property(
178 conjecture: *hypothesis.ConjectureData,
179 property_allocator: Allocator,
180 ) !void {
181 const events = try drawEvents(conjecture, property_allocator);
182 defer property_allocator.free(events);
183
184 for (events) |item| {
185 try expectJsonRoundtrip(property_allocator, item);
186 }
187
188 var tmp = std.testing.tmpDir(.{});
189 defer tmp.cleanup();
190
191 const tmp_root = try tmp.parent_dir.realPathFileAlloc(
192 sys.fs.debugIo(),
193 tmp.sub_path[0..],
194 property_allocator,
195 );
196 defer property_allocator.free(tmp_root);
197
198 const trace_path = try sys.path.join(property_allocator, &.{ tmp_root, "property.trace" });
199 defer property_allocator.free(trace_path);
200
201 const manifest = trace.Manifest{ .build_id = "trace-pbt" };
202 const limits = trace.TraceLimits{
203 .max_event_bytes = 4096,
204 .max_chunk_bytes = 16 * 1024,
205 };
206 var writer_storage = try TraceWriterStorage.init(
207 property_allocator,
208 TraceWriterLimits.fromOpen(trace_path, manifest, limits),
209 );
210 defer writer_storage.deinit(property_allocator);
211 writer_storage.activate();
212 var trace_writer: TraceWriter = .{};
213 try trace_writer.open(&writer_storage, trace_path, manifest, limits);
214 defer trace_writer.deinit();
215 for (events) |item| try trace_writer.append(item);
216 try trace_writer.finish();
217
218 var reader_storage = try TraceReaderStorage.init(
219 property_allocator,
220 TraceReaderLimits.fromOpen(trace_path, limits),
221 );
222 defer reader_storage.deinit(property_allocator);
223 reader_storage.activate();
224 const verified = try trace.store.verify(&reader_storage, trace_path);
225 try std.testing.expectEqual(@as(u64, @intCast(events.len)), verified.event_count);
226
227 var trace_reader: TraceReader = .{};
228 try trace_reader.open(&reader_storage, trace_path);
229 defer trace_reader.deinit();
230 const source = trace_reader.source();
231 var index: usize = 0;
232 while (try source.peek()) |item| {
233 try std.testing.expect(index < events.len);
234 try std.testing.expect(events[index].eqlForReplay(item.*));
235 source.advance();
236 index += 1;
237 }
238 try std.testing.expectEqual(events.len, index);
239 }
240 };
241
242 test "property: trace events persist with replay identity" {
243 try hypothesis.checkNamed(
244 TracePersistenceProperty,
245 "trace-persistence",
246 settings(),
247 );
248 }