lib/windowing/src/properties/input.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const hypothesis = @import("hypothesis");
3 const windowing = @import("windowing");
4
5 const State = windowing.input.State;
6 const Point = windowing.input.Point;
7 const Wheel = windowing.input.Wheel;
8 const Key = windowing.Key;
9 const MouseButton = windowing.MouseButton;
10
11 const input_limits = windowing.input.Limits{
12 .retained_pressed_key_count = 8,
13 .retained_text_codepoint_count = 8,
14 };
15 const pressed_key_capacity = input_limits.retained_pressed_key_count;
16 const text_codepoint_capacity = input_limits.retained_text_codepoint_count;
17 const max_text_input_bytes = @typeInfo(@TypeOf((windowing.TextInputEvent{}).bytes)).array.len;
18
19 const valid_keys = [_]Key{
20 .a,
21 .b,
22 .enter,
23 .tab,
24 .left_shift,
25 .right_shift,
26 .left_ctrl,
27 .right_ctrl,
28 .left_alt,
29 .right_alt,
30 .left_super,
31 .right_super,
32 };
33 const invalid_keys = [_]Key{
34 .unknown,
35 @fromBackingInt(@intCast(1)),
36 @fromBackingInt(@intCast(31)),
37 @fromBackingInt(@intCast(100)),
38 @fromBackingInt(@intCast(349)),
39 @fromBackingInt(@intCast(4096)),
40 @fromBackingInt(@intCast(std.math.maxInt(u16))),
41 };
42 const mouse_buttons = [_]MouseButton{
43 .left,
44 .right,
45 .middle,
46 .x1,
47 .x2,
48 };
49 const coordinates = [_]i32{
50 std.math.minInt(i32),
51 std.math.minInt(i32) + 1,
52 -1024,
53 -1,
54 0,
55 1,
56 1024,
57 std.math.maxInt(i32) - 1,
58 std.math.maxInt(i32),
59 };
60 const wheel_values = [_]f32{
61 -120.0,
62 -1.5,
63 -0.25,
64 0.0,
65 0.25,
66 1.5,
67 120.0,
68 };
69 const text_inputs = [_][]const u8{
70 "",
71 "\n",
72 "\x7f",
73 "\xff",
74 "a",
75 "z",
76 "A",
77 " ",
78 "lambda",
79 "a\xce\xbb",
80 "1234567890123456",
81 "12345678901234567",
82 };
83
84 pub fn settings() hypothesis.Settings {
85 return hypothesis.Settings.quick()
86 .withSeed(0x5447_4657_494e_5055)
87 .withDatabase("zig-out/hypothesis-failures/windowing");
88 }
89
90 fn drawUsize(
91 data: *hypothesis.ConjectureData,
92 min: usize,
93 max: usize,
94 shrink_towards: usize,
95 ) !usize {
96 return @intCast(try data.drawInteger(
97 @intCast(min),
98 @intCast(max),
99 @intCast(shrink_towards),
100 ));
101 }
102
103 fn drawCoordinate(data: *hypothesis.ConjectureData) !i32 {
104 if (try data.drawBoolean()) {
105 return coordinates[try drawUsize(data, 0, coordinates.len - 1, 4)];
106 }
107 const raw = try drawUsize(data, 0, 2048, 1024);
108 return @as(i32, @intCast(raw)) - 1024;
109 }
110
111 fn drawKey(data: *hypothesis.ConjectureData) !Key {
112 if (try data.drawBoolean()) {
113 return valid_keys[try drawUsize(data, 0, valid_keys.len - 1, 0)];
114 }
115 return invalid_keys[try drawUsize(data, 0, invalid_keys.len - 1, 0)];
116 }
117
118 fn drawMouseButton(data: *hypothesis.ConjectureData) !MouseButton {
119 return mouse_buttons[try drawUsize(data, 0, mouse_buttons.len - 1, 0)];
120 }
121
122 fn drawWheel(data: *hypothesis.ConjectureData) !f32 {
123 return wheel_values[try drawUsize(data, 0, wheel_values.len - 1, 3)];
124 }
125
126 const TextAppend = struct {
127 accepted: bool,
128 codepoints: [max_text_input_bytes]u21 = undefined,
129 count: usize = 0,
130 };
131
132 const Model = struct {
133 keys: [valid_keys.len]bool = @as([valid_keys.len]bool, @splat(false)),
134 pressed: [valid_keys.len]bool = @as([valid_keys.len]bool, @splat(false)),
135 released: [valid_keys.len]bool = @as([valid_keys.len]bool, @splat(false)),
136 pressed_keys: [pressed_key_capacity]Key = undefined,
137 pressed_key_read_index: usize = 0,
138 pressed_key_count: usize = 0,
139 dropped_pressed_key_count: u64 = 0,
140 mouse_buttons: [mouse_buttons.len]bool = @as([mouse_buttons.len]bool, @splat(false)),
141 mouse_pressed: [mouse_buttons.len]bool = @as([mouse_buttons.len]bool, @splat(false)),
142 mouse_released: [mouse_buttons.len]bool = @as([mouse_buttons.len]bool, @splat(false)),
143 mouse_position: Point = .{},
144 mouse_delta: Point = .{},
145 mouse_wheel: Wheel = .{},
146 text_codepoints: [text_codepoint_capacity]u21 = undefined,
147 text_read_index: usize = 0,
148 text_count: usize = 0,
149 dropped_text_codepoint_count: u64 = 0,
150
151 fn beginPoll(self: *Model) void {
152 @memset(&self.pressed, false);
153 @memset(&self.released, false);
154 self.pressed_key_read_index = 0;
155 self.pressed_key_count = 0;
156 @memset(&self.mouse_pressed, false);
157 @memset(&self.mouse_released, false);
158 self.mouse_delta = .{};
159 self.mouse_wheel = .{};
160 self.text_read_index = 0;
161 self.text_count = 0;
162 }
163
164 fn pressKey(self: *Model, key: Key) void {
165 const index = validKeyIndex(key) orelse return;
166 self.keys[index] = true;
167 self.pressed[index] = true;
168 if (self.pressed_key_count < self.pressed_keys.len) {
169 self.pressed_keys[self.pressed_key_count] = key;
170 self.pressed_key_count += 1;
171 } else {
172 self.dropped_pressed_key_count +|= 1;
173 }
174 }
175
176 fn releaseKey(self: *Model, key: Key) void {
177 const index = validKeyIndex(key) orelse return;
178 self.keys[index] = false;
179 self.released[index] = true;
180 }
181
182 fn nextPressedKey(self: *Model) ?Key {
183 if (self.pressed_key_read_index >= self.pressed_key_count) return null;
184 const key = self.pressed_keys[self.pressed_key_read_index];
185 self.pressed_key_read_index += 1;
186 return key;
187 }
188
189 fn pressMouse(self: *Model, button: MouseButton) void {
190 const index = mouseIndex(button);
191 self.mouse_buttons[index] = true;
192 self.mouse_pressed[index] = true;
193 }
194
195 fn releaseMouse(self: *Model, button: MouseButton) void {
196 const index = mouseIndex(button);
197 self.mouse_buttons[index] = false;
198 self.mouse_released[index] = true;
199 }
200
201 fn setMousePosition(self: *Model, x: i32, y: i32) void {
202 self.mouse_position = .{ .x = x, .y = y };
203 }
204
205 fn moveMouse(self: *Model, x: i32, y: i32) void {
206 const previous = self.mouse_position;
207 self.mouse_position = .{ .x = x, .y = y };
208 self.mouse_delta.x = saturatingI32Add(
209 self.mouse_delta.x,
210 saturatingI32Diff(x, previous.x),
211 );
212 self.mouse_delta.y = saturatingI32Add(
213 self.mouse_delta.y,
214 saturatingI32Diff(y, previous.y),
215 );
216 }
217
218 fn addMouseWheel(self: *Model, x: f32, y: f32) void {
219 self.mouse_wheel.x += x;
220 self.mouse_wheel.y += y;
221 }
222
223 fn pushTextInput(self: *Model, bytes: []const u8) bool {
224 const validated = validateText(bytes);
225 if (!validated.accepted) return false;
226
227 var index: usize = 0;
228 while (index < validated.count) : (index += 1) {
229 if (self.text_count >= self.text_codepoints.len) {
230 self.dropped_text_codepoint_count +|= 1;
231 continue;
232 }
233 self.text_codepoints[self.text_count] = validated.codepoints[index];
234 self.text_count += 1;
235 }
236 return true;
237 }
238
239 fn nextTextCodepoint(self: *Model) ?u21 {
240 if (self.text_read_index >= self.text_count) return null;
241 const codepoint = self.text_codepoints[self.text_read_index];
242 self.text_read_index += 1;
243 return codepoint;
244 }
245 };
246
247 fn expectState(state: *State, model: *const Model) !void {
248 for (valid_keys, 0..) |key, index| {
249 try std.testing.expectEqual(model.keys[index], state.isKeyDown(key));
250 try std.testing.expectEqual(model.pressed[index], state.isKeyPressed(key));
251 try std.testing.expectEqual(model.released[index], state.isKeyReleased(key));
252 }
253
254 for (invalid_keys) |key| {
255 try std.testing.expect(!state.isKeyDown(key));
256 try std.testing.expect(!state.isKeyPressed(key));
257 try std.testing.expect(!state.isKeyReleased(key));
258 }
259
260 const modifiers = state.modifiers();
261 try std.testing.expectEqual(
262 model.keys[validKeyIndex(.left_shift).?] or model.keys[validKeyIndex(.right_shift).?],
263 modifiers.shift,
264 );
265 try std.testing.expectEqual(
266 model.keys[validKeyIndex(.left_ctrl).?] or model.keys[validKeyIndex(.right_ctrl).?],
267 modifiers.ctrl,
268 );
269 try std.testing.expectEqual(
270 model.keys[validKeyIndex(.left_alt).?] or model.keys[validKeyIndex(.right_alt).?],
271 modifiers.alt,
272 );
273 try std.testing.expectEqual(
274 model.keys[validKeyIndex(.left_super).?] or model.keys[validKeyIndex(.right_super).?],
275 modifiers.super,
276 );
277
278 for (mouse_buttons, 0..) |button, index| {
279 try std.testing.expectEqual(model.mouse_buttons[index], state.isMouseButtonDown(button));
280 try std.testing.expectEqual(model.mouse_pressed[index], state.isMouseButtonPressed(button));
281 try std.testing.expectEqual(model.mouse_released[index], state.isMouseButtonReleased(button));
282 }
283
284 try std.testing.expectEqual(model.mouse_position, state.mouse_position);
285 try std.testing.expectEqual(model.mouse_delta, state.mouse_delta);
286 try std.testing.expectEqual(model.mouse_wheel, state.mouse_wheel);
287 try std.testing.expectEqual(windowing.input.Status{
288 .dropped_pressed_key_count = model.dropped_pressed_key_count,
289 .dropped_text_codepoint_count = model.dropped_text_codepoint_count,
290 }, state.status());
291 }
292
293 fn expectDrainedQueues(state: *State, model: *Model) !void {
294 while (true) {
295 const expected = model.nextPressedKey();
296 const actual = state.nextPressedKey();
297 if (expected) |key| {
298 try std.testing.expect(actual != null);
299 try std.testing.expectEqual(@backingInt(key), @backingInt(actual.?));
300 } else {
301 try std.testing.expect(actual == null);
302 break;
303 }
304 }
305
306 while (true) {
307 const expected = model.nextTextCodepoint();
308 const actual = state.nextTextCodepoint();
309 if (expected) |codepoint| {
310 try std.testing.expect(actual != null);
311 try std.testing.expectEqual(codepoint, actual.?);
312 } else {
313 try std.testing.expect(actual == null);
314 break;
315 }
316 }
317 }
318
319 fn validKeyIndex(key: Key) ?usize {
320 for (valid_keys, 0..) |candidate, index| {
321 if (candidate == key) return index;
322 }
323 return null;
324 }
325
326 fn mouseIndex(button: MouseButton) usize {
327 return @backingInt(button);
328 }
329
330 fn validateText(bytes: []const u8) TextAppend {
331 if (bytes.len == 0 or bytes.len > max_text_input_bytes) return .{ .accepted = false };
332 if (!std.unicode.utf8ValidateSlice(bytes)) return .{ .accepted = false };
333
334 var out = TextAppend{ .accepted = true };
335 var iterator = std.unicode.Utf8Iterator{ .bytes = bytes, .i = 0 };
336 while (iterator.nextCodepoint()) |codepoint| {
337 if (codepoint < 0x20 or codepoint == 0x7f) return .{ .accepted = false };
338 out.codepoints[out.count] = codepoint;
339 out.count += 1;
340 }
341 return out;
342 }
343
344 fn saturatingI32FromI64(value: i64) i32 {
345 if (value < std.math.minInt(i32)) return std.math.minInt(i32);
346 if (value > std.math.maxInt(i32)) return std.math.maxInt(i32);
347 return @intCast(value);
348 }
349
350 fn saturatingI32Add(left: i32, right: i32) i32 {
351 return std.math.add(i32, left, right) catch if (right < 0)
352 std.math.minInt(i32)
353 else
354 std.math.maxInt(i32);
355 }
356
357 fn saturatingI32Diff(next: i32, previous: i32) i32 {
358 return saturatingI32FromI64(@as(i64, next) - @as(i64, previous));
359 }
360
361 pub const TraceProperty = struct {
362 pub fn property(conjecture: *hypothesis.ConjectureData, allocator: std.mem.Allocator) !void {
363 var state = try State.init(allocator, try windowing.input.Capacity.derive(input_limits));
364 defer state.deinit();
365 var expected = Model{};
366
367 const step_count = try drawUsize(conjecture, 1, 128, 16);
368 var step_index: usize = 0;
369 while (step_index < step_count) : (step_index += 1) {
370 switch (try drawUsize(conjecture, 0, 9, 0)) {
371 0 => {
372 state.beginPoll();
373 expected.beginPoll();
374 },
375 1 => {
376 const key = try drawKey(conjecture);
377 state.pressKey(key);
378 expected.pressKey(key);
379 },
380 2 => {
381 const key = try drawKey(conjecture);
382 state.releaseKey(key);
383 expected.releaseKey(key);
384 },
385 3 => {
386 const expected_key = expected.nextPressedKey();
387 const actual = state.nextPressedKey();
388 if (expected_key) |key| {
389 try std.testing.expect(actual != null);
390 try std.testing.expectEqual(@backingInt(key), @backingInt(actual.?));
391 } else {
392 try std.testing.expect(actual == null);
393 }
394 },
395 4 => {
396 const button = try drawMouseButton(conjecture);
397 state.pressMouse(button);
398 expected.pressMouse(button);
399 },
400 5 => {
401 const button = try drawMouseButton(conjecture);
402 state.releaseMouse(button);
403 expected.releaseMouse(button);
404 },
405 6 => {
406 const x = try drawCoordinate(conjecture);
407 const y = try drawCoordinate(conjecture);
408 state.setMousePosition(x, y);
409 expected.setMousePosition(x, y);
410 },
411 7 => {
412 const x = try drawCoordinate(conjecture);
413 const y = try drawCoordinate(conjecture);
414 state.moveMouse(x, y);
415 expected.moveMouse(x, y);
416 },
417 8 => {
418 const x = try drawWheel(conjecture);
419 const y = try drawWheel(conjecture);
420 state.addMouseWheel(x, y);
421 expected.addMouseWheel(x, y);
422 },
423 else => {
424 const bytes = text_inputs[try drawUsize(conjecture, 0, text_inputs.len - 1, 4)];
425 try std.testing.expectEqual(expected.pushTextInput(bytes), state.pushTextInput(bytes));
426 },
427 }
428
429 try expectState(&state, &expected);
430 }
431
432 try expectDrainedQueues(&state, &expected);
433 try expectState(&state, &expected);
434 }
435 };
436
437 test "property: input state matches generated trace model" {
438 try hypothesis.checkNamed(TraceProperty, "windowing-input-trace", settings());
439 }