1pub struct Rng {
26 state: u64,
27}
28
29impl Rng {
30 pub fn seeded(seed: u64) -> Self {
32 Self { state: seed }
33 }
34
35 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 pub fn range(&mut self, n: u64) -> u64 {
49 if n == 0 {
50 return 0;
51 }
52 self.next_u64() % n
53 }
54}
55
56pub mod csv {
58 use super::Rng;
59
60 pub fn generate<F>(headers: &[&str], rows: usize, seed: u64, mut row_factory: F) -> String
96 where
97 F: FnMut(&mut Rng) -> Vec<String>,
98 {
99 let mut rng = Rng::seeded(seed);
100 let mut out = String::new();
101 let header_row: Vec<String> = headers.iter().map(|h| escape_field(h)).collect();
102 out.push_str(&header_row.join(","));
103 out.push('\n');
104 for _ in 0..rows {
105 let row = row_factory(&mut rng);
106 let escaped: Vec<String> = row.iter().map(|f| escape_field(f)).collect();
107 out.push_str(&escaped.join(","));
108 out.push('\n');
109 }
110 out
111 }
112
113 pub fn escape_field(value: &str) -> String {
119 if value.contains(',')
120 || value.contains('"')
121 || value.contains('\n')
122 || value.contains('\r')
123 {
124 let escaped = value.replace('"', "\"\"");
125 format!("\"{}\"", escaped)
126 } else {
127 value.to_string()
128 }
129 }
130}
131
132pub mod json_array {
134 use super::Rng;
135
136 pub fn generate<F>(count: usize, seed: u64, mut element_factory: F) -> String
151 where
152 F: FnMut(&mut Rng) -> String,
153 {
154 let mut rng = Rng::seeded(seed);
155 let mut out = String::new();
156 out.push('[');
157 for i in 0..count {
158 if i > 0 {
159 out.push(',');
160 }
161 out.push_str(&element_factory(&mut rng));
162 }
163 out.push(']');
164 out
165 }
166}
167
168pub mod bytes {
170 use super::Rng;
171
172 pub fn zeros(n: usize) -> Vec<u8> {
174 vec![0u8; n]
175 }
176
177 pub fn patterned(n: usize, pattern: &[u8]) -> Vec<u8> {
187 if pattern.is_empty() {
188 return zeros(n);
189 }
190 let mut out = Vec::with_capacity(n);
191 while out.len() < n {
192 out.push(pattern[out.len() % pattern.len()]);
193 }
194 out
195 }
196
197 pub fn random(n: usize, seed: u64) -> Vec<u8> {
199 let mut rng = Rng::seeded(seed);
200 let mut out = Vec::with_capacity(n);
201 while out.len() < n {
202 let v = rng.next_u64();
203 for b in v.to_le_bytes() {
204 if out.len() < n {
205 out.push(b);
206 }
207 }
208 }
209 out
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[test]
218 fn rng_is_deterministic() {
219 let mut a = Rng::seeded(42);
220 let mut b = Rng::seeded(42);
221 for _ in 0..16 {
222 assert_eq!(a.next_u64(), b.next_u64());
223 }
224 }
225
226 #[test]
227 fn rng_differs_with_seed() {
228 let mut a = Rng::seeded(1);
229 let mut b = Rng::seeded(2);
230 assert_ne!(a.next_u64(), b.next_u64());
231 }
232
233 #[test]
234 fn rng_range_bounds() {
235 let mut r = Rng::seeded(7);
236 for _ in 0..1000 {
237 let v = r.range(10);
238 assert!(v < 10);
239 }
240 assert_eq!(Rng::seeded(0).range(0), 0);
241 }
242
243 #[test]
244 fn csv_generate_is_deterministic() {
245 let g = |seed| {
246 csv::generate(&["a", "b"], 5, seed, |rng| {
247 vec![rng.range(100).to_string(), rng.range(100).to_string()]
248 })
249 };
250 assert_eq!(g(42), g(42));
251 assert_ne!(g(42), g(43));
252 }
253
254 #[test]
255 fn csv_has_header_and_row_count() {
256 let csv = csv::generate(&["x", "y"], 3, 0, |rng| {
257 vec![rng.range(10).to_string(), rng.range(10).to_string()]
258 });
259 assert!(csv.starts_with("x,y\n"));
260 assert_eq!(csv.lines().count(), 4);
261 }
262
263 #[test]
264 fn csv_escapes_commas_quotes_and_newlines() {
265 let csv = csv::generate(&["a", "b"], 1, 0, |_rng| {
266 vec![
267 "value, with comma".into(),
268 "value with \"quote\" and\nnewline".into(),
269 ]
270 });
271 assert!(csv.contains("\"value, with comma\""));
272 assert!(csv.contains("\"value with \"\"quote\"\" and\nnewline\""));
273 }
274
275 #[test]
276 fn csv_escapes_in_headers_too() {
277 let csv = csv::generate(&["plain", "with, comma"], 0, 0, |_rng| vec![]);
278 assert_eq!(csv.trim(), "plain,\"with, comma\"");
279 }
280
281 #[test]
282 fn csv_unescaped_when_no_special_chars() {
283 let csv = csv::generate(&["a", "b"], 1, 0, |_rng| {
284 vec!["plain".into(), "also plain".into()]
285 });
286 assert!(csv.contains("plain,also plain"));
287 assert!(!csv.contains("\""));
289 }
290
291 #[test]
292 fn json_array_round_trip_shape() {
293 let json = json_array::generate(3, 0, |rng| format!("{{\"id\":{}}}", rng.range(100)));
294 assert!(json.starts_with("["));
295 assert!(json.ends_with("]"));
296 assert_eq!(json.matches(',').count(), 2);
298 }
299
300 #[test]
301 fn bytes_zeros_and_patterned() {
302 assert_eq!(bytes::zeros(4), vec![0, 0, 0, 0]);
303 assert_eq!(bytes::patterned(5, &[1, 2]), vec![1, 2, 1, 2, 1]);
304 assert_eq!(bytes::patterned(3, &[]), vec![0, 0, 0]);
305 }
306
307 #[test]
308 fn bytes_random_is_deterministic() {
309 assert_eq!(bytes::random(64, 7), bytes::random(64, 7));
310 assert_ne!(bytes::random(64, 7), bytes::random(64, 8));
311 }
312}