lib/hypothesis/src/strategy.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
7 pub fn Strategy(comptime T: type) type {
8 return struct {
9 context: *const anyopaque,
10 drawFn: *const fn (*const anyopaque, *ConjectureData, Allocator) DrawError!T,
11
12 const Self = @This();
13
14 pub fn draw(self: Self, data: *ConjectureData, allocator: Allocator) DrawError!T {
15 return self.drawFn(self.context, data, allocator);
16 }
17
18 pub fn from(comptime S: type, ptr: *const S) Self {
19 return .{
20 .context = @ptrCast(ptr),
21 .drawFn = &struct {
22 fn call(ctx: *const anyopaque, data: *ConjectureData, allocator: Allocator) DrawError!T {
23 const self: *const S = @ptrCast(@alignCast(ctx));
24 return self.draw(data, allocator);
25 }
26 }.call,
27 };
28 }
29 };
30 }
31
32 pub fn IntegerStrategy(comptime Int: type) type {
33 const info = @typeInfo(Int);
34 if (info != .int) @compileError("IntegerStrategy requires an integer type");
35
36 return struct {
37 min: Int,
38 max: Int,
39 shrink_towards: Int,
40
41 const Self = @This();
42
43 pub fn draw(self: *const Self, data: *ConjectureData, _: Allocator) DrawError!Int {
44 const raw = try data.drawInteger(
45 intToU64(Int, self.min),
46 intToU64(Int, self.max),
47 intToU64(Int, self.shrink_towards),
48 );
49 return u64ToInt(Int, raw);
50 }
51
52 pub fn strategy(self: *const Self) Strategy(Int) {
53 return Strategy(Int).from(Self, self);
54 }
55 };
56 }
57
58 pub fn integers(comptime Int: type, min: Int, max: Int) IntegerStrategy(Int) {
59 return .{
60 .min = min,
61 .max = max,
62 .shrink_towards = if (min <= 0 and 0 <= max) 0 else min,
63 };
64 }
65
66 pub const BooleanStrategy = struct {
67 pub fn draw(_: *const BooleanStrategy, data: *ConjectureData, _: Allocator) DrawError!bool {
68 return data.drawBoolean();
69 }
70
71 pub fn strategy(self: *const BooleanStrategy) Strategy(bool) {
72 return Strategy(bool).from(BooleanStrategy, self);
73 }
74 };
75
76 pub fn booleans() BooleanStrategy {
77 return .{};
78 }
79
80 pub fn FloatStrategy(comptime Float: type) type {
81 const info = @typeInfo(Float);
82 if (info != .float) @compileError("FloatStrategy requires a float type");
83
84 return struct {
85 min: Float,
86 max: Float,
87
88 const Self = @This();
89
90 pub fn draw(self: *const Self, data: *ConjectureData, _: Allocator) DrawError!Float {
91 const raw = try data.drawFloat(
92 @floatCast(self.min),
93 @floatCast(self.max),
94 );
95 return @floatCast(raw);
96 }
97
98 pub fn strategy(self: *const Self) Strategy(Float) {
99 return Strategy(Float).from(Self, self);
100 }
101 };
102 }
103
104 pub fn floats(comptime Float: type, min: Float, max: Float) FloatStrategy(Float) {
105 return .{
106 .min = min,
107 .max = max,
108 };
109 }
110
111 pub const BytesStrategy = struct {
112 min_size: usize,
113 max_size: usize,
114
115 pub fn draw(self: *const BytesStrategy, data: *ConjectureData, _: Allocator) DrawError![]const u8 {
116 return data.drawBytes(self.min_size, self.max_size);
117 }
118
119 pub fn strategy(self: *const BytesStrategy) Strategy([]const u8) {
120 return Strategy([]const u8).from(BytesStrategy, self);
121 }
122 };
123
124 pub fn bytesOf(min_size: usize, max_size: usize) BytesStrategy {
125 return .{
126 .min_size = min_size,
127 .max_size = max_size,
128 };
129 }
130
131 pub const ascii_printable = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~";
132 pub const alphanumeric = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
133 pub const url_safe_tokens = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-";
134
135 pub fn drawCharFromCharset(data: *ConjectureData, charset: []const u8) DrawError!u8 {
136 std.debug.assert(charset.len > 0);
137 const max_index: u64 = @intCast(charset.len - 1);
138 const idx = try data.drawInteger(0, max_index, 0);
139 return charset[@intCast(idx)];
140 }
141
142 pub fn intToU64(comptime Int: type, value: Int) u64 {
143 const info = @typeInfo(Int).int;
144 if (info.signedness == .signed) {
145 const Unsigned = @Int(.unsigned, info.bits);
146 const sign_bit: Unsigned = @as(Unsigned, 1) << @intCast(info.bits - 1);
147 const as_unsigned: Unsigned = @bitCast(value);
148 return @as(u64, as_unsigned ^ sign_bit);
149 } else {
150 return @intCast(value);
151 }
152 }
153
154 pub fn u64ToInt(comptime Int: type, raw: u64) Int {
155 const info = @typeInfo(Int).int;
156 if (info.signedness == .signed) {
157 const Unsigned = @Int(.unsigned, info.bits);
158 const sign_bit: Unsigned = @as(Unsigned, 1) << @intCast(info.bits - 1);
159 const as_unsigned: Unsigned = @truncate(raw);
160 return @bitCast(as_unsigned ^ sign_bit);
161 } else {
162 return @intCast(raw);
163 }
164 }
165
166 test "IntegerStrategy draws in range" {
167 const allocator = std.testing.allocator;
168 var data = ConjectureData.init(allocator, 42);
169 defer data.deinit();
170
171 const s = integers(i32, -100, 100);
172 for (0..100) |_| {
173 const v = try s.strategy().draw(&data, allocator);
174 try std.testing.expect(v >= -100 and v <= 100);
175 }
176 }
177
178 test "BooleanStrategy draws booleans" {
179 const allocator = std.testing.allocator;
180 var data = ConjectureData.init(allocator, 42);
181 defer data.deinit();
182
183 const s = booleans();
184 var saw_true = false;
185 var saw_false = false;
186 for (0..100) |_| {
187 const v = try s.strategy().draw(&data, allocator);
188 if (v) saw_true = true else saw_false = true;
189 }
190 try std.testing.expect(saw_true and saw_false);
191 }
192
193 test "IntegerStrategy u8 draws in range" {
194 const allocator = std.testing.allocator;
195 var data = ConjectureData.init(allocator, 42);
196 defer data.deinit();
197
198 const s = integers(u8, 0, 255);
199 for (0..100) |_| {
200 const v = try s.strategy().draw(&data, allocator);
201 try std.testing.expect(v <= 255);
202 }
203 }
204
205 test "signed integer encoding roundtrips" {
206 try std.testing.expectEqual(@as(i8, -128), u64ToInt(i8, intToU64(i8, -128)));
207 try std.testing.expectEqual(@as(i8, -1), u64ToInt(i8, intToU64(i8, -1)));
208 try std.testing.expectEqual(@as(i8, 0), u64ToInt(i8, intToU64(i8, 0)));
209 try std.testing.expectEqual(@as(i8, 127), u64ToInt(i8, intToU64(i8, 127)));
210 try std.testing.expectEqual(@as(i32, -1000), u64ToInt(i32, intToU64(i32, -1000)));
211 try std.testing.expectEqual(@as(i32, 1000), u64ToInt(i32, intToU64(i32, 1000)));
212 }
213
214 test "signed integer encoding is order-preserving" {
215 try std.testing.expect(intToU64(i8, -128) < intToU64(i8, 0));
216 try std.testing.expect(intToU64(i8, 0) < intToU64(i8, 127));
217 try std.testing.expect(intToU64(i32, -100) < intToU64(i32, 100));
218 try std.testing.expect(intToU64(i32, std.math.minInt(i32)) <
219 intToU64(i32, std.math.maxInt(i32)));
220 try std.testing.expectEqual(@as(u64, 0), intToU64(i8, std.math.minInt(i8)));
221 try std.testing.expectEqual(@as(u64, 0xFF), intToU64(i8, std.math.maxInt(i8)));
222 try std.testing.expectEqual(@as(u64, 0), intToU64(i32, std.math.minInt(i32)));
223 try std.testing.expectEqual(
224 @as(u64, 0xFFFF_FFFF),
225 intToU64(i32, std.math.maxInt(i32)),
226 );
227 }
228
229 test "IntegerStrategy spans signed range without clamp collapse" {
230 const allocator = std.testing.allocator;
231 var data = ConjectureData.init(allocator, 42);
232 defer data.deinit();
233
234 const s = integers(i32, -100, 100);
235 var saw_negative = false;
236 var saw_positive = false;
237 for (0..200) |_| {
238 const v = try s.strategy().draw(&data, allocator);
239 try std.testing.expect(v >= -100 and v <= 100);
240 if (v < 0) saw_negative = true;
241 if (v > 0) saw_positive = true;
242 }
243 try std.testing.expect(saw_negative and saw_positive);
244 }
245
246 test "FloatStrategy draws in range" {
247 const allocator = std.testing.allocator;
248 var data = ConjectureData.init(allocator, 42);
249 defer data.deinit();
250
251 const s = floats(f32, -1.0, 1.0);
252 for (0..100) |_| {
253 const v = try s.strategy().draw(&data, allocator);
254 try std.testing.expect(v >= -1.0 and v <= 1.0);
255 }
256 }
257
258 test "BytesStrategy draws in size range" {
259 const allocator = std.testing.allocator;
260 var data = ConjectureData.init(allocator, 42);
261 defer data.deinit();
262
263 const s = bytesOf(2, 8);
264 for (0..20) |_| {
265 const v = try s.strategy().draw(&data, allocator);
266 try std.testing.expect(v.len >= 2 and v.len <= 8);
267 }
268 }
269
270 test "drawCharFromCharset draws from charset" {
271 const allocator = std.testing.allocator;
272 var data = ConjectureData.init(allocator, 42);
273 defer data.deinit();
274
275 const charset = "abc123";
276 for (0..100) |_| {
277 const ch = try drawCharFromCharset(&data, charset);
278 try std.testing.expect(std.mem.indexOfScalar(u8, charset, ch) != null);
279 }
280 }