use oxicrypto_core::Rng;
use oxicrypto_rand::{
check_entropy, random_range, random_range_to, random_range_unbiased, random_u64, OxiRng,
ReseedingRng,
};
#[test]
fn test_chi_squared_byte_distribution() {
let mut rng = OxiRng::new().expect("OxiRng::new must succeed");
const N: usize = 256 * 1000; let mut counts = [0u64; 256];
let mut buf = [0u8; 4096];
for _ in 0..(N / buf.len()) {
rng.fill(&mut buf).expect("fill must succeed");
for &b in &buf {
counts[b as usize] += 1;
}
}
let expected = N as f64 / 256.0;
let chi_sq: f64 = counts
.iter()
.map(|&c| {
let diff = c as f64 - expected;
diff * diff / expected
})
.sum();
assert!(
chi_sq < 400.0,
"chi-squared {chi_sq:.2} too high — potential bias in OxiRng"
);
assert!(
chi_sq > 150.0,
"chi-squared {chi_sq:.2} suspiciously low — check for zero-fill or identical bytes"
);
}
#[test]
fn test_independent_instances_differ() {
let mut rng1 = OxiRng::new().expect("rng1");
let mut rng2 = OxiRng::new().expect("rng2");
let mut buf1 = [0u8; 32];
let mut buf2 = [0u8; 32];
rng1.fill(&mut buf1).expect("fill1");
rng2.fill(&mut buf2).expect("fill2");
assert_ne!(
buf1, buf2,
"two independently seeded OxiRng instances must not produce identical 32-byte output"
);
}
#[test]
fn test_reseeding_rng_reseeds_on_threshold() {
const THRESHOLD: u64 = 1024;
let mut rng =
ReseedingRng::with_threshold(THRESHOLD).expect("ReseedingRng::with_threshold must succeed");
assert_eq!(rng.bytes_generated(), 0);
let mut buf = [0u8; 512];
let mut total: u64 = 0;
for _ in 0..5 {
rng.fill(&mut buf)
.expect("ReseedingRng fill must succeed after reseed");
total += buf.len() as u64;
}
assert!(
rng.bytes_generated() < total,
"bytes_generated() ({}) should be less than total ({}) after reseeding",
rng.bytes_generated(),
total
);
assert!(
rng.bytes_generated() <= 512,
"bytes_generated() ({}) after last reseed must be ≤512 (one chunk since last reseed)",
rng.bytes_generated()
);
}
#[test]
fn test_random_range_0_to_1_always_0() {
let mut rng = OxiRng::new().expect("rng");
for _ in 0..20 {
let v = random_range_unbiased(&mut rng, 0, 1).expect("range [0,1)");
assert_eq!(
v, 0u64,
"random_range_unbiased(rng, 0, 1) must always return 0"
);
}
}
#[test]
fn test_random_range_free_fn_0_to_1_always_0() {
for _ in 0..20 {
let v = random_range(0, 1).expect("range [0,1)");
assert_eq!(v, 0u64, "random_range(0, 1) must always return 0");
}
}
#[test]
fn test_fill_zero_length_buffer() {
let mut rng = OxiRng::new().expect("rng");
let mut buf: [u8; 0] = [];
rng.fill(&mut buf)
.expect("fill on a zero-length buffer must succeed");
}
#[test]
fn test_random_range_to_zero_is_error() {
let result = random_range_to(0);
assert!(
result.is_err(),
"random_range_to(0) must return an error; got Ok({:?})",
result.ok()
);
}
#[test]
fn test_random_range_min_equals_max_is_error() {
let result = random_range(5, 5);
assert!(
result.is_err(),
"random_range(5, 5) must return an error (empty range)"
);
}
#[test]
fn test_random_range_min_gt_max_is_error() {
let result = random_range(10, 3);
assert!(
result.is_err(),
"random_range(10, 3) must return an error (inverted range)"
);
}
#[test]
fn test_random_u64_produces_different_values() {
let v1 = random_u64().expect("random_u64 #1");
let v2 = random_u64().expect("random_u64 #2");
assert_ne!(v1, v2, "two consecutive random_u64 calls must differ");
}
#[test]
fn test_check_entropy_passes() {
check_entropy().expect("check_entropy must succeed on a functioning OS RNG");
}
#[test]
fn test_runs_nist_sp800_22_1mib() {
let n_bytes = 1024 * 1024;
let data = oxicrypto_rand::random_bytes(n_bytes).expect("random_bytes failed");
let total_bits = n_bytes * 8;
let mut runs = 1u64;
let mut prev_bit = (data[0] >> 7) & 1;
for byte in &data {
for shift in (0..8).rev() {
let bit = (byte >> shift) & 1;
if bit != prev_bit {
runs += 1;
}
prev_bit = bit;
}
}
let expected = (total_bits as f64) / 2.0 + 1.0;
let tolerance = expected * 0.10;
assert!(
(runs as f64 - expected).abs() < tolerance,
"Runs test failed: {runs} runs, expected ~{expected:.0} ± {tolerance:.0}"
);
}
#[test]
fn test_serial_correlation() {
let data = oxicrypto_rand::random_bytes(10_000).expect("random_bytes failed");
let n = data.len() as f64;
let mean = data.iter().map(|&b| b as f64).sum::<f64>() / n;
let variance = data.iter().map(|&b| (b as f64 - mean).powi(2)).sum::<f64>() / n;
let covariance = data
.windows(2)
.map(|w| (w[0] as f64 - mean) * (w[1] as f64 - mean))
.sum::<f64>()
/ (n - 1.0);
let corr = if variance > 0.0 {
covariance / variance
} else {
0.0
};
assert!(
corr.abs() < 0.05,
"Serial correlation too high: {corr:.4} (expected < 0.05)"
);
}
#[cfg(unix)]
#[test]
fn test_fork_produces_different_output() {
let result1 = oxicrypto_rand::random_bytes(32).expect("random_bytes failed");
let result2 = oxicrypto_rand::random_bytes(32).expect("random_bytes failed");
assert_ne!(
result1, result2,
"Sequential random_bytes calls should differ"
);
}
#[cfg(feature = "std")]
mod std_read {
use oxicrypto_rand::{OxiRng, ReseedingRng};
use std::io::Read;
#[test]
fn test_oxi_rng_read_fills_buffer() {
let mut rng = OxiRng::new().expect("rng");
let mut buf = [0u8; 64];
let n = rng.read(&mut buf).expect("Read::read must succeed");
assert_eq!(n, 64, "Read::read must return buf.len()");
assert_ne!(buf, [0u8; 64], "read buffer must not be all zeros");
}
#[test]
fn test_oxi_rng_read_empty_buffer() {
let mut rng = OxiRng::new().expect("rng");
let mut buf: [u8; 0] = [];
let n = rng
.read(&mut buf)
.expect("Read::read on empty buffer must succeed");
assert_eq!(n, 0);
}
#[test]
fn test_reseeding_rng_read_fills_buffer() {
let mut rng = ReseedingRng::new().expect("ReseedingRng::new");
let mut buf = [0u8; 128];
let n = rng
.read(&mut buf)
.expect("ReseedingRng Read::read must succeed");
assert_eq!(n, 128, "Read::read must return buf.len()");
}
}