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 }