use std::ops::{Bound, RangeBounds};
use std::panic::Location;
use crate::TestCaseContext;
#[track_caller]
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 {
if let Some(value) = attempt(ctx) {
return value;
}
}
if !ctx.is_inside_runner() {
panic!(
"noprop::sample_with_rejection: all {max_attempts} attempts were rejected. \
Outside a Runner::run closure there is no iteration boundary to unwind to, \
so exhaustion cannot be reported as a rejection. Wrap the call in a Runner, \
raise max_attempts, or ensure at least one attempt returns Some(_)."
);
}
ctx.reject_case()
}
#[track_caller]
fn sample_below(ctx: &mut TestCaseContext, n: u64) -> 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 {
return u64::from_le_bytes(raw_bytes(ctx)) % n;
}
let bound = u64::MAX - r;
sample_with_rejection(ctx, DEFAULT_MAX_ATTEMPTS, |ctx| {
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(ctx, choices.len() as u64) 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 {
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_u64_in<R: RangeBounds<u64>>(ctx: &mut TestCaseContext, range: R) -> u64 {
let loc = Location::caller();
let lo = match range.start_bound() {
Bound::Included(&s) => s,
Bound::Excluded(&s) => s.checked_add(1).expect("sample_u64_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_u64_in: empty range"),
Bound::Unbounded => u64::MAX,
};
assert!(lo <= hi, "sample_u64_in: empty range");
let v = if lo == 0 && hi == u64::MAX {
u64::from_le_bytes(raw_bytes(ctx))
} else {
let width = hi - lo + 1;
lo + sample_below(ctx, width)
};
ctx.record_generated(&v, loc);
v
}
#[track_caller]
pub fn sample_u8_in<R: RangeBounds<u8>>(ctx: &mut TestCaseContext, range: R) -> u8 {
let loc = Location::caller();
let lo = match range.start_bound() {
Bound::Included(&s) => s,
Bound::Excluded(&s) => s.checked_add(1).expect("sample_u8_in: empty range"),
Bound::Unbounded => u8::MIN,
};
let hi = match range.end_bound() {
Bound::Included(&e) => e,
Bound::Excluded(&e) => e.checked_sub(1).expect("sample_u8_in: empty range"),
Bound::Unbounded => u8::MAX,
};
assert!(lo <= hi, "sample_u8_in: empty range");
let v = (lo as u64 + sample_below(ctx, hi as u64 - lo as u64 + 1)) as u8;
ctx.record_generated(&v, loc);
v
}
#[track_caller]
pub fn sample_u16_in<R: RangeBounds<u16>>(ctx: &mut TestCaseContext, range: R) -> u16 {
let loc = Location::caller();
let lo = match range.start_bound() {
Bound::Included(&s) => s,
Bound::Excluded(&s) => s.checked_add(1).expect("sample_u16_in: empty range"),
Bound::Unbounded => u16::MIN,
};
let hi = match range.end_bound() {
Bound::Included(&e) => e,
Bound::Excluded(&e) => e.checked_sub(1).expect("sample_u16_in: empty range"),
Bound::Unbounded => u16::MAX,
};
assert!(lo <= hi, "sample_u16_in: empty range");
let v = (lo as u64 + sample_below(ctx, hi as u64 - lo as u64 + 1)) as u16;
ctx.record_generated(&v, loc);
v
}
#[track_caller]
pub fn sample_u32_in<R: RangeBounds<u32>>(ctx: &mut TestCaseContext, range: R) -> u32 {
let loc = Location::caller();
let lo = match range.start_bound() {
Bound::Included(&s) => s,
Bound::Excluded(&s) => s.checked_add(1).expect("sample_u32_in: empty range"),
Bound::Unbounded => u32::MIN,
};
let hi = match range.end_bound() {
Bound::Included(&e) => e,
Bound::Excluded(&e) => e.checked_sub(1).expect("sample_u32_in: empty range"),
Bound::Unbounded => u32::MAX,
};
assert!(lo <= hi, "sample_u32_in: empty range");
let v = (lo as u64 + sample_below(ctx, hi as u64 - lo as u64 + 1)) as u32;
ctx.record_generated(&v, loc);
v
}
#[track_caller]
pub fn sample_i8_in<R: RangeBounds<i8>>(ctx: &mut TestCaseContext, range: R) -> i8 {
let loc = Location::caller();
let lo = match range.start_bound() {
Bound::Included(&s) => s,
Bound::Excluded(&s) => s.checked_add(1).expect("sample_i8_in: empty range"),
Bound::Unbounded => i8::MIN,
};
let hi = match range.end_bound() {
Bound::Included(&e) => e,
Bound::Excluded(&e) => e.checked_sub(1).expect("sample_i8_in: empty range"),
Bound::Unbounded => i8::MAX,
};
assert!(lo <= hi, "sample_i8_in: empty range");
let width = (hi as i64 - lo as i64) as u64 + 1;
let v = (lo as i64 + sample_below(ctx, width) as i64) as i8;
ctx.record_generated(&v, loc);
v
}
#[track_caller]
pub fn sample_i16_in<R: RangeBounds<i16>>(ctx: &mut TestCaseContext, range: R) -> i16 {
let loc = Location::caller();
let lo = match range.start_bound() {
Bound::Included(&s) => s,
Bound::Excluded(&s) => s.checked_add(1).expect("sample_i16_in: empty range"),
Bound::Unbounded => i16::MIN,
};
let hi = match range.end_bound() {
Bound::Included(&e) => e,
Bound::Excluded(&e) => e.checked_sub(1).expect("sample_i16_in: empty range"),
Bound::Unbounded => i16::MAX,
};
assert!(lo <= hi, "sample_i16_in: empty range");
let width = (hi as i64 - lo as i64) as u64 + 1;
let v = (lo as i64 + sample_below(ctx, width) as i64) as i16;
ctx.record_generated(&v, loc);
v
}
#[track_caller]
pub fn sample_i32_in<R: RangeBounds<i32>>(ctx: &mut TestCaseContext, range: R) -> i32 {
let loc = Location::caller();
let lo = match range.start_bound() {
Bound::Included(&s) => s,
Bound::Excluded(&s) => s.checked_add(1).expect("sample_i32_in: empty range"),
Bound::Unbounded => i32::MIN,
};
let hi = match range.end_bound() {
Bound::Included(&e) => e,
Bound::Excluded(&e) => e.checked_sub(1).expect("sample_i32_in: empty range"),
Bound::Unbounded => i32::MAX,
};
assert!(lo <= hi, "sample_i32_in: empty range");
let width = (hi as i64 - lo as i64) as u64 + 1;
let v = (lo as i64 + sample_below(ctx, width) as i64) as i32;
ctx.record_generated(&v, loc);
v
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Ratio {
numerator: u32,
denominator: u32,
}
impl Ratio {
#[track_caller]
pub const fn new(numerator: u32, denominator: u32) -> Self {
if denominator == 0 {
panic!("Ratio::new: denominator must be non-zero");
}
if numerator > denominator {
panic!("Ratio::new: numerator must not exceed denominator");
}
Self {
numerator,
denominator,
}
}
#[track_caller]
pub const fn one_nth(n: u32) -> Self {
if n == 0 {
panic!("Ratio::one_nth: n must be non-zero");
}
Self {
numerator: 1,
denominator: n,
}
}
}
#[track_caller]
pub fn sample_ratio(ctx: &mut TestCaseContext, ratio: Ratio) -> bool {
let loc = Location::caller();
let v = if ratio.numerator == 0 {
false
} else if ratio.numerator == ratio.denominator {
true
} else {
sample_below(ctx, ratio.denominator as u64) < ratio.numerator as u64
};
ctx.record_generated(&v, loc);
v
}
#[track_caller]
pub fn sample_with_boundaries<T, F>(
ctx: &mut TestCaseContext,
boundaries: &[T],
ratio: Ratio,
sample: F,
) -> T
where
T: Clone + std::fmt::Debug + 'static,
F: FnOnce(&mut TestCaseContext) -> T,
{
assert!(
!boundaries.is_empty(),
"sample_with_boundaries: empty boundaries"
);
if sample_ratio(ctx, ratio) {
sample_choice(ctx, boundaries)
} else {
sample(ctx)
}
}
#[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();
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();
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();
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();
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();
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();
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();
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();
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();
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();
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();
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();
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 + sample_below(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 + sample_below(ctx, 95)) as u8 as char
}
#[track_caller]
pub fn sample_f32_in(ctx: &mut TestCaseContext, min: f32, max: f32) -> f32 {
assert!(
min.is_finite() && max.is_finite(),
"sample_f32_in: min and max must be finite"
);
assert!(min < max, "sample_f32_in: min must be less than max");
let span = max - min;
assert!(
span.is_finite(),
"sample_f32_in: max - min must be finite (max - min = {span}); split the range"
);
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 + span * unit).min(max.next_down());
ctx.record_generated(&v, loc);
v
}
#[track_caller]
pub fn sample_f64_in(ctx: &mut TestCaseContext, min: f64, max: f64) -> f64 {
assert!(
min.is_finite() && max.is_finite(),
"sample_f64_in: min and max must be finite"
);
assert!(min < max, "sample_f64_in: min must be less than max");
let span = max - min;
assert!(
span.is_finite(),
"sample_f64_in: max - min must be finite (max - min = {span}); split the range"
);
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 + span * unit).min(max.next_down());
ctx.record_generated(&v, loc);
v
}
#[track_caller]
pub fn sample_f32(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(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)
.expect("sampled value must come from the choices array");
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_ascii_printable_char_matches_unbiased_core() {
let mut core = TestCaseContext::new(0x9E37_79B9_7F4A_7C15);
let mut sampler = TestCaseContext::new(0x9E37_79B9_7F4A_7C15);
for _ in 0..1024 {
let expected = (0x20 + sample_below(&mut core, 95)) as u8 as char;
assert_eq!(sample_ascii_printable_char(&mut sampler), expected);
}
}
#[test]
fn sample_ascii_printable_char_is_roughly_uniform() {
let mut ctx = TestCaseContext::new(7);
let mut counts = [0usize; 95];
let total = 190_000;
for _ in 0..total {
let idx = (sample_ascii_printable_char(&mut ctx) as u32 - 0x20) as usize;
counts[idx] += 1;
}
let expected = total / 95;
for (i, c) in counts.iter().enumerate() {
assert!(
c.abs_diff(expected) < expected / 10,
"bucket {i} count off: {c} vs {expected}"
);
}
}
#[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 sample_ascii_printable_string_matches_unbiased_core() {
let mut core = TestCaseContext::new(0xDEAD_BEEF_CAFE_F00D);
let mut sampler = TestCaseContext::new(0xDEAD_BEEF_CAFE_F00D);
for len in [1, 7, 32] {
let expected: String = (0..len)
.map(|_| (0x20 + sample_below(&mut core, 95)) as u8 as char)
.collect();
assert_eq!(sample_ascii_printable_string(&mut sampler, len), expected);
}
}
#[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_in(&mut a, 0.0, 1.0),
sample_f32_in(&mut b, 0.0, 1.0)
);
assert_eq!(
sample_f64_in(&mut a, -100.0, 100.0),
sample_f64_in(&mut b, -100.0, 100.0)
);
}
#[test]
fn sample_f32_in_stays_in_range() {
let mut ctx = TestCaseContext::new(1);
for _ in 0..1000 {
let v = sample_f32_in(&mut ctx, 10.0, 20.0);
assert!((10.0..20.0).contains(&v), "out of range: {v}");
}
}
#[test]
fn sample_f64_in_stays_in_range() {
let mut ctx = TestCaseContext::new(1);
for _ in 0..1000 {
let v = sample_f64_in(&mut ctx, -1.0, 1.0);
assert!((-1.0..1.0).contains(&v), "out of range: {v}");
}
}
#[test]
fn sample_f32_in_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_in(&mut ctx, 0.0, 1.0);
low |= v < 0.5;
high |= v >= 0.5;
if low && high {
return;
}
}
panic!("sample_f32_in covered only one half of the range");
}
#[test]
#[should_panic(expected = "must be less than")]
fn sample_f32_in_panics_when_min_equals_max() {
let mut ctx = TestCaseContext::new(0);
let _ = sample_f32_in(&mut ctx, 5.0, 5.0);
}
#[test]
#[should_panic(expected = "must be finite")]
fn sample_f32_in_panics_on_nan() {
let mut ctx = TestCaseContext::new(0);
let _ = sample_f32_in(&mut ctx, f32::NAN, 1.0);
}
#[test]
#[should_panic(expected = "must be finite")]
fn sample_f32_in_panics_on_infinity() {
let mut ctx = TestCaseContext::new(0);
let _ = sample_f32_in(&mut ctx, 0.0, f32::INFINITY);
}
#[test]
#[should_panic(expected = "must be finite")]
fn sample_f64_in_panics_on_nan() {
let mut ctx = TestCaseContext::new(0);
let _ = sample_f64_in(&mut ctx, 0.0, f64::NAN);
}
#[test]
#[should_panic(expected = "must be finite")]
fn sample_f64_in_panics_on_infinity() {
let mut ctx = TestCaseContext::new(0);
let _ = sample_f64_in(&mut ctx, 0.0, f64::INFINITY);
}
#[test]
#[should_panic(expected = "must be less than")]
fn sample_f64_in_panics_when_min_equals_max() {
let mut ctx = TestCaseContext::new(0);
let _ = sample_f64_in(&mut ctx, 5.0, 5.0);
}
#[test]
#[should_panic(expected = "max - min must be finite")]
fn sample_f32_in_panics_when_span_overflows() {
let mut ctx = TestCaseContext::new(0);
let _ = sample_f32_in(&mut ctx, f32::MIN, f32::MAX);
}
#[test]
#[should_panic(expected = "max - min must be finite")]
fn sample_f64_in_panics_when_span_overflows() {
let mut ctx = TestCaseContext::new(0);
let _ = sample_f64_in(&mut ctx, f64::MIN, f64::MAX);
}
#[test]
fn sample_f32_in_stays_finite_at_widest_admissible_span() {
let mut ctx = TestCaseContext::new(1);
for _ in 0..1000 {
let v = sample_f32_in(&mut ctx, 0.0, f32::MAX);
assert!(v.is_finite(), "expected finite, got {v}");
assert!((0.0..f32::MAX).contains(&v), "out of range: {v}");
}
}
#[test]
fn sample_f64_in_stays_finite_at_widest_admissible_span() {
let mut ctx = TestCaseContext::new(1);
for _ in 0..1000 {
let v = sample_f64_in(&mut ctx, 0.0, f64::MAX);
assert!(v.is_finite(), "expected finite, got {v}");
assert!((0.0..f64::MAX).contains(&v), "out of range: {v}");
}
}
#[test]
fn sample_f32_in_stays_strictly_below_max_at_ulp_span() {
let mut ctx = TestCaseContext::new(1);
let min = 1.0f32;
let max = 1.0f32 + 2f32.powi(-23);
for _ in 0..10_000 {
let v = sample_f32_in(&mut ctx, min, max);
assert!(v >= min, "v={v} < min={min}");
assert!(v < max, "v={v} reached the excluded upper bound {max}");
}
}
#[test]
fn sample_f32_in_stays_strictly_below_max_across_exponent_band() {
let mut ctx = TestCaseContext::new(1);
let (min, max) = (2f32.powi(20), 2f32.powi(20) + 1.0);
for _ in 0..10_000 {
let v = sample_f32_in(&mut ctx, min, max);
assert!(v < max, "v={v} reached the excluded upper bound {max}");
}
}
#[test]
fn sample_f64_in_stays_strictly_below_max_at_ulp_span() {
let mut ctx = TestCaseContext::new(1);
let min = 1.0f64;
let max = 1.0f64 + 2f64.powi(-52);
for _ in 0..10_000 {
let v = sample_f64_in(&mut ctx, min, max);
assert!(v >= min, "v={v} < min={min}");
assert!(v < max, "v={v} reached the excluded upper bound {max}");
}
}
#[test]
fn sample_f32_always_returns_finite() {
let mut ctx = TestCaseContext::new(1);
for _ in 0..10_000 {
let v = sample_f32(&mut ctx);
assert!(v.is_finite(), "expected finite, got {v:?}");
assert!(!v.is_nan(), "expected non-NaN, got {v:?}");
}
}
#[test]
fn sample_f64_always_returns_finite() {
let mut ctx = TestCaseContext::new(1);
for _ in 0..10_000 {
let v = sample_f64(&mut ctx);
assert!(v.is_finite(), "expected finite, got {v:?}");
assert!(!v.is_nan(), "expected non-NaN, got {v:?}");
}
}
#[test]
fn full_domain_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(&mut a), sample_f32(&mut b));
assert_eq!(sample_f64(&mut a), sample_f64(&mut b));
}
}
#[test]
fn sample_f32_covers_both_signs() {
let mut ctx = TestCaseContext::new(2);
let (mut pos, mut neg) = (false, false);
for _ in 0..256 {
let v = sample_f32(&mut ctx);
if v > 0.0 {
pos = true;
} else if v < 0.0 {
neg = true;
}
if pos && neg {
return;
}
}
panic!("sample_f32 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,
reason = "the reversed range is the panic condition under test"
)]
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_u64_in_exclusive_stays_in_range() {
let mut ctx = TestCaseContext::new(1);
for _ in 0..1000 {
let v = sample_u64_in(&mut ctx, 10..20);
assert!((10..20).contains(&v));
}
}
#[test]
fn sample_u64_in_inclusive_stays_in_range() {
let mut ctx = TestCaseContext::new(1);
for _ in 0..1000 {
let v = sample_u64_in(&mut ctx, 10..=20);
assert!((10..=20).contains(&v));
}
}
#[test]
fn sample_u64_in_single_element_returns_that_element() {
let mut ctx = TestCaseContext::new(1);
let mut fresh = TestCaseContext::new(1);
assert_eq!(sample_u64_in(&mut ctx, 5..=5), 5);
assert_state_unadvanced(&mut ctx, &mut fresh);
}
#[test]
fn sample_u64_in_hits_both_endpoints() {
let mut ctx = TestCaseContext::new(1);
let (mut lo, mut hi) = (false, false);
for _ in 0..1024 {
let v = sample_u64_in(&mut ctx, 0..=3);
lo |= v == 0;
hi |= v == 3;
if lo && hi {
return;
}
}
panic!("sample_u64_in did not cover both endpoints");
}
#[test]
fn sample_u64_in_full_range_stays_in_range() {
let mut ctx = TestCaseContext::new(1);
for _ in 0..100 {
let _v = sample_u64_in(&mut ctx, ..);
}
}
#[test]
fn sample_u64_in_inclusive_up_to_max_stays_in_range() {
let mut ctx = TestCaseContext::new(1);
for _ in 0..100 {
let v = sample_u64_in(&mut ctx, 1..=u64::MAX);
assert!(v >= 1);
}
}
#[test]
fn sample_u64_in_unbounded_end_stays_in_range() {
let mut ctx = TestCaseContext::new(1);
for _ in 0..100 {
let v = sample_u64_in(&mut ctx, 100..);
assert!(v >= 100);
}
}
#[test]
fn sample_u64_in_is_deterministic() {
let mut a = TestCaseContext::new(999);
let mut b = TestCaseContext::new(999);
for _ in 0..64 {
assert_eq!(sample_u64_in(&mut a, 0..137), sample_u64_in(&mut b, 0..137));
}
}
#[test]
#[should_panic(expected = "empty range")]
fn sample_u64_in_panics_on_empty_exclusive() {
let mut ctx = TestCaseContext::new(0);
let _ = sample_u64_in(&mut ctx, 5..5);
}
#[test]
#[should_panic(expected = "empty range")]
fn sample_u64_in_panics_on_reversed_inclusive() {
let mut ctx = TestCaseContext::new(0);
#[expect(
clippy::reversed_empty_ranges,
reason = "the reversed range is the panic condition under test"
)]
let _ = sample_u64_in(&mut ctx, 5..=4);
}
#[test]
#[should_panic(expected = "empty range")]
fn sample_u64_in_panics_on_zero_exclusive_end() {
let mut ctx = TestCaseContext::new(0);
let _ = sample_u64_in(&mut ctx, ..0);
}
#[test]
#[should_panic(expected = "empty range")]
fn sample_u64_in_panics_on_excluded_max_start() {
let mut ctx = TestCaseContext::new(0);
let _ = sample_u64_in(
&mut ctx,
(
std::ops::Bound::Excluded(u64::MAX),
std::ops::Bound::<u64>::Unbounded,
),
);
}
#[test]
fn typed_in_samplers_stay_in_range() {
let mut ctx = TestCaseContext::new(1);
for _ in 0..256 {
assert!((10..20).contains(&sample_u8_in(&mut ctx, 10..20)));
assert!((10..=20).contains(&sample_u16_in(&mut ctx, 10..=20)));
assert!((10..=20).contains(&sample_u32_in(&mut ctx, 10..=20)));
assert!((-10..0).contains(&sample_i8_in(&mut ctx, -10..0)));
assert!((-10..=0).contains(&sample_i16_in(&mut ctx, -10..=0)));
assert!((-10..=0).contains(&sample_i32_in(&mut ctx, -10..=0)));
}
}
#[test]
fn typed_in_samplers_full_width_stay_in_range() {
let mut ctx = TestCaseContext::new(1);
for _ in 0..64 {
let _ = sample_u8_in(&mut ctx, ..);
let _ = sample_u16_in(&mut ctx, ..);
let _ = sample_u32_in(&mut ctx, ..);
let _ = sample_i8_in(&mut ctx, i8::MIN..=i8::MAX);
let _ = sample_i16_in(&mut ctx, i16::MIN..=i16::MAX);
let _ = sample_i32_in(&mut ctx, i32::MIN..=i32::MAX);
}
}
#[test]
fn typed_in_samplers_inclusive_max_stays_in_range() {
let mut ctx = TestCaseContext::new(1);
for _ in 0..64 {
assert!(sample_u8_in(&mut ctx, 1..=u8::MAX) >= 1);
assert!(sample_u16_in(&mut ctx, 1..=u16::MAX) >= 1);
assert!(sample_u32_in(&mut ctx, 1..=u32::MAX) >= 1);
assert!(sample_i8_in(&mut ctx, i8::MIN..=i8::MAX - 1) < i8::MAX);
assert!(sample_i16_in(&mut ctx, i16::MIN..=i16::MAX - 1) < i16::MAX);
assert!(sample_i32_in(&mut ctx, i32::MIN..=i32::MAX - 1) < i32::MAX);
}
}
#[test]
fn typed_in_samplers_single_element_returns_that_element() {
let mut ctx = TestCaseContext::new(1);
assert_eq!(sample_u8_in(&mut ctx, 5..=5), 5);
assert_eq!(sample_u16_in(&mut ctx, 5..=5), 5);
assert_eq!(sample_u32_in(&mut ctx, 5..=5), 5);
assert_eq!(sample_i8_in(&mut ctx, -5..=-5), -5);
assert_eq!(sample_i16_in(&mut ctx, -5..=-5), -5);
assert_eq!(sample_i32_in(&mut ctx, -5..=-5), -5);
}
#[test]
fn typed_in_samplers_hit_both_endpoints() {
let mut ctx = TestCaseContext::new(1);
let (mut lo, mut hi) = (false, false);
for _ in 0..1024 {
let v = sample_i8_in(&mut ctx, -2..=2);
lo |= v == -2;
hi |= v == 2;
if lo && hi {
return;
}
}
panic!("sample_i8_in did not cover both endpoints");
}
#[test]
fn typed_in_samplers_are_deterministic() {
let mut a = TestCaseContext::new(999);
let mut b = TestCaseContext::new(999);
for _ in 0..64 {
assert_eq!(sample_u8_in(&mut a, 0..137), sample_u8_in(&mut b, 0..137));
assert_eq!(
sample_i32_in(&mut a, -100..=100),
sample_i32_in(&mut b, -100..=100)
);
}
}
#[test]
#[should_panic(expected = "sample_u8_in: empty range")]
fn sample_u8_in_panics_on_empty_exclusive() {
let mut ctx = TestCaseContext::new(0);
let _ = sample_u8_in(&mut ctx, 5..5);
}
#[test]
#[should_panic(expected = "sample_u8_in: empty range")]
fn sample_u8_in_panics_on_excluded_max_start() {
let mut ctx = TestCaseContext::new(0);
let _ = sample_u8_in(
&mut ctx,
(
std::ops::Bound::Excluded(u8::MAX),
std::ops::Bound::<u8>::Unbounded,
),
);
}
#[test]
#[should_panic(expected = "sample_i8_in: empty range")]
fn sample_i8_in_panics_on_reversed_inclusive() {
let mut ctx = TestCaseContext::new(0);
#[expect(
clippy::reversed_empty_ranges,
reason = "the reversed range is the panic condition under test"
)]
let _ = sample_i8_in(&mut ctx, 5..=4);
}
#[test]
#[should_panic(expected = "sample_i32_in: empty range")]
fn sample_i32_in_panics_on_reversed_exclusive() {
let mut ctx = TestCaseContext::new(0);
let _ = sample_i32_in(&mut ctx, 0..0);
}
#[test]
fn ratio_new_accepts_valid() {
let ratio = Ratio::new(1, 3);
assert_eq!(ratio.numerator, 1);
assert_eq!(ratio.denominator, 3);
assert_eq!(Ratio::new(0, 5).numerator, 0);
assert_eq!(Ratio::new(5, 5).denominator, 5);
}
#[test]
#[should_panic(expected = "Ratio::new: denominator must be non-zero")]
fn ratio_new_panics_on_zero_denominator() {
let _ = Ratio::new(1, 0);
}
#[test]
#[should_panic(expected = "Ratio::new: denominator must be non-zero")]
fn ratio_new_panics_on_zero_zero() {
let _ = Ratio::new(0, 0);
}
#[test]
#[should_panic(expected = "Ratio::new: numerator must not exceed denominator")]
fn ratio_new_panics_when_numerator_exceeds_denominator() {
let _ = Ratio::new(2, 1);
}
#[test]
#[should_panic(expected = "Ratio::new: numerator must not exceed denominator")]
fn ratio_new_panics_when_numerator_exceeds_denominator_larger() {
let _ = Ratio::new(11, 10);
}
#[test]
fn ratio_one_nth_matches_new_1_over_n() {
assert_eq!(Ratio::one_nth(2), Ratio::new(1, 2));
assert_eq!(Ratio::one_nth(3), Ratio::new(1, 3));
assert_eq!(Ratio::one_nth(10), Ratio::new(1, 10));
assert_eq!(Ratio::one_nth(100), Ratio::new(1, 100));
}
#[test]
#[should_panic(expected = "Ratio::one_nth: n must be non-zero")]
fn ratio_one_nth_panics_on_zero() {
let _ = Ratio::one_nth(0);
}
#[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, Ratio::new(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, Ratio::new(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, Ratio::one_nth(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, Ratio::new(3, 7)),
sample_ratio(&mut b, Ratio::new(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, Ratio::one_nth(10)) {
trues += 1;
}
}
let expected = total / 10;
assert!(
trues.abs_diff(expected) < expected / 10,
"sample_ratio(Ratio::one_nth(10)) frequency off: {trues}/{total}"
);
}
#[test]
fn sample_with_boundaries_full_ratio_always_boundary() {
let mut ctx = TestCaseContext::new(1);
for _ in 0..64 {
let v = sample_with_boundaries(&mut ctx, &[7, 8], Ratio::new(2, 2), sample_u32);
assert!(v == 7 || v == 8, "unexpected value: {v}");
}
}
#[test]
fn sample_with_boundaries_zero_ratio_always_uniform() {
let mut ctx = TestCaseContext::new(1);
for _ in 0..64 {
let v = sample_with_boundaries(&mut ctx, &[7, 8], Ratio::new(0, 3), sample_u32);
assert!(v != 7 && v != 8, "unexpected boundary value: {v}");
}
}
#[test]
fn sample_with_boundaries_produces_both_paths() {
let mut ctx = TestCaseContext::new(1);
let (mut boundary, mut uniform) = (false, false);
for _ in 0..256 {
let v = sample_with_boundaries(&mut ctx, &[u32::MAX], Ratio::one_nth(2), sample_u32);
if v == u32::MAX {
boundary = true;
} else {
uniform = true;
}
if boundary && uniform {
return;
}
}
panic!("sample_with_boundaries did not take both paths");
}
#[test]
fn sample_with_boundaries_boundary_frequency_matches_ratio() {
let mut ctx = TestCaseContext::new(1);
let mut boundary: usize = 0;
let total: usize = 10_000;
for _ in 0..total {
let v = sample_with_boundaries(&mut ctx, &[u32::MAX], Ratio::one_nth(10), sample_u32);
if v == u32::MAX {
boundary += 1;
}
}
let expected = total / 10;
assert!(
boundary.abs_diff(expected) < expected / 10,
"sample_with_boundaries(one_nth(10)) frequency off: {boundary}/{total}"
);
}
#[test]
fn sample_with_boundaries_boundary_path_hits_every_element() {
let mut ctx = TestCaseContext::new(1);
let mut seen = [false; 2];
for _ in 0..256 {
let v = sample_with_boundaries(&mut ctx, &[7, 8], Ratio::new(2, 2), sample_u32);
match v {
7 => seen[0] = true,
8 => seen[1] = true,
_ => panic!("unexpected value: {v}"),
}
if seen.iter().all(|&s| s) {
return;
}
}
panic!("sample_with_boundaries boundary path did not cover all elements");
}
#[test]
fn sample_with_boundaries_is_deterministic() {
let mut a = TestCaseContext::new(1234);
let mut b = TestCaseContext::new(1234);
for _ in 0..64 {
assert_eq!(
sample_with_boundaries(&mut a, &[0, 1], Ratio::one_nth(4), sample_u64),
sample_with_boundaries(&mut b, &[0, 1], Ratio::one_nth(4), sample_u64)
);
}
}
#[test]
fn sample_with_boundaries_recipe_is_byte_equivalent() {
let mut a = TestCaseContext::new(42);
let mut b = TestCaseContext::new(42);
for _ in 0..256 {
let hand_written = if sample_usize_in(&mut a, 0..10) == 0 {
0
} else {
sample_u32(&mut a)
};
let helper = sample_with_boundaries(&mut b, &[0], Ratio::one_nth(10), sample_u32);
assert_eq!(hand_written, helper, "streams diverged");
}
}
#[test]
#[should_panic(expected = "empty boundaries")]
fn sample_with_boundaries_panics_on_empty_boundaries() {
let mut ctx = TestCaseContext::new(0);
let _ = sample_with_boundaries(&mut ctx, &[] as &[u32], Ratio::one_nth(2), sample_u32);
}
#[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) < expected / 10,
"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]);
}
#[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]
#[should_panic(expected = "noprop::sample_with_rejection")]
fn sample_with_rejection_exhaustion_outside_runner_names_the_primitive() {
let mut ctx = TestCaseContext::new(0);
let _: u32 = sample_with_rejection(&mut ctx, 3, |_ctx| None);
}
#[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);
}
}