lib/trace/src/properties/session.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const hypothesis = @import("hypothesis");
3 const trace = @import("trace");
4
5 const Allocator = std.mem.Allocator;
6 const Event = trace.Event;
7 const Session = trace.Session;
8 const Timepoint = trace.Timepoint;
9
10 const max_actions = 8;
11 const labels = [_][]const u8{ "", "alpha", "beta" };
12 const payloads = [_][]const u8{ "", "value", "tiny\x00trace" };
13
14 const ActionKind = enum {
15 session_start,
16 session_end,
17 function_enter,
18 function_exit,
19 safepoint,
20 allocation,
21 free,
22 user,
23 };
24
25 const Action = struct {
26 kind: ActionKind,
27 thread_id: trace.ThreadId,
28 variant: usize,
29
30 fn expected(self: Action, seq: u64) Event {
31 const timepoint = Timepoint{ .thread_id = self.thread_id, .seq = seq };
32 const value: u64 = @intCast(self.variant + 1);
33 return switch (self.kind) {
34 .session_start => Event.sessionStart(timepoint, labels[self.variant]),
35 .session_end => Event.sessionEnd(timepoint, @intCast(self.variant)),
36 .function_enter => Event.functionEnter(timepoint, .{
37 .function_id = value,
38 .site_id = value + 10,
39 }),
40 .function_exit => Event.functionExit(timepoint, .{
41 .function_id = value,
42 .site_id = value + 20,
43 }),
44 .safepoint => Event.safepointReached(timepoint, .{
45 .function_id = value,
46 .site_id = value + 30,
47 }),
48 .allocation => Event.allocation(
49 timepoint,
50 value,
51 value * 8,
52 8,
53 labels[self.variant],
54 ),
55 .free => Event.free(timepoint, value),
56 .user => Event.user(timepoint, labels[self.variant], payloads[self.variant]),
57 };
58 }
59
60 fn apply(self: Action, session: *Session) !Timepoint {
61 const value: u64 = @intCast(self.variant + 1);
62 return switch (self.kind) {
63 .session_start => session.sessionStart(self.thread_id, labels[self.variant]),
64 .session_end => session.sessionEnd(self.thread_id, @intCast(self.variant)),
65 .function_enter => session.functionEnter(self.thread_id, .{
66 .function_id = value,
67 .site_id = value + 10,
68 }),
69 .function_exit => session.functionExit(self.thread_id, .{
70 .function_id = value,
71 .site_id = value + 20,
72 }),
73 .safepoint => session.safepoint(self.thread_id, .{
74 .function_id = value,
75 .site_id = value + 30,
76 }),
77 .allocation => session.allocation(
78 self.thread_id,
79 value,
80 value * 8,
81 8,
82 labels[self.variant],
83 ),
84 .free => session.free(self.thread_id, value),
85 .user => session.userEvent(
86 self.thread_id,
87 labels[self.variant],
88 payloads[self.variant],
89 ),
90 };
91 }
92
93 fn mismatched(self: Action) Action {
94 var changed = self;
95 changed.thread_id += 1;
96 return changed;
97 }
98 };
99
100 const SourceProbe = struct {
101 events: []const Event,
102 cursor: usize = 0,
103 fail_next: bool = false,
104
105 fn source(self: *SourceProbe) trace.EventSource {
106 return .{ .context = self, .peekFn = peek, .advanceFn = advance, .countFn = count };
107 }
108
109 fn peek(context: *anyopaque) !?*const Event {
110 const self: *SourceProbe = @ptrCast(@alignCast(context));
111 if (self.fail_next) {
112 self.fail_next = false;
113 return error.InjectedSourceFailure;
114 }
115 if (self.cursor == self.events.len) return null;
116 return &self.events[self.cursor];
117 }
118
119 fn advance(context: *anyopaque) void {
120 const self: *SourceProbe = @ptrCast(@alignCast(context));
121 self.cursor += 1;
122 }
123
124 fn count(context: *anyopaque) u64 {
125 const self: *SourceProbe = @ptrCast(@alignCast(context));
126 return self.events.len;
127 }
128 };
129
130 const SinkProbe = struct {
131 expected: []const Event,
132 len: usize = 0,
133 fail_next: bool = false,
134
135 fn sink(self: *SinkProbe) trace.EventSink {
136 return .{ .context = self, .appendFn = append };
137 }
138
139 fn append(context: *anyopaque, item: Event) !void {
140 const self: *SinkProbe = @ptrCast(@alignCast(context));
141 if (self.fail_next) {
142 self.fail_next = false;
143 return error.InjectedSinkFailure;
144 }
145 if (self.len == self.expected.len) return error.TestSinkFull;
146 if (!item.eqlForReplay(self.expected[self.len])) return error.TestEventMismatch;
147 self.len += 1;
148 }
149 };
150
151 const Scenario = struct {
152 actions: [max_actions]Action = undefined,
153 events: [max_actions]Event = undefined,
154 count: usize,
155 fault_index: usize,
156 source_failure: bool,
157
158 fn draw(data: *hypothesis.ConjectureData) !Scenario {
159 var scenario = Scenario{
160 .count = try drawUsize(data, 1, max_actions, 1),
161 .fault_index = 0,
162 .source_failure = try data.drawBoolean(),
163 };
164 scenario.fault_index = try drawUsize(data, 0, scenario.count - 1, 0);
165 for (scenario.actions[0..scenario.count], 0..) |*action, index| {
166 action.* = try drawAction(data);
167 scenario.events[index] = action.expected(@intCast(index + 1));
168 }
169 return scenario;
170 }
171 };
172
173 fn drawUsize(
174 data: *hypothesis.ConjectureData,
175 min: usize,
176 max: usize,
177 shrink_towards: usize,
178 ) !usize {
179 return @intCast(try data.drawInteger(
180 @intCast(min),
181 @intCast(max),
182 @intCast(shrink_towards),
183 ));
184 }
185
186 fn drawAction(data: *hypothesis.ConjectureData) !Action {
187 return .{
188 .kind = @fromBackingInt(@intCast(try drawUsize(data, 0, 7, 0))),
189 .thread_id = @intCast(try drawUsize(data, 0, 3, 1)),
190 .variant = try drawUsize(data, 0, labels.len - 1, 1),
191 };
192 }
193
194 fn settings() hypothesis.Settings {
195 return hypothesis.Settings.quick()
196 .withSeed(0x5452_4345_4154_4f4d)
197 .withDatabase("zig-out/hypothesis-failures/trace");
198 }
199
200 fn expectProgressEqual(expected: trace.ReplayProgress, actual: trace.ReplayProgress) !void {
201 try std.testing.expectEqual(expected.cursor, actual.cursor);
202 try std.testing.expectEqual(expected.event_count, actual.event_count);
203 try std.testing.expectEqual(expected.remaining_count, actual.remaining_count);
204 }
205
206 fn expectTimepoint(expected: Timepoint, actual: Timepoint) !void {
207 try std.testing.expect(trace.event.Timepoint.eql(expected, actual));
208 }
209
210 fn replayPrefix(session: *Session, scenario: *const Scenario) !void {
211 for (scenario.actions[0..scenario.fault_index], 0..) |action, index| {
212 const actual = try action.apply(session);
213 try expectTimepoint(scenario.events[index].timepoint, actual);
214 }
215 }
216
217 fn replaySuffix(
218 session: *Session,
219 source: *const SourceProbe,
220 scenario: *const Scenario,
221 ) !void {
222 for (scenario.actions[scenario.fault_index + 1 .. scenario.count], scenario.fault_index + 1..scenario.count) |action, index| {
223 const actual = try action.apply(session);
224 try expectTimepoint(scenario.events[index].timepoint, actual);
225 try std.testing.expectEqual(index + 1, source.cursor);
226 }
227 }
228
229 pub const ReplayFailureAtomicityProperty = struct {
230 pub fn property(
231 data: *hypothesis.ConjectureData,
232 property_allocator: Allocator,
233 ) !void {
234 const scenario = try Scenario.draw(data);
235 var source = SourceProbe{ .events = scenario.events[0..scenario.count] };
236 var session = Session.initReplay(property_allocator, source.source());
237 defer session.deinit();
238
239 try replayPrefix(&session, &scenario);
240 const before = session.replayProgress();
241 try std.testing.expectEqual(scenario.fault_index, source.cursor);
242
243 const action = scenario.actions[scenario.fault_index];
244 if (scenario.source_failure) {
245 source.fail_next = true;
246 try std.testing.expectError(error.InjectedSourceFailure, action.apply(&session));
247 } else {
248 try std.testing.expectError(
249 error.ReplayEventMismatch,
250 action.mismatched().apply(&session),
251 );
252 }
253 try std.testing.expectEqual(scenario.fault_index, source.cursor);
254 try expectProgressEqual(before, session.replayProgress());
255
256 const retried = try action.apply(&session);
257 try expectTimepoint(scenario.events[scenario.fault_index].timepoint, retried);
258 try std.testing.expectEqual(scenario.fault_index + 1, source.cursor);
259 try replaySuffix(&session, &source, &scenario);
260 try session.verifyReplayComplete();
261 try std.testing.expect(session.replayProgress().complete());
262 }
263 };
264
265 pub const RecordFailureAtomicityProperty = struct {
266 pub fn property(
267 data: *hypothesis.ConjectureData,
268 property_allocator: Allocator,
269 ) !void {
270 const scenario = try Scenario.draw(data);
271 var sink = SinkProbe{ .expected = scenario.events[0..scenario.count] };
272 var session = Session.initRecord(property_allocator, sink.sink());
273 defer session.deinit();
274
275 for (scenario.actions[0..scenario.count], 0..) |action, index| {
276 if (index == scenario.fault_index) {
277 sink.fail_next = true;
278 try std.testing.expectError(error.InjectedSinkFailure, action.apply(&session));
279 try std.testing.expectEqual(index, sink.len);
280 }
281 const actual = try action.apply(&session);
282 try expectTimepoint(scenario.events[index].timepoint, actual);
283 try std.testing.expectEqual(index + 1, sink.len);
284 }
285 try std.testing.expectEqual(scenario.count, sink.len);
286 }
287 };
288
289 test "property: session transition failure atomicity" {
290 try hypothesis.checkNamed(
291 ReplayFailureAtomicityProperty,
292 "trace-session-replay-failure-atomicity",
293 settings(),
294 );
295 try hypothesis.checkNamed(
296 RecordFailureAtomicityProperty,
297 "trace-session-record-failure-atomicity",
298 settings(),
299 );
300 }