Skip to main content

dev_fixtures/
mock.rs

1//! Deterministic mock data generators.
2//!
3//! Generators are seeded by a `u64`. Same seed + same configuration
4//! produces byte-identical output across runs and machines. No
5//! external dependencies; deterministic via splitmix64.
6//!
7//! ## Generators
8//!
9//! - [`csv`] — CSV with a configurable schema and row count.
10//! - [`json_array`] — JSON array of records with a shape template.
11//! - [`bytes`] — raw byte streams: random, zeroed, patterned.
12
13/// A deterministic pseudo-random number generator.
14///
15/// Internally splitmix64. Cheap to construct, no allocation.
16///
17/// # Example
18///
19/// ```
20/// use dev_fixtures::mock::Rng;
21/// let mut a = Rng::seeded(42);
22/// let mut b = Rng::seeded(42);
23/// assert_eq!(a.next_u64(), b.next_u64());
24/// ```
25pub struct Rng {
26    state: u64,
27}
28
29impl Rng {
30    /// Build a new RNG from a seed.
31    pub fn seeded(seed: u64) -> Self {
32        Self { state: seed }
33    }
34
35    /// Step and return the next 64-bit value.
36    pub fn next_u64(&mut self) -> u64 {
37        self.state = self.state.wrapping_add(0x9E37_79B9_7F4A_7C15);
38        let mut z = self.state;
39        z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
40        z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
41        z ^= z >> 31;
42        z
43    }
44
45    /// Return a value uniformly in `[0, n)`. For small `n` this is
46    /// biased; the bias is bounded by `1 / 2^32` for `n` up to
47    /// `2^31`, which is fine for fixture purposes.
48    pub fn range(&mut self, n: u64) -> u64 {
49        if n == 0 {
50            return 0;
51        }
52        self.next_u64() % n
53    }
54}
55
56/// Module containing CSV generation.
57pub mod csv {
58    use super::Rng;
59
60    /// Generate a CSV string with `rows` rows, one per `Vec<String>`
61    /// produced by `row_factory`. The first line is the header.
62    ///
63    /// # Example
64    ///
65    /// ```
66    /// use dev_fixtures::mock::{csv::generate, Rng};
67    /// let csv = generate(
68    ///     &["id", "name"],
69    ///     3,
70    ///     42,
71    ///     |rng| vec![rng.range(1000).to_string(), format!("name_{}", rng.range(100))],
72    /// );
73    /// assert!(csv.starts_with("id,name\n"));
74    /// assert_eq!(csv.lines().count(), 4); // 1 header + 3 rows
75    /// ```
76    pub fn generate<F>(headers: &[&str], rows: usize, seed: u64, mut row_factory: F) -> String
77    where
78        F: FnMut(&mut Rng) -> Vec<String>,
79    {
80        let mut rng = Rng::seeded(seed);
81        let mut out = String::new();
82        out.push_str(&headers.join(","));
83        out.push('\n');
84        for _ in 0..rows {
85            let row = row_factory(&mut rng);
86            out.push_str(&row.join(","));
87            out.push('\n');
88        }
89        out
90    }
91}
92
93/// Module containing JSON array generation.
94pub mod json_array {
95    use super::Rng;
96
97    /// Generate a JSON array of `count` elements. Each element is
98    /// produced by `element_factory(rng)` and embedded verbatim in the
99    /// array (the factory MUST return valid JSON).
100    ///
101    /// # Example
102    ///
103    /// ```
104    /// use dev_fixtures::mock::{json_array::generate, Rng};
105    /// let json = generate(3, 7, |rng| {
106    ///     format!("{{\"id\": {}}}", rng.range(1000))
107    /// });
108    /// assert!(json.starts_with("["));
109    /// assert!(json.ends_with("]"));
110    /// ```
111    pub fn generate<F>(count: usize, seed: u64, mut element_factory: F) -> String
112    where
113        F: FnMut(&mut Rng) -> String,
114    {
115        let mut rng = Rng::seeded(seed);
116        let mut out = String::new();
117        out.push('[');
118        for i in 0..count {
119            if i > 0 {
120                out.push(',');
121            }
122            out.push_str(&element_factory(&mut rng));
123        }
124        out.push(']');
125        out
126    }
127}
128
129/// Module containing raw-byte generation.
130pub mod bytes {
131    use super::Rng;
132
133    /// `n` bytes of zeros.
134    pub fn zeros(n: usize) -> Vec<u8> {
135        vec![0u8; n]
136    }
137
138    /// `n` bytes of a repeating pattern.
139    ///
140    /// # Example
141    ///
142    /// ```
143    /// use dev_fixtures::mock::bytes::patterned;
144    /// let bytes = patterned(7, &[0xAB, 0xCD]);
145    /// assert_eq!(bytes, vec![0xAB, 0xCD, 0xAB, 0xCD, 0xAB, 0xCD, 0xAB]);
146    /// ```
147    pub fn patterned(n: usize, pattern: &[u8]) -> Vec<u8> {
148        if pattern.is_empty() {
149            return zeros(n);
150        }
151        let mut out = Vec::with_capacity(n);
152        while out.len() < n {
153            out.push(pattern[out.len() % pattern.len()]);
154        }
155        out
156    }
157
158    /// `n` deterministic random bytes from `seed`.
159    pub fn random(n: usize, seed: u64) -> Vec<u8> {
160        let mut rng = Rng::seeded(seed);
161        let mut out = Vec::with_capacity(n);
162        while out.len() < n {
163            let v = rng.next_u64();
164            for b in v.to_le_bytes() {
165                if out.len() < n {
166                    out.push(b);
167                }
168            }
169        }
170        out
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    #[test]
179    fn rng_is_deterministic() {
180        let mut a = Rng::seeded(42);
181        let mut b = Rng::seeded(42);
182        for _ in 0..16 {
183            assert_eq!(a.next_u64(), b.next_u64());
184        }
185    }
186
187    #[test]
188    fn rng_differs_with_seed() {
189        let mut a = Rng::seeded(1);
190        let mut b = Rng::seeded(2);
191        assert_ne!(a.next_u64(), b.next_u64());
192    }
193
194    #[test]
195    fn rng_range_bounds() {
196        let mut r = Rng::seeded(7);
197        for _ in 0..1000 {
198            let v = r.range(10);
199            assert!(v < 10);
200        }
201        assert_eq!(Rng::seeded(0).range(0), 0);
202    }
203
204    #[test]
205    fn csv_generate_is_deterministic() {
206        let g = |seed| {
207            csv::generate(&["a", "b"], 5, seed, |rng| {
208                vec![rng.range(100).to_string(), rng.range(100).to_string()]
209            })
210        };
211        assert_eq!(g(42), g(42));
212        assert_ne!(g(42), g(43));
213    }
214
215    #[test]
216    fn csv_has_header_and_row_count() {
217        let csv = csv::generate(&["x", "y"], 3, 0, |rng| {
218            vec![rng.range(10).to_string(), rng.range(10).to_string()]
219        });
220        assert!(csv.starts_with("x,y\n"));
221        assert_eq!(csv.lines().count(), 4);
222    }
223
224    #[test]
225    fn json_array_round_trip_shape() {
226        let json = json_array::generate(3, 0, |rng| format!("{{\"id\":{}}}", rng.range(100)));
227        assert!(json.starts_with("["));
228        assert!(json.ends_with("]"));
229        // 3 elements -> 2 commas at top level.
230        assert_eq!(json.matches(',').count(), 2);
231    }
232
233    #[test]
234    fn bytes_zeros_and_patterned() {
235        assert_eq!(bytes::zeros(4), vec![0, 0, 0, 0]);
236        assert_eq!(bytes::patterned(5, &[1, 2]), vec![1, 2, 1, 2, 1]);
237        assert_eq!(bytes::patterned(3, &[]), vec![0, 0, 0]);
238    }
239
240    #[test]
241    fn bytes_random_is_deterministic() {
242        assert_eq!(bytes::random(64, 7), bytes::random(64, 7));
243        assert_ne!(bytes::random(64, 7), bytes::random(64, 8));
244    }
245}