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 }