use std::any::Any;
use std::collections::VecDeque;
use std::panic::Location;
const DEDUP_HEAD: usize = 8;
const DEDUP_TAIL: usize = 8;
pub struct TestCaseContext {
state: XoshiroState,
generated: Vec<GeneratedValue>,
dedup: DedupState,
rejection: Option<RejectionState>,
inside_runner: bool,
total_samples: usize,
}
struct XoshiroState {
state: [u64; 4],
}
impl XoshiroState {
fn from_seed(seed: u64) -> Self {
let mut sm = SplitMix64 { state: seed };
Self {
state: [sm.next(), sm.next(), sm.next(), sm.next()],
}
}
fn next_u64(&mut self) -> u64 {
let result = self.state[1].wrapping_mul(5).rotate_left(7).wrapping_mul(9);
let t = self.state[1] << 17;
self.state[2] ^= self.state[0];
self.state[3] ^= self.state[1];
self.state[1] ^= self.state[2];
self.state[0] ^= self.state[3];
self.state[2] ^= t;
self.state[3] = self.state[3].rotate_left(45);
result
}
fn fill(&mut self, dst: &mut [u8]) {
let mut i = 0;
while i + 8 <= dst.len() {
let bytes = self.next_u64().to_le_bytes();
dst[i..i + 8].copy_from_slice(&bytes);
i += 8;
}
if i < dst.len() {
let bytes = self.next_u64().to_le_bytes();
let remaining = dst.len() - i;
dst[i..].copy_from_slice(&bytes[..remaining]);
}
}
}
pub(crate) struct IterationRejected;
#[derive(Debug, Clone, Copy)]
pub(crate) struct RejectionState {
pub location: &'static Location<'static>,
}
pub struct GeneratedValue {
type_name: &'static str,
kind: EntryKind,
location: &'static Location<'static>,
}
enum EntryKind {
Value(Box<dyn std::fmt::Debug + 'static>),
Elided { count: usize },
}
impl GeneratedValue {
pub fn type_name(&self) -> &'static str {
self.type_name
}
pub fn location(&self) -> &'static Location<'static> {
self.location
}
pub fn value_repr(&self) -> Option<String> {
match &self.kind {
EntryKind::Value(v) => Some(format!("{v:?}")),
EntryKind::Elided { .. } => None,
}
}
pub fn is_elided(&self) -> bool {
matches!(self.kind, EntryKind::Elided { .. })
}
pub fn elided_count(&self) -> Option<usize> {
if let EntryKind::Elided { count } = self.kind {
Some(count)
} else {
None
}
}
}
impl std::fmt::Debug for GeneratedValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.kind {
EntryKind::Value(v) => write!(
f,
"- {} = {v:?} (at {}:{})",
self.type_name,
self.location.file(),
self.location.line(),
),
EntryKind::Elided { count } => write!(
f,
"... {count} more {} elided at {}:{} ...",
self.type_name,
self.location.file(),
self.location.line(),
),
}
}
}
#[derive(Default)]
struct DedupState {
location: Option<&'static Location<'static>>,
head_count: usize,
tail: VecDeque<GeneratedValue>,
elided: usize,
}
impl TestCaseContext {
pub fn new(seed: u64) -> Self {
Self {
state: XoshiroState::from_seed(seed),
generated: Vec::new(),
dedup: DedupState::default(),
rejection: None,
inside_runner: false,
total_samples: 0,
}
}
#[track_caller]
pub fn reject_case(&mut self) -> ! {
if !self.inside_runner {
panic!(
"noprop::TestCaseContext::reject_case can only be called from inside a Runner::run \
property closure. Constructing a TestCaseContext directly via \
TestCaseContext::new does not create a Runner boundary."
);
}
let location = Location::caller();
self.rejection = Some(RejectionState { location });
std::panic::resume_unwind(Box::new(IterationRejected));
}
pub(crate) fn fill(&mut self, dst: &mut [u8]) {
if dst.is_empty() {
return;
}
self.state.fill(dst);
}
pub(crate) fn take_rejection(&mut self) -> Option<RejectionState> {
self.rejection.take()
}
#[track_caller]
pub(crate) fn record_generated<T: std::fmt::Debug + Clone + 'static>(
&mut self,
value: &T,
location: &'static Location<'static>,
) {
self.total_samples = self.total_samples.saturating_add(1);
let same_as_last = self
.dedup
.location
.is_some_and(|l| same_location(l, location));
if !same_as_last {
self.flush_dedup_tail();
self.dedup.location = Some(location);
self.dedup.head_count = 0;
}
let entry = GeneratedValue {
type_name: std::any::type_name::<T>(),
kind: EntryKind::Value(Box::new(value.clone())),
location,
};
if self.dedup.head_count < DEDUP_HEAD {
self.generated.push(entry);
self.dedup.head_count += 1;
} else {
if self.dedup.tail.len() == DEDUP_TAIL {
self.dedup.tail.pop_front();
self.dedup.elided += 1;
}
self.dedup.tail.push_back(entry);
}
}
fn flush_dedup_tail(&mut self) {
if self.dedup.elided > 0 {
let location = self
.dedup
.location
.expect("elided implies a prior location");
let type_name = self.dedup.tail.front().map(|e| e.type_name).unwrap_or("?");
self.generated.push(GeneratedValue {
type_name,
kind: EntryKind::Elided {
count: self.dedup.elided,
},
location,
});
}
self.generated.extend(self.dedup.tail.drain(..));
self.dedup.elided = 0;
}
pub(crate) fn take_generated(&mut self) -> Vec<GeneratedValue> {
self.flush_dedup_tail();
self.dedup = DedupState::default();
std::mem::take(&mut self.generated)
}
pub(crate) fn clear_generated(&mut self) {
self.generated.clear();
self.dedup = DedupState::default();
}
pub(crate) fn set_inside_runner(&mut self) {
self.inside_runner = true;
}
pub(crate) fn is_inside_runner(&self) -> bool {
self.inside_runner
}
pub(crate) fn total_samples(&self) -> usize {
self.total_samples
}
}
pub(crate) fn is_iteration_rejected(payload: &(dyn Any + Send)) -> bool {
payload.is::<IterationRejected>()
}
fn same_location(a: &Location<'_>, b: &Location<'_>) -> bool {
a.file() == b.file() && a.line() == b.line() && a.column() == b.column()
}
struct SplitMix64 {
state: u64,
}
impl SplitMix64 {
fn next(&mut self) -> u64 {
self.state = self.state.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.state;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn same_seed_gives_same_sequence() {
let mut a = TestCaseContext::new(0xDEAD_BEEF);
let mut b = TestCaseContext::new(0xDEAD_BEEF);
for _ in 0..256 {
let mut buf_a = [0u8; 8];
let mut buf_b = [0u8; 8];
a.fill(&mut buf_a);
b.fill(&mut buf_b);
assert_eq!(buf_a, buf_b);
}
}
#[test]
fn different_seeds_differ() {
let mut a = TestCaseContext::new(1);
let mut b = TestCaseContext::new(2);
let mut buf_a = [0u8; 8];
let mut buf_b = [0u8; 8];
a.fill(&mut buf_a);
b.fill(&mut buf_b);
assert_ne!(buf_a, buf_b);
}
#[test]
fn seed_zero_produces_nonzero_output() {
let mut ctx = TestCaseContext::new(0);
let mut buf = [0u8; 8];
ctx.fill(&mut buf);
assert_ne!(buf, [0u8; 8]);
}
#[test]
fn fill_matches_le_bytes_of_xoshiro_step() {
let mut ctx = TestCaseContext::new(42);
let mut xs = XoshiroState::from_seed(42);
let mut buf = [0u8; 24];
ctx.fill(&mut buf);
for chunk in buf.as_chunks::<8>().0 {
assert_eq!(chunk, &xs.next_u64().to_le_bytes());
}
}
#[test]
fn fill_is_deterministic_for_non_multiple_of_eight() {
let mut a = TestCaseContext::new(7);
let mut b = TestCaseContext::new(7);
let mut buf_a = [0u8; 5];
let mut buf_b = [0u8; 5];
a.fill(&mut buf_a);
b.fill(&mut buf_b);
assert_eq!(buf_a, buf_b);
}
#[test]
fn fill_empty_buffer_does_not_advance() {
let mut ctx = TestCaseContext::new(1);
let empty: &mut [u8] = &mut [];
ctx.fill(empty);
let mut fresh = TestCaseContext::new(1);
let mut buf_after = [0u8; 8];
let mut buf_fresh = [0u8; 8];
ctx.fill(&mut buf_after);
fresh.fill(&mut buf_fresh);
assert_eq!(buf_after, buf_fresh);
}
#[test]
fn fill_bit_exact_multiple_of_eight() {
let mut ctx = TestCaseContext::new(0xDEAD_BEEF);
let mut buf = [0u8; 16];
ctx.fill(&mut buf);
let mut fresh = TestCaseContext::new(0xDEAD_BEEF);
let mut expected = [0u8; 16];
fresh.fill(&mut expected);
assert_eq!(buf, expected);
assert_eq!(buf, expected_bytes(0xDEAD_BEEF, 16).as_slice(),);
}
#[test]
fn fill_bit_exact_non_multiple_of_eight_tail() {
let mut ctx = TestCaseContext::new(0xDEAD_BEEF);
let mut buf = [0u8; 5];
ctx.fill(&mut buf);
assert_eq!(buf, expected_bytes(0xDEAD_BEEF, 5).as_slice());
}
#[test]
fn fill_bit_exact_empty_slice_leaves_state_untouched() {
let mut ctx = TestCaseContext::new(0xDEAD_BEEF);
let empty: &mut [u8] = &mut [];
ctx.fill(empty);
let mut buf = [0u8; 8];
ctx.fill(&mut buf);
assert_eq!(buf, expected_bytes(0xDEAD_BEEF, 8).as_slice());
}
fn expected_bytes(seed: u64, n: usize) -> Vec<u8> {
let mut xs = XoshiroState::from_seed(seed);
let mut out = vec![0u8; n];
xs.fill(&mut out);
out
}
#[test]
fn xoshiro_state_from_seed_is_deterministic() {
let mut a = XoshiroState::from_seed(123);
let mut b = XoshiroState::from_seed(123);
for _ in 0..64 {
assert_eq!(a.next_u64(), b.next_u64());
}
}
#[test]
fn xoshiro_state_fill_matches_next_u64() {
let mut xs_fill = XoshiroState::from_seed(9);
let mut xs_step = XoshiroState::from_seed(9);
let mut buf = [0u8; 24];
xs_fill.fill(&mut buf);
for chunk in buf.as_chunks::<8>().0 {
assert_eq!(chunk, &xs_step.next_u64().to_le_bytes());
}
}
#[test]
#[should_panic(expected = "Runner::run")]
fn reject_case_outside_runner_panics_with_helpful_message() {
let mut ctx = TestCaseContext::new(1);
ctx.reject_case();
}
}