lib/hypothesis/src/value.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const engine = @import("engine.zig");
  3 const conjecture = @import("conjecture.zig");
  4 const strategy = @import("strategy.zig");
  5 
  6 const ConjectureData = conjecture.ConjectureData;
  7 
  8 pub const Config = struct {
  9     max_examples: u32 = 100,
 10     max_replays: u32 = 100,
 11     max_shrinks: u32 = 1000,
 12     max_choices: usize = 8192,
 13     max_input_bytes: usize = conjecture.default_max_input_bytes,
 14     seed: ?u64 = null,
 15     persist_failures: bool = true,
 16     report_failures: bool = true,
 17     test_name: ?[]const u8 = null,
 18 
 19     pub fn quick() Config {
 20         return .{
 21             .max_examples = 10,
 22             .max_replays = 10,
 23             .max_shrinks = 100,
 24             .max_choices = 4096,
 25             .max_input_bytes = 256 * 1024,
 26         };
 27     }
 28 
 29     pub fn dev() Config {
 30         return .{};
 31     }
 32 
 33     pub fn ci() Config {
 34         return .{
 35             .max_examples = 1000,
 36             .max_replays = 1000,
 37             .max_shrinks = 5000,
 38             .max_input_bytes = 4 * 1024 * 1024,
 39         };
 40     }
 41 
 42     pub fn expectedSlow(max_examples: u32, _: u64) Config {
 43         return .{ .max_examples = max_examples, .max_replays = max_examples };
 44     }
 45 
 46     pub fn toSettings(self: Config) engine.Settings {
 47         return .{
 48             .max_examples = @intCast(self.max_examples),
 49             .max_replays = @intCast(self.max_replays),
 50             .max_choices = self.max_choices,
 51             .max_input_bytes = self.max_input_bytes,
 52             .max_shrinks = @intCast(self.max_shrinks),
 53             .seed = self.seed,
 54             .database_path = if (self.persist_failures) "zig-out/pbt-failures" else null,
 55             .database_namespace = self.test_name,
 56             .report_failure = self.report_failures,
 57         };
 58     }
 59 };
 60 
 61 pub fn Generator(comptime T: type) type {
 62     return struct {
 63         drawFn: *const fn (*ConjectureData) anyerror!T,
 64 
 65         const Self = @This();
 66 
 67         pub fn draw(self: Self, data: *ConjectureData) anyerror!T {
 68             return self.drawFn(data);
 69         }
 70 
 71         pub fn map(
 72             comptime self: Self,
 73             comptime U: type,
 74             comptime f: *const fn (T) U,
 75         ) Generator(U) {
 76             return .{ .drawFn = &MappedGeneratorType(T, U, self, f).draw };
 77         }
 78 
 79         pub fn filter(comptime self: Self, comptime pred: *const fn (T) bool) Self {
 80             return .{ .drawFn = &FilteredGeneratorType(T, self, pred).draw };
 81         }
 82 
 83         pub fn flatMap(
 84             comptime self: Self,
 85             comptime U: type,
 86             comptime f: *const fn (T) Generator(U),
 87         ) Generator(U) {
 88             return .{ .drawFn = &FlatMappedGeneratorType(T, U, self, f).draw };
 89         }
 90     };
 91 }
 92 
 93 fn MappedGeneratorType(
 94     comptime T: type,
 95     comptime U: type,
 96     comptime source: Generator(T),
 97     comptime transform: *const fn (T) U,
 98 ) type {
 99     return struct {
100         fn draw(data: *ConjectureData) anyerror!U {
101             return transform(try source.drawFn(data));
102         }
103     };
104 }
105 
106 fn FilteredGeneratorType(
107     comptime T: type,
108     comptime source: Generator(T),
109     comptime predicate: *const fn (T) bool,
110 ) type {
111     return struct {
112         fn draw(data: *ConjectureData) anyerror!T {
113             for (0..10) |_| {
114                 const value = try source.drawFn(data);
115                 if (predicate(value)) return value;
116             }
117             data.markInvalid();
118             return error.Rejected;
119         }
120     };
121 }
122 
123 fn FlatMappedGeneratorType(
124     comptime T: type,
125     comptime U: type,
126     comptime source: Generator(T),
127     comptime transform: *const fn (T) Generator(U),
128 ) type {
129     return struct {
130         fn draw(data: *ConjectureData) anyerror!U {
131             const value = try source.drawFn(data);
132             return transform(value).drawFn(data);
133         }
134     };
135 }
136 
137 fn IntegerGeneratorType(
138     comptime T: type,
139     comptime min_value: T,
140     comptime max_value: T,
141 ) type {
142     return struct {
143         fn draw(data: *ConjectureData) anyerror!T {
144             const shrink_towards: T = if (min_value <= 0 and 0 <= max_value)
145                 0
146             else
147                 min_value;
148             const raw = try data.drawInteger(
149                 strategy.intToU64(T, min_value),
150                 strategy.intToU64(T, max_value),
151                 strategy.intToU64(T, shrink_towards),
152             );
153             return strategy.u64ToInt(T, raw);
154         }
155     };
156 }
157 
158 pub fn integer(comptime T: type, comptime min_val: T, comptime max_val: T) Generator(T) {
159     comptime {
160         const info = @typeInfo(T);
161         if (info != .int) @compileError("integer generator requires an integer type");
162         if (min_val > max_val) @compileError("min must be <= max");
163     }
164 
165     return .{ .drawFn = &IntegerGeneratorType(T, min_val, max_val).draw };
166 }
167 
168 fn Float32GeneratorType(comptime min_value: f32, comptime max_value: f32) type {
169     return struct {
170         fn draw(data: *ConjectureData) anyerror!f32 {
171             return @floatCast(try data.drawFloat(min_value, max_value));
172         }
173     };
174 }
175 
176 pub fn float32(comptime min_val: f32, comptime max_val: f32) Generator(f32) {
177     return .{ .drawFn = &Float32GeneratorType(min_val, max_val).draw };
178 }
179 
180 fn Float64GeneratorType(comptime min_value: f64, comptime max_value: f64) type {
181     return struct {
182         fn draw(data: *ConjectureData) anyerror!f64 {
183             return try data.drawFloat(min_value, max_value);
184         }
185     };
186 }
187 
188 pub fn float64(comptime min_val: f64, comptime max_val: f64) Generator(f64) {
189     return .{ .drawFn = &Float64GeneratorType(min_val, max_val).draw };
190 }
191 
192 fn draw_boolean(data: *ConjectureData) anyerror!bool {
193     return try data.drawBoolean();
194 }
195 
196 pub fn boolean() Generator(bool) {
197     return .{ .drawFn = &draw_boolean };
198 }
199 
200 fn BytesGeneratorType(comptime max_len: usize) type {
201     return struct {
202         fn draw(data: *ConjectureData) anyerror![]const u8 {
203             return try data.drawBytes(0, max_len);
204         }
205     };
206 }
207 
208 pub fn bytes(comptime max_len: usize) Generator([]const u8) {
209     return .{ .drawFn = &BytesGeneratorType(max_len).draw };
210 }
211 
212 pub fn BoundedArray(comptime T: type, comptime capacity: usize) type {
213     return struct {
214         buf: [capacity]T = undefined,
215         len: usize = 0,
216 
217         pub fn slice(self: *const @This()) []const T {
218             return self.buf[0..self.len];
219         }
220     };
221 }
222 
223 fn ListGeneratorType(
224     comptime T: type,
225     comptime inner: Generator(T),
226     comptime max_len: usize,
227 ) type {
228     return struct {
229         fn draw(data: *ConjectureData) anyerror!BoundedArray(T, max_len) {
230             var result: BoundedArray(T, max_len) = .{};
231             for (0..max_len) |_| {
232                 if (!try data.drawBoolean()) break;
233                 result.buf[result.len] = try inner.drawFn(data);
234                 result.len += 1;
235             }
236             return result;
237         }
238     };
239 }
240 
241 pub fn list(
242     comptime T: type,
243     comptime inner: Generator(T),
244     comptime max_len: usize,
245 ) Generator(BoundedArray(T, max_len)) {
246     return .{ .drawFn = &ListGeneratorType(T, inner, max_len).draw };
247 }
248 
249 fn FixedArrayGeneratorType(
250     comptime T: type,
251     comptime inner: Generator(T),
252     comptime length: usize,
253 ) type {
254     return struct {
255         fn draw(data: *ConjectureData) anyerror![length]T {
256             var result: [length]T = undefined;
257             for (&result) |*slot| slot.* = try inner.drawFn(data);
258             return result;
259         }
260     };
261 }
262 
263 pub fn fixedArray(
264     comptime T: type,
265     comptime inner: Generator(T),
266     comptime N: usize,
267 ) Generator([N]T) {
268     return .{ .drawFn = &FixedArrayGeneratorType(T, inner, N).draw };
269 }
270 
271 fn GenPayload(comptime G: type) type {
272     const draw_fn_ptr = @typeInfo(G).@"struct".field_types[0];
273     const draw_fn = @typeInfo(draw_fn_ptr).pointer.child;
274     const ret = @typeInfo(draw_fn).@"fn".return_type.?;
275     return @typeInfo(ret).error_union.payload;
276 }
277 
278 fn TupleResult(comptime gens: anytype) type {
279     var fields: [gens.len]type = undefined;
280     inline for (0..gens.len) |i| {
281         fields[i] = GenPayload(@TypeOf(gens[i]));
282     }
283     return @Tuple(&fields);
284 }
285 
286 fn TupleGeneratorType(comptime generators: anytype) type {
287     return struct {
288         fn draw(data: *ConjectureData) anyerror!TupleResult(generators) {
289             var result: TupleResult(generators) = undefined;
290             inline for (0..generators.len) |index| {
291                 result[index] = try generators[index].drawFn(data);
292             }
293             return result;
294         }
295     };
296 }
297 
298 pub fn tuple(comptime gens: anytype) Generator(TupleResult(gens)) {
299     return .{ .drawFn = &TupleGeneratorType(gens).draw };
300 }
301 
302 fn OneOfGeneratorType(comptime T: type, comptime generators: anytype) type {
303     return struct {
304         fn draw(data: *ConjectureData) anyerror!T {
305             const index = try data.drawInteger(0, generators.len - 1, 0);
306             inline for (0..generators.len) |generator_index| {
307                 if (index == generator_index) {
308                     return generators[generator_index].drawFn(data);
309                 }
310             }
311             unreachable;
312         }
313     };
314 }
315 
316 pub fn oneOf(comptime T: type, comptime gens: anytype) Generator(T) {
317     const n = gens.len;
318     if (n == 0) @compileError("oneOf requires at least one generator");
319     return .{ .drawFn = &OneOfGeneratorType(T, gens).draw };
320 }
321 
322 pub fn gen_point(comptime min: f32, comptime max: f32) Generator([3]f32) {
323     return comptime fixedArray(f32, float32(min, max), 3);
324 }
325 
326 fn is_nonzero_vec3(value: [3]f32) bool {
327     return value[0] * value[0] + value[1] * value[1] + value[2] * value[2] > 0.01;
328 }
329 
330 fn normalize_vec3(value: [3]f32) [3]f32 {
331     const norm = @sqrt(
332         value[0] * value[0] +
333             value[1] * value[1] +
334             value[2] * value[2],
335     );
336     return .{ value[0] / norm, value[1] / norm, value[2] / norm };
337 }
338 
339 pub fn gen_unit_vec3() Generator([3]f32) {
340     return comptime gen_point(-1.0, 1.0)
341         .filter(&is_nonzero_vec3)
342         .map([3]f32, &normalize_vec3);
343 }
344 
345 fn is_nonzero_quaternion(value: [4]f32) bool {
346     return value[0] * value[0] +
347         value[1] * value[1] +
348         value[2] * value[2] +
349         value[3] * value[3] > 0.01;
350 }
351 
352 fn normalize_quaternion(value: [4]f32) [4]f32 {
353     const norm = @sqrt(
354         value[0] * value[0] +
355             value[1] * value[1] +
356             value[2] * value[2] +
357             value[3] * value[3],
358     );
359     return .{
360         value[0] / norm,
361         value[1] / norm,
362         value[2] / norm,
363         value[3] / norm,
364     };
365 }
366 
367 pub fn gen_quaternion() Generator([4]f32) {
368     return comptime fixedArray(f32, float32(-1.0, 1.0), 4)
369         .filter(&is_nonzero_quaternion)
370         .map([4]f32, &normalize_quaternion);
371 }
372 
373 fn CheckValuePropertyType(
374     comptime T: type,
375     comptime generator: Generator(T),
376     comptime property: *const fn (T) anyerror!void,
377 ) type {
378     return struct {
379         fn run(data: *ConjectureData, _: std.mem.Allocator) anyerror!void {
380             const value = generator.draw(data) catch |err| switch (err) {
381                 error.Overrun => return,
382                 error.Rejected => {
383                     data.markInvalid();
384                     return;
385                 },
386                 else => return err,
387             };
388             if (data.status != .valid) return;
389             try property(value);
390         }
391     };
392 }
393 
394 fn CheckValueAllocPropertyType(
395     comptime T: type,
396     comptime generator: Generator(T),
397     comptime property: *const fn (std.mem.Allocator, T) anyerror!void,
398 ) type {
399     return struct {
400         fn run(
401             data: *ConjectureData,
402             property_allocator: std.mem.Allocator,
403         ) anyerror!void {
404             const value = generator.draw(data) catch |err| switch (err) {
405                 error.Overrun => return,
406                 error.Rejected => {
407                     data.markInvalid();
408                     return;
409                 },
410                 else => return err,
411             };
412             if (data.status != .valid) return;
413             try property(property_allocator, value);
414         }
415     };
416 }
417 
418 pub fn checkValue(
419     comptime T: type,
420     comptime gen: Generator(T),
421     comptime property: *const fn (T) anyerror!void,
422     config: Config,
423     allocator: std.mem.Allocator,
424 ) !void {
425     const Property = CheckValuePropertyType(T, gen, property);
426 
427     var settings = config.toSettings();
428     if (settings.database_namespace == null) {
429         settings.database_namespace = @typeName(Property);
430     }
431 
432     var result = try engine.run(allocator, &Property.run, settings);
433     defer result.deinit();
434 
435     if (!result.passed) {
436         if (settings.report_failure) @import("testing.zig").printFailure(&result);
437         return result.failing_error orelse error.PropertyFailed;
438     }
439 }
440 
441 pub fn checkValueAlloc(
442     comptime T: type,
443     comptime gen: Generator(T),
444     comptime property: *const fn (std.mem.Allocator, T) anyerror!void,
445     config: Config,
446     allocator: std.mem.Allocator,
447 ) !void {
448     const Property = CheckValueAllocPropertyType(T, gen, property);
449 
450     var settings = config.toSettings();
451     if (settings.database_namespace == null) {
452         settings.database_namespace = @typeName(Property);
453     }
454 
455     var result = try engine.run(allocator, &Property.run, settings);
456     defer result.deinit();
457 
458     if (!result.passed) {
459         if (settings.report_failure) @import("testing.zig").printFailure(&result);
460         return result.failing_error orelse error.PropertyFailed;
461     }
462 }