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 }