Skip to main content

keelson_factory/
faker.rs

1use std::sync::atomic::{AtomicU64, Ordering};
2use std::time::{SystemTime, UNIX_EPOCH};
3
4/// The seedable random-value source every template's random defaults draw
5/// from.
6///
7/// A SplitMix64 generator (Steele, Lea & Flood's public-domain algorithm),
8/// implemented here rather than depended upon — see the crate docs for the
9/// `fake`-vs-`rand`-vs-in-crate evaluation. The output sequence for a given
10/// seed is **pinned by test in this crate**: two [`Faker::seeded`] instances
11/// with the same seed produce identical values, on every platform, forever —
12/// that is the determinism switch's whole contract.
13///
14/// Not cryptographic, and not meant to be: this generates test data.
15#[derive(Debug, Clone)]
16pub struct Faker {
17    state: u64,
18}
19
20impl Faker {
21    /// A faker whose entire output is determined by `seed` — the
22    /// reproducibility switch. Same seed, same values, always.
23    pub fn seeded(seed: u64) -> Self {
24        Faker { state: seed }
25    }
26
27    /// A faker seeded from wall-clock entropy plus a process-global counter —
28    /// what `create`/`create_many` use when the caller does not care about
29    /// reproducing the run.
30    pub fn from_entropy() -> Self {
31        static COUNTER: AtomicU64 = AtomicU64::new(0);
32        let nanos = SystemTime::now()
33            .duration_since(UNIX_EPOCH)
34            .unwrap_or_default()
35            .as_nanos() as u64;
36        let salt = COUNTER
37            .fetch_add(1, Ordering::Relaxed)
38            .wrapping_mul(0x9E37_79B9_7F4A_7C15);
39        Faker::seeded(nanos ^ salt)
40    }
41
42    /// The next raw 64-bit value.
43    pub fn next_u64(&mut self) -> u64 {
44        // SplitMix64.
45        self.state = self.state.wrapping_add(0x9E37_79B9_7F4A_7C15);
46        let mut z = self.state;
47        z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
48        z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
49        z ^ (z >> 31)
50    }
51
52    /// A value in `0..n`. The multiply-shift reduction carries a negligible
53    /// modulo bias — acceptable for test data, and deterministic, which is
54    /// what matters here.
55    ///
56    /// # Panics
57    ///
58    /// If `n` is zero — an empty range has no value to produce.
59    pub fn below(&mut self, n: u64) -> u64 {
60        assert!(n > 0, "Faker::below(0): empty range");
61        ((u128::from(self.next_u64()) * u128::from(n)) >> 64) as u64
62    }
63
64    /// A value in `lo..=hi`.
65    ///
66    /// # Panics
67    ///
68    /// If `lo > hi`.
69    pub fn i64_in(&mut self, lo: i64, hi: i64) -> i64 {
70        assert!(lo <= hi, "Faker::i64_in: empty range {lo}..={hi}");
71        let span = hi.wrapping_sub(lo) as u64;
72        if span == u64::MAX {
73            return self.next_u64() as i64;
74        }
75        lo.wrapping_add(self.below(span + 1) as i64)
76    }
77
78    /// A value in `lo..=hi`, `i32`-typed for dialects whose `integer` is
79    /// 32-bit.
80    ///
81    /// # Panics
82    ///
83    /// If `lo > hi`.
84    pub fn i32_in(&mut self, lo: i32, hi: i32) -> i32 {
85        self.i64_in(i64::from(lo), i64::from(hi)) as i32
86    }
87
88    /// A random lowercase-alphanumeric string of `len` characters —
89    /// `"user-{alnum}"`-style default values are built from this.
90    pub fn alnum(&mut self, len: usize) -> String {
91        const CHARS: &[u8] = b"abcdefghijklmnopqrstuvwxyz0123456789";
92        (0..len)
93            .map(|_| CHARS[self.below(CHARS.len() as u64) as usize] as char)
94            .collect()
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    /// The reproducibility contract, pinned to exact values: this sequence may
103    /// never change, on any platform, in any release.
104    #[test]
105    fn the_seeded_output_sequence_is_pinned() {
106        let mut f = Faker::seeded(42);
107        assert_eq!(f.next_u64(), 13_679_457_532_755_275_413);
108        assert_eq!(f.next_u64(), 2_949_826_092_126_892_291);
109        assert_eq!(f.next_u64(), 5_139_283_748_462_763_858);
110        assert_eq!(Faker::seeded(7).alnum(8), "oa6uqiql");
111    }
112
113    #[test]
114    fn same_seed_same_values_different_seed_different_values() {
115        let mut a = Faker::seeded(1);
116        let mut b = Faker::seeded(1);
117        let mut c = Faker::seeded(2);
118        for _ in 0..16 {
119            assert_eq!(a.next_u64(), b.next_u64());
120        }
121        assert_ne!(Faker::seeded(1).next_u64(), c.next_u64());
122    }
123
124    #[test]
125    fn ranged_values_stay_in_range() {
126        let mut f = Faker::seeded(3);
127        for _ in 0..256 {
128            let v = f.i64_in(18, 90);
129            assert!((18..=90).contains(&v));
130            let v = f.i32_in(-5, 5);
131            assert!((-5..=5).contains(&v));
132            assert!(f.below(3) < 3);
133        }
134        // Degenerate and full ranges hold too.
135        assert_eq!(f.i64_in(9, 9), 9);
136        let _ = f.i64_in(i64::MIN, i64::MAX);
137    }
138
139    #[test]
140    fn alnum_is_lowercase_alphanumeric_of_the_asked_length() {
141        let mut f = Faker::from_entropy();
142        let s = f.alnum(32);
143        assert_eq!(s.len(), 32);
144        assert!(
145            s.chars()
146                .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit())
147        );
148    }
149
150    #[test]
151    fn entropy_fakers_differ_even_when_created_back_to_back() {
152        // The counter salt makes same-nanosecond construction distinct.
153        let mut a = Faker::from_entropy();
154        let mut b = Faker::from_entropy();
155        assert_ne!(a.next_u64(), b.next_u64());
156    }
157}