master
  1const std = @import("../std.zig");
  2const math = std.math;
  3const Random = std.Random;
  4const DefaultPrng = Random.DefaultPrng;
  5const SplitMix64 = Random.SplitMix64;
  6const DefaultCsprng = Random.DefaultCsprng;
  7const expect = std.testing.expect;
  8const expectEqual = std.testing.expectEqual;
  9
 10const SequentialPrng = struct {
 11    const Self = @This();
 12    next_value: u8,
 13
 14    pub fn init() Self {
 15        return Self{
 16            .next_value = 0,
 17        };
 18    }
 19
 20    pub fn random(self: *Self) Random {
 21        return Random.init(self, fill);
 22    }
 23
 24    pub fn fill(self: *Self, buf: []u8) void {
 25        for (buf) |*b| {
 26            b.* = self.next_value;
 27        }
 28        self.next_value +%= 1;
 29    }
 30};
 31
 32/// Do not use this PRNG! It is meant to be predictable, for the purposes of test reproducibility and coverage.
 33/// Its output is just a repeat of a user-specified byte pattern.
 34/// Name is a reference to this comic: https://dilbert.com/strip/2001-10-25
 35const Dilbert = struct {
 36    pattern: []const u8 = undefined,
 37    curr_idx: usize = 0,
 38
 39    pub fn init(pattern: []const u8) !Dilbert {
 40        if (pattern.len == 0)
 41            return error.EmptyPattern;
 42        var self = Dilbert{};
 43        self.pattern = pattern;
 44        self.curr_idx = 0;
 45        return self;
 46    }
 47
 48    pub fn random(self: *Dilbert) Random {
 49        return Random.init(self, fill);
 50    }
 51
 52    pub fn fill(self: *Dilbert, buf: []u8) void {
 53        for (buf) |*byte| {
 54            byte.* = self.pattern[self.curr_idx];
 55            self.curr_idx = (self.curr_idx + 1) % self.pattern.len;
 56        }
 57    }
 58
 59    test "Dilbert fill" {
 60        var r = try Dilbert.init("9nine");
 61
 62        const seq = [_]u64{
 63            0x396E696E65396E69,
 64            0x6E65396E696E6539,
 65            0x6E696E65396E696E,
 66            0x65396E696E65396E,
 67            0x696E65396E696E65,
 68        };
 69
 70        for (seq) |s| {
 71            var buf0: [8]u8 = undefined;
 72            var buf1: [8]u8 = undefined;
 73            std.mem.writeInt(u64, &buf0, s, .big);
 74            r.fill(&buf1);
 75            try std.testing.expect(std.mem.eql(u8, buf0[0..], buf1[0..]));
 76        }
 77    }
 78};
 79
 80test "Random int" {
 81    try testRandomInt();
 82    try comptime testRandomInt();
 83}
 84fn testRandomInt() !void {
 85    var rng = SequentialPrng.init();
 86    const random = rng.random();
 87
 88    try expect(random.int(u0) == 0);
 89
 90    rng.next_value = 0;
 91    try expect(random.int(u1) == 0);
 92    try expect(random.int(u1) == 1);
 93    try expect(random.int(u2) == 2);
 94    try expect(random.int(u2) == 3);
 95    try expect(random.int(u2) == 0);
 96
 97    rng.next_value = 0xff;
 98    try expect(random.int(u8) == 0xff);
 99    rng.next_value = 0x11;
100    try expect(random.int(u8) == 0x11);
101
102    rng.next_value = 0xff;
103    try expect(random.int(u32) == 0xffffffff);
104    rng.next_value = 0x11;
105    try expect(random.int(u32) == 0x11111111);
106
107    rng.next_value = 0xff;
108    try expect(random.int(i32) == -1);
109    rng.next_value = 0x11;
110    try expect(random.int(i32) == 0x11111111);
111
112    rng.next_value = 0xff;
113    try expect(random.int(i8) == -1);
114    rng.next_value = 0x11;
115    try expect(random.int(i8) == 0x11);
116
117    rng.next_value = 0xff;
118    try expect(random.int(u33) == 0x1ffffffff);
119    rng.next_value = 0xff;
120    try expect(random.int(i1) == -1);
121    rng.next_value = 0xff;
122    try expect(random.int(i2) == -1);
123    rng.next_value = 0xff;
124    try expect(random.int(i33) == -1);
125}
126
127test "Random boolean" {
128    try testRandomBoolean();
129    try comptime testRandomBoolean();
130}
131fn testRandomBoolean() !void {
132    var rng = SequentialPrng.init();
133    const random = rng.random();
134
135    try expect(random.boolean() == false);
136    try expect(random.boolean() == true);
137    try expect(random.boolean() == false);
138    try expect(random.boolean() == true);
139}
140
141test "Random enum" {
142    try testRandomEnumValue();
143    try comptime testRandomEnumValue();
144}
145fn testRandomEnumValue() !void {
146    const TestEnum = enum {
147        First,
148        Second,
149        Third,
150    };
151    var rng = SequentialPrng.init();
152    const random = rng.random();
153    rng.next_value = 0;
154    try expect(random.enumValue(TestEnum) == TestEnum.First);
155    try expect(random.enumValue(TestEnum) == TestEnum.First);
156    try expect(random.enumValue(TestEnum) == TestEnum.First);
157}
158
159test "Random intLessThan" {
160    @setEvalBranchQuota(10000);
161    try testRandomIntLessThan();
162    try comptime testRandomIntLessThan();
163}
164fn testRandomIntLessThan() !void {
165    var rng = SequentialPrng.init();
166    const random = rng.random();
167
168    rng.next_value = 0xff;
169    try expect(random.uintLessThan(u8, 4) == 3);
170    try expect(rng.next_value == 0);
171    try expect(random.uintLessThan(u8, 4) == 0);
172    try expect(rng.next_value == 1);
173
174    rng.next_value = 0;
175    try expect(random.uintLessThan(u64, 32) == 0);
176
177    // trigger the bias rejection code path
178    rng.next_value = 0;
179    try expect(random.uintLessThan(u8, 3) == 0);
180    // verify we incremented twice
181    try expect(rng.next_value == 2);
182
183    rng.next_value = 0xff;
184    try expect(random.intRangeLessThan(u8, 0, 0x80) == 0x7f);
185    rng.next_value = 0xff;
186    try expect(random.intRangeLessThan(u8, 0x7f, 0xff) == 0xfe);
187
188    rng.next_value = 0xff;
189    try expect(random.intRangeLessThan(i8, 0, 0x40) == 0x3f);
190    rng.next_value = 0xff;
191    try expect(random.intRangeLessThan(i8, -0x40, 0x40) == 0x3f);
192    rng.next_value = 0xff;
193    try expect(random.intRangeLessThan(i8, -0x80, 0) == -1);
194
195    rng.next_value = 0xff;
196    try expect(random.intRangeLessThan(i3, -4, 0) == -1);
197    rng.next_value = 0xff;
198    try expect(random.intRangeLessThan(i3, -2, 2) == 1);
199}
200
201test "Random intAtMost" {
202    @setEvalBranchQuota(10000);
203    try testRandomIntAtMost();
204    try comptime testRandomIntAtMost();
205}
206fn testRandomIntAtMost() !void {
207    var rng = SequentialPrng.init();
208    const random = rng.random();
209
210    rng.next_value = 0xff;
211    try expect(random.uintAtMost(u8, 3) == 3);
212    try expect(rng.next_value == 0);
213    try expect(random.uintAtMost(u8, 3) == 0);
214
215    // trigger the bias rejection code path
216    rng.next_value = 0;
217    try expect(random.uintAtMost(u8, 2) == 0);
218    // verify we incremented twice
219    try expect(rng.next_value == 2);
220
221    rng.next_value = 0xff;
222    try expect(random.intRangeAtMost(u8, 0, 0x7f) == 0x7f);
223    rng.next_value = 0xff;
224    try expect(random.intRangeAtMost(u8, 0x7f, 0xfe) == 0xfe);
225
226    rng.next_value = 0xff;
227    try expect(random.intRangeAtMost(i8, 0, 0x3f) == 0x3f);
228    rng.next_value = 0xff;
229    try expect(random.intRangeAtMost(i8, -0x40, 0x3f) == 0x3f);
230    rng.next_value = 0xff;
231    try expect(random.intRangeAtMost(i8, -0x80, -1) == -1);
232
233    rng.next_value = 0xff;
234    try expect(random.intRangeAtMost(i3, -4, -1) == -1);
235    rng.next_value = 0xff;
236    try expect(random.intRangeAtMost(i3, -2, 1) == 1);
237
238    try expect(random.uintAtMost(u0, 0) == 0);
239}
240
241test "Random Biased" {
242    var prng = DefaultPrng.init(0);
243    const random = prng.random();
244    // Not thoroughly checking the logic here.
245    // Just want to execute all the paths with different types.
246
247    try expect(random.uintLessThanBiased(u1, 1) == 0);
248    try expect(random.uintLessThanBiased(u32, 10) < 10);
249    try expect(random.uintLessThanBiased(u64, 20) < 20);
250
251    try expect(random.uintAtMostBiased(u0, 0) == 0);
252    try expect(random.uintAtMostBiased(u1, 0) <= 0);
253    try expect(random.uintAtMostBiased(u32, 10) <= 10);
254    try expect(random.uintAtMostBiased(u64, 20) <= 20);
255
256    try expect(random.intRangeLessThanBiased(u1, 0, 1) == 0);
257    try expect(random.intRangeLessThanBiased(i1, -1, 0) == -1);
258    try expect(random.intRangeLessThanBiased(u32, 10, 20) >= 10);
259    try expect(random.intRangeLessThanBiased(i32, 10, 20) >= 10);
260    try expect(random.intRangeLessThanBiased(u64, 20, 40) >= 20);
261    try expect(random.intRangeLessThanBiased(i64, 20, 40) >= 20);
262
263    // uncomment for broken module error:
264    //expect(random.intRangeAtMostBiased(u0, 0, 0) == 0);
265    try expect(random.intRangeAtMostBiased(u1, 0, 1) >= 0);
266    try expect(random.intRangeAtMostBiased(i1, -1, 0) >= -1);
267    try expect(random.intRangeAtMostBiased(u32, 10, 20) >= 10);
268    try expect(random.intRangeAtMostBiased(i32, 10, 20) >= 10);
269    try expect(random.intRangeAtMostBiased(u64, 20, 40) >= 20);
270    try expect(random.intRangeAtMostBiased(i64, 20, 40) >= 20);
271}
272
273test "splitmix64 sequence" {
274    var r = SplitMix64.init(0xaeecf86f7878dd75);
275
276    const seq = [_]u64{
277        0x5dbd39db0178eb44,
278        0xa9900fb66b397da3,
279        0x5c1a28b1aeebcf5c,
280        0x64a963238f776912,
281        0xc6d4177b21d1c0ab,
282        0xb2cbdbdb5ea35394,
283    };
284
285    for (seq) |s| {
286        try expect(s == r.next());
287    }
288}
289
290// Actual Random helper function tests, pcg engine is assumed correct.
291test "Random float correctness" {
292    var prng = DefaultPrng.init(0);
293    const random = prng.random();
294
295    var i: usize = 0;
296    while (i < 1000) : (i += 1) {
297        const val1 = random.float(f32);
298        try expect(val1 >= 0.0);
299        try expect(val1 < 1.0);
300
301        const val2 = random.float(f64);
302        try expect(val2 >= 0.0);
303        try expect(val2 < 1.0);
304    }
305}
306
307// Check the "astronomically unlikely" code paths.
308test "Random float coverage" {
309    var prng = try Dilbert.init(&[_]u8{0});
310    const random = prng.random();
311
312    const rand_f64 = random.float(f64);
313    const rand_f32 = random.float(f32);
314
315    try expect(rand_f32 == 0.0);
316    try expect(rand_f64 == 0.0);
317}
318
319test "Random float chi-square goodness of fit" {
320    const num_numbers = 100000;
321    const num_buckets = 1000;
322
323    var f32_hist = std.AutoHashMap(u32, u32).init(std.testing.allocator);
324    defer f32_hist.deinit();
325    var f64_hist = std.AutoHashMap(u64, u32).init(std.testing.allocator);
326    defer f64_hist.deinit();
327
328    var prng = DefaultPrng.init(0);
329    const random = prng.random();
330
331    var i: usize = 0;
332    while (i < num_numbers) : (i += 1) {
333        const rand_f32 = random.float(f32);
334        const rand_f64 = random.float(f64);
335        const f32_put = try f32_hist.getOrPut(@as(u32, @intFromFloat(rand_f32 * @as(f32, @floatFromInt(num_buckets)))));
336        if (f32_put.found_existing) {
337            f32_put.value_ptr.* += 1;
338        } else {
339            f32_put.value_ptr.* = 1;
340        }
341        const f64_put = try f64_hist.getOrPut(@as(u32, @intFromFloat(rand_f64 * @as(f64, @floatFromInt(num_buckets)))));
342        if (f64_put.found_existing) {
343            f64_put.value_ptr.* += 1;
344        } else {
345            f64_put.value_ptr.* = 1;
346        }
347    }
348
349    var f32_total_variance: f64 = 0;
350    var f64_total_variance: f64 = 0;
351
352    {
353        var j: u32 = 0;
354        while (j < num_buckets) : (j += 1) {
355            const count = @as(f64, @floatFromInt((if (f32_hist.get(j)) |v| v else 0)));
356            const expected = @as(f64, @floatFromInt(num_numbers)) / @as(f64, @floatFromInt(num_buckets));
357            const delta = count - expected;
358            const variance = (delta * delta) / expected;
359            f32_total_variance += variance;
360        }
361    }
362
363    {
364        var j: u64 = 0;
365        while (j < num_buckets) : (j += 1) {
366            const count = @as(f64, @floatFromInt((if (f64_hist.get(j)) |v| v else 0)));
367            const expected = @as(f64, @floatFromInt(num_numbers)) / @as(f64, @floatFromInt(num_buckets));
368            const delta = count - expected;
369            const variance = (delta * delta) / expected;
370            f64_total_variance += variance;
371        }
372    }
373
374    // Accept p-values >= 0.05.
375    // Critical value is calculated by opening a Python interpreter and running:
376    // scipy.stats.chi2.isf(0.05, num_buckets - 1)
377    const critical_value = 1073.6426506574246;
378    try expect(f32_total_variance < critical_value);
379    try expect(f64_total_variance < critical_value);
380}
381
382test "Random shuffle" {
383    var prng = DefaultPrng.init(0);
384    const random = prng.random();
385
386    var seq = [_]u8{ 0, 1, 2, 3, 4 };
387    var seen = [_]bool{false} ** 5;
388
389    var i: usize = 0;
390    while (i < 1000) : (i += 1) {
391        random.shuffle(u8, seq[0..]);
392        seen[seq[0]] = true;
393        try expect(sumArray(seq[0..]) == 10);
394    }
395
396    // we should see every entry at the head at least once
397    for (seen) |e| {
398        try expect(e == true);
399    }
400}
401
402fn sumArray(s: []const u8) u32 {
403    var r: u32 = 0;
404    for (s) |e|
405        r += e;
406    return r;
407}
408
409test "Random range" {
410    var prng = DefaultPrng.init(0);
411    const random = prng.random();
412
413    try testRange(random, -4, 3);
414    try testRange(random, -4, -1);
415    try testRange(random, 10, 14);
416    try testRange(random, -0x80, 0x7f);
417}
418
419fn testRange(r: Random, start: i8, end: i8) !void {
420    try testRangeBias(r, start, end, true);
421    try testRangeBias(r, start, end, false);
422}
423fn testRangeBias(r: Random, start: i8, end: i8, biased: bool) !void {
424    const count = @as(usize, @intCast(@as(i32, end) - @as(i32, start)));
425    var values_buffer = [_]bool{false} ** 0x100;
426    const values = values_buffer[0..count];
427    var i: usize = 0;
428    while (i < count) {
429        const value: i32 = if (biased) r.intRangeLessThanBiased(i8, start, end) else r.intRangeLessThan(i8, start, end);
430        const index = @as(usize, @intCast(value - start));
431        if (!values[index]) {
432            i += 1;
433            values[index] = true;
434        }
435    }
436}
437
438test "CSPRNG" {
439    var secret_seed: [DefaultCsprng.secret_seed_length]u8 = undefined;
440    std.crypto.random.bytes(&secret_seed);
441    var csprng = DefaultCsprng.init(secret_seed);
442    const random = csprng.random();
443    const a = random.int(u64);
444    const b = random.int(u64);
445    const c = random.int(u64);
446    try expect(a ^ b ^ c != 0);
447}
448
449test "Random weightedIndex" {
450    // Make sure weightedIndex works for various integers and floats
451    inline for (.{ u64, i4, f32, f64 }) |T| {
452        var prng = DefaultPrng.init(0);
453        const random = prng.random();
454
455        const proportions = [_]T{ 2, 1, 1, 2 };
456        var counts = [_]f64{ 0, 0, 0, 0 };
457
458        const n_trials: u64 = 10_000;
459        var i: usize = 0;
460        while (i < n_trials) : (i += 1) {
461            const pick = random.weightedIndex(T, &proportions);
462            counts[pick] += 1;
463        }
464
465        // We expect the first and last counts to be roughly 2x the second and third
466        const approxEqRel = std.math.approxEqRel;
467        // Define "roughly" to be within 10%
468        const tolerance = 0.1;
469        try std.testing.expect(approxEqRel(f64, counts[0], counts[1] * 2, tolerance));
470        try std.testing.expect(approxEqRel(f64, counts[1], counts[2], tolerance));
471        try std.testing.expect(approxEqRel(f64, counts[2] * 2, counts[3], tolerance));
472    }
473}