lib/hypothesis/src/auto.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const Allocator = std.mem.Allocator;
3 const conjecture = @import("conjecture.zig");
4 const ConjectureData = conjecture.ConjectureData;
5 const DrawError = conjecture.DrawError;
6 const strategy_mod = @import("strategy.zig");
7 const Strategy = strategy_mod.Strategy;
8 const intToU64 = strategy_mod.intToU64;
9 const u64ToInt = strategy_mod.u64ToInt;
10
11 const default_float_abs: f64 = 1.0e6;
12
13 const default_slice_max: usize = 8;
14
15 fn AutoStrategy(comptime T: type) type {
16 return struct {
17 const Self = @This();
18
19 pub fn draw(_: *const Self, data: *ConjectureData, allocator: Allocator) DrawError!T {
20 return autoDraw(T, data, allocator);
21 }
22
23 pub fn strategy(self: *const Self) Strategy(T) {
24 return Strategy(T).from(Self, self);
25 }
26 };
27 }
28
29 pub fn auto(comptime T: type) Strategy(T) {
30 const Helper = struct {
31 const inst: AutoStrategy(T) = .{};
32 };
33 return Strategy(T).from(AutoStrategy(T), &Helper.inst);
34 }
35
36 fn autoDraw(comptime T: type, data: *ConjectureData, allocator: Allocator) DrawError!T {
37 switch (@typeInfo(T)) {
38 .bool => return data.drawBoolean(),
39 .int => |info| {
40 _ = info;
41 const lo = std.math.minInt(T);
42 const hi = std.math.maxInt(T);
43 const shrink_target: T = if (lo <= 0 and hi >= 0) 0 else lo;
44 const raw = try data.drawInteger(
45 intToU64(T, lo),
46 intToU64(T, hi),
47 intToU64(T, shrink_target),
48 );
49 return u64ToInt(T, raw);
50 },
51 .float => {
52 const lo: T = -@as(T, @floatCast(default_float_abs));
53 const hi: T = @as(T, @floatCast(default_float_abs));
54 const f = try data.drawFloat(@floatCast(lo), @floatCast(hi));
55 return @floatCast(f);
56 },
57 .@"enum" => |info| {
58 if (info.field_names.len == 0)
59 @compileError("auto cannot derive a strategy for an empty enum: " ++ @typeName(T));
60 if (info.field_names.len == 1)
61 return @field(T, info.field_names[0]);
62 const max_idx: u64 = @intCast(info.field_names.len - 1);
63 const idx = try data.drawInteger(0, max_idx, 0);
64 inline for (info.field_names, 0..) |field_name, i| {
65 if (idx == @as(u64, @intCast(i))) return @field(T, field_name);
66 }
67 unreachable;
68 },
69 .@"struct" => |info| {
70 var result: T = undefined;
71 inline for (info.field_names, info.field_types) |field_name, field_type| {
72 @field(result, field_name) = try autoDraw(field_type, data, allocator);
73 }
74 return result;
75 },
76 .@"union" => |info| {
77 if (info.tag_type == null)
78 @compileError("auto requires a tagged union, got " ++ @typeName(T));
79 if (info.field_names.len == 0)
80 @compileError("auto cannot derive a strategy for an empty union: " ++ @typeName(T));
81 const idx: u64 = if (info.field_names.len == 1)
82 0
83 else blk: {
84 const max_idx: u64 = @intCast(info.field_names.len - 1);
85 break :blk try data.drawInteger(0, max_idx, 0);
86 };
87 inline for (info.field_names, info.field_types, 0..) |field_name, field_type, i| {
88 if (idx == @as(u64, @intCast(i))) {
89 if (field_type == void) return @unionInit(T, field_name, {});
90 const payload = try autoDraw(field_type, data, allocator);
91 return @unionInit(T, field_name, payload);
92 }
93 }
94 unreachable;
95 },
96 .optional => |opt| {
97 const present = try data.drawBoolean();
98 if (!present) return null;
99 return try autoDraw(opt.child, data, allocator);
100 },
101 .pointer => |ptr| {
102 if (ptr.size != .slice)
103 @compileError("auto only supports slice pointers, got " ++ @typeName(T));
104 try data.beginSpan("auto-slice");
105 const len = try data.drawInteger(0, default_slice_max, 0);
106 const Child = ptr.child;
107 const items = try allocator.alloc(Child, @intCast(len));
108 for (items) |*item| {
109 item.* = try autoDraw(Child, data, allocator);
110 }
111 data.endSpan();
112 return items;
113 },
114 else => @compileError("auto does not support " ++ @typeName(T)),
115 }
116 }
117
118 test "auto bool draws both values" {
119 const allocator = std.testing.allocator;
120 var data = ConjectureData.init(allocator, 42);
121 defer data.deinit();
122
123 const s = auto(bool);
124 var saw_true = false;
125 var saw_false = false;
126 for (0..100) |_| {
127 const v = try s.draw(&data, allocator);
128 if (v) saw_true = true else saw_false = true;
129 }
130 try std.testing.expect(saw_true and saw_false);
131 }
132
133 test "auto u8 draws in full range" {
134 const allocator = std.testing.allocator;
135 var data = ConjectureData.init(allocator, 42);
136 defer data.deinit();
137
138 const s = auto(u8);
139 for (0..100) |_| {
140 const v = try s.draw(&data, allocator);
141 _ = v;
142 }
143 }
144
145 test "auto i32 draws in signed range" {
146 const allocator = std.testing.allocator;
147 var data = ConjectureData.init(allocator, 42);
148 defer data.deinit();
149
150 const s = auto(i32);
151 var saw_negative = false;
152 for (0..200) |_| {
153 const v = try s.draw(&data, allocator);
154 if (v < 0) saw_negative = true;
155 }
156 try std.testing.expect(saw_negative);
157 }
158
159 test "auto f64 draws in default range" {
160 const allocator = std.testing.allocator;
161 var data = ConjectureData.init(allocator, 42);
162 defer data.deinit();
163
164 const s = auto(f64);
165 for (0..100) |_| {
166 const v = try s.draw(&data, allocator);
167 try std.testing.expect(v >= -default_float_abs);
168 try std.testing.expect(v <= default_float_abs);
169 }
170 }
171
172 test "auto enum picks every variant" {
173 const Color = enum { red, green, blue };
174
175 const allocator = std.testing.allocator;
176 var data = ConjectureData.init(allocator, 42);
177 defer data.deinit();
178
179 const s = auto(Color);
180 var seen = [_]bool{ false, false, false };
181 for (0..100) |_| {
182 const v = try s.draw(&data, allocator);
183 seen[@backingInt(v)] = true;
184 }
185 try std.testing.expect(seen[0] and seen[1] and seen[2]);
186 }
187
188 const AutoStructFixture = struct {
189 x: i16,
190 y: i16,
191 flag: bool,
192 };
193
194 test "auto struct draws every field" {
195 const allocator = std.testing.allocator;
196 var data = ConjectureData.init(allocator, 42);
197 defer data.deinit();
198
199 const s = auto(AutoStructFixture);
200 const v = try s.draw(&data, allocator);
201 _ = v.x;
202 _ = v.y;
203 _ = v.flag;
204 }
205
206 test "auto optional yields both null and present" {
207 const allocator = std.testing.allocator;
208 var data = ConjectureData.init(allocator, 42);
209 defer data.deinit();
210
211 const s = auto(?u8);
212 var saw_null = false;
213 var saw_present = false;
214 for (0..100) |_| {
215 const v = try s.draw(&data, allocator);
216 if (v == null) saw_null = true else saw_present = true;
217 }
218 try std.testing.expect(saw_null and saw_present);
219 }
220
221 const AutoUnionFixture = union(enum) {
222 none: void,
223 one: u8,
224 two: i16,
225 };
226
227 test "auto tagged union picks every variant" {
228 const allocator = std.testing.allocator;
229 var data = ConjectureData.init(allocator, 42);
230 defer data.deinit();
231
232 const s = auto(AutoUnionFixture);
233 var saw_none = false;
234 var saw_one = false;
235 var saw_two = false;
236 for (0..200) |_| {
237 const v = try s.draw(&data, allocator);
238 switch (v) {
239 .none => saw_none = true,
240 .one => saw_one = true,
241 .two => saw_two = true,
242 }
243 }
244 try std.testing.expect(saw_none and saw_one and saw_two);
245 }
246
247 test "auto slice yields varied lengths within default bound" {
248 const allocator = std.testing.allocator;
249 var data = ConjectureData.init(allocator, 42);
250 defer data.deinit();
251
252 const s = auto([]u8);
253 var saw_empty = false;
254 var saw_nonempty = false;
255 for (0..200) |_| {
256 const v = try s.draw(&data, allocator);
257 defer allocator.free(v);
258 if (v.len == 0) saw_empty = true else saw_nonempty = true;
259 try std.testing.expect(v.len <= default_slice_max);
260 }
261 try std.testing.expect(saw_empty and saw_nonempty);
262 }