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 }