use std::ops::{Bound, RangeBounds};
use std::panic::Location;
use crate::TestCaseContext;
use crate::rng::{AttemptVerdict, ChoiceMeta};
fn raw_bytes<const N: usize>(ctx: &mut TestCaseContext) -> [u8; N] {
let mut buf = [0u8; N];
ctx.fill(&mut buf);
buf
}
pub(crate) const DEFAULT_MAX_ATTEMPTS: usize = 64;
#[track_caller]
pub fn sample_with_rejection<T, F>(
ctx: &mut TestCaseContext,
max_attempts: usize,
mut attempt: F,
) -> T
where
F: FnMut(&mut TestCaseContext) -> Option<T>,
{
assert!(
max_attempts > 0,
"sample_with_rejection: max_attempts must be > 0"
);
for _ in 0..max_attempts {
let id = ctx.begin_attempt();
match attempt(ctx) {
Some(value) => {
ctx.end_attempt(id, AttemptVerdict::Accepted);
return value;
}
None => {
ctx.end_attempt(id, AttemptVerdict::Rejected);
}
}
}
ctx.reject_case()
}
#[track_caller]
fn sample_below(ctx: &mut TestCaseContext, n: u64) -> u64 {
sample_below_with_meta(ctx, n, ChoiceMeta::Bounded { bound: n })
}
#[track_caller]
fn sample_below_with_meta(ctx: &mut TestCaseContext, n: u64, meta: ChoiceMeta) -> u64 {
debug_assert!(n > 0, "sample_below: n must be non-zero");
if n == 1 {
return 0;
}
let r = u64::MAX % n;
if r == n - 1 {
ctx.set_next_choice_meta(meta);
return u64::from_le_bytes(raw_bytes(ctx)) % n;
}
let bound = u64::MAX - r;
sample_with_rejection(ctx, DEFAULT_MAX_ATTEMPTS, |ctx| {
ctx.set_next_choice_meta(meta);
let x = u64::from_le_bytes(raw_bytes(ctx));
(x < bound).then_some(x % n)
})
}
#[track_caller]
pub fn sample_choice<T: Clone + std::fmt::Debug + 'static>(
ctx: &mut TestCaseContext,
choices: &[T],
) -> T {
assert!(!choices.is_empty(), "sample_choice: empty slice");
let loc = Location::caller();
let idx = sample_below_with_meta(
ctx,
choices.len() as u64,
ChoiceMeta::Choice { len: choices.len() },
) as usize;
let v = choices[idx].clone();
ctx.record_generated(&v, loc);
v
}
#[track_caller]
pub fn sample_usize_in<R: RangeBounds<usize>>(ctx: &mut TestCaseContext, range: R) -> usize {
let loc = Location::caller();
let lo = match range.start_bound() {
Bound::Included(&s) => s,
Bound::Excluded(&s) => s.checked_add(1).expect("sample_usize_in: empty range"),
Bound::Unbounded => 0,
};
let hi = match range.end_bound() {
Bound::Included(&e) => e,
Bound::Excluded(&e) => e.checked_sub(1).expect("sample_usize_in: empty range"),
Bound::Unbounded => usize::MAX,
};
assert!(lo <= hi, "sample_usize_in: empty range");
let v = if lo == 0 && hi == usize::MAX {
ctx.set_next_choice_meta(crate::rng::ChoiceMeta::Integer);
usize::from_le_bytes(raw_bytes(ctx))
} else {
let width = (hi - lo) as u64 + 1;
lo + sample_below(ctx, width) as usize
};
ctx.record_generated(&v, loc);
v
}
#[track_caller]
pub fn sample_ratio(ctx: &mut TestCaseContext, numerator: u32, denominator: u32) -> bool {
let loc = Location::caller();
assert!(
denominator != 0,
"sample_ratio: denominator must be non-zero"
);
assert!(
numerator <= denominator,
"sample_ratio: numerator ({numerator}) must be <= denominator ({denominator})"
);
let v = if numerator == 0 {
false
} else if numerator == denominator {
true
} else {
sample_below(ctx, denominator as u64) < numerator as u64
};
ctx.record_generated(&v, loc);
v
}
#[track_caller]
pub fn sample_weighted_index(ctx: &mut TestCaseContext, weights: &[u32]) -> usize {
let loc = Location::caller();
assert!(!weights.is_empty(), "sample_weighted_index: empty weights");
let mut total: u64 = 0;
for &w in weights {
total = total
.checked_add(w as u64)
.expect("sample_weighted_index: weight sum overflows u64");
}
assert!(total > 0, "sample_weighted_index: all weights are zero");
let mut pick = sample_below(ctx, total);
let mut chosen = weights.len(); for (i, &w) in weights.iter().enumerate() {
let w = w as u64;
if pick < w {
chosen = i;
break;
}
pick -= w;
}
debug_assert!(chosen < weights.len());
ctx.record_generated(&chosen, loc);
chosen
}
#[track_caller]
pub fn sample_bytes<const N: usize>(ctx: &mut TestCaseContext) -> [u8; N] {
let loc = Location::caller();
let bytes = raw_bytes::<N>(ctx);
ctx.record_generated(&bytes, loc);
bytes
}
#[track_caller]
pub fn sample_bytes_vec(ctx: &mut TestCaseContext, len: usize) -> Vec<u8> {
let loc = Location::caller();
let mut bytes = vec![0u8; len];
ctx.fill(&mut bytes);
ctx.record_generated(&bytes, loc);
bytes
}
#[track_caller]
pub fn sample_bool(ctx: &mut TestCaseContext) -> bool {
let loc = Location::caller();
let v = raw_bytes::<1>(ctx)[0] & 1 != 0;
ctx.record_generated(&v, loc);
v
}
#[track_caller]
pub fn sample_u8(ctx: &mut TestCaseContext) -> u8 {
let loc = Location::caller();
ctx.set_next_choice_meta(crate::rng::ChoiceMeta::Integer);
let v = raw_bytes::<1>(ctx)[0];
ctx.record_generated(&v, loc);
v
}
#[track_caller]
pub fn sample_u16(ctx: &mut TestCaseContext) -> u16 {
let loc = Location::caller();
ctx.set_next_choice_meta(crate::rng::ChoiceMeta::Integer);
let v = u16::from_le_bytes(raw_bytes(ctx));
ctx.record_generated(&v, loc);
v
}
#[track_caller]
pub fn sample_u32(ctx: &mut TestCaseContext) -> u32 {
let loc = Location::caller();
ctx.set_next_choice_meta(crate::rng::ChoiceMeta::Integer);
let v = u32::from_le_bytes(raw_bytes(ctx));
ctx.record_generated(&v, loc);
v
}
#[track_caller]
pub fn sample_u64(ctx: &mut TestCaseContext) -> u64 {
let loc = Location::caller();
ctx.set_next_choice_meta(crate::rng::ChoiceMeta::Integer);
let v = u64::from_le_bytes(raw_bytes(ctx));
ctx.record_generated(&v, loc);
v
}
#[track_caller]
pub fn sample_u128(ctx: &mut TestCaseContext) -> u128 {
let loc = Location::caller();
ctx.set_next_choice_meta(crate::rng::ChoiceMeta::Integer);
let v = u128::from_le_bytes(raw_bytes(ctx));
ctx.record_generated(&v, loc);
v
}
#[track_caller]
pub fn sample_usize(ctx: &mut TestCaseContext) -> usize {
let loc = Location::caller();
ctx.set_next_choice_meta(crate::rng::ChoiceMeta::Integer);
let v = usize::from_le_bytes(raw_bytes(ctx));
ctx.record_generated(&v, loc);
v
}
#[track_caller]
pub fn sample_i8(ctx: &mut TestCaseContext) -> i8 {
let loc = Location::caller();
ctx.set_next_choice_meta(crate::rng::ChoiceMeta::Integer);
let v = raw_bytes::<1>(ctx)[0] as i8;
ctx.record_generated(&v, loc);
v
}
#[track_caller]
pub fn sample_i16(ctx: &mut TestCaseContext) -> i16 {
let loc = Location::caller();
ctx.set_next_choice_meta(crate::rng::ChoiceMeta::Integer);
let v = i16::from_le_bytes(raw_bytes(ctx));
ctx.record_generated(&v, loc);
v
}
#[track_caller]
pub fn sample_i32(ctx: &mut TestCaseContext) -> i32 {
let loc = Location::caller();
ctx.set_next_choice_meta(crate::rng::ChoiceMeta::Integer);
let v = i32::from_le_bytes(raw_bytes(ctx));
ctx.record_generated(&v, loc);
v
}
#[track_caller]
pub fn sample_i64(ctx: &mut TestCaseContext) -> i64 {
let loc = Location::caller();
ctx.set_next_choice_meta(crate::rng::ChoiceMeta::Integer);
let v = i64::from_le_bytes(raw_bytes(ctx));
ctx.record_generated(&v, loc);
v
}
#[track_caller]
pub fn sample_i128(ctx: &mut TestCaseContext) -> i128 {
let loc = Location::caller();
ctx.set_next_choice_meta(crate::rng::ChoiceMeta::Integer);
let v = i128::from_le_bytes(raw_bytes(ctx));
ctx.record_generated(&v, loc);
v
}
#[track_caller]
pub fn sample_isize(ctx: &mut TestCaseContext) -> isize {
let loc = Location::caller();
ctx.set_next_choice_meta(crate::rng::ChoiceMeta::Integer);
let v = isize::from_le_bytes(raw_bytes(ctx));
ctx.record_generated(&v, loc);
v
}
#[track_caller]
pub fn sample_char(ctx: &mut TestCaseContext) -> char {
let loc = Location::caller();
let c = sample_with_rejection(ctx, DEFAULT_MAX_ATTEMPTS, |ctx| {
let n = u32::from_le_bytes(raw_bytes(ctx)) & 0x1F_FFFF;
char::from_u32(n)
});
ctx.record_generated(&c, loc);
c
}
#[track_caller]
pub fn sample_ascii_char(ctx: &mut TestCaseContext) -> char {
let loc = Location::caller();
let v = (raw_bytes::<1>(ctx)[0] & 0x7F) as char;
ctx.record_generated(&v, loc);
v
}
#[track_caller]
pub fn sample_ascii_printable_char(ctx: &mut TestCaseContext) -> char {
let loc = Location::caller();
let v = (0x20 + u32::from_le_bytes(raw_bytes(ctx)) % 95) as u8 as char;
ctx.record_generated(&v, loc);
v
}
#[track_caller]
pub fn sample_string(ctx: &mut TestCaseContext, len: usize) -> String {
let loc = Location::caller();
let s: String = (0..len).map(|_| sample_char_raw(ctx)).collect();
ctx.record_generated(&s, loc);
s
}
#[track_caller]
pub fn sample_ascii_string(ctx: &mut TestCaseContext, len: usize) -> String {
let loc = Location::caller();
let s: String = (0..len).map(|_| sample_ascii_char_raw(ctx)).collect();
ctx.record_generated(&s, loc);
s
}
#[track_caller]
pub fn sample_ascii_printable_string(ctx: &mut TestCaseContext, len: usize) -> String {
let loc = Location::caller();
let s: String = (0..len)
.map(|_| sample_ascii_printable_char_raw(ctx))
.collect();
ctx.record_generated(&s, loc);
s
}
#[track_caller]
fn sample_char_raw(ctx: &mut TestCaseContext) -> char {
sample_with_rejection(ctx, DEFAULT_MAX_ATTEMPTS, |ctx| {
let n = u32::from_le_bytes(raw_bytes(ctx)) & 0x1F_FFFF;
char::from_u32(n)
})
}
fn sample_ascii_char_raw(ctx: &mut TestCaseContext) -> char {
(raw_bytes::<1>(ctx)[0] & 0x7F) as char
}
fn sample_ascii_printable_char_raw(ctx: &mut TestCaseContext) -> char {
(0x20 + u32::from_le_bytes(raw_bytes(ctx)) % 95) as u8 as char
}
#[track_caller]
pub fn sample_f32(ctx: &mut TestCaseContext, min: f32, max: f32) -> f32 {
assert!(
min.is_finite() && max.is_finite(),
"sample_f32: min and max must be finite"
);
assert!(min < max, "sample_f32: min must be less than max");
let loc = Location::caller();
let bits = 0x3F80_0000 | (u32::from_le_bytes(raw_bytes(ctx)) >> 9);
let unit = f32::from_bits(bits) - 1.0;
let v = min + (max - min) * unit;
ctx.record_generated(&v, loc);
v
}
#[track_caller]
pub fn sample_f64(ctx: &mut TestCaseContext, min: f64, max: f64) -> f64 {
assert!(
min.is_finite() && max.is_finite(),
"sample_f64: min and max must be finite"
);
assert!(min < max, "sample_f64: min must be less than max");
let loc = Location::caller();
let bits = 0x3FF0_0000_0000_0000 | (u64::from_le_bytes(raw_bytes(ctx)) >> 12);
let unit = f64::from_bits(bits) - 1.0;
let v = min + (max - min) * unit;
ctx.record_generated(&v, loc);
v
}
#[track_caller]
pub fn sample_f32_finite(ctx: &mut TestCaseContext) -> f32 {
let loc = Location::caller();
let v = sample_with_rejection(ctx, DEFAULT_MAX_ATTEMPTS, |ctx| {
let candidate = f32::from_bits(u32::from_le_bytes(raw_bytes(ctx)));
candidate.is_finite().then_some(candidate)
});
ctx.record_generated(&v, loc);
v
}
#[track_caller]
pub fn sample_f64_finite(ctx: &mut TestCaseContext) -> f64 {
let loc = Location::caller();
let v = sample_with_rejection(ctx, DEFAULT_MAX_ATTEMPTS, |ctx| {
let candidate = f64::from_bits(u64::from_le_bytes(raw_bytes(ctx)));
candidate.is_finite().then_some(candidate)
});
ctx.record_generated(&v, loc);
v
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_state_unadvanced(ctx: &mut TestCaseContext, fresh: &mut TestCaseContext) {
let mut a = [0u8; 8];
let mut b = [0u8; 8];
ctx.fill(&mut a);
fresh.fill(&mut b);
assert_eq!(a, b, "ctx advanced when it should not have");
}
#[test]
fn primitives_are_deterministic() {
let mut a = TestCaseContext::new(123);
let mut b = TestCaseContext::new(123);
assert_eq!(sample_bool(&mut a), sample_bool(&mut b));
assert_eq!(sample_u8(&mut a), sample_u8(&mut b));
assert_eq!(sample_u16(&mut a), sample_u16(&mut b));
assert_eq!(sample_u32(&mut a), sample_u32(&mut b));
assert_eq!(sample_u64(&mut a), sample_u64(&mut b));
assert_eq!(sample_u128(&mut a), sample_u128(&mut b));
assert_eq!(sample_usize(&mut a), sample_usize(&mut b));
assert_eq!(sample_i8(&mut a), sample_i8(&mut b));
assert_eq!(sample_i16(&mut a), sample_i16(&mut b));
assert_eq!(sample_i32(&mut a), sample_i32(&mut b));
assert_eq!(sample_i64(&mut a), sample_i64(&mut b));
assert_eq!(sample_i128(&mut a), sample_i128(&mut b));
assert_eq!(sample_isize(&mut a), sample_isize(&mut b));
}
#[test]
fn bool_produces_both_values() {
let mut ctx = TestCaseContext::new(1);
let (mut t, mut f) = (false, false);
for _ in 0..64 {
match sample_bool(&mut ctx) {
true => t = true,
false => f = true,
}
if t && f {
return;
}
}
panic!("bool samples covered only one value");
}
#[test]
fn u8_covers_both_halves_of_range() {
let mut ctx = TestCaseContext::new(1);
let (mut low, mut high) = (false, false);
for _ in 0..64 {
let v = sample_u8(&mut ctx);
low |= v < 128;
high |= v >= 128;
if low && high {
return;
}
}
panic!("u8 samples covered only one half of the range");
}
#[test]
fn i8_can_be_negative_and_nonnegative() {
let mut ctx = TestCaseContext::new(1);
let (mut neg, mut nonneg) = (false, false);
for _ in 0..64 {
let v = sample_i8(&mut ctx);
neg |= v < 0;
nonneg |= v >= 0;
if neg && nonneg {
return;
}
}
panic!("i8 samples covered only one sign");
}
#[test]
fn sample_choice_returns_only_from_slice() {
let mut ctx = TestCaseContext::new(1);
let choices = [10, 20, 30];
for _ in 0..256 {
assert!(choices.contains(&sample_choice(&mut ctx, &choices)));
}
}
#[test]
fn sample_choice_can_hit_every_element() {
let mut ctx = TestCaseContext::new(1);
let choices = [10, 20, 30];
let mut seen = [false; 3];
for _ in 0..256 {
let v = sample_choice(&mut ctx, &choices);
let idx = choices.iter().position(|&x| x == v).unwrap();
seen[idx] = true;
if seen.iter().all(|&s| s) {
return;
}
}
panic!("sample_choice did not cover all elements");
}
#[test]
#[should_panic(expected = "empty slice")]
fn sample_choice_panics_on_empty() {
let mut ctx = TestCaseContext::new(0);
let empty: [u32; 0] = [];
let _ = sample_choice(&mut ctx, &empty);
}
#[test]
fn sample_choice_works_with_clone_only_types() {
let mut ctx = TestCaseContext::new(1);
let choices = vec![String::from("a"), String::from("b"), String::from("c")];
let picked = sample_choice(&mut ctx, &choices);
assert!(choices.contains(&picked));
}
#[test]
fn char_generators_are_deterministic() {
let mut a = TestCaseContext::new(789);
let mut b = TestCaseContext::new(789);
assert_eq!(sample_char(&mut a), sample_char(&mut b));
assert_eq!(sample_ascii_char(&mut a), sample_ascii_char(&mut b));
assert_eq!(
sample_ascii_printable_char(&mut a),
sample_ascii_printable_char(&mut b)
);
}
#[test]
fn sample_ascii_char_always_in_ascii_range() {
let mut ctx = TestCaseContext::new(1);
for _ in 0..1000 {
let c = sample_ascii_char(&mut ctx);
assert!(c.is_ascii());
}
}
#[test]
fn sample_ascii_printable_char_always_in_range() {
let mut ctx = TestCaseContext::new(1);
for _ in 0..1000 {
let c = sample_ascii_printable_char(&mut ctx);
let n = c as u32;
assert!((0x20..=0x7E).contains(&n));
}
}
#[test]
fn sample_string_returns_requested_code_point_count() {
let mut ctx = TestCaseContext::new(1);
for len in [0, 1, 7, 32] {
let s = sample_string(&mut ctx, len);
assert_eq!(
s.chars().count(),
len,
"sample_string(ctx, {len}) chars().count() differed"
);
}
}
#[test]
fn sample_ascii_string_returns_ascii_bytes_of_requested_length() {
let mut ctx = TestCaseContext::new(1);
for len in [0, 1, 7, 32] {
let s = sample_ascii_string(&mut ctx, len);
assert_eq!(s.chars().count(), len);
assert_eq!(s.len(), len, "ASCII code point count == byte count");
assert!(s.is_ascii());
}
}
#[test]
fn sample_ascii_printable_string_returns_printable_range() {
let mut ctx = TestCaseContext::new(1);
for len in [0, 1, 7, 32] {
let s = sample_ascii_printable_string(&mut ctx, len);
assert_eq!(s.chars().count(), len);
assert_eq!(s.len(), len);
assert!(
s.chars().all(|c| (0x20..=0x7E).contains(&(c as u32))),
"found non-printable in {s:?}"
);
}
}
#[test]
fn string_generators_are_deterministic() {
let mut a = TestCaseContext::new(0x517A_2E70);
let mut b = TestCaseContext::new(0x517A_2E70);
for len in [0, 3, 16] {
assert_eq!(sample_string(&mut a, len), sample_string(&mut b, len));
assert_eq!(
sample_ascii_string(&mut a, len),
sample_ascii_string(&mut b, len)
);
assert_eq!(
sample_ascii_printable_string(&mut a, len),
sample_ascii_printable_string(&mut b, len)
);
}
}
#[test]
fn sample_string_zero_length_is_empty() {
let mut ctx = TestCaseContext::new(1);
assert_eq!(sample_string(&mut ctx, 0), "");
assert_eq!(sample_ascii_string(&mut ctx, 0), "");
assert_eq!(sample_ascii_printable_string(&mut ctx, 0), "");
}
#[test]
fn sample_char_produces_varied_values() {
let mut ctx = TestCaseContext::new(1);
let mut seen = std::collections::HashSet::new();
for _ in 0..256 {
seen.insert(sample_char(&mut ctx));
}
assert!(
seen.len() > 200,
"sample_char produced too few distinct values: {}",
seen.len()
);
}
#[test]
fn float_generators_are_deterministic() {
let mut a = TestCaseContext::new(999);
let mut b = TestCaseContext::new(999);
assert_eq!(sample_f32(&mut a, 0.0, 1.0), sample_f32(&mut b, 0.0, 1.0));
assert_eq!(
sample_f64(&mut a, -100.0, 100.0),
sample_f64(&mut b, -100.0, 100.0)
);
}
#[test]
fn sample_f32_stays_in_range() {
let mut ctx = TestCaseContext::new(1);
for _ in 0..1000 {
let v = sample_f32(&mut ctx, 10.0, 20.0);
assert!((10.0..20.0).contains(&v), "out of range: {v}");
}
}
#[test]
fn sample_f64_stays_in_range() {
let mut ctx = TestCaseContext::new(1);
for _ in 0..1000 {
let v = sample_f64(&mut ctx, -1.0, 1.0);
assert!((-1.0..1.0).contains(&v), "out of range: {v}");
}
}
#[test]
fn sample_f32_covers_both_halves_of_range() {
let mut ctx = TestCaseContext::new(1);
let (mut low, mut high) = (false, false);
for _ in 0..64 {
let v = sample_f32(&mut ctx, 0.0, 1.0);
low |= v < 0.5;
high |= v >= 0.5;
if low && high {
return;
}
}
panic!("sample_f32 covered only one half of the range");
}
#[test]
#[should_panic(expected = "must be less than")]
fn sample_f32_panics_when_min_equals_max() {
let mut ctx = TestCaseContext::new(0);
let _ = sample_f32(&mut ctx, 5.0, 5.0);
}
#[test]
#[should_panic(expected = "must be finite")]
fn sample_f32_panics_on_nan() {
let mut ctx = TestCaseContext::new(0);
let _ = sample_f32(&mut ctx, f32::NAN, 1.0);
}
#[test]
#[should_panic(expected = "must be finite")]
fn sample_f32_panics_on_infinity() {
let mut ctx = TestCaseContext::new(0);
let _ = sample_f32(&mut ctx, 0.0, f32::INFINITY);
}
#[test]
#[should_panic(expected = "must be finite")]
fn sample_f64_panics_on_nan() {
let mut ctx = TestCaseContext::new(0);
let _ = sample_f64(&mut ctx, 0.0, f64::NAN);
}
#[test]
fn sample_f32_finite_always_returns_finite() {
let mut ctx = TestCaseContext::new(1);
for _ in 0..10_000 {
let v = sample_f32_finite(&mut ctx);
assert!(v.is_finite(), "expected finite, got {v:?}");
assert!(!v.is_nan(), "expected non-NaN, got {v:?}");
}
}
#[test]
fn sample_f64_finite_always_returns_finite() {
let mut ctx = TestCaseContext::new(1);
for _ in 0..10_000 {
let v = sample_f64_finite(&mut ctx);
assert!(v.is_finite(), "expected finite, got {v:?}");
assert!(!v.is_nan(), "expected non-NaN, got {v:?}");
}
}
#[test]
fn finite_float_generators_are_deterministic() {
let mut a = TestCaseContext::new(0xF10A_7000);
let mut b = TestCaseContext::new(0xF10A_7000);
for _ in 0..64 {
assert_eq!(sample_f32_finite(&mut a), sample_f32_finite(&mut b));
assert_eq!(sample_f64_finite(&mut a), sample_f64_finite(&mut b));
}
}
#[test]
fn sample_f32_finite_covers_both_signs() {
let mut ctx = TestCaseContext::new(2);
let (mut pos, mut neg) = (false, false);
for _ in 0..256 {
let v = sample_f32_finite(&mut ctx);
if v > 0.0 {
pos = true;
} else if v < 0.0 {
neg = true;
}
if pos && neg {
return;
}
}
panic!("sample_f32_finite did not cover both signs");
}
#[test]
fn sample_below_one_returns_zero_without_drawing() {
let mut ctx = TestCaseContext::new(1);
let mut fresh = TestCaseContext::new(1);
assert_eq!(sample_below(&mut ctx, 1), 0);
assert_state_unadvanced(&mut ctx, &mut fresh);
}
#[test]
fn sample_below_stays_in_range() {
let mut ctx = TestCaseContext::new(42);
for _ in 0..10_000 {
let v = sample_below(&mut ctx, 7);
assert!(v < 7);
}
}
#[test]
fn sample_below_hits_every_value() {
let mut ctx = TestCaseContext::new(42);
let mut seen = [false; 5];
for _ in 0..1024 {
let v = sample_below(&mut ctx, 5) as usize;
seen[v] = true;
if seen.iter().all(|&s| s) {
return;
}
}
panic!("sample_below did not cover all values");
}
#[test]
fn sample_below_is_roughly_uniform_on_non_divisor() {
let mut ctx = TestCaseContext::new(7);
let mut counts = [0usize; 3];
let total = 30_000;
for _ in 0..total {
counts[sample_below(&mut ctx, 3) as usize] += 1;
}
let expected = total / 3;
for c in counts {
assert!(
c.abs_diff(expected) < expected / 10,
"bucket count off: {c} vs {expected}"
);
}
}
#[test]
fn sample_below_accepts_full_u64_domain() {
let mut ctx = TestCaseContext::new(1);
let _v = sample_below(&mut ctx, u64::MAX);
}
#[test]
fn sample_usize_in_exclusive_stays_in_range() {
let mut ctx = TestCaseContext::new(1);
for _ in 0..1000 {
let v = sample_usize_in(&mut ctx, 10..20);
assert!((10..20).contains(&v));
}
}
#[test]
fn sample_usize_in_inclusive_stays_in_range() {
let mut ctx = TestCaseContext::new(1);
for _ in 0..1000 {
let v = sample_usize_in(&mut ctx, 10..=20);
assert!((10..=20).contains(&v));
}
}
#[test]
fn sample_usize_in_single_element_returns_that_element() {
let mut ctx = TestCaseContext::new(1);
let mut fresh = TestCaseContext::new(1);
assert_eq!(sample_usize_in(&mut ctx, 5..=5), 5);
assert_state_unadvanced(&mut ctx, &mut fresh);
}
#[test]
fn sample_usize_in_hits_both_endpoints() {
let mut ctx = TestCaseContext::new(1);
let (mut lo, mut hi) = (false, false);
for _ in 0..1024 {
let v = sample_usize_in(&mut ctx, 0..=3);
lo |= v == 0;
hi |= v == 3;
if lo && hi {
return;
}
}
panic!("sample_usize_in did not cover both endpoints");
}
#[test]
fn sample_usize_in_full_range_stays_in_range() {
let mut ctx = TestCaseContext::new(1);
for _ in 0..100 {
let _v = sample_usize_in(&mut ctx, ..);
}
}
#[test]
fn sample_usize_in_inclusive_up_to_max_stays_in_range() {
let mut ctx = TestCaseContext::new(1);
for _ in 0..100 {
let v = sample_usize_in(&mut ctx, 1..=usize::MAX);
assert!(v >= 1);
}
}
#[test]
fn sample_usize_in_unbounded_end_stays_in_range() {
let mut ctx = TestCaseContext::new(1);
for _ in 0..100 {
let v = sample_usize_in(&mut ctx, 100..);
assert!(v >= 100);
}
}
#[test]
fn sample_usize_in_is_deterministic() {
let mut a = TestCaseContext::new(999);
let mut b = TestCaseContext::new(999);
for _ in 0..64 {
assert_eq!(
sample_usize_in(&mut a, 0..137),
sample_usize_in(&mut b, 0..137)
);
}
}
#[test]
#[should_panic(expected = "empty range")]
fn sample_usize_in_panics_on_empty_exclusive() {
let mut ctx = TestCaseContext::new(0);
let _ = sample_usize_in(&mut ctx, 5..5);
}
#[test]
#[should_panic(expected = "empty range")]
fn sample_usize_in_panics_on_reversed_inclusive() {
let mut ctx = TestCaseContext::new(0);
#[expect(clippy::reversed_empty_ranges)]
let _ = sample_usize_in(&mut ctx, 5..=4);
}
#[test]
#[should_panic(expected = "empty range")]
fn sample_usize_in_panics_on_zero_exclusive_end() {
let mut ctx = TestCaseContext::new(0);
let _ = sample_usize_in(&mut ctx, ..0);
}
#[test]
#[should_panic(expected = "empty range")]
fn sample_usize_in_panics_on_excluded_max_start() {
let mut ctx = TestCaseContext::new(0);
let _ = sample_usize_in(
&mut ctx,
(
std::ops::Bound::Excluded(usize::MAX),
std::ops::Bound::<usize>::Unbounded,
),
);
}
#[test]
fn sample_ratio_zero_numerator_always_false_and_draws_nothing() {
let mut ctx = TestCaseContext::new(1);
let mut fresh = TestCaseContext::new(1);
for _ in 0..64 {
assert!(!sample_ratio(&mut ctx, 0, 10));
}
assert_state_unadvanced(&mut ctx, &mut fresh);
}
#[test]
fn sample_ratio_full_numerator_always_true_and_draws_nothing() {
let mut ctx = TestCaseContext::new(1);
let mut fresh = TestCaseContext::new(1);
for _ in 0..64 {
assert!(sample_ratio(&mut ctx, 7, 7));
}
assert_state_unadvanced(&mut ctx, &mut fresh);
}
#[test]
fn sample_ratio_produces_both_outcomes() {
let mut ctx = TestCaseContext::new(1);
let (mut t, mut f) = (false, false);
for _ in 0..256 {
match sample_ratio(&mut ctx, 1, 2) {
true => t = true,
false => f = true,
}
if t && f {
return;
}
}
panic!("sample_ratio did not produce both outcomes");
}
#[test]
fn sample_ratio_is_deterministic() {
let mut a = TestCaseContext::new(999);
let mut b = TestCaseContext::new(999);
for _ in 0..64 {
assert_eq!(sample_ratio(&mut a, 3, 7), sample_ratio(&mut b, 3, 7));
}
}
#[test]
fn sample_ratio_biased_matches_expected_frequency() {
let mut ctx = TestCaseContext::new(1);
let mut trues: usize = 0;
let total: usize = 10_000;
for _ in 0..total {
if sample_ratio(&mut ctx, 1, 10) {
trues += 1;
}
}
let expected = total / 10;
assert!(
trues.abs_diff(expected) < expected / 2,
"sample_ratio(1, 10) frequency off: {trues}/{total}"
);
}
#[test]
#[should_panic(expected = "denominator must be non-zero")]
fn sample_ratio_panics_on_zero_denominator() {
let mut ctx = TestCaseContext::new(0);
let _ = sample_ratio(&mut ctx, 0, 0);
}
#[test]
#[should_panic(expected = "must be <= denominator")]
fn sample_ratio_panics_when_numerator_exceeds_denominator() {
let mut ctx = TestCaseContext::new(0);
let _ = sample_ratio(&mut ctx, 11, 10);
}
#[test]
fn sample_weighted_index_stays_in_range() {
let mut ctx = TestCaseContext::new(1);
for _ in 0..1000 {
let idx = sample_weighted_index(&mut ctx, &[1, 2, 3, 4]);
assert!(idx < 4);
}
}
#[test]
fn sample_weighted_index_hits_every_nonzero_index() {
let mut ctx = TestCaseContext::new(1);
let weights = [1, 1, 1];
let mut seen = [false; 3];
for _ in 0..1024 {
seen[sample_weighted_index(&mut ctx, &weights)] = true;
if seen.iter().all(|&s| s) {
return;
}
}
panic!("sample_weighted_index did not cover all non-zero indices");
}
#[test]
fn sample_weighted_index_skips_zero_weight_slot() {
let mut ctx = TestCaseContext::new(1);
for _ in 0..1000 {
let idx = sample_weighted_index(&mut ctx, &[3, 0, 5]);
assert_ne!(idx, 1, "index 1 has weight 0 and must never be picked");
}
}
#[test]
fn sample_weighted_index_single_nonzero_always_returns_it() {
let mut ctx = TestCaseContext::new(1);
for _ in 0..100 {
assert_eq!(sample_weighted_index(&mut ctx, &[0, 0, 42, 0]), 2);
}
}
#[test]
fn sample_weighted_index_is_deterministic() {
let mut a = TestCaseContext::new(123);
let mut b = TestCaseContext::new(123);
let weights = [4, 1, 2, 3];
for _ in 0..64 {
assert_eq!(
sample_weighted_index(&mut a, &weights),
sample_weighted_index(&mut b, &weights)
);
}
}
#[test]
fn sample_weighted_index_frequencies_approximate_weights() {
let mut ctx = TestCaseContext::new(1);
let weights = [1, 2, 3];
let mut counts = [0usize; 3];
let total = 12_000;
for _ in 0..total {
counts[sample_weighted_index(&mut ctx, &weights)] += 1;
}
for (i, (&c, &w)) in counts.iter().zip(weights.iter()).enumerate() {
let expected = total * w as usize / 6;
assert!(
c.abs_diff(expected) * 100 < expected * 30,
"index {i}: got {c}, expected ~{expected}"
);
}
}
#[test]
#[should_panic(expected = "empty weights")]
fn sample_weighted_index_panics_on_empty() {
let mut ctx = TestCaseContext::new(0);
let _ = sample_weighted_index(&mut ctx, &[]);
}
#[test]
#[should_panic(expected = "all weights are zero")]
fn sample_weighted_index_panics_when_all_weights_zero() {
let mut ctx = TestCaseContext::new(0);
let _ = sample_weighted_index(&mut ctx, &[0, 0, 0]);
}
use crate::rng::{ChoiceSequence, RecordingSession, ReplayError, ReplaySession};
fn composite_case(
ctx: &mut TestCaseContext,
) -> (Vec<char>, Vec<std::num::NonZero<u8>>, Vec<u8>, u32) {
let branch = sample_usize_in(ctx, 0..3);
let chars = if branch == 0 {
Vec::new()
} else {
(0..branch).map(|_| sample_char(ctx)).collect()
};
let nz_count = sample_usize_in(ctx, 1..=4);
let mut nzs = Vec::with_capacity(nz_count);
for _ in 0..nz_count {
let nz = sample_with_rejection(ctx, DEFAULT_MAX_ATTEMPTS, |ctx| {
std::num::NonZero::new(sample_u8(ctx))
});
nzs.push(nz);
}
let buf_len = sample_usize_in(ctx, 0..=16);
let bytes = sample_bytes_vec(ctx, buf_len);
let tail = match sample_usize_in(ctx, 0..3) {
0 => sample_u32(ctx),
1 => sample_u32(ctx).wrapping_add(1),
_ => 0,
};
(chars, nzs, bytes, tail)
}
#[test]
fn replay_reproduces_composite_generator_bit_exact() {
for seed in [1u64, 42, 0xDEAD_BEEF, 0xFEED_FACE] {
let (expected, seq) = RecordingSession::new(seed).run(composite_case);
let replayed = ReplaySession::new(seq)
.run(composite_case)
.expect("replay of same generator must succeed");
assert_eq!(replayed, expected, "seed {seed:#x}");
}
}
#[test]
fn replay_stops_generator_on_first_structural_mismatch() {
let (_, seq) = RecordingSession::new(1).run(|ctx| {
let _ = sample_u8(ctx);
let _ = sample_u8(ctx);
});
let flag = std::cell::Cell::new(false);
let result = ReplaySession::new(seq).run(|ctx| {
let _ = sample_u8(ctx); let _ = sample_u32(ctx); flag.set(true); });
assert!(
matches!(
result,
Err(ReplayError::DrawLengthMismatch {
expected: 1,
actual: 4,
})
),
"unexpected replay result: {result:?}"
);
assert!(!flag.get(), "generator continued past replay mismatch");
}
#[test]
fn replay_after_sample_bytes_vec_matches_bytes() {
let (recorded, seq) = RecordingSession::new(7).run(|ctx| sample_bytes_vec(ctx, 100));
let replayed = ReplaySession::new(seq)
.run(|ctx| sample_bytes_vec(ctx, 100))
.expect("same-length replay must succeed");
assert_eq!(replayed, recorded);
}
#[test]
fn recorded_composite_case_matches_plain_prng() {
let seed = 0xABCD_1234u64;
let (recorded_value, _seq) = RecordingSession::new(seed).run(composite_case);
let mut prng = TestCaseContext::new(seed);
let plain_value = composite_case(&mut prng);
assert_eq!(recorded_value, plain_value);
}
#[test]
fn empty_sequence_replay_succeeds_when_generator_draws_nothing() {
let seq = ChoiceSequence::default();
let result = ReplaySession::new(seq).run(|_ctx| 7u32);
assert_eq!(result, Ok(7));
}
#[test]
#[should_panic(expected = "max_attempts must be > 0")]
fn sample_with_rejection_panics_on_zero_max_attempts() {
let mut ctx = TestCaseContext::new(0);
let _: u32 = sample_with_rejection(&mut ctx, 0, |_| Some(1));
}
#[test]
fn sample_with_rejection_returns_first_accepted_value() {
let mut ctx = TestCaseContext::new(0);
let v = sample_with_rejection(&mut ctx, 8, |_ctx| Some(42u32));
assert_eq!(v, 42);
}
#[test]
fn sample_with_rejection_skips_rejected_attempts_and_returns_accepted() {
let mut ctx = TestCaseContext::new(0);
let counter = std::cell::Cell::new(0usize);
let v = sample_with_rejection(&mut ctx, 8, |_ctx| {
let n = counter.get();
counter.set(n + 1);
if n == 3 { Some(n as u32) } else { None }
});
assert_eq!(v, 3);
assert_eq!(counter.get(), 4);
}
#[test]
fn sample_with_rejection_records_nested_spans_in_recording_mode() {
let (v, seq): ((u8, u8), _) = RecordingSession::new(1).run(|ctx| {
sample_with_rejection(ctx, 8, |ctx| {
let x = sample_u8(ctx);
let y = sample_with_rejection(ctx, 8, |ctx| Some(sample_u8(ctx)));
Some((x, y))
})
});
assert_eq!(v.0, seq.draws()[0][0]);
assert_eq!(v.1, seq.draws()[1][0]);
assert_eq!(seq.spans().len(), 2);
assert_eq!(seq.spans()[0].parent, None);
assert_eq!(seq.spans()[0].verdict, AttemptVerdict::Accepted);
assert_eq!(seq.spans()[1].parent, Some(0));
assert_eq!(seq.spans()[1].verdict, AttemptVerdict::Accepted);
}
#[test]
fn sample_with_rejection_records_rejected_spans_in_recording_mode() {
let (v, seq) = RecordingSession::new(2).run(|ctx| {
let counter = std::cell::Cell::new(0);
sample_with_rejection(ctx, 8, |_ctx| {
let n = counter.get();
counter.set(n + 1);
if n < 2 { None } else { Some(n as u32) }
})
});
assert_eq!(v, 2);
assert_eq!(seq.spans().len(), 3);
assert_eq!(seq.spans()[0].verdict, AttemptVerdict::Rejected);
assert_eq!(seq.spans()[1].verdict, AttemptVerdict::Rejected);
assert_eq!(seq.spans()[2].verdict, AttemptVerdict::Accepted);
assert!(seq.spans().iter().all(|s| s.parent.is_none()));
}
#[test]
fn sample_with_rejection_allows_drawless_rejected_attempts() {
let (v, seq) = RecordingSession::new(1).run(|ctx| {
let counter = std::cell::Cell::new(0);
sample_with_rejection(ctx, 4, |_ctx| {
let n = counter.get();
counter.set(n + 1);
if n < 2 { None } else { Some(n as u32) }
})
});
assert_eq!(v, 2);
assert_eq!(seq.spans().len(), 3);
for span in seq.spans() {
assert_eq!(span.start_draw, span.end_draw); }
assert!(seq.draws().is_empty());
}
}