use std::any::Any;
use std::collections::VecDeque;
#[cfg(test)]
use std::panic::AssertUnwindSafe;
use std::panic::Location;
const DEDUP_HEAD: usize = 8;
const DEDUP_TAIL: usize = 8;
const MAX_CHOICES_PER_CASE: usize = 4096;
const MAX_FEATURES_PER_CASE: usize = 64;
pub struct TestCaseContext {
source: RandomSource,
generated: Vec<GeneratedValue>,
dedup: DedupState,
rejection: Option<RejectionState>,
inside_runner: bool,
total_samples: usize,
feedback: FeedbackState,
}
#[derive(Debug, Clone)]
pub(crate) enum FeedbackState {
Disabled,
SemanticCoverage(SemanticCoverage),
}
#[derive(Debug, Clone, Default)]
pub(crate) struct SemanticCoverage {
features: Vec<Feature>,
event_counts: Vec<(&'static str, usize)>,
}
impl SemanticCoverage {
fn report_event(&mut self, label: &'static str) {
let count = match self.event_counts.iter_mut().find(|(l, _)| *l == label) {
Some((_, c)) => {
*c += 1;
*c
}
None => {
if self.event_counts.len() >= MAX_FEATURES_PER_CASE {
return;
}
self.event_counts.push((label, 1));
1
}
};
self.report(label, FeatureKind::Event(event_bucket(count)));
}
fn report(&mut self, label: &'static str, kind: FeatureKind) {
if matches!(kind, FeatureKind::Event(_)) {
if let Some(f) = self
.features
.iter_mut()
.find(|f| f.label == label && matches!(f.kind, FeatureKind::Event(_)))
{
f.kind = kind;
return;
}
} else if self
.features
.iter()
.any(|f| f.label == label && f.kind == kind)
{
return;
}
if self.features.len() < MAX_FEATURES_PER_CASE {
self.features.push(Feature { label, kind });
}
}
#[cfg(test)]
pub(crate) fn features(&self) -> &[Feature] {
&self.features
}
pub(crate) fn take_features(&mut self) -> Vec<Feature> {
std::mem::take(&mut self.features)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct Feature {
pub label: &'static str,
pub kind: FeatureKind,
}
impl Feature {
pub(crate) fn display_repr(&self) -> String {
match self.kind {
FeatureKind::Event(_) => format!("event({:?})", self.label),
FeatureKind::Bucket { value } => format!("bucket({:?}, {value})", self.label),
FeatureKind::Transition { from, to } => {
format!("transition({:?}, {from}, {to})", self.label)
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) enum FeatureKind {
Event(EventBucket),
Bucket { value: u64 },
Transition { from: u64, to: u64 },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) enum EventBucket {
One,
TwoThree,
FourSeven,
EightPlus,
}
fn event_bucket(count: usize) -> EventBucket {
match count {
1 => EventBucket::One,
2..=3 => EventBucket::TwoThree,
4..=7 => EventBucket::FourSeven,
_ => EventBucket::EightPlus,
}
}
enum RandomSource {
Prng(XoshiroState),
Recording {
state: XoshiroState,
sequence: ChoiceSequence,
current_parent: Option<usize>,
pending_choice: Option<ChoiceMeta>,
},
Replay {
sequence: ChoiceSequence,
next_draw: usize,
next_span: usize,
current_parent: Option<usize>,
error: Option<ReplayError>,
explore: Option<XoshiroState>,
consumed: usize,
pending_choice: Option<ChoiceMeta>,
},
}
pub(crate) struct XoshiroState {
state: [u64; 4],
}
impl XoshiroState {
pub(crate) fn from_seed(seed: u64) -> Self {
let mut sm = SplitMix64 { state: seed };
Self {
state: [sm.next(), sm.next(), sm.next(), sm.next()],
}
}
pub(crate) 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
}
pub(crate) 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) fn sample_below(&mut self, n: u64) -> u64 {
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 self.next_u64() % n;
}
let bound = u64::MAX - r;
loop {
let x = self.next_u64();
if x < bound {
return x % n;
}
}
}
}
#[derive(Default, Clone)]
pub(crate) struct ChoiceSequence {
draws: Vec<Vec<u8>>,
spans: Vec<AttemptSpan>,
metas: Vec<ChoiceMeta>,
}
impl ChoiceSequence {
#[cfg(test)]
pub(crate) fn draws(&self) -> &[Vec<u8>] {
&self.draws
}
#[cfg(test)]
pub(crate) fn spans(&self) -> &[AttemptSpan] {
&self.spans
}
#[cfg(test)]
pub(crate) fn metas(&self) -> &[ChoiceMeta] {
&self.metas
}
#[cfg(test)]
pub(crate) fn draws_mut(&mut self) -> &mut [Vec<u8>] {
&mut self.draws
}
pub(crate) fn draws_and_metas(&mut self) -> (&mut [Vec<u8>], &[ChoiceMeta]) {
(&mut self.draws, &self.metas)
}
pub(crate) fn push_draw(&mut self, bytes: Vec<u8>, meta: ChoiceMeta) {
self.draws.push(bytes);
self.metas.push(meta);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ChoiceMeta {
Bounded { bound: u64 },
Choice { len: usize },
Integer,
Raw,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct AttemptSpan {
pub parent: Option<usize>,
pub start_draw: usize,
pub end_draw: usize,
pub verdict: AttemptVerdict,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum AttemptVerdict {
Pending,
Accepted,
Rejected,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ReplayError {
SequenceExhausted { requested: usize },
DrawLengthMismatch { expected: usize, actual: usize },
#[cfg_attr(not(test), expect(dead_code))]
LeftoverDraws { unused: usize },
SpanMismatch {
at_span: usize,
reason: SpanMismatchReason,
},
#[cfg_attr(not(test), expect(dead_code))]
LeftoverSpans { unused: usize },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum SpanMismatchReason {
SequenceExhausted,
ParentMismatch,
StartDrawMismatch { expected: usize, actual: usize },
EndDrawMismatch { expected: usize, actual: usize },
VerdictMismatch {
expected: AttemptVerdict,
actual: AttemptVerdict,
},
}
struct ReplayAbort;
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,
}
#[track_caller]
fn within_generated_draw_cap(consumed: &mut usize, rejection: &mut Option<RejectionState>) -> bool {
*consumed += 1;
if *consumed > MAX_CHOICES_PER_CASE {
*rejection = Some(RejectionState {
location: std::panic::Location::caller(),
});
return false;
}
true
}
impl TestCaseContext {
pub fn new(seed: u64) -> Self {
Self {
source: RandomSource::Prng(XoshiroState::from_seed(seed)),
generated: Vec::new(),
dedup: DedupState::default(),
rejection: None,
inside_runner: false,
total_samples: 0,
feedback: FeedbackState::Disabled,
}
}
pub(crate) fn recording(seed: u64) -> Self {
Self {
source: RandomSource::Recording {
state: XoshiroState::from_seed(seed),
sequence: ChoiceSequence::default(),
current_parent: None,
pending_choice: None,
},
generated: Vec::new(),
dedup: DedupState::default(),
rejection: None,
inside_runner: false,
total_samples: 0,
feedback: FeedbackState::Disabled,
}
}
pub(crate) fn exploring(sequence: ChoiceSequence, prng_seed: u64) -> Self {
Self {
source: RandomSource::Replay {
sequence,
next_draw: 0,
next_span: 0,
current_parent: None,
error: None,
explore: Some(XoshiroState::from_seed(prng_seed)),
consumed: 0,
pending_choice: None,
},
generated: Vec::new(),
dedup: DedupState::default(),
rejection: None,
inside_runner: false,
total_samples: 0,
feedback: FeedbackState::Disabled,
}
}
pub(crate) fn take_sequence(&mut self) -> Option<ChoiceSequence> {
match &mut self.source {
RandomSource::Recording { sequence, .. } => Some(std::mem::take(sequence)),
RandomSource::Replay {
sequence,
next_draw,
..
} => {
if *next_draw < sequence.draws.len() {
sequence.draws.truncate(*next_draw);
sequence.metas.truncate(*next_draw);
}
sequence.spans.clear();
Some(std::mem::take(sequence))
}
_ => None,
}
}
#[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 or \
Runner::run_feedback_guided 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));
}
#[track_caller]
pub(crate) fn fill(&mut self, dst: &mut [u8]) {
if dst.is_empty() {
return;
}
match &mut self.source {
RandomSource::Prng(state) => state.fill(dst),
RandomSource::Recording {
state,
sequence,
pending_choice,
..
} => {
state.fill(dst);
let meta = pending_choice.take().unwrap_or(ChoiceMeta::Raw);
sequence.push_draw(dst.to_vec(), meta);
}
RandomSource::Replay {
sequence,
next_draw,
error,
explore,
consumed,
pending_choice,
..
} => {
if error.is_none() {
if *next_draw < sequence.draws.len() {
let draw_len = sequence.draws[*next_draw].len();
if draw_len == dst.len() {
if explore.is_some() {
let meta = pending_choice.take().unwrap_or(ChoiceMeta::Raw);
let idx = *next_draw;
if sequence.metas[idx] != meta {
if let Some(prng) = explore {
prng.fill(&mut sequence.draws[idx]);
}
sequence.metas[idx] = meta;
}
}
dst.copy_from_slice(&sequence.draws[*next_draw]);
*next_draw += 1;
return;
}
if explore.is_none() {
*error = Some(ReplayError::DrawLengthMismatch {
expected: draw_len,
actual: dst.len(),
});
std::panic::resume_unwind(Box::new(ReplayAbort));
}
if let Some(prng) = explore {
if !within_generated_draw_cap(consumed, &mut self.rejection) {
std::panic::resume_unwind(Box::new(IterationRejected));
}
prng.fill(dst);
let meta = pending_choice.take().unwrap_or(ChoiceMeta::Raw);
sequence.draws[*next_draw] = dst.to_vec();
sequence.metas[*next_draw] = meta;
*next_draw += 1;
return;
}
} else if explore.is_none() {
*error = Some(ReplayError::SequenceExhausted {
requested: dst.len(),
});
std::panic::resume_unwind(Box::new(ReplayAbort));
}
if let Some(prng) = explore {
if !within_generated_draw_cap(consumed, &mut self.rejection) {
std::panic::resume_unwind(Box::new(IterationRejected));
}
prng.fill(dst);
let meta = pending_choice.take().unwrap_or(ChoiceMeta::Raw);
sequence.push_draw(dst.to_vec(), meta);
*next_draw += 1;
return;
}
}
std::panic::resume_unwind(Box::new(ReplayAbort));
}
}
}
pub(crate) fn begin_attempt(&mut self) -> Option<usize> {
match &mut self.source {
RandomSource::Prng(_) => None,
RandomSource::Recording {
sequence,
current_parent,
..
} => {
let idx = sequence.spans.len();
let start_draw = sequence.draws.len();
sequence.spans.push(AttemptSpan {
parent: *current_parent,
start_draw,
end_draw: start_draw,
verdict: AttemptVerdict::Pending,
});
*current_parent = Some(idx);
Some(idx)
}
RandomSource::Replay {
sequence,
next_draw,
next_span,
current_parent,
error,
explore,
..
} => {
if explore.is_some() {
return None;
}
if error.is_some() {
std::panic::resume_unwind(Box::new(ReplayAbort));
}
let idx = *next_span;
if idx >= sequence.spans.len() {
*error = Some(ReplayError::SpanMismatch {
at_span: idx,
reason: SpanMismatchReason::SequenceExhausted,
});
std::panic::resume_unwind(Box::new(ReplayAbort));
}
let recorded = &sequence.spans[idx];
if recorded.parent != *current_parent {
*error = Some(ReplayError::SpanMismatch {
at_span: idx,
reason: SpanMismatchReason::ParentMismatch,
});
std::panic::resume_unwind(Box::new(ReplayAbort));
}
if recorded.start_draw != *next_draw {
*error = Some(ReplayError::SpanMismatch {
at_span: idx,
reason: SpanMismatchReason::StartDrawMismatch {
expected: recorded.start_draw,
actual: *next_draw,
},
});
std::panic::resume_unwind(Box::new(ReplayAbort));
}
*next_span += 1;
*current_parent = Some(idx);
Some(idx)
}
}
}
pub(crate) fn end_attempt(&mut self, id: Option<usize>, verdict: AttemptVerdict) {
debug_assert!(!matches!(verdict, AttemptVerdict::Pending));
match &mut self.source {
RandomSource::Prng(_) => {
let _ = id;
let _ = verdict;
}
RandomSource::Recording {
sequence,
current_parent,
..
} => {
let idx = id.expect("Recording mode always yields an attempt id");
let end_draw = sequence.draws.len();
let span = &mut sequence.spans[idx];
span.end_draw = end_draw;
span.verdict = verdict;
*current_parent = span.parent;
}
RandomSource::Replay {
sequence,
next_draw,
current_parent,
error,
explore,
..
} => {
if explore.is_some() {
return;
}
if error.is_some() {
std::panic::resume_unwind(Box::new(ReplayAbort));
}
let idx = id.expect("Replay mode always yields an attempt id");
let recorded = &sequence.spans[idx];
if recorded.end_draw != *next_draw {
*error = Some(ReplayError::SpanMismatch {
at_span: idx,
reason: SpanMismatchReason::EndDrawMismatch {
expected: recorded.end_draw,
actual: *next_draw,
},
});
std::panic::resume_unwind(Box::new(ReplayAbort));
}
if recorded.verdict != verdict {
*error = Some(ReplayError::SpanMismatch {
at_span: idx,
reason: SpanMismatchReason::VerdictMismatch {
expected: recorded.verdict,
actual: verdict,
},
});
std::panic::resume_unwind(Box::new(ReplayAbort));
}
*current_parent = recorded.parent;
}
}
}
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 fn event(&mut self, label: &'static str) {
if let FeedbackState::SemanticCoverage(cov) = &mut self.feedback {
cov.report_event(label);
}
}
pub fn bucket(&mut self, label: &'static str, value: u64) {
if let FeedbackState::SemanticCoverage(cov) = &mut self.feedback {
cov.report(label, FeatureKind::Bucket { value });
}
}
pub fn transition(&mut self, label: &'static str, from: u64, to: u64) {
if let FeedbackState::SemanticCoverage(cov) = &mut self.feedback {
cov.report(label, FeatureKind::Transition { from, to });
}
}
pub(crate) fn enable_corpus_guided(&mut self) {
self.feedback = FeedbackState::SemanticCoverage(SemanticCoverage::default());
}
pub(crate) fn take_feedback(&mut self) -> FeedbackState {
std::mem::replace(&mut self.feedback, FeedbackState::Disabled)
}
pub(crate) fn set_next_choice_meta(&mut self, meta: ChoiceMeta) {
match &mut self.source {
RandomSource::Recording { pending_choice, .. }
| RandomSource::Replay { pending_choice, .. } => {
*pending_choice = Some(meta);
}
RandomSource::Prng(_) => {}
}
}
pub(crate) fn total_samples(&self) -> usize {
self.total_samples
}
}
#[cfg(test)]
pub(crate) struct RecordingSession {
seed: u64,
}
#[cfg(test)]
impl RecordingSession {
pub(crate) fn new(seed: u64) -> Self {
Self { seed }
}
pub(crate) fn run<T, F>(self, f: F) -> (T, ChoiceSequence)
where
F: FnOnce(&mut TestCaseContext) -> T,
{
let mut ctx = TestCaseContext::recording(self.seed);
let value = f(&mut ctx);
let sequence = ctx
.take_sequence()
.expect("recording mode always yields a sequence");
(value, sequence)
}
}
#[cfg(test)]
pub(crate) struct ReplaySession {
sequence: ChoiceSequence,
}
#[cfg(test)]
impl ReplaySession {
pub(crate) fn new(sequence: ChoiceSequence) -> Self {
Self { sequence }
}
pub(crate) fn run<T, F>(self, f: F) -> Result<T, ReplayError>
where
F: FnOnce(&mut TestCaseContext) -> T,
{
let total_spans = self.sequence.spans.len();
let mut ctx = TestCaseContext {
source: RandomSource::Replay {
sequence: self.sequence,
next_draw: 0,
next_span: 0,
current_parent: None,
error: None,
explore: None,
consumed: 0,
pending_choice: None,
},
generated: Vec::new(),
dedup: DedupState::default(),
rejection: None,
inside_runner: false,
total_samples: 0,
feedback: FeedbackState::Disabled,
};
let outcome = std::panic::catch_unwind(AssertUnwindSafe(|| f(&mut ctx)));
let (sequence, next_draw, next_span, error) = match ctx.source {
RandomSource::Replay {
sequence,
next_draw,
next_span,
error,
..
} => (sequence, next_draw, next_span, error),
_ => unreachable!("ReplaySession constructs Replay variant"),
};
if let Some(err) = error {
if let Err(payload) = outcome
&& !is_replay_abort(&*payload)
{
std::panic::resume_unwind(payload);
}
return Err(err);
}
match outcome {
Ok(value) => {
if next_draw < sequence.draws.len() {
Err(ReplayError::LeftoverDraws {
unused: sequence.draws.len() - next_draw,
})
} else if next_span < total_spans {
Err(ReplayError::LeftoverSpans {
unused: total_spans - next_span,
})
} else {
Ok(value)
}
}
Err(payload) => {
if is_replay_abort(&*payload) {
Err(ReplayError::SequenceExhausted { requested: 0 })
} else {
std::panic::resume_unwind(payload)
}
}
}
}
}
pub(crate) fn is_iteration_rejected(payload: &(dyn Any + Send)) -> bool {
payload.is::<IterationRejected>()
}
#[cfg(test)]
fn is_replay_abort(payload: &(dyn Any + Send)) -> bool {
payload.is::<ReplayAbort>()
}
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.chunks_exact(8) {
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.chunks_exact(8) {
assert_eq!(chunk, &xs_step.next_u64().to_le_bytes());
}
}
#[test]
fn recording_session_captures_one_draw_per_non_empty_fill() {
let ((), seq) = RecordingSession::new(1).run(|ctx| {
let mut a = [0u8; 4];
ctx.fill(&mut a);
let mut b = [0u8; 3];
ctx.fill(&mut b);
let empty: &mut [u8] = &mut [];
ctx.fill(empty);
let mut c = [0u8; 8];
ctx.fill(&mut c);
});
assert_eq!(seq.draws().len(), 3);
assert_eq!(seq.draws()[0].len(), 4);
assert_eq!(seq.draws()[1].len(), 3);
assert_eq!(seq.draws()[2].len(), 8);
}
#[test]
fn replay_reproduces_recorded_bytes() {
let ((), seq) = RecordingSession::new(0xFEED).run(|ctx| {
let mut a = [0u8; 4];
ctx.fill(&mut a);
let mut b = [0u8; 8];
ctx.fill(&mut b);
});
let recorded_a = seq.draws()[0].clone();
let recorded_b = seq.draws()[1].clone();
let result = ReplaySession::new(seq)
.run(|ctx| {
let mut a = [0u8; 4];
ctx.fill(&mut a);
let mut b = [0u8; 8];
ctx.fill(&mut b);
(a.to_vec(), b.to_vec())
})
.expect("replay of same shape must succeed");
assert_eq!(result.0, recorded_a);
assert_eq!(result.1, recorded_b);
}
#[test]
fn replay_reports_sequence_exhausted() {
let (_, seq) = RecordingSession::new(1).run(|ctx| {
let mut a = [0u8; 4];
ctx.fill(&mut a);
});
let result = ReplaySession::new(seq).run(|ctx| {
let mut a = [0u8; 4];
ctx.fill(&mut a);
let mut b = [0u8; 4];
ctx.fill(&mut b);
});
assert!(matches!(
result,
Err(ReplayError::SequenceExhausted { requested: 4 })
));
}
#[test]
fn replay_reports_draw_length_mismatch() {
let (_, seq) = RecordingSession::new(1).run(|ctx| {
let mut a = [0u8; 4];
ctx.fill(&mut a);
});
let result = ReplaySession::new(seq).run(|ctx| {
let mut a = [0u8; 8];
ctx.fill(&mut a);
});
assert!(matches!(
result,
Err(ReplayError::DrawLengthMismatch {
expected: 4,
actual: 8,
})
));
}
#[test]
fn replay_reports_leftover_draws() {
let (_, seq) = RecordingSession::new(1).run(|ctx| {
let mut a = [0u8; 4];
ctx.fill(&mut a);
let mut b = [0u8; 4];
ctx.fill(&mut b);
});
let result = ReplaySession::new(seq).run(|ctx| {
let mut a = [0u8; 4];
ctx.fill(&mut a);
});
assert!(matches!(
result,
Err(ReplayError::LeftoverDraws { unused: 1 })
));
}
#[test]
fn replay_error_persists_when_user_catches_marker() {
let (_, seq) = RecordingSession::new(1).run(|ctx| {
let mut a = [0u8; 4];
ctx.fill(&mut a);
});
let result = ReplaySession::new(seq).run(|ctx| {
let _ = std::panic::catch_unwind(AssertUnwindSafe(|| {
let mut a = [0u8; 4];
ctx.fill(&mut a);
let mut b = [0u8; 4];
ctx.fill(&mut b);
}));
42u32
});
assert!(matches!(
result,
Err(ReplayError::SequenceExhausted { requested: 4 })
));
}
#[test]
fn replay_reraises_non_marker_user_panic() {
let (_, seq) = RecordingSession::new(1).run(|ctx| {
let mut a = [0u8; 4];
ctx.fill(&mut a);
});
let outcome = std::panic::catch_unwind(AssertUnwindSafe(|| {
let _ = ReplaySession::new(seq).run(|_ctx| panic!("user panic"));
}));
let payload = outcome.expect_err("user panic must escape replay");
let msg = payload
.downcast_ref::<&'static str>()
.copied()
.or_else(|| payload.downcast_ref::<String>().map(|s| s.as_str()))
.unwrap_or("");
assert!(msg.contains("user panic"), "unexpected payload: {msg:?}");
}
#[test]
fn prng_source_allocates_no_choice_sequence() {
let ctx = TestCaseContext::new(1);
assert!(matches!(ctx.source, RandomSource::Prng(_)));
}
#[test]
fn recording_and_prng_produce_same_bytes_for_same_seed() {
let mut prng = TestCaseContext::new(0x00C0_FFEE);
let mut expected = [0u8; 32];
prng.fill(&mut expected);
let ((), seq) = RecordingSession::new(0x00C0_FFEE).run(|ctx| {
let mut buf = [0u8; 32];
ctx.fill(&mut buf);
});
assert_eq!(seq.draws().len(), 1);
assert_eq!(seq.draws()[0], expected);
}
#[test]
#[should_panic(expected = "Runner::run")]
fn reject_case_outside_runner_panics_with_helpful_message() {
let mut ctx = TestCaseContext::new(1);
ctx.reject_case();
}
#[test]
fn recording_captures_flat_attempt_spans() {
let ((), seq) = RecordingSession::new(1).run(|ctx| {
let id = ctx.begin_attempt();
let mut a = [0u8; 4];
ctx.fill(&mut a);
ctx.end_attempt(id, AttemptVerdict::Rejected);
let id = ctx.begin_attempt();
let mut b = [0u8; 4];
ctx.fill(&mut b);
ctx.end_attempt(id, AttemptVerdict::Accepted);
});
assert_eq!(seq.spans().len(), 2);
assert_eq!(seq.spans()[0].parent, None);
assert_eq!(seq.spans()[0].start_draw, 0);
assert_eq!(seq.spans()[0].end_draw, 1);
assert_eq!(seq.spans()[0].verdict, AttemptVerdict::Rejected);
assert_eq!(seq.spans()[1].parent, None);
assert_eq!(seq.spans()[1].start_draw, 1);
assert_eq!(seq.spans()[1].end_draw, 2);
assert_eq!(seq.spans()[1].verdict, AttemptVerdict::Accepted);
}
#[test]
fn recording_captures_nested_attempt_spans() {
let ((), seq) = RecordingSession::new(1).run(|ctx| {
let outer = ctx.begin_attempt();
let mut a = [0u8; 4];
ctx.fill(&mut a);
let inner = ctx.begin_attempt();
let mut b = [0u8; 4];
ctx.fill(&mut b);
ctx.end_attempt(inner, AttemptVerdict::Rejected);
let inner2 = ctx.begin_attempt();
let mut c = [0u8; 4];
ctx.fill(&mut c);
ctx.end_attempt(inner2, AttemptVerdict::Accepted);
ctx.end_attempt(outer, AttemptVerdict::Accepted);
});
assert_eq!(seq.spans().len(), 3);
assert_eq!(seq.spans()[0].parent, None);
assert_eq!(seq.spans()[1].parent, Some(0));
assert_eq!(seq.spans()[2].parent, Some(0));
assert_eq!(seq.spans()[0].start_draw, 0);
assert_eq!(seq.spans()[0].end_draw, 3);
assert_eq!(seq.spans()[1].start_draw, 1);
assert_eq!(seq.spans()[1].end_draw, 2);
assert_eq!(seq.spans()[2].start_draw, 2);
assert_eq!(seq.spans()[2].end_draw, 3);
}
#[test]
fn replay_reproduces_nested_attempt_spans() {
let ((), seq) = RecordingSession::new(9).run(|ctx| {
let outer = ctx.begin_attempt();
let mut a = [0u8; 4];
ctx.fill(&mut a);
let inner = ctx.begin_attempt();
let mut b = [0u8; 4];
ctx.fill(&mut b);
ctx.end_attempt(inner, AttemptVerdict::Rejected);
ctx.end_attempt(outer, AttemptVerdict::Accepted);
});
let result = ReplaySession::new(seq).run(|ctx| {
let outer = ctx.begin_attempt();
let mut a = [0u8; 4];
ctx.fill(&mut a);
let inner = ctx.begin_attempt();
let mut b = [0u8; 4];
ctx.fill(&mut b);
ctx.end_attempt(inner, AttemptVerdict::Rejected);
ctx.end_attempt(outer, AttemptVerdict::Accepted);
});
assert_eq!(result, Ok(()));
}
#[test]
fn replay_flags_verdict_mismatch() {
let ((), seq) = RecordingSession::new(1).run(|ctx| {
let id = ctx.begin_attempt();
let mut a = [0u8; 4];
ctx.fill(&mut a);
ctx.end_attempt(id, AttemptVerdict::Accepted);
});
let result = ReplaySession::new(seq).run(|ctx| {
let id = ctx.begin_attempt();
let mut a = [0u8; 4];
ctx.fill(&mut a);
ctx.end_attempt(id, AttemptVerdict::Rejected);
});
assert!(matches!(
result,
Err(ReplayError::SpanMismatch {
at_span: 0,
reason: SpanMismatchReason::VerdictMismatch {
expected: AttemptVerdict::Accepted,
actual: AttemptVerdict::Rejected,
}
})
));
}
#[test]
fn replay_flags_parent_mismatch() {
let ((), seq) = RecordingSession::new(1).run(|ctx| {
let outer = ctx.begin_attempt();
let inner = ctx.begin_attempt();
ctx.end_attempt(inner, AttemptVerdict::Accepted);
ctx.end_attempt(outer, AttemptVerdict::Accepted);
});
let result = ReplaySession::new(seq).run(|ctx| {
let outer = ctx.begin_attempt();
ctx.end_attempt(outer, AttemptVerdict::Accepted);
let sibling = ctx.begin_attempt();
ctx.end_attempt(sibling, AttemptVerdict::Accepted);
});
assert!(
matches!(
result,
Err(ReplayError::SpanMismatch {
at_span: 1,
reason: SpanMismatchReason::ParentMismatch,
})
),
"got {result:?}"
);
}
#[test]
fn replay_flags_start_draw_mismatch() {
let ((), seq) = RecordingSession::new(1).run(|ctx| {
let mut a = [0u8; 4];
ctx.fill(&mut a);
let id = ctx.begin_attempt();
ctx.end_attempt(id, AttemptVerdict::Accepted);
});
let result = ReplaySession::new(seq).run(|ctx| {
let id = ctx.begin_attempt();
ctx.end_attempt(id, AttemptVerdict::Accepted);
let mut a = [0u8; 4];
ctx.fill(&mut a);
});
assert!(
matches!(
result,
Err(ReplayError::SpanMismatch {
at_span: 0,
reason: SpanMismatchReason::StartDrawMismatch {
expected: 1,
actual: 0,
},
})
),
"got {result:?}"
);
}
#[test]
fn replay_flags_end_draw_mismatch() {
let ((), seq) = RecordingSession::new(1).run(|ctx| {
let id = ctx.begin_attempt();
let mut a = [0u8; 4];
ctx.fill(&mut a);
ctx.end_attempt(id, AttemptVerdict::Accepted);
});
let result = ReplaySession::new(seq).run(|ctx| {
let id = ctx.begin_attempt();
ctx.end_attempt(id, AttemptVerdict::Accepted);
let mut a = [0u8; 4];
ctx.fill(&mut a);
});
assert!(
matches!(
result,
Err(ReplayError::SpanMismatch {
at_span: 0,
reason: SpanMismatchReason::EndDrawMismatch {
expected: 1,
actual: 0,
},
})
),
"got {result:?}"
);
}
#[test]
fn replay_flags_span_sequence_exhausted() {
let ((), seq) = RecordingSession::new(1).run(|ctx| {
let id = ctx.begin_attempt();
let mut a = [0u8; 4];
ctx.fill(&mut a);
ctx.end_attempt(id, AttemptVerdict::Accepted);
});
let result = ReplaySession::new(seq).run(|ctx| {
let id = ctx.begin_attempt();
let mut a = [0u8; 4];
ctx.fill(&mut a);
ctx.end_attempt(id, AttemptVerdict::Accepted);
let extra = ctx.begin_attempt();
ctx.end_attempt(extra, AttemptVerdict::Accepted);
});
assert!(matches!(
result,
Err(ReplayError::SpanMismatch {
at_span: 1,
reason: SpanMismatchReason::SequenceExhausted,
})
));
}
#[test]
fn replay_flags_leftover_spans() {
let ((), seq) = RecordingSession::new(1).run(|ctx| {
let id = ctx.begin_attempt();
let mut a = [0u8; 4];
ctx.fill(&mut a);
ctx.end_attempt(id, AttemptVerdict::Accepted);
});
let result = ReplaySession::new(seq).run(|ctx| {
let mut a = [0u8; 4];
ctx.fill(&mut a);
});
assert!(matches!(
result,
Err(ReplayError::LeftoverSpans { unused: 1 })
));
}
#[test]
fn zero_draw_attempts_are_recorded_as_empty_range() {
let ((), seq) = RecordingSession::new(1).run(|ctx| {
let id = ctx.begin_attempt();
ctx.end_attempt(id, AttemptVerdict::Rejected);
let id = ctx.begin_attempt();
ctx.end_attempt(id, AttemptVerdict::Accepted);
});
assert_eq!(seq.spans().len(), 2);
assert_eq!(seq.spans()[0].start_draw, seq.spans()[0].end_draw);
assert_eq!(seq.spans()[1].start_draw, seq.spans()[1].end_draw);
assert_eq!(seq.draws().len(), 0);
}
}
#[cfg(test)]
mod targeted_tests {
use super::*;
#[test]
fn semantic_methods_are_noop_when_disabled() {
let mut ctx = TestCaseContext::new(1);
ctx.event("e");
ctx.bucket("b", 7);
ctx.transition("t", 0, 1);
assert!(matches!(ctx.take_feedback(), FeedbackState::Disabled));
}
#[test]
fn corpus_guided_collects_event_bucket_and_transition() {
let mut ctx = TestCaseContext::new(1);
ctx.enable_corpus_guided();
ctx.event("snapshot-install");
ctx.bucket("queue-length", 3);
ctx.transition("raft-role", 0, 1);
match ctx.take_feedback() {
FeedbackState::SemanticCoverage(cov) => {
let reprs: Vec<String> = cov.features().iter().map(|f| f.display_repr()).collect();
assert_eq!(
reprs,
[
"event(\"snapshot-install\")",
"bucket(\"queue-length\", 3)",
"transition(\"raft-role\", 0, 1)",
]
);
}
other => panic!("expected feedback-guided feedback, got {other:?}"),
}
}
#[test]
fn event_saturates_into_hit_count_buckets() {
let mut ctx = TestCaseContext::new(1);
ctx.enable_corpus_guided();
ctx.event("visit");
ctx.event("visit");
ctx.event("visit");
match ctx.take_feedback() {
FeedbackState::SemanticCoverage(cov) => {
let reprs: Vec<String> = cov.features().iter().map(|f| f.display_repr()).collect();
assert_eq!(reprs, ["event(\"visit\")"]);
assert!(matches!(
cov.features()[0].kind,
FeatureKind::Event(EventBucket::TwoThree)
));
}
other => panic!("expected feedback-guided feedback, got {other:?}"),
}
}
#[test]
fn event_bucket_boundaries() {
assert_eq!(event_bucket(1), EventBucket::One);
assert_eq!(event_bucket(2), EventBucket::TwoThree);
assert_eq!(event_bucket(3), EventBucket::TwoThree);
assert_eq!(event_bucket(4), EventBucket::FourSeven);
assert_eq!(event_bucket(7), EventBucket::FourSeven);
assert_eq!(event_bucket(8), EventBucket::EightPlus);
assert_eq!(event_bucket(100), EventBucket::EightPlus);
}
#[test]
fn duplicate_bucket_and_transition_features_deduplicate() {
let mut ctx = TestCaseContext::new(1);
ctx.enable_corpus_guided();
ctx.bucket("q", 3);
ctx.bucket("q", 3);
ctx.transition("r", 0, 1);
ctx.transition("r", 0, 1);
match ctx.take_feedback() {
FeedbackState::SemanticCoverage(cov) => {
assert_eq!(cov.features().len(), 2);
}
other => panic!("expected feedback-guided feedback, got {other:?}"),
}
}
#[test]
fn per_case_feature_cap_discards_excess() {
let mut ctx = TestCaseContext::new(1);
ctx.enable_corpus_guided();
for i in 0..(MAX_FEATURES_PER_CASE + 10) {
ctx.bucket("b", i as u64);
}
match ctx.take_feedback() {
FeedbackState::SemanticCoverage(cov) => {
assert_eq!(cov.features().len(), MAX_FEATURES_PER_CASE);
}
other => panic!("expected feedback-guided feedback, got {other:?}"),
}
}
#[test]
fn take_feedback_resets_corpus_guided_to_disabled() {
let mut ctx = TestCaseContext::new(1);
ctx.enable_corpus_guided();
ctx.event("e");
let _ = ctx.take_feedback();
assert!(matches!(ctx.feedback, FeedbackState::Disabled));
}
#[test]
fn recording_tags_bounded_choice_and_integer_draws() {
let ((), seq) = RecordingSession::new(1).run(|ctx| {
let _ = crate::sample_usize_in(ctx, 0..10);
let _ = crate::sample_choice(ctx, &[1, 2, 3, 4]);
let _ = crate::sample_u32(ctx);
});
let metas = seq.metas();
assert!(matches!(metas[0], ChoiceMeta::Bounded { bound: 10 }));
assert!(matches!(metas[1], ChoiceMeta::Choice { len: 4 }));
assert!(matches!(metas[2], ChoiceMeta::Integer));
}
#[test]
fn exploring_replays_recorded_draws_then_generates() {
let ((), seq) = RecordingSession::new(1).run(|ctx| {
let mut buf = [0u8; 4];
ctx.fill(&mut buf);
});
let recorded = seq.draws()[0].clone();
assert_eq!(recorded.len(), 4);
let mut ctx = TestCaseContext::exploring(seq, 42);
let mut buf = [0u8; 4];
ctx.fill(&mut buf);
assert_eq!(
buf,
recorded.as_slice(),
"the first draw must replay the recorded bytes"
);
ctx.fill(&mut buf);
let seq = ctx.take_sequence().expect("exploring must record draws");
assert_eq!(seq.draws().len(), 2, "the generated draw must be recorded");
}
#[test]
fn exploring_replays_mutated_draws() {
let ((), mut seq) = RecordingSession::new(1).run(|ctx| {
let mut buf = [0u8; 8];
ctx.fill(&mut buf);
});
seq.draws_mut()[0][..8].copy_from_slice(&7u64.to_le_bytes());
let mut ctx = TestCaseContext::exploring(seq, 42);
let mut buf = [0u8; 8];
ctx.fill(&mut buf);
assert_eq!(
u64::from_le_bytes(buf),
7,
"a mutated draw must be replayed with its new value"
);
}
#[test]
fn take_sequence_works_in_exploring_mode() {
let mut ctx = TestCaseContext::exploring(ChoiceSequence::default(), 1);
let mut buf = [0u8; 4];
ctx.fill(&mut buf);
let seq = ctx
.take_sequence()
.expect("exploring must yield a sequence");
assert_eq!(seq.draws().len(), 1);
}
#[test]
fn exploring_aborts_at_choice_cap() {
let mut ctx = TestCaseContext::exploring(ChoiceSequence::default(), 1);
let mut buf = [0u8; 4];
for _ in 0..MAX_CHOICES_PER_CASE {
ctx.fill(&mut buf);
}
let outcome = std::panic::catch_unwind(AssertUnwindSafe(|| {
ctx.fill(&mut buf);
}));
assert!(outcome.is_err(), "drawing past the cap must unwind");
assert!(
ctx.take_rejection().is_some(),
"the cap abort must be recorded as a rejection"
);
}
#[test]
fn exploring_aborts_at_cap_when_replacing_dead_draws() {
let ((), seq) = RecordingSession::new(1).run(|ctx| {
let mut buf = [0u8; 4];
ctx.fill(&mut buf);
});
let mut ctx = TestCaseContext::exploring(seq, 1);
let mut buf = [0u8; 8];
for _ in 0..MAX_CHOICES_PER_CASE {
ctx.fill(&mut buf);
}
let outcome = std::panic::catch_unwind(AssertUnwindSafe(|| {
ctx.fill(&mut buf);
}));
assert!(outcome.is_err(), "drawing past the cap must unwind");
assert!(
ctx.take_rejection().is_some(),
"the cap abort must be recorded as a rejection"
);
}
}
#[cfg(test)]
mod regression_tests {
use crate::rng::{ChoiceMeta, RecordingSession, TestCaseContext};
#[test]
fn exploring_adopts_new_meta_on_replay() {
let ((), seq) = RecordingSession::new(1).run(|ctx| {
let mut buf = [0u8; 8];
ctx.fill(&mut buf);
});
let recorded_value = seq.draws()[0].clone();
let mut ctx = TestCaseContext::exploring(seq, 42);
let mut buf = [0u8; 8];
ctx.set_next_choice_meta(ChoiceMeta::Bounded { bound: 100 });
ctx.fill(&mut buf);
ctx.fill(&mut buf);
let seq = ctx.take_sequence().expect("sequence");
assert!(
matches!(seq.metas()[0], ChoiceMeta::Bounded { bound: 100 }),
"replayed draw must adopt the new declared domain: {:?}",
seq.metas()[0]
);
assert_ne!(
seq.draws()[0],
recorded_value,
"the value at a changed constraint must be regenerated"
);
assert!(
matches!(seq.metas()[1], ChoiceMeta::Raw),
"stale meta must not leak into a generated draw: {:?}",
seq.metas()[1]
);
}
#[test]
fn exploring_replaces_dead_draws_in_place() {
let ((), seq) = RecordingSession::new(1).run(|ctx| {
let mut buf = [0u8; 4];
ctx.fill(&mut buf);
});
let mut ctx = TestCaseContext::exploring(seq, 42);
let mut buf = [0u8; 8];
ctx.fill(&mut buf);
let seq = ctx.take_sequence().expect("sequence");
assert_eq!(
seq.draws().len(),
1,
"dead draw must be replaced, not appended"
);
assert_eq!(seq.draws()[0].len(), 8);
}
#[test]
fn exploring_replays_long_records_without_hitting_cap() {
let ((), seq) = RecordingSession::new(1).run(|ctx| {
for _ in 0..5000 {
let mut buf = [0u8; 4];
ctx.fill(&mut buf);
}
});
let mut ctx = TestCaseContext::exploring(seq, 42);
let mut buf = [0u8; 4];
for _ in 0..5000 {
ctx.fill(&mut buf);
}
ctx.fill(&mut buf);
let seq = ctx.take_sequence().expect("sequence");
assert_eq!(seq.draws().len(), 5001);
}
}
#[cfg(test)]
mod suffix_tests {
use crate::rng::{RecordingSession, TestCaseContext};
#[test]
fn take_sequence_discards_unconsumed_suffix() {
let ((), seq) = RecordingSession::new(1).run(|ctx| {
let mut a = [0u8; 4];
ctx.fill(&mut a);
let mut b = [0u8; 4];
ctx.fill(&mut b);
let mut c = [0u8; 4];
ctx.fill(&mut c);
});
assert_eq!(seq.draws().len(), 3);
let mut ctx = TestCaseContext::exploring(seq, 42);
let mut buf = [0u8; 4];
ctx.fill(&mut buf);
let seq = ctx.take_sequence().expect("sequence");
assert_eq!(seq.draws().len(), 1, "unconsumed suffix must be discarded");
}
}