use std::collections::{HashMap, HashSet};
use std::fmt::Debug;
use std::sync::{LazyLock, Mutex};
use rand::RngExt;
use rand::rngs::SmallRng;
use super::choices::{
BooleanChoice, ChoiceKind, ChoiceNode, ChoiceValue, FloatChoice, IntegerChoice,
InterestingOrigin, Status, StopTest,
};
use super::float_index::lex_to_float;
use super::{BOUNDARY_PROBABILITY, BUFFER_SIZE};
pub struct ManyState {
pub min_size: usize,
pub max_size: f64,
pub p_continue: f64,
pub count: usize,
pub rejections: usize,
pub force_stop: bool,
}
impl ManyState {
pub fn new(min_size: usize, max_size: Option<usize>) -> Self {
let max_f = max_size.map_or(f64::INFINITY, |n| n as f64);
let min_f = min_size as f64;
let average = f64::min(f64::max(min_f * 2.0, min_f + 5.0), 0.5 * (min_f + max_f));
let desired_extra = average - min_f;
let max_extra = max_f - min_f;
let p_continue = if desired_extra >= max_extra {
0.99
} else if max_f.is_infinite() {
1.0 - 1.0 / (1.0 + desired_extra)
} else {
1.0 - 1.0 / (2.0 + desired_extra)
};
ManyState {
min_size,
max_size: max_f,
p_continue,
count: 0,
rejections: 0,
force_stop: false,
}
}
}
static GLOBAL_CONSTANTS_INTEGERS: LazyLock<Vec<i128>> = LazyLock::new(|| {
let mut base: Vec<i128> = Vec::new();
for n in 16u32..66 {
base.push(1i128 << n);
}
let mut p10 = 100_000i128;
for _ in 5..20u32 {
base.push(p10);
p10 *= 10;
}
let mut f = 362_880i128; base.push(f);
for i in 10u32..=20 {
f *= i as i128;
base.push(f);
}
base.extend_from_slice(&[
510_510i128,
6_469_693_230,
304_250_263_527_210,
32_589_158_477_190_044_730,
]);
let n_base = base.len();
for i in 0..n_base {
base.push(base[i] - 1);
base.push(base[i] + 1);
}
let n_half = base.len();
for i in 0..n_half {
base.push(-base[i]);
}
base.sort_unstable();
base.dedup();
base
});
pub(crate) fn biased_integer_sample(ic: &IntegerChoice, rng: &mut SmallRng) -> i128 {
if ic.min_value == ic.max_value {
return ic.min_value;
}
let mut nasty: Vec<i128> = vec![ic.min_value, ic.max_value];
let interesting: &[i128] = &[
0,
1,
-1,
2,
-2,
7,
-7,
8,
-8,
15,
-15,
16,
-16,
31,
-31,
32,
-32,
63,
-63,
64,
-64,
127,
-127,
128,
-128,
255,
-255,
256,
-256,
511,
-511,
512,
-512,
1023,
-1023,
1024,
-1024,
2047,
-2047,
2048,
-2048,
4095,
-4095,
4096,
-4096,
8191,
-8191,
8192,
-8192,
i16::MAX as i128,
i16::MIN as i128,
i32::MAX as i128,
i32::MIN as i128,
i64::MAX as i128,
i64::MIN as i128,
];
for &v in interesting {
if ic.validate(v) && !nasty.contains(&v) {
nasty.push(v);
}
}
for &v in GLOBAL_CONSTANTS_INTEGERS.iter() {
if ic.validate(v) && !nasty.contains(&v) {
nasty.push(v);
}
}
let threshold = nasty.len() as f64 * BOUNDARY_PROBABILITY;
if rng.random::<f64>() < threshold {
let idx = rng.random_range(0..nasty.len());
nasty[idx]
} else {
rng.random_range(ic.min_value..=ic.max_value)
}
}
pub(crate) fn biased_float_sample(fc: &FloatChoice, rng: &mut SmallRng) -> f64 {
let bounded = fc.min_value.is_finite() && fc.max_value.is_finite();
let half_bounded = !bounded && (fc.min_value.is_finite() || fc.max_value.is_finite());
let candidates = [
fc.min_value,
fc.max_value,
0.0,
-0.0_f64,
1.0,
-1.0,
f64::INFINITY,
f64::NEG_INFINITY,
f64::NAN,
f64::MIN_POSITIVE,
f64::MAX,
-f64::MAX,
];
let nasty: Vec<f64> = candidates
.iter()
.copied()
.filter(|&v| fc.validate(v))
.collect();
let nasty_threshold = nasty.len() as f64 * BOUNDARY_PROBABILITY;
if rng.random::<f64>() < nasty_threshold {
let idx = rng.random_range(0..nasty.len());
return nasty[idx];
}
let f = if bounded {
let r: f64 = rng.random();
let v = fc.min_value + r * (fc.max_value - fc.min_value);
v.max(fc.min_value).min(fc.max_value)
} else if half_bounded {
let use_inf = fc.allow_infinity && rng.random::<f64>() < 0.05;
if use_inf {
if fc.max_value == f64::INFINITY {
f64::INFINITY
} else {
f64::NEG_INFINITY
}
} else {
loop {
let bits: u64 = rng.random();
let mag = lex_to_float(bits).abs();
if mag.is_finite() {
break if fc.min_value.is_finite() {
fc.min_value + mag
} else {
fc.max_value - mag
};
}
}
}
} else if fc.allow_nan && rng.random::<f64>() < 0.01 {
let exponent: u64 = 0x7FF << 52;
let sign: u64 = (rng.random::<u64>() >> 63) << 63;
let mantissa: u64 = (rng.random::<u64>() & ((1u64 << 52) - 1)).max(1);
f64::from_bits(sign | exponent | mantissa)
} else {
loop {
let bits: u64 = rng.random();
let v = lex_to_float(bits);
if !v.is_nan() {
break v;
}
}
};
if fc.validate(f) { f } else { fc.simplest() }
}
pub struct NativeVariables {
last_id: i128,
variables: Vec<i128>,
removed: std::collections::HashSet<i128>,
}
impl NativeVariables {
pub fn new() -> Self {
NativeVariables {
last_id: 0,
variables: Vec::new(),
removed: std::collections::HashSet::new(),
}
}
pub fn next(&mut self) -> i128 {
self.last_id += 1;
self.variables.push(self.last_id);
self.last_id
}
pub fn active(&self) -> Vec<i128> {
self.variables
.iter()
.filter(|id| !self.removed.contains(*id))
.copied()
.collect()
}
pub fn consume(&mut self, variable_id: i128) {
self.removed.insert(variable_id);
while let Some(&last) = self.variables.last() {
if self.removed.contains(&last) {
self.variables.pop();
self.removed.remove(&last);
} else {
break;
}
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Span {
pub start: usize,
pub end: usize,
pub label: String,
pub depth: u32,
pub parent: Option<usize>,
pub discarded: bool,
}
pub const MAX_DEPTH: u32 = 100;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct CoverageTag {
pub label: u64,
}
static STRUCTURAL_COVERAGE_CACHE: LazyLock<Mutex<HashMap<u64, &'static CoverageTag>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
pub fn structural_coverage(label: u64) -> &'static CoverageTag {
let mut cache = STRUCTURAL_COVERAGE_CACHE.lock().unwrap();
cache
.entry(label)
.or_insert_with(|| Box::leak(Box::new(CoverageTag { label })))
}
#[derive(Clone, Debug, Default)]
pub struct Spans {
inner: Vec<Span>,
}
impl Spans {
pub fn new() -> Self {
Spans { inner: Vec::new() }
}
pub fn len(&self) -> usize {
self.inner.len()
}
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
pub fn push(&mut self, span: Span) {
self.inner.push(span);
}
pub fn get_mut(&mut self, i: usize) -> Option<&mut Span> {
self.inner.get_mut(i)
}
pub fn get(&self, i: usize) -> Option<&Span> {
self.inner.get(i)
}
pub fn get_signed(&self, i: i64) -> Option<&Span> {
let n = self.inner.len() as i64;
if i < -n || i >= n {
return None;
}
let idx = if i < 0 { (i + n) as usize } else { i as usize };
self.inner.get(idx)
}
pub fn children(&self, i: usize) -> Vec<usize> {
self.inner
.iter()
.enumerate()
.filter_map(|(j, s)| (s.parent == Some(i)).then_some(j))
.collect()
}
pub fn as_slice(&self) -> &[Span] {
&self.inner
}
pub fn as_mut_slice(&mut self) -> &mut [Span] {
&mut self.inner
}
pub fn into_vec(self) -> Vec<Span> {
self.inner
}
}
impl From<Vec<Span>> for Spans {
fn from(inner: Vec<Span>) -> Self {
Spans { inner }
}
}
impl std::ops::Deref for Spans {
type Target = [Span];
fn deref(&self) -> &[Span] {
&self.inner
}
}
impl<'a> IntoIterator for &'a Spans {
type Item = &'a Span;
type IntoIter = std::slice::Iter<'a, Span>;
fn into_iter(self) -> Self::IntoIter {
self.inner.iter()
}
}
impl std::ops::Index<usize> for Spans {
type Output = Span;
fn index(&self, i: usize) -> &Span {
&self.inner[i]
}
}
impl std::ops::Index<i64> for Spans {
type Output = Span;
fn index(&self, i: i64) -> &Span {
let n = self.inner.len();
self.get_signed(i).unwrap_or_else(|| {
panic!("Index {i} out of range [-{n}, {n})");
})
}
}
pub trait DataObserver: Send {
fn draw_boolean(&mut self, _value: bool, _was_forced: bool) {}
fn draw_integer(&mut self, _value: i128, _was_forced: bool) {}
fn draw_float(&mut self, _value: f64, _was_forced: bool) {}
fn conclude_test(&mut self, _status: Status, _origin: Option<InterestingOrigin>) {}
}
pub struct NativeTestCase {
prefix: Vec<ChoiceValue>,
prefix_nodes: Option<Vec<ChoiceNode>>,
rng: Option<SmallRng>,
max_size: usize,
pub nodes: Vec<ChoiceNode>,
pub status: Option<Status>,
frozen: bool,
pub collections: HashMap<i64, ManyState>,
next_collection_id: i64,
pub variable_pools: Vec<NativeVariables>,
pub spans: Spans,
pub span_stack: Vec<usize>,
pub has_discards: bool,
pub tags: HashSet<&'static CoverageTag>,
labels_for_structure_stack: Vec<HashSet<u64>>,
observer: Option<Box<dyn DataObserver>>,
interesting_origin: Option<InterestingOrigin>,
}
impl NativeTestCase {
pub fn new_random(rng: SmallRng) -> Self {
NativeTestCase {
prefix: Vec::new(),
prefix_nodes: None,
rng: Some(rng),
max_size: BUFFER_SIZE,
nodes: Vec::new(),
status: None,
frozen: false,
collections: HashMap::new(),
next_collection_id: 0,
variable_pools: Vec::new(),
spans: Spans::new(),
span_stack: Vec::new(),
has_discards: false,
tags: HashSet::new(),
labels_for_structure_stack: Vec::new(),
observer: None,
interesting_origin: None,
}
}
pub fn for_choices(
choices: &[ChoiceValue],
prefix_nodes: Option<&[ChoiceNode]>,
observer: Option<Box<dyn DataObserver>>,
) -> Self {
NativeTestCase {
prefix: choices.to_vec(),
prefix_nodes: prefix_nodes.map(|n| n.to_vec()),
rng: None,
max_size: choices.len(),
nodes: Vec::new(),
status: None,
frozen: false,
collections: HashMap::new(),
next_collection_id: 0,
variable_pools: Vec::new(),
spans: Spans::new(),
span_stack: Vec::new(),
has_discards: false,
tags: HashSet::new(),
labels_for_structure_stack: Vec::new(),
observer,
interesting_origin: None,
}
}
pub fn for_probe(prefix: &[ChoiceValue], rng: SmallRng, max_size: usize) -> Self {
NativeTestCase {
prefix: prefix.to_vec(),
prefix_nodes: None,
rng: Some(rng),
max_size,
nodes: Vec::new(),
status: None,
frozen: false,
collections: HashMap::new(),
next_collection_id: 0,
variable_pools: Vec::new(),
spans: Spans::new(),
span_stack: Vec::new(),
has_discards: false,
tags: HashSet::new(),
labels_for_structure_stack: Vec::new(),
observer: None,
interesting_origin: None,
}
}
pub fn record_span(&mut self, start: usize, end: usize, label: String) {
if end > start {
let parent = self.span_stack.last().copied();
let depth = self.span_stack.len() as u32;
self.spans.push(Span {
start,
end,
label,
depth,
parent,
discarded: false,
});
}
}
pub fn start_span(&mut self, label: u64) -> usize {
let parent = self.span_stack.last().copied();
let depth = self.span_stack.len() as u32;
let idx = self.spans.len();
let start = self.nodes.len();
self.spans.push(Span {
start,
end: start,
label: label.to_string(),
depth,
parent,
discarded: false,
});
self.span_stack.push(idx);
let mut frame = HashSet::new();
frame.insert(label);
self.labels_for_structure_stack.push(frame);
if depth + 1 > MAX_DEPTH && self.status.is_none() {
self.status = Some(Status::Invalid);
self.freeze();
}
idx
}
pub fn stop_span(&mut self, discard: bool) {
let Some(idx) = self.span_stack.pop() else {
return;
};
let end = self.nodes.len();
if let Some(span) = self.spans.get_mut(idx) {
span.end = end;
span.discarded = discard;
}
if discard {
self.has_discards = true;
}
let labels = self.labels_for_structure_stack.pop().unwrap_or_default();
if !discard {
if let Some(parent) = self.labels_for_structure_stack.last_mut() {
parent.extend(labels);
} else {
self.tags
.extend(labels.into_iter().map(structural_coverage));
}
}
}
pub fn freeze(&mut self) {
if self.frozen {
return;
}
self.frozen = true;
let end = self.nodes.len();
while let Some(idx) = self.span_stack.pop() {
if let Some(span) = self.spans.get_mut(idx) {
span.end = end;
}
}
if self.status.is_none() {
self.status = Some(Status::Valid);
}
if let Some(ref mut obs) = self.observer {
let origin = self.interesting_origin.clone();
obs.conclude_test(self.status.unwrap(), origin);
}
}
pub fn new_collection(&mut self, state: ManyState) -> i64 {
let id = self.next_collection_id;
self.next_collection_id += 1;
self.collections.insert(id, state);
id
}
pub fn draw_integer(&mut self, min_value: i128, max_value: i128) -> Result<i128, StopTest> {
assert!(
min_value <= max_value,
"Invalid range [{min_value}, {max_value}]"
);
let kind = IntegerChoice {
min_value,
max_value,
shrink_towards: 0,
};
let (value, was_forced) = self.resolve_choice(
&ChoiceKind::Integer(kind.clone()),
|| ChoiceValue::Integer(kind.simplest()),
|| ChoiceValue::Integer(kind.unit()),
|v| matches!(v, ChoiceValue::Integer(n) if kind.validate(*n)),
|rng| ChoiceValue::Integer(biased_integer_sample(&kind, rng)),
)?;
let ChoiceValue::Integer(v) = value else {
unreachable!("kind/value invariant violated: outer match guaranteed this variant")
};
self.nodes.push(ChoiceNode {
kind: ChoiceKind::Integer(kind),
value: ChoiceValue::Integer(v),
was_forced,
});
if let Some(ref mut obs) = self.observer {
obs.draw_integer(v, was_forced);
}
Ok(v)
}
pub fn draw_float(
&mut self,
min_value: f64,
max_value: f64,
allow_nan: bool,
allow_infinity: bool,
) -> Result<f64, StopTest> {
let kind = FloatChoice {
min_value,
max_value,
allow_nan,
allow_infinity,
};
let (value, was_forced) = self.resolve_choice(
&ChoiceKind::Float(kind.clone()),
|| ChoiceValue::Float(kind.simplest()),
|| ChoiceValue::Float(kind.unit()),
|v| matches!(v, ChoiceValue::Float(f) if kind.validate(*f)),
|rng| ChoiceValue::Float(biased_float_sample(&kind, rng)),
)?;
let ChoiceValue::Float(v) = value else {
unreachable!("kind/value invariant violated: outer match guaranteed this variant")
};
self.nodes.push(ChoiceNode {
kind: ChoiceKind::Float(kind),
value: ChoiceValue::Float(v),
was_forced,
});
if let Some(ref mut obs) = self.observer {
obs.draw_float(v, was_forced);
}
Ok(v)
}
pub fn weighted(&mut self, p: f64, forced: Option<bool>) -> Result<bool, StopTest> {
let kind = BooleanChoice;
let forced_value = forced.or(if p <= 0.0 {
Some(false)
} else if p >= 1.0 {
Some(true)
} else {
None
});
let (value, was_forced) = if let Some(f) = forced_value {
self.pre_choice()?;
(ChoiceValue::Boolean(f), true)
} else {
self.resolve_choice(
&ChoiceKind::Boolean(kind.clone()),
|| ChoiceValue::Boolean(kind.simplest()),
|| ChoiceValue::Boolean(kind.unit()),
|v| matches!(v, ChoiceValue::Boolean(_)),
|rng| ChoiceValue::Boolean(rng.random::<f64>() <= p),
)?
};
let ChoiceValue::Boolean(v) = value else {
unreachable!("kind/value invariant violated: outer match guaranteed this variant")
};
self.nodes.push(ChoiceNode {
kind: ChoiceKind::Boolean(kind),
value: ChoiceValue::Boolean(v),
was_forced,
});
if let Some(ref mut obs) = self.observer {
obs.draw_boolean(v, was_forced);
}
Ok(v)
}
fn pre_choice(&mut self) -> Result<(), StopTest> {
if self.status.is_some() {
return Err(StopTest);
}
if self.nodes.len() >= self.max_size {
self.status = Some(Status::EarlyStop);
return Err(StopTest);
}
Ok(())
}
fn resolve_choice(
&mut self,
_kind: &ChoiceKind,
simplest: impl FnOnce() -> ChoiceValue,
unit: impl FnOnce() -> ChoiceValue,
validate: impl FnOnce(&ChoiceValue) -> bool,
random: impl FnOnce(&mut SmallRng) -> ChoiceValue,
) -> Result<(ChoiceValue, bool), StopTest> {
self.pre_choice()?;
let idx = self.nodes.len();
if idx < self.prefix.len() {
let prefix_value = &self.prefix[idx];
if validate(prefix_value) {
Ok((prefix_value.clone(), false))
} else {
let is_simplest = self
.prefix_nodes
.as_ref()
.and_then(|pn| pn.get(idx))
.is_some_and(|pn| *prefix_value == pn.kind.simplest());
if is_simplest {
Ok((simplest(), false))
} else {
Ok((unit(), false))
}
}
} else {
let rng = self
.rng
.as_mut()
.expect("No RNG available for random generation");
Ok((random(rng), false))
}
}
}
#[cfg(test)]
#[path = "../../../tests/embedded/native/state_tests.rs"]
mod tests;