zsh/ported/modules/random.rs
1//! Random number module - port of Modules/random.c
2//!
3//! Provides access to kernel random sources for cryptographically secure
4//! random number generation.
5
6use std::fs::metadata;
7use std::io;
8use std::io::Read;
9use std::os::fd::IntoRawFd;
10use std::os::unix::fs::FileTypeExt;
11use std::sync::atomic::Ordering;
12
13use crate::ported::utils::zwarn;
14use crate::random_real::random_real;
15use crate::zsh_h::{features, module};
16use std::sync::{Mutex, OnceLock};
17
18/// Fill a buffer with cryptographically random bytes.
19/// Port of `getrandom_buffer(void *buf, size_t len)` from Src/Modules/random.c:62 — the
20/// C source dispatches to `getentropy(3)` on BSD, `getrandom(2)` on
21/// Linux, or `/dev/urandom` as a portable fallback. We map onto
22/// `arc4random_buf(3)` for macOS (BSD-derived), `getrandom(2)` on
23/// Linux, and `/dev/urandom` everywhere else.
24#[cfg(target_os = "macos")]
25/// WARNING: param names don't match C — Rust=(buf) vs C=(buf, len)
26pub fn getrandom_buffer(buf: &mut [u8]) -> io::Result<()> {
27 // c:62
28 unsafe {
29 libc::arc4random_buf(buf.as_mut_ptr() as *mut libc::c_void, buf.len());
30 }
31 Ok(())
32}
33
34// Per-evaluator random-buffer state — bucket-1 dissolution per
35// C source has TWO file-statics at Src/Modules/random.c:50-51:
36//
37// static uint32_t rand_buff[8];
38// static int buf_cnt = -1;
39//
40// Mirrored as two `thread_local!`s — each worker thread owns its
41// own buffer (file-static semantics preserve under threading per
42// PORT_PLAN bucket-1 rule).
43
44thread_local! {
45 /// Port of file-static `static uint32_t rand_buff[8];` at
46 /// `Src/Modules/random.c:50`. Pre-loaded buffer of u32s
47 /// drained one entry at a time by `get_srandom()`.
48 static RAND_BUFF: std::cell::RefCell<[u32; RAND_BUFF_SIZE]> = const {
49 std::cell::RefCell::new([0; RAND_BUFF_SIZE])
50 };
51 /// Port of file-static `static int buf_cnt = -1;` at
52 /// `Src/Modules/random.c:51`. Index of the next unread entry
53 /// in `RAND_BUFF`; zero triggers a refill via
54 /// `getrandom_buffer`.
55 static BUF_CNT: std::cell::Cell<usize> = const {
56 std::cell::Cell::new(0)
57 };
58}
59
60/// Port of `getrandom_buffer(void *buf, size_t len)` from `Src/Modules/random.c:62`,
61/// `#elif defined(HAVE_GETRANDOM)` branch (c:75-76):
62/// `ret = getrandom(bufptr, (len - val), 0);`
63/// with the C EINTR-retry loop at c:80-85.
64#[cfg(target_os = "linux")]
65/// WARNING: param names don't match C — Rust=(buf) vs C=(buf, len)
66pub fn getrandom_buffer(buf: &mut [u8]) -> io::Result<()> {
67 // c:62
68 let mut filled = 0;
69
70 while filled < buf.len() {
71 let ret = unsafe {
72 libc::getrandom(
73 buf[filled..].as_mut_ptr() as *mut libc::c_void,
74 buf.len() - filled,
75 0,
76 )
77 };
78
79 if ret < 0 {
80 let err = io::Error::last_os_error();
81 if err.kind() == io::ErrorKind::Interrupted {
82 continue;
83 }
84 return Err(err);
85 }
86
87 filled += ret as usize;
88 }
89
90 Ok(())
91}
92
93/// Port of `getrandom_buffer(void *buf, size_t len)` from `Src/Modules/random.c:62`.
94#[cfg(not(any(target_os = "macos", target_os = "linux")))]
95pub fn getrandom_buffer(m: &mut [u8]) -> io::Result<()> {
96 // c:62
97
98 let mut file = File::open("/dev/urandom")?;
99 file.read_exact(m)?;
100 Ok(())
101}
102
103/// Port of `void get_bound_random_buffer(uint32_t *buffer, size_t count,
104/// uint32_t max)` from `Src/Modules/random.c:104`. Lemire (2016)
105/// fast-random-shuffling: multiply, threshold = -max % max, rejection-
106/// sample only the rare `leftover < max` slot. `count` is folded into
107/// `buffer.len()` per Rust idiom.
108/// WARNING: param names don't match C — Rust=(buffer, max) vs C=(buffer, count, max)
109pub fn get_bound_random_buffer(buffer: &mut [u32], max: u32) {
110 // c:104
111 // c:112 getrandom_buffer(buffer, count*sizeof(uint32_t)) — fill u32s.
112 let mut bytes: Vec<u8> = vec![0u8; buffer.len() * 4];
113 let _ = getrandom_buffer(&mut bytes);
114 for (i, chunk) in bytes.chunks_exact(4).enumerate() {
115 buffer[i] = u32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
116 }
117 if max == u32::MAX {
118 // c:113 UINT32_MAX
119 return; // c:114
120 }
121 for i in 0..buffer.len() {
122 // c:116
123 let mut multi_result: u64 = (buffer[i] as u64) * (max as u64); // c:117
124 let mut leftover: u32 = multi_result as u32; // c:118
125 if leftover < max {
126 // c:124
127 let threshold: u32 = (max.wrapping_neg()) % max; // c:125 -max % max
128 while leftover < threshold {
129 // c:126
130 let j: u32 = get_srandom(); // c:127 get_srandom(NULL)
131 multi_result = (j as u64) * (max as u64); // c:128
132 leftover = multi_result as u32; // c:129
133 }
134 }
135 buffer[i] = (multi_result >> 32) as u32; // c:132
136 }
137}
138
139/// Port of `get_srandom(UNUSED(Param pm))` from `Src/Modules/random.c:58`. The
140/// `getfn` slot the C source wires for the `$SRANDOM` special
141/// parameter. Refills `rand_buff` via `getrandom_buffer()` when
142/// drained, then returns the next pre-loaded u32.
143/// WARNING: param names don't match C — Rust=() vs C=(pm)
144pub fn get_srandom() -> u32 {
145 // c:58
146 let cnt = BUF_CNT.with(|c| c.get());
147 if cnt == 0 {
148 // c:145
149 let mut bytes = [0u8; RAND_BUFF_SIZE * 4]; // c:143
150 if getrandom_buffer(&mut bytes).is_ok() {
151 // c:143
152 RAND_BUFF.with(|r| {
153 let mut buf = r.borrow_mut();
154 for (i, chunk) in bytes.chunks_exact(4).enumerate() {
155 // c:143
156 buf[i] = u32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
157 }
158 });
159 }
160 BUF_CNT.with(|c| c.set(RAND_BUFF_SIZE)); // c:145
161 }
162 let new_cnt = BUF_CNT.with(|c| c.get()) - 1; // c:145
163 BUF_CNT.with(|c| c.set(new_cnt));
164 RAND_BUFF.with(|r| r.borrow()[new_cnt]) // c:145
165}
166
167/// `math_zrand_int(upper, lower, inclusive)` math function.
168/// Port of `math_zrand_int(UNUSED(char *name), int argc, mnumber *argv, UNUSED(int id))` from Src/Modules/random.c:161 — the
169/// C source's math-function entry point exposed to `${(( ... ))}`.
170/// All three arguments are optional; behaviour matches the C
171/// source's bound-checks (`lower < 0`, `upper < lower`, etc.).
172/// WARNING: param names don't match C — Rust=(upper, lower, inclusive) vs C=(name, argc, argv, id)
173pub fn math_zrand_int(
174 upper: Option<i64>,
175 lower: Option<i64>,
176 inclusive: bool,
177) -> Result<i64, String> {
178 // c:161
179 let lower = lower.unwrap_or(0);
180 let upper = upper.unwrap_or(u32::MAX as i64);
181
182 // c:179-185 — bound checks are a WARN-ONLY if/else-if chain:
183 //
184 // if (lower < 0 || lower >= UINT32_MAX) {
185 // zwarn("Lower bound (%z) out of range: 0-4294967295",lower);
186 // } else if (upper < lower) {
187 // zwarn("Upper bound (%z) must be greater than Lower Bound (%z)",upper,lower);
188 // } else if (upper < 0 || upper >= UINT32_MAX) {
189 // zwarn("Upper bound (%z) out of range: 0-4294967295",upper);
190 // }
191 //
192 // if ( diff == 0 ) {
193 // ret.u.l=upper; /* still not convinced this shouldn't be an error. */
194 // } else {
195 // get_bound_random_buffer(&i,1,(uint32_t) diff);
196 // ret.u.l=i+lower;
197 // }
198 //
199 // No return after any warning — C falls through to the diff
200 // computation and random draw, with `(uint32_t) diff` truncating
201 // wrapped values exactly as written (the c:188 comment shows the
202 // author knew and shipped it anyway). At most ONE warning fires
203 // (else-if chain). The prior Rust port aborted with Err on each
204 // check: `zrand_int(5, 10)` (upper<lower) errored out where zsh
205 // warns once and still yields a number from the wrapped range.
206 let incl: i64 = if inclusive { 1 } else { 0 }; // c:173
207 let diff: i64 = upper - lower + incl; // c:176
208
209 if lower < 0 || lower >= u32::MAX as i64 {
210 // c:179
211 crate::ported::utils::zwarn(&format!(
212 "Lower bound ({}) out of range: 0-4294967295",
213 lower
214 )); // c:180
215 } else if upper < lower {
216 // c:181
217 crate::ported::utils::zwarn(&format!(
218 "Upper bound ({}) must be greater than Lower Bound ({})",
219 upper, lower
220 )); // c:182
221 } else if upper < 0 || upper >= u32::MAX as i64 {
222 // c:183
223 crate::ported::utils::zwarn(&format!(
224 "Upper bound ({}) out of range: 0-4294967295",
225 upper
226 )); // c:184
227 }
228
229 if diff == 0 {
230 // c:187
231 /* still not convinced this shouldn't be an error. */
232 return Ok(upper); // c:188
233 }
234 // c:190 — `get_bound_random_buffer(&i,1,(uint32_t) diff);` — the
235 // cast truncates exactly like C for warned out-of-range inputs.
236 let r = bounded(diff as u32);
237 Ok(r as i64 + lower) // c:191
238}
239
240/// `math_zrand_float()` math function.
241/// Port of `math_zrand_float(UNUSED(char *name), UNUSED(int argc), UNUSED(mnumber *argv), UNUSED(int id))` from Src/Modules/random.c:204 —
242/// the C source's math-function entry point that returns a
243/// uniform double in `[0, 1)`.
244///
245/// C body (verbatim):
246/// r = random_real();
247/// if (r < 0) {
248/// zwarnnam(name, "Failed to get sufficient random data.");
249/// }
250/// ret.type = MN_FLOAT;
251/// ret.u.d = r;
252/// return ret;
253///
254/// `random_real` returns -1 (via the thread_local sentinel set in
255/// random_real.rs) when the entropy syscall fails. C warns then still
256/// returns -1 as the math-function result; prior Rust port skipped
257/// the warn so callers had no signal that `[[ $((zrand_float())) ]]`
258/// produced a sentinel instead of a real probability.
259/// WARNING: param names don't match C — Rust=() vs C=(name, argc, argv, id)
260pub fn math_zrand_float() -> f64 {
261 // c:204
262 let r = random_real(); // c:210
263 if r < 0.0 {
264 // c:211
265 crate::ported::utils::zwarnnam(
266 "zrand_float", // c:212 — C uses `name`, the math-function name.
267 "Failed to get sufficient random data.",
268 );
269 }
270 r // c:215
271}
272
273/// Port of `setup_(UNUSED(Module m))` from `Src/Modules/random.c:243`.
274#[allow(unused_variables)]
275pub fn setup_(m: *const module) -> i32 {
276 // c:243
277 // c:243-261 — USE_URANDOM block: stat /dev/urandom; verify
278 // S_ISCHR. We probe via std::fs::metadata + file_type().
279 match metadata("/dev/urandom") {
280 // c:251
281 Ok(md) => {
282 if !md.file_type().is_char_device() {
283 // c:256
284 // c:257 — `zwarn("Error getting kernel random pool: %m");`
285 zwarn("Error getting kernel random pool: not a char device");
286 return 1;
287 }
288 }
289 Err(e) => {
290 zwarn(&format!("Error getting kernel random pool: {}", e));
291 return 1;
292 }
293 }
294 0 // c:275
295}
296
297/// Port of `features_(UNUSED(Module m), UNUSED(char ***features))` from `Src/Modules/random.c:267`.
298pub fn features_(m: *const module, features: &mut Vec<String>) -> i32 {
299 // c:267
300 *features = featuresarray(m, module_features());
301 0
302}
303
304/// Port of `enables_(UNUSED(Module m), UNUSED(int **enables))` from `Src/Modules/random.c:275`.
305pub fn enables_(m: *const module, enables: &mut Option<Vec<i32>>) -> i32 {
306 // c:275
307 handlefeatures(m, module_features(), enables)
308}
309
310/// Port of `boot_(UNUSED(Module m))` from `Src/Modules/random.c:282-308`.
311#[allow(unused_variables)]
312pub fn boot_(m: *const module) -> i32 {
313 // c:282
314 // c:296 — `if ((tmpfd = open("/dev/urandom", O_RDONLY)) < 0)`
315 let f = match std::fs::OpenOptions::new().read(true).open("/dev/urandom") {
316 Ok(f) => f,
317 Err(e) => {
318 // c:297 — `zwarn("Could not access kernel random pool: %e.", errno);`
319 zwarn(&format!("Could not access kernel random pool: {}", e));
320 return 1; // c:298
321 }
322 };
323 // c:300 — `randfd = movefd(tmpfd);` — relocate to a high fd so the
324 // urandom handle doesn't clash with shell-side fd 3-9 use. Prior
325 // port skipped movefd, so randfd typically landed at fd 3 or 4 —
326 // colliding with redirect-save slots from `exec 3>file` style
327 // user commands. The shell's redirect machinery would then
328 // dup-over the urandom fd, leaving random_real reading from
329 // whatever the user redirected.
330 let tmpfd = f.into_raw_fd(); // c:293 `int tmpfd = -1`
331 let fd = crate::ported::utils::movefd(tmpfd);
332 // c:301 — `addmodulefd(randfd, FDT_MODULE);` — register the urandom
333 // fd in the global fdtable as FDT_MODULE so closem and exec
334 // redirect-save recognize it as module-owned. Same fix as the
335 // db_gdbm addmodulefd port (c0dad2eb83) but with FDT_MODULE
336 // (matches C: random.c uses FDT_MODULE, db_gdbm.c uses FDT_INTERNAL).
337 crate::ported::utils::addmodulefd(fd, crate::ported::zsh_h::FDT_MODULE);
338 // c:302-305 — `if (randfd < 0) { zwarn(...); return 1; }` — movefd
339 // failure (out of fd slots). Rust movefd returns -1 on failure
340 // matching C.
341 if fd < 0 {
342 zwarn("Could not access kernel random pool.");
343 return 1;
344 }
345 RANDFD.store(fd, Ordering::SeqCst);
346 0 // c:307
347}
348
349/// Re-export of the canonical `random_real()` from
350/// `Src/Modules/random_real.c:147` — Campbell's algorithm for
351/// distribution-correct uniform doubles in `[0, 1)`. The simpler
352/// "53-bit mantissa" approximation that previously lived here was
353/// removed because it biases ~3% of the interval; the C author
354/// (Taylor R. Campbell) explicitly warns against it in the random_real.c
355/// header comment.
356
357/// Generate a random integer in `[min, max]`.
358// =====================================================================
359// static struct features module_features c:255 (random.c)
360// =====================================================================
361
362/// Port of `cleanup_(UNUSED(Module m))` from `Src/Modules/random.c:312`.
363pub fn cleanup_(m: *const module) -> i32 {
364 // c:312
365 setfeatureenables(m, module_features(), None)
366}
367
368/// Port of `finish_(UNUSED(Module m))` from `Src/Modules/random.c:319-326`.
369#[allow(unused_variables)]
370pub fn finish_(m: *const module) -> i32 {
371 // c:319
372 // c:322-323 — `if (randfd >= 0) zclose(randfd);`
373 let fd = RANDFD.swap(-1, Ordering::SeqCst);
374 if fd >= 0 {
375 // Clear the fdtable entry BEFORE close so the post-close
376 // kernel-reuse of this fd number doesn't inherit the
377 // FDT_MODULE marker we set in boot_ (port c0dad2eb83 +
378 // c3a5125d9f pattern). C's zclose internally calls
379 // fdtable_set(fd, FDT_UNUSED) so the canonical path is safe;
380 // the raw libc::close below skips that step.
381 crate::ported::utils::fdtable_set(fd, crate::ported::zsh_h::FDT_UNUSED);
382 unsafe { libc::close(fd) }; // c:323 zclose
383 }
384 0 // c:325
385}
386
387/// Buffer size for pre-loading random integers
388// buffer to pre-load integers for SRANDOM to lessen the context switches // c:49
389const RAND_BUFF_SIZE: usize = 8;
390
391// `mftab` — port of `static struct mathfunc mftab[]` (random.c).
392
393// `patab` — port of `static struct paramdef patab[]` (random.c).
394
395// `module_features` — port of `static struct features module_features`
396// from random.c:255.
397
398/// `RANDFD` — port of the file-static `int randfd` in
399/// `Src/Modules/random.c:243`. Holds the open fd for `/dev/urandom`.
400/// Set in `boot_()`, closed in `finish_()`.
401pub static RANDFD: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(-1); // c:34
402
403// WARNING: NOT IN RANDOM.C — Rust-only convenience helpers.
404// C inlines the equivalent logic inside `get_bound_random_buffer()`
405// (Src/Modules/random.c:104) and the math-fn wrappers; the Rust
406// port factors them out because callers in this same file (the
407// Fisher-Yates shuffle in `bin_zshuffle`, the math ported
408// `math_zrand_int`/`math_zrand_real`, the `get_bound_random_buffer`
409// loop) would each inline identical 4-line getrandom-and-decode
410// blocks. Names are Rust-original; renaming to match a C name
411// would mislead.
412
413/// WARNING: NOT IN RANDOM.C — one-shot u32 helper; C inlines the 4-byte getrandom_buffer read
414/// (equivalent C logic at Src/Modules/random.c:79).
415/// One-shot u32 read. C inlines the equivalent at random.c:79
416/// (4-byte getrandom_buffer + decode).
417pub fn random_u32() -> u32 {
418 let mut buf = [0u8; 4];
419 let _ = getrandom_buffer(&mut buf);
420 u32::from_ne_bytes(buf)
421}
422
423/// WARNING: NOT IN RANDOM.C — two-word helper for random_real(); C inlines the 8-byte read
424/// (equivalent C logic at Src/Modules/random_real.c:158).
425/// Two-word read used by `random_real()` at random_real.c:158-175
426/// for uniform-real sampling. C reads 8 bytes inline.
427pub fn random_u64() -> u64 {
428 let mut buf = [0u8; 8];
429 let _ = getrandom_buffer(&mut buf);
430 u64::from_ne_bytes(buf)
431}
432
433/// Get a random integer in `[0, max)` using Lemire's unbiased
434/// rejection. Port of the inline bound-rejection logic inside
435/// `get_bound_random_buffer()` (random.c:104) — extracted as a
436/// per-element scalar helper since multiple callers need the
437/// single-value form.
438pub fn bounded(max: u32) -> u32 {
439 if max == 0 {
440 return 0;
441 }
442 if max == u32::MAX {
443 return random_u32();
444 }
445 let mut x = random_u32();
446 let mut m = (x as u64) * (max as u64);
447 let mut l = m as u32;
448 if l < max {
449 let threshold = (-(max as i64) as u64 % max as u64) as u32;
450 while l < threshold {
451 x = random_u32();
452 m = (x as u64) * (max as u64);
453 l = m as u32;
454 }
455 }
456 (m >> 32) as u32
457}
458
459static MODULE_FEATURES: OnceLock<Mutex<features>> = OnceLock::new();
460
461// Local stubs for the per-module entry points. C uses generic
462// `featuresarray`/`handlefeatures`/`setfeatureenables` (module.c:
463// 3275/3370/3445) but those take `Builtin` + `Features` pointer
464// fields the Rust port doesn't carry. The hardcoded descriptor
465// list mirrors the C bintab/conddefs/mathfuncs/paramdefs.
466// WARNING: NOT IN RANDOM.C — Rust-only module-framework shim.
467// C uses generic featuresarray/handlefeatures/setfeatureenables from
468// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
469// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
470fn featuresarray(_m: *const module, _f: &Mutex<features>) -> Vec<String> {
471 vec![
472 "f:zrand_float".to_string(),
473 "f:zrand_int".to_string(),
474 "p:SRANDOM".to_string(),
475 ]
476}
477
478// WARNING: NOT IN RANDOM.C — Rust-only module-framework shim.
479// C uses generic featuresarray/handlefeatures/setfeatureenables from
480// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
481// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
482fn handlefeatures(_m: *const module, _f: &Mutex<features>, enables: &mut Option<Vec<i32>>) -> i32 {
483 if enables.is_none() {
484 *enables = Some(vec![1; 3]);
485 }
486 0
487}
488
489// WARNING: NOT IN RANDOM.C — Rust-only module-framework shim.
490// C uses generic featuresarray/handlefeatures/setfeatureenables from
491// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
492// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
493fn setfeatureenables(_m: *const module, _f: &Mutex<features>, _e: Option<&[i32]>) -> i32 {
494 0
495}
496
497// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
498// ─── RUST-ONLY ACCESSORS ───
499//
500// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
501// RwLock<T>>` globals declared above. C zsh uses direct global
502// access; Rust needs these wrappers because `OnceLock::get_or_init`
503// is the only way to lazily construct shared state. These ported sit
504// here so the body of this file reads in C source order without
505// the accessor wrappers interleaved between real port ported.
506// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
507
508// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
509// ─── RUST-ONLY ACCESSORS ───
510//
511// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
512// RwLock<T>>` globals declared above. C zsh uses direct global
513// access; Rust needs these wrappers because `OnceLock::get_or_init`
514// is the only way to lazily construct shared state. These ported sit
515// here so the body of this file reads in C source order without
516// the accessor wrappers interleaved between real port ported.
517// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
518
519// WARNING: NOT IN RANDOM.C — Rust-only module-framework shim.
520// C uses generic featuresarray/handlefeatures/setfeatureenables from
521// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
522// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
523fn module_features() -> &'static Mutex<features> {
524 MODULE_FEATURES.get_or_init(|| {
525 Mutex::new(features {
526 bn_list: None,
527 bn_size: 0,
528 cd_list: None,
529 cd_size: 0,
530 mf_list: None,
531 mf_size: 2,
532 pd_list: None,
533 pd_size: 1,
534 n_abstract: 0,
535 })
536 })
537}
538
539#[cfg(test)]
540mod tests {
541 use super::*;
542
543 #[test]
544 fn test_random_state() {
545 let _g = crate::test_util::global_state_lock();
546
547 let r1 = get_srandom();
548 let r2 = get_srandom();
549 let r3 = get_srandom();
550 assert!(r1 != r2 || r2 != r3);
551 }
552
553 #[test]
554 fn test_get_random_u32() {
555 let _g = crate::test_util::global_state_lock();
556 let r1 = random_u32();
557 let r2 = random_u32();
558 let r3 = random_u32();
559 assert!(r1 != r2 || r2 != r3);
560 }
561
562 #[test]
563 fn test_get_random_u64() {
564 let _g = crate::test_util::global_state_lock();
565 let r1 = random_u64();
566 let r2 = random_u64();
567 assert_ne!(r1, r2);
568 }
569
570 #[test]
571 fn test_bounded_random() {
572 let _g = crate::test_util::global_state_lock();
573 for _ in 0..100 {
574 let r = bounded(10);
575 assert!(r < 10);
576 }
577 }
578
579 #[test]
580 fn test_bounded_random_one() {
581 let _g = crate::test_util::global_state_lock();
582 for _ in 0..10 {
583 let r = bounded(1);
584 assert_eq!(r, 0);
585 }
586 }
587
588 #[test]
589 fn test_zrand_int() {
590 let _g = crate::test_util::global_state_lock();
591 let r = math_zrand_int(Some(100), Some(50), false).unwrap();
592 assert!((50..100).contains(&r));
593
594 let r = math_zrand_int(Some(100), Some(50), true).unwrap();
595 assert!((50..=100).contains(&r));
596 }
597
598 #[test]
599 fn test_zrand_int_no_args() {
600 let _g = crate::test_util::global_state_lock();
601 let r = math_zrand_int(None, None, false).unwrap();
602 assert!(r >= 0);
603 }
604
605 #[test]
606 fn test_zrand_int_bad_bounds_warn_and_continue() {
607 let _g = crate::test_util::global_state_lock();
608 // c:179-185 — bound violations WARN (zwarn, no return); C
609 // falls through to the (uint32_t)diff truncating draw, so the
610 // call still yields a value.
611 assert!(math_zrand_int(Some(50), Some(100), false).is_ok());
612 assert!(math_zrand_int(Some(-1), None, false).is_ok());
613 }
614
615 #[test]
616 fn test_zrand_float() {
617 let _g = crate::test_util::global_state_lock();
618 for _ in 0..100 {
619 let r = math_zrand_float();
620 assert!((0.0..1.0).contains(&r));
621 }
622 }
623
624 #[test]
625 fn test_random_real() {
626 let _g = crate::test_util::global_state_lock();
627 for _ in 0..100 {
628 let r = random_real();
629 assert!((0.0..1.0).contains(&r));
630 }
631 }
632
633 #[test]
634 fn test_shuffle() {
635 let _g = crate::test_util::global_state_lock();
636 // Fisher–Yates shuffle, inlined here since the helper is gone.
637 let mut arr = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
638 let original = arr.clone();
639 let n = arr.len();
640 for i in (1..n).rev() {
641 let j = bounded((i + 1) as u32) as usize;
642 arr.swap(i, j);
643 }
644 arr.sort();
645 assert_eq!(arr, original.to_vec());
646 }
647
648 #[test]
649 fn test_fill_random_bytes() {
650 let _g = crate::test_util::global_state_lock();
651 let mut buf = [0u8; 32];
652 getrandom_buffer(&mut buf).unwrap();
653 assert!(!buf.iter().all(|&b| b == 0));
654 }
655
656 /// c:161 — `math_zrand_int(upper, lower, inclusive=true)` returns
657 /// values in `[lower, upper]`. 50 iterations to verify EVERY
658 /// returned value lies in range — regression returning out-of-
659 /// bounds values would silently corrupt arithmetic-driven scripts.
660 #[test]
661 fn math_zrand_int_inclusive_range_respects_bounds() {
662 let _g = crate::test_util::global_state_lock();
663 for _ in 0..50 {
664 let v = math_zrand_int(Some(10), Some(5), true).unwrap();
665 assert!(
666 (5..=10).contains(&v),
667 "value {v} out of inclusive range [5, 10]"
668 );
669 }
670 }
671
672 /// c:161 — `inclusive=false` excludes upper bound. `random_int(0,10)`
673 /// must NEVER return 10 in this mode.
674 #[test]
675 fn math_zrand_int_exclusive_excludes_upper_bound() {
676 let _g = crate::test_util::global_state_lock();
677 for _ in 0..50 {
678 let v = math_zrand_int(Some(10), Some(5), false).unwrap();
679 assert!(
680 (5..10).contains(&v),
681 "value {v} out of exclusive range [5, 10)"
682 );
683 }
684 }
685
686 /// c:204 — `math_zrand_float` returns a float in `[0.0, 1.0)`.
687 /// Regression returning negative or >=1.0 would break PRNGs users
688 /// layer on top.
689 #[test]
690 fn math_zrand_float_in_unit_interval() {
691 let _g = crate::test_util::global_state_lock();
692 for _ in 0..50 {
693 let v = math_zrand_float();
694 assert!(
695 (0.0..1.0).contains(&v),
696 "value {v} out of unit interval [0.0, 1.0)"
697 );
698 }
699 }
700
701 /// `getrandom_buffer` produces different output on successive
702 /// calls. Catches a regression where the RNG is fixed-seeded.
703 #[test]
704 fn getrandom_buffer_two_calls_differ() {
705 let _g = crate::test_util::global_state_lock();
706 let mut a = [0u8; 32];
707 let mut b = [0u8; 32];
708 getrandom_buffer(&mut a).unwrap();
709 getrandom_buffer(&mut b).unwrap();
710 assert_ne!(a, b, "two random reads must differ (or RNG is broken)");
711 }
712
713 // ─── zsh-corpus pins for random helpers ────────────────────────
714
715 /// `get_srandom` returns u32 with variance across calls.
716 #[test]
717 fn random_corpus_get_srandom_varies() {
718 let _g = crate::test_util::global_state_lock();
719 let a = get_srandom();
720 let b = get_srandom();
721 let c = get_srandom();
722 assert!(
723 a != b || b != c || a != c,
724 "3 srandom calls all returned same value: {a} {b} {c}"
725 );
726 }
727
728 /// `math_zrand_float` always in [0.0, 1.0).
729 #[test]
730 fn random_corpus_math_zrand_float_unit_interval() {
731 let _g = crate::test_util::global_state_lock();
732 for _ in 0..50 {
733 let v = math_zrand_float();
734 assert!((0.0..1.0).contains(&v), "value {v} out of [0.0, 1.0)");
735 }
736 }
737
738 /// `get_bound_random_buffer` with max=100 fills with values < 100.
739 #[test]
740 fn random_corpus_bound_random_under_max() {
741 let _g = crate::test_util::global_state_lock();
742 let mut buf = [0u32; 50];
743 get_bound_random_buffer(&mut buf, 100);
744 for &v in &buf {
745 assert!(v < 100, "{v} should be < 100");
746 }
747 }
748
749 /// `get_bound_random_buffer` with max=1 fills with all zeros.
750 #[test]
751 fn random_corpus_bound_random_max_one_all_zero() {
752 let _g = crate::test_util::global_state_lock();
753 let mut buf = [0u32; 20];
754 get_bound_random_buffer(&mut buf, 1);
755 for &v in &buf {
756 assert_eq!(v, 0, "max=1 → all values 0, got {v}");
757 }
758 }
759
760 /// `getrandom_buffer` with empty slice doesn't panic.
761 #[test]
762 fn random_corpus_getrandom_empty_buffer_no_panic() {
763 let _g = crate::test_util::global_state_lock();
764 let mut empty: [u8; 0] = [];
765 getrandom_buffer(&mut empty).unwrap();
766 }
767
768 /// `getrandom_buffer` fills 1-byte buffer.
769 #[test]
770 fn random_corpus_getrandom_single_byte() {
771 let _g = crate::test_util::global_state_lock();
772 let mut buf = [0u8; 1];
773 getrandom_buffer(&mut buf).unwrap();
774 }
775
776 // ═══════════════════════════════════════════════════════════════════
777 // Additional C-parity tests for Src/Modules/random.c.
778 // ═══════════════════════════════════════════════════════════════════
779
780 /// c:161 — `math_zrand_int(None, None, false)` returns Ok value in
781 /// [0, u32::MAX) range (defaults: lower=0, upper=u32::MAX, exclusive).
782 #[test]
783 fn math_zrand_int_default_bounds_returns_ok() {
784 let _g = crate::test_util::global_state_lock();
785 let r = math_zrand_int(None, None, false).expect("default bounds → Ok");
786 assert!(r >= 0, "result must be ≥ 0");
787 assert!(r <= u32::MAX as i64, "result must fit in u32 range");
788 }
789
790 /// c:179-180 — `lower < 0` fires the "Lower bound" zwarn but the
791 /// draw proceeds: diff = 100-(-1) = 101, result ∈ [-1, 99].
792 #[test]
793 fn math_zrand_int_negative_lower_warns_and_continues() {
794 let _g = crate::test_util::global_state_lock();
795 let r = math_zrand_int(Some(100), Some(-1), false);
796 let v = r.expect("c:179 warns without returning");
797 assert!((-1..100).contains(&v), "result in wrapped range, got {}", v);
798 }
799
800 /// c:179-180 — `lower >= UINT32_MAX` warns; C still computes the
801 /// (negative) diff and truncates it through (uint32_t).
802 #[test]
803 fn math_zrand_int_lower_above_u32_max_warns_and_continues() {
804 let _g = crate::test_util::global_state_lock();
805 let r = math_zrand_int(Some(100), Some((u32::MAX as i64) + 1), false);
806 assert!(r.is_ok(), "c:179 warns without returning");
807 }
808
809 /// c:181-182 — `upper < lower` fires the "must be greater" zwarn;
810 /// the wrapped-diff draw still happens (C's else-if chain has no
811 /// return).
812 #[test]
813 fn math_zrand_int_upper_below_lower_warns_and_continues() {
814 let _g = crate::test_util::global_state_lock();
815 let r = math_zrand_int(Some(5), Some(10), false);
816 assert!(r.is_ok(), "c:181 warns without returning");
817 }
818
819 /// c:161 — `upper == lower` with exclusive returns the bound
820 /// (diff=0 short-circuit branch).
821 #[test]
822 fn math_zrand_int_upper_equals_lower_returns_bound() {
823 let _g = crate::test_util::global_state_lock();
824 let r = math_zrand_int(Some(7), Some(7), false).expect("equal bounds OK");
825 assert_eq!(r, 7, "diff=0 → returns upper");
826 }
827
828 /// c:161 — `inclusive=true` with same lower=upper returns lower
829 /// (diff = 0 - 0 + 1 = 1, range [lower, upper]).
830 #[test]
831 fn math_zrand_int_inclusive_single_point_returns_lower() {
832 let _g = crate::test_util::global_state_lock();
833 let r = math_zrand_int(Some(42), Some(42), true).expect("OK");
834 assert_eq!(r, 42, "inclusive single-point → that point");
835 }
836
837 /// c:161 — result always within [lower, upper] (or upper-1 if exclusive).
838 #[test]
839 fn math_zrand_int_result_in_range() {
840 let _g = crate::test_util::global_state_lock();
841 for _ in 0..50 {
842 let r = math_zrand_int(Some(10), Some(0), false).unwrap();
843 assert!(r >= 0 && r < 10, "exclusive [0,10): got {}", r);
844 }
845 for _ in 0..50 {
846 let r = math_zrand_int(Some(10), Some(0), true).unwrap();
847 assert!(r >= 0 && r <= 10, "inclusive [0,10]: got {}", r);
848 }
849 }
850
851 /// c:204 — `math_zrand_float()` returns value in [0.0, 1.0).
852 #[test]
853 fn math_zrand_float_in_zero_one_range() {
854 let _g = crate::test_util::global_state_lock();
855 for _ in 0..50 {
856 let r = math_zrand_float();
857 assert!(r >= 0.0 && r < 1.0, "must be in [0,1): got {}", r);
858 }
859 }
860
861 /// c:204 — `math_zrand_float` is not always the same value
862 /// (basic randomness sanity — 100 calls should produce ≥ 2
863 /// distinct values).
864 #[test]
865 fn math_zrand_float_produces_varied_output() {
866 let _g = crate::test_util::global_state_lock();
867 let first = math_zrand_float();
868 let any_different = (0..100).any(|_| math_zrand_float() != first);
869 assert!(
870 any_different,
871 "100 calls should produce ≥ 1 different value"
872 );
873 }
874
875 /// c:58 — `get_srandom()` returns u32, repeated calls produce
876 /// varied values (PRNG behavior pin).
877 #[test]
878 fn get_srandom_produces_varied_values() {
879 let _g = crate::test_util::global_state_lock();
880 let first = get_srandom();
881 let any_different = (0..100).any(|_| get_srandom() != first);
882 assert!(
883 any_different,
884 "100 calls should produce ≥ 1 different value"
885 );
886 }
887
888 // ═══════════════════════════════════════════════════════════════════
889 // Additional C-parity tests for Src/Modules/random.c
890 // c:26 getrandom_buffer / c:109 get_bound_random_buffer / c:144 get_srandom
891 // c:173 math_zrand_int / c:219 math_zrand_float / lifecycle
892 // ═══════════════════════════════════════════════════════════════════
893
894 /// c:26 — `getrandom_buffer(&mut [])` empty buf no-panic.
895 #[test]
896 fn getrandom_buffer_empty_buffer_returns_ok() {
897 let _g = crate::test_util::global_state_lock();
898 let mut buf: [u8; 0] = [];
899 assert!(getrandom_buffer(&mut buf).is_ok());
900 }
901
902 /// c:26 — `getrandom_buffer` returns Result type.
903 #[test]
904 fn getrandom_buffer_returns_io_result_type() {
905 let _g = crate::test_util::global_state_lock();
906 let mut buf = [0u8; 1];
907 let _: io::Result<()> = getrandom_buffer(&mut buf);
908 }
909
910 /// c:26 — `getrandom_buffer` actually fills (high probability of
911 /// at least one non-zero byte in 256 bytes).
912 #[test]
913 fn getrandom_buffer_fills_with_nonzero_bytes() {
914 let _g = crate::test_util::global_state_lock();
915 let mut buf = [0u8; 256];
916 getrandom_buffer(&mut buf).unwrap();
917 assert!(
918 buf.iter().any(|&b| b != 0),
919 "256 random bytes should have ≥ 1 non-zero"
920 );
921 }
922
923 /// c:109 — `get_bound_random_buffer(buf, 1)` fills all zeros (only
924 /// value < 1 is 0).
925 #[test]
926 fn get_bound_random_buffer_max_one_all_zero() {
927 let _g = crate::test_util::global_state_lock();
928 let mut buf = [0u32; 100];
929 get_bound_random_buffer(&mut buf, 1);
930 for &v in &buf {
931 assert_eq!(v, 0, "max=1 → all values must be 0");
932 }
933 }
934
935 /// c:109 — `get_bound_random_buffer(buf, max)` all values < max.
936 #[test]
937 fn get_bound_random_buffer_respects_max_bound() {
938 let _g = crate::test_util::global_state_lock();
939 let mut buf = [0u32; 1000];
940 let max = 100u32;
941 get_bound_random_buffer(&mut buf, max);
942 for &v in &buf {
943 assert!(v < max, "value {} must be < max {}", v, max);
944 }
945 }
946
947 /// c:144 — `get_srandom` returns u32 (compile-time type pin).
948 #[test]
949 fn get_srandom_returns_u32_type() {
950 let _g = crate::test_util::global_state_lock();
951 let _: u32 = get_srandom();
952 }
953
954 /// c:219 — `math_zrand_float()` returns f64 strictly in [0.0, 1.0).
955 #[test]
956 fn math_zrand_float_strictly_in_half_open_unit() {
957 let _g = crate::test_util::global_state_lock();
958 for _ in 0..50 {
959 let v = math_zrand_float();
960 assert!(
961 v >= 0.0 && v < 1.0,
962 "math_zrand_float = {} must be in [0.0, 1.0)",
963 v
964 );
965 }
966 }
967
968 /// c:226-303 — full lifecycle setup→features→enables→boot→cleanup→finish.
969 #[test]
970 fn random_full_lifecycle_returns_zero_for_all() {
971 let _g = crate::test_util::global_state_lock();
972 let null = std::ptr::null();
973 assert_eq!(setup_(null), 0);
974 let mut feats = Vec::new();
975 let _ = features_(null, &mut feats);
976 let mut enables: Option<Vec<i32>> = None;
977 let _ = enables_(null, &mut enables);
978 assert_eq!(boot_(null), 0);
979 assert_eq!(cleanup_(null), 0);
980 assert_eq!(finish_(null), 0);
981 }
982
983 /// c:343 — `random_u32` produces varied values.
984 #[test]
985 fn random_u32_produces_varied_values() {
986 let _g = crate::test_util::global_state_lock();
987 let first = random_u32();
988 let any_diff = (0..100).any(|_| random_u32() != first);
989 assert!(any_diff, "100 u32 randoms should differ from first");
990 }
991
992 /// c:353 — `random_u64` produces varied values.
993 #[test]
994 fn random_u64_produces_varied_values() {
995 let _g = crate::test_util::global_state_lock();
996 let first = random_u64();
997 let any_diff = (0..100).any(|_| random_u64() != first);
998 assert!(any_diff, "100 u64 randoms should differ from first");
999 }
1000
1001 // ═══════════════════════════════════════════════════════════════════
1002 // Additional C-parity tests for Src/Modules/random.c
1003 // c:343 random_u32 / c:353 random_u64 / c:364 bounded /
1004 // c:144 get_srandom + lifecycle type pins
1005 // ═══════════════════════════════════════════════════════════════════
1006
1007 /// c:343 — `random_u32` returns u32 (compile-time type pin).
1008 #[test]
1009 fn random_u32_returns_u32_type() {
1010 let _g = crate::test_util::global_state_lock();
1011 let _: u32 = random_u32();
1012 }
1013
1014 /// c:353 — `random_u64` returns u64 (compile-time type pin).
1015 #[test]
1016 fn random_u64_returns_u64_type() {
1017 let _g = crate::test_util::global_state_lock();
1018 let _: u64 = random_u64();
1019 }
1020
1021 /// c:364 — `bounded(0)` returns 0 (degenerate range).
1022 #[test]
1023 fn bounded_zero_max_returns_zero() {
1024 let _g = crate::test_util::global_state_lock();
1025 assert_eq!(bounded(0), 0, "max=0 always returns 0");
1026 }
1027
1028 /// c:364 — `bounded(1)` always returns 0 (only value < 1).
1029 #[test]
1030 fn bounded_one_max_always_zero() {
1031 let _g = crate::test_util::global_state_lock();
1032 for _ in 0..50 {
1033 assert_eq!(bounded(1), 0, "max=1 → result must be 0");
1034 }
1035 }
1036
1037 /// c:364 — `bounded(max)` result always strictly less than max.
1038 #[test]
1039 fn bounded_result_strictly_less_than_max() {
1040 let _g = crate::test_util::global_state_lock();
1041 for &max in &[2u32, 10, 100, 1000, 1_000_000] {
1042 for _ in 0..20 {
1043 let v = bounded(max);
1044 assert!(v < max, "bounded({}) = {} must be < max", max, v);
1045 }
1046 }
1047 }
1048
1049 /// c:364 — `bounded` returns u32 (compile-time type pin).
1050 #[test]
1051 fn bounded_returns_u32_type() {
1052 let _g = crate::test_util::global_state_lock();
1053 let _: u32 = bounded(100);
1054 }
1055
1056 /// c:364 — `bounded(u32::MAX)` short-circuit branch returns
1057 /// arbitrary u32 (degenerate special case at c:368).
1058 #[test]
1059 fn bounded_u32_max_returns_u32_range() {
1060 let _g = crate::test_util::global_state_lock();
1061 // No bound to check beyond "doesn't panic" + type.
1062 let _: u32 = bounded(u32::MAX);
1063 }
1064
1065 /// c:226 — `setup_` returns i32 (compile-time type pin).
1066 #[test]
1067 fn random_setup_returns_i32_type() {
1068 let _g = crate::test_util::global_state_lock();
1069 let _: i32 = setup_(std::ptr::null());
1070 }
1071
1072 /// c:249 — features list contains the canonical 3 entries
1073 /// (f:zrand_float, f:zrand_int, p:SRANDOM).
1074 #[test]
1075 fn random_features_canonical_three_entries() {
1076 let _g = crate::test_util::global_state_lock();
1077 let mut feats = Vec::new();
1078 features_(std::ptr::null(), &mut feats);
1079 assert_eq!(feats.len(), 3, "random advertises 3 features");
1080 assert!(
1081 feats.iter().any(|f| f == "f:zrand_float"),
1082 "must contain f:zrand_float"
1083 );
1084 assert!(
1085 feats.iter().any(|f| f == "f:zrand_int"),
1086 "must contain f:zrand_int"
1087 );
1088 assert!(
1089 feats.iter().any(|f| f == "p:SRANDOM"),
1090 "must contain p:SRANDOM"
1091 );
1092 }
1093
1094 /// c:296 — `cleanup_` idempotent.
1095 #[test]
1096 fn random_cleanup_idempotent() {
1097 let _g = crate::test_util::global_state_lock();
1098 for _ in 0..10 {
1099 assert_eq!(cleanup_(std::ptr::null()), 0);
1100 }
1101 }
1102
1103 /// c:303 — `finish_` idempotent.
1104 #[test]
1105 fn random_finish_idempotent() {
1106 let _g = crate::test_util::global_state_lock();
1107 for _ in 0..10 {
1108 assert_eq!(finish_(std::ptr::null()), 0);
1109 }
1110 }
1111
1112 /// c:263 — `boot_` idempotent.
1113 #[test]
1114 fn random_boot_idempotent() {
1115 let _g = crate::test_util::global_state_lock();
1116 for _ in 0..10 {
1117 assert_eq!(boot_(std::ptr::null()), 0);
1118 }
1119 }
1120
1121 // ═══════════════════════════════════════════════════════════════════
1122 // Additional C-parity tests for Src/Modules/random.c
1123 // c:144 get_srandom / c:219 math_zrand_float / c:343 random_u32 /
1124 // c:353 random_u64 / c:364 bounded / c:109 get_bound_random_buffer
1125 // ═══════════════════════════════════════════════════════════════════
1126
1127 /// c:144 — `get_srandom` returns u32 (compile-time pin, alt).
1128 #[test]
1129 fn get_srandom_returns_u32_pin_alt() {
1130 let _g = crate::test_util::global_state_lock();
1131 let _: u32 = get_srandom();
1132 }
1133
1134 /// c:144 — `get_srandom` is non-deterministic across two calls
1135 /// (probability of equal = 1/2^32 ≈ 2.3e-10 — cosmically unlikely).
1136 #[test]
1137 fn get_srandom_two_calls_differ() {
1138 let _g = crate::test_util::global_state_lock();
1139 let a = get_srandom();
1140 let b = get_srandom();
1141 assert_ne!(a, b, "two get_srandom() calls must differ");
1142 }
1143
1144 /// c:343 — `random_u32` returns u32 (compile-time pin, alt).
1145 #[test]
1146 fn random_u32_returns_u32_pin_alt() {
1147 let _g = crate::test_util::global_state_lock();
1148 let _: u32 = random_u32();
1149 }
1150
1151 /// c:353 — `random_u64` returns u64 (compile-time pin, alt).
1152 #[test]
1153 fn random_u64_returns_u64_pin_alt() {
1154 let _g = crate::test_util::global_state_lock();
1155 let _: u64 = random_u64();
1156 }
1157
1158 /// c:353 — `random_u64` eventually exceeds u32::MAX threshold
1159 /// (proves it's a full 64-bit value, not a u32 zero-extended).
1160 #[test]
1161 fn random_u64_eventually_exceeds_u32_max() {
1162 let _g = crate::test_util::global_state_lock();
1163 let any_large = (0..200).any(|_| random_u64() > (u32::MAX as u64));
1164 assert!(
1165 any_large,
1166 "200 random_u64 values must include ≥ 1 above u32::MAX"
1167 );
1168 }
1169
1170 /// c:219 — `math_zrand_float` returns f64 (compile-time pin).
1171 #[test]
1172 fn math_zrand_float_returns_f64_type() {
1173 let _g = crate::test_util::global_state_lock();
1174 let _: f64 = math_zrand_float();
1175 }
1176
1177 /// c:219 — `math_zrand_float` outputs always finite (no NaN/Inf).
1178 #[test]
1179 fn math_zrand_float_always_finite() {
1180 let _g = crate::test_util::global_state_lock();
1181 for _ in 0..500 {
1182 let v = math_zrand_float();
1183 assert!(
1184 v.is_finite(),
1185 "math_zrand_float must always be finite, got {}",
1186 v
1187 );
1188 }
1189 }
1190
1191 /// c:364 — `bounded(1)` always returns 0 (only one valid value).
1192 #[test]
1193 fn bounded_one_always_returns_zero() {
1194 let _g = crate::test_util::global_state_lock();
1195 for _ in 0..50 {
1196 assert_eq!(
1197 bounded(1),
1198 0,
1199 "bounded(1) must always return 0 (only valid value)"
1200 );
1201 }
1202 }
1203
1204 /// c:364 — `bounded(2)` returns only 0 or 1.
1205 #[test]
1206 fn bounded_two_returns_zero_or_one() {
1207 let _g = crate::test_util::global_state_lock();
1208 for _ in 0..50 {
1209 let v = bounded(2);
1210 assert!(v < 2, "bounded(2) must be 0 or 1; got {}", v);
1211 }
1212 }
1213
1214 /// c:109 — `get_bound_random_buffer` fills entire buffer with
1215 /// values strictly less than max.
1216 #[test]
1217 fn get_bound_random_buffer_all_under_max() {
1218 let _g = crate::test_util::global_state_lock();
1219 let mut buf = vec![0u32; 100];
1220 get_bound_random_buffer(&mut buf, 10);
1221 for &v in &buf {
1222 assert!(v < 10, "buffer value {} must be < max=10", v);
1223 }
1224 }
1225
1226 /// c:226/249/256/263/296/303 — each lifecycle hook returns 0 individually
1227 /// (tighter failure resolution).
1228 #[test]
1229 fn random_each_lifecycle_hook_returns_zero_individually() {
1230 let _g = crate::test_util::global_state_lock();
1231 let null = std::ptr::null();
1232 let mut v: Vec<String> = Vec::new();
1233 let mut e: Option<Vec<i32>> = None;
1234 assert_eq!(setup_(null), 0, "c:226 setup_");
1235 assert_eq!(features_(null, &mut v), 0, "c:249 features_");
1236 assert_eq!(enables_(null, &mut e), 0, "c:256 enables_");
1237 assert_eq!(boot_(null), 0, "c:263 boot_");
1238 assert_eq!(cleanup_(null), 0, "c:296 cleanup_");
1239 assert_eq!(finish_(null), 0, "c:303 finish_");
1240 }
1241}