use std::collections::{HashMap, HashSet};
use std::fmt::Debug;
use std::sync::{LazyLock, Mutex};
use rand::{Rng, RngExt};
use crate::native::rng::EngineRng;
use super::choices::{
BooleanChoice, BytesChoice, ChoiceKind, ChoiceNode, ChoiceTemplate, ChoiceTemplateKind,
ChoiceValue, EngineError, FloatChoice, IntegerChoice, InterestingOrigin, Status, StringChoice,
};
use super::float_index::index_to_float;
use super::{BOUNDARY_PROBABILITY, BUFFER_SIZE};
use crate::native::bignum::{BigInt, BigUint, ToPrimitive, Zero};
use crate::native::floats::{next_down, next_up};
use crate::native::intervalsets::IntervalSet;
use crate::native::statistics::{
Distribution, LogStudentTDistribution, PiecewiseDistribution, UniformDistribution,
};
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 {
ManyState {
min_size,
max_size: max_size.map_or(f64::INFINITY, |n| n as f64),
p_continue: length_p_continue(min_size, max_size),
count: 0,
rejections: 0,
force_stop: false,
}
}
}
pub(crate) fn length_p_continue(min_size: usize, max_size: Option<usize>) -> f64 {
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;
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)
}
}
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
});
fn many_draw_length(rng: &mut EngineRng, min_size: usize, max_size: usize) -> usize {
if min_size == max_size {
return min_size;
}
let p_continue = length_p_continue(min_size, Some(max_size));
let u: f64 = rng.random();
let extra = (u.ln() / p_continue.ln()).floor();
assert!(extra >= 0.0);
min_size.saturating_add(extra as usize).min(max_size)
}
static INTEGERS_DISTRIBUTION: LazyLock<
PiecewiseDistribution<UniformDistribution, LogStudentTDistribution>,
> = LazyLock::new(|| {
PiecewiseDistribution::new(
UniformDistribution::new(256.0),
LogStudentTDistribution::new(13.0, 2),
256.0,
)
});
fn integer_sample_from_distribution(min_value: i128, max_value: i128, rng: &mut EngineRng) -> i128 {
let dist = &*INTEGERS_DISTRIBUTION;
let lo = dist.cdf(min_value as f64 - 0.5);
let hi = dist.cdf(max_value as f64 + 0.5);
if hi - lo < 1e-13 {
return rng.random_range(min_value..=max_value);
}
let p = (lo + rng.random::<f64>() * (hi - lo)).max(f64::MIN_POSITIVE);
(dist.inverse_cdf(p).round() as i128).clamp(min_value, max_value)
}
static INTERESTING_INTEGERS: &[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,
];
static SORTED_NASTY_POOL: LazyLock<Vec<i128>> = LazyLock::new(|| {
let mut all: Vec<i128> = INTERESTING_INTEGERS
.iter()
.copied()
.chain(GLOBAL_CONSTANTS_INTEGERS.iter().copied())
.collect();
all.sort_unstable();
all.dedup();
all
});
pub(crate) fn biased_integer_sample(ic: &IntegerChoice, rng: &mut EngineRng) -> BigInt {
match (ic.min_value.to_i128(), ic.max_value.to_i128()) {
(Some(min_i), Some(max_i)) => BigInt::from(biased_i128_sample(min_i, max_i, rng)),
_ => biguint_sample_in_range(&ic.min_value, &ic.max_value, rng),
}
}
fn biased_i128_sample(min_value: i128, max_value: i128, rng: &mut EngineRng) -> i128 {
if min_value == max_value {
return min_value;
}
let pool = &*SORTED_NASTY_POOL;
let lo = pool.partition_point(|&v| v < min_value);
let hi = pool.partition_point(|&v| v <= max_value);
let static_slice = &pool[lo..hi];
let need_min = static_slice.first() != Some(&min_value);
let need_max = static_slice.last() != Some(&max_value);
let count = static_slice.len() + (need_min as usize) + (need_max as usize);
let threshold = (count as f64 * BOUNDARY_PROBABILITY).min(0.5);
if rng.random::<f64>() < threshold {
let idx = rng.random_range(0..count);
if need_min && idx == 0 {
min_value
} else if need_max && idx == count - 1 {
max_value
} else {
static_slice[idx - need_min as usize]
}
} else {
integer_sample_from_distribution(min_value, max_value, rng)
}
}
fn biguint_sample_in_range(min: &BigInt, max: &BigInt, rng: &mut EngineRng) -> BigInt {
if min == max {
return min.clone();
}
let span: BigUint = (max - min).magnitude();
let bits = span.bits();
let mut nasty: Vec<BigInt> = vec![min.clone(), max.clone()];
let push_in_range = |v: BigInt, nasty: &mut Vec<BigInt>| {
if &v >= min && &v <= max {
nasty.push(v);
}
};
push_in_range(BigInt::from(0), &mut nasty);
push_in_range(BigInt::from(1), &mut nasty);
push_in_range(BigInt::from(-1), &mut nasty);
for k in 0..=bits.min(128) {
let p2 = BigInt::from(BigUint::from(1u32) << (k as usize));
push_in_range(-p2.clone(), &mut nasty);
push_in_range(p2, &mut nasty);
}
nasty.sort();
nasty.dedup();
let threshold = (nasty.len() as f64 * BOUNDARY_PROBABILITY).min(0.5);
if rng.random::<f64>() < threshold {
let idx = rng.random_range(0..nasty.len());
return nasty[idx].clone();
}
min + BigInt::from(sample_biguint_at_most(&span, rng))
}
fn sample_biguint_at_most(span: &BigUint, rng: &mut EngineRng) -> BigUint {
let bits = span.bits();
if bits == 0 {
unreachable!("sample_biguint_at_most requires a positive span");
}
let n_bytes = bits.div_ceil(8) as usize;
let top_bits = (bits % 8) as u32;
loop {
let mut bytes: Vec<u8> = (0..n_bytes).map(|_| rng.random::<u8>()).collect();
if top_bits != 0 {
let mask = (1u8 << top_bits) - 1;
let last = bytes.len() - 1;
bytes[last] &= mask;
}
let candidate = BigUint::from_bytes_le(&bytes);
if &candidate <= span {
return candidate;
}
}
}
pub(crate) fn biased_float_sample(fc: &FloatChoice, rng: &mut EngineRng) -> f64 {
const SIGNALING_NAN: f64 = f64::from_bits(0x7FF0_0000_0000_0001);
let candidates = [
fc.min_value,
fc.max_value,
next_up(fc.min_value),
fc.min_value + 1.0,
fc.max_value - 1.0,
next_down(fc.max_value),
0.0,
-0.0_f64,
1.0,
-1.0,
f64::INFINITY,
f64::NEG_INFINITY,
f64::NAN,
-f64::NAN,
SIGNALING_NAN,
-SIGNALING_NAN,
f64::MIN_POSITIVE,
fc.smallest_nonzero_magnitude,
-fc.smallest_nonzero_magnitude,
f64::MAX,
-f64::MAX,
];
let valid_count = candidates.iter().filter(|&&v| fc.validate(v)).count();
let nasty_threshold = (valid_count as f64 * BOUNDARY_PROBABILITY).min(0.5);
if rng.random::<f64>() < nasty_threshold {
let idx = rng.random_range(0..valid_count);
let mut skip = idx;
for &v in candidates.iter() {
if fc.validate(v) {
if skip == 0 {
return v;
}
skip -= 1;
}
}
unreachable!("valid_count agrees with the second validate pass");
}
let mag = index_to_float(rng.random::<u64>());
let raw = if rng.random::<u64>() & 1 == 1 {
-mag
} else {
mag
};
let f = if fc.validate(raw) {
raw
} else {
float_clamp(fc, raw)
};
if fc.validate(f) { f } else { fc.simplest() }
}
fn float_clamp(fc: &FloatChoice, raw: f64) -> f64 {
let (min_value, max_value) = if fc.allow_infinity {
(fc.min_value, fc.max_value)
} else {
(fc.min_value.max(-f64::MAX), fc.max_value.min(f64::MAX))
};
const MANTISSA_MASK: u64 = (1u64 << 52) - 1;
let range_size = (max_value - min_value).min(f64::MAX);
let mant = raw.abs().to_bits() & MANTISSA_MASK;
let mut f = min_value + range_size * (mant as f64 / MANTISSA_MASK as f64);
if f != 0.0 && f.abs() < fc.smallest_nonzero_magnitude {
f = fc.smallest_nonzero_magnitude;
if fc.smallest_nonzero_magnitude > max_value {
f = -f;
}
}
f.max(min_value).min(max_value)
}
pub(crate) fn biased_bytes_sample(bc: &BytesChoice, rng: &mut EngineRng) -> Vec<u8> {
let want_zero = bc.min_size == 0 && bc.max_size > 0;
let want_ff = bc.min_size <= 1 && bc.max_size >= 1;
let count = 1 + want_zero as usize + want_ff as usize;
let nasty_threshold = count as f64 * BOUNDARY_PROBABILITY;
if rng.random::<f64>() < nasty_threshold {
let mut slot = rng.random_range(0..count);
if slot == 0 {
return bc.simplest();
}
slot -= 1;
if want_zero {
if slot == 0 {
return vec![0u8];
}
slot -= 1;
}
debug_assert!(want_ff && slot == 0);
return vec![0xffu8];
}
let len = many_draw_length(rng, bc.min_size, bc.max_size);
(0..len).map(|_| rng.random::<u8>()).collect()
}
pub(crate) fn weighted_boolean_sample(p: f64, rng: &mut EngineRng) -> bool {
let falsey = (256.0 * (1.0 - p)).floor().max(1.0) as u32;
let mut byte = [0u8; 1];
rng.fill_bytes(&mut byte);
u32::from(byte[0]) >= falsey
}
static GLOBAL_CONSTANTS_STRINGS: LazyLock<Vec<Vec<u32>>> = LazyLock::new(|| {
let strings: &[&str] = &[
"undefined",
"null",
"NULL",
"nil",
"NIL",
"true",
"false",
"True",
"False",
"TRUE",
"FALSE",
"None",
"none",
"if",
"then",
"else",
"__dict__",
"__proto__",
"0",
"1e100",
"0..0",
"0/0",
"1/0",
"+0.0",
"Infinity",
"-Infinity",
"Inf",
"INF",
"NaN",
"999999999999999999999999999999",
",./;'[]\\-=<>?:\"{}|_+!@#$%^&*()`~",
"Ω≈ç√∫˜µ≤≥÷åß∂ƒ©˙∆˚¬…æœ∑´®†¥¨ˆøπ\u{201C}\u{2018}¡™£¢∞§¶•ªº–≠¸˛Ç◊ı˜Â¯˘¿ÅÍÎÏ˝ÓÔÒÚÆ☃Œ„´‰ˇÁ¨ˆØ∏\u{201D}\u{2019}`⁄€‹›fifl‡°·‚—±",
"Ⱥ",
"Ⱦ",
"æœÆŒffʤʨß",
"(╯°□°)╯︵ ┻━┻)",
"😍",
"🇺🇸",
"🏻",
"👍🏻",
"الكل في المجمو عة",
"᚛ᚄᚓᚐᚋᚒᚄ ᚑᚄᚂᚑᚏᚅ᚜",
"กา",
"ก ำกำ",
"𝐓𝐡𝐞 𝐪𝐮𝐢𝐜𝐤 𝐛𝐫𝐨𝐰𝐧 𝐟𝐨𝐱 𝐣𝐮𝐦𝐩𝐬 𝐨𝐯𝐞𝐫 𝐭𝐡𝐞 𝐥𝐚𝐳𝐲 𝐝𝐨𝐠",
"𝕿𝖍𝖊 𝖖𝖚𝖎𝖈𝖐 𝖇𝖗𝖔𝖜𝖓 𝖋𝖔𝖝 𝖏𝖚𝖒𝖕𝖘 𝖔𝖛𝖊𝖗 𝖙𝖍𝖊 𝖑𝖆𝖟𝖞 𝖉𝖔𝖌",
"𝑻𝒉𝒆 𝒒𝒖𝒊𝒄𝒌 𝒃𝒓𝒐𝒘𝒏 𝒇𝒐𝒙 𝒋𝒖𝒎𝒑𝒔 𝒐𝒗𝒆𝒓 𝒕𝒉𝒆 𝒍𝒂𝒛𝒚 𝒅𝒐𝒈",
"𝓣𝓱𝓮 𝓺𝓾𝓲𝓬𝓴 𝓫𝓻𝓸𝔀𝓷 𝓯𝓸𝔁 𝓳𝓾𝓶𝓹𝓼 𝓸𝓿𝓮𝓻 𝓽𝓱𝓮 𝓵𝓪𝔃𝔂 𝓭𝓸𝓰",
"𝕋𝕙𝕖 𝕢𝕦𝕚𝕔𝕜 𝕓𝕣𝕠𝕨𝕟 𝕗𝕠𝕩 𝕛𝕦𝕞𝕡𝕤 𝕠𝕧𝕖𝕣 𝕥𝕙𝕖 𝕝𝕒𝕫𝕪 𝕕𝕠𝕘",
"ʇǝɯɐ ʇᴉs ɹolop ɯnsdᴉ ɯǝɹo˥",
"NUL",
"COM1",
"LPT1",
"Scunthorpe",
"Ṱ̺̺̕o͞ ̷i̲̬͇̪͙n̝̗͕v̟̜̘̦͟o̶̙̰̠kè͚̮̺̪̹̱̤ ̖t̝͕̳̣̻̪͞h̼͓̲̦̳̘̲e͇̣̰̦̬͎ ̢̼̻̱̘h͚͎͙̜̣̲ͅi̦̲̣̰̤v̻͍e̺̭̳̪̰-m̢iͅn̖̺̞̲̯̰d̵̼̟͙̩̼̘̳ ̞̥̱̳̭r̛̗̘e͙p͠r̼̞̻̭̗e̺̠̣͟s̘͇̳͍̝͉e͉̥̯̞̲͚̬͜ǹ̬͎͎̟̖͇̤t͍̬̤͓̼̭͘ͅi̪̱n͠g̴͉ ͏͉ͅc̬̟h͡a̫̻̯͘o̫̟̖͍̙̝͉s̗̦̲.̨̹͈̣",
"मनीष منش",
"पन्ह पन्ह त्र र्च कृकृ ड्ड न्हृे إلا بسم الله",
"lorem لا بسم الله ipsum 你好1234你好",
"a\u{000A}b\u{000D}c\u{0085}d\u{000B}e\u{000C}f\u{2028}g\u{2029}h\u{000D}\u{000A}i",
];
strings
.iter()
.map(|s| s.chars().map(|c| c as u32).collect::<Vec<u32>>())
.collect()
});
pub(crate) fn biased_string_sample(sc: &StringChoice, rng: &mut EngineRng) -> Vec<u32> {
let want_empty = sc.min_size == 0 && sc.max_size > 0;
let want_one = sc.min_size <= 1 && sc.max_size >= 1;
let want_two = sc.min_size <= 2 && sc.max_size >= 2;
let small_count = 1 + want_empty as usize + want_one as usize + want_two as usize;
let global_pool = &*GLOBAL_CONSTANTS_STRINGS;
let valid_global_count = global_pool.iter().filter(|cps| sc.validate(cps)).count();
let count = small_count + valid_global_count;
let threshold = (count as f64 * BOUNDARY_PROBABILITY).min(0.5);
if rng.random::<f64>() < threshold {
let idx = rng.random_range(0..count);
if idx < small_count {
let simplest_cp = sc.simplest_codepoint();
let mut slot = idx;
if slot == 0 {
return sc.simplest();
}
slot -= 1;
if want_empty {
if slot == 0 {
return Vec::new();
}
slot -= 1;
}
if want_one {
if slot == 0 {
return vec![simplest_cp];
}
slot -= 1;
}
debug_assert!(want_two && slot == 0);
return vec![simplest_cp, simplest_cp];
}
let mut skip = idx - small_count;
for cps in global_pool.iter() {
if sc.validate(cps) {
if skip == 0 {
return cps.clone();
}
skip -= 1;
}
}
unreachable!("valid_global_count agrees with the second validate pass");
}
let alpha = sc.intervals.len();
let pick_position = |rng: &mut EngineRng| -> usize {
if alpha > 256 {
if rng.random::<f64>() < 0.2 {
rng.random_range(256..alpha)
} else {
rng.random_range(0..256)
}
} else {
rng.random_range(0..alpha)
}
};
let alpha_size = rng.random_range(1..=10.min(alpha));
let mut sub_alphabet: Vec<u32> = Vec::with_capacity(alpha_size);
while sub_alphabet.len() < alpha_size {
let cp = sc.intervals.char_in_shrink_order(pick_position(rng)) as u32;
sub_alphabet.push(cp);
}
let len = many_draw_length(rng, sc.min_size, sc.max_size);
(0..len)
.map(|_| sub_alphabet[rng.random_range(0..sub_alphabet.len())])
.collect()
}
pub(crate) fn codepoints_to_string(cps: &[u32]) -> String {
cps.iter().filter_map(|&cp| char::from_u32(cp)).collect()
}
pub struct NativeVariables {
last_id: i64,
variables: Vec<i64>,
removed: std::collections::HashSet<i64>,
}
impl NativeVariables {
pub fn new() -> Self {
NativeVariables {
last_id: 0,
variables: Vec::new(),
removed: std::collections::HashSet::new(),
}
}
pub fn next(&mut self) -> i64 {
self.last_id += 1;
self.variables.push(self.last_id);
self.last_id
}
pub fn active(&self) -> Vec<i64> {
self.variables
.iter()
.filter(|id| !self.removed.contains(*id))
.copied()
.collect()
}
pub fn consume(&mut self, variable_id: i64) {
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 trivial(&self, span_idx: usize, nodes: &[ChoiceNode]) -> bool {
let Some(span) = self.inner.get(span_idx) else {
return false;
};
let end = span.end.min(nodes.len());
nodes[span.start..end]
.iter()
.all(|n| n.was_forced || n.value == n.kind.simplest())
}
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: &BigInt, _was_forced: bool) {}
fn draw_float(&mut self, _value: f64, _was_forced: bool) {}
fn draw_bytes(&mut self, _value: &[u8], _was_forced: bool) {}
fn draw_string(&mut self, _value: &str, _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<EngineRng>,
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>,
trailing_template: Option<ChoiceTemplate>,
}
impl NativeTestCase {
pub fn new_random(rng: EngineRng) -> Self {
Self::for_choices_and_template(&[], None, None, BUFFER_SIZE, None).with_random(rng)
}
pub fn for_choices_and_template(
choices: &[ChoiceValue],
prefix_nodes: Option<&[ChoiceNode]>,
trailing: Option<ChoiceTemplate>,
max_size: usize,
observer: Option<Box<dyn DataObserver>>,
) -> Self {
NativeTestCase {
prefix: choices.to_vec(),
prefix_nodes: prefix_nodes.map(|n| n.to_vec()),
rng: None,
max_size: max_size.max(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,
trailing_template: trailing,
}
}
pub fn for_simplest(max_size: usize) -> Self {
Self::for_choices_and_template(
&[],
None,
Some(ChoiceTemplate::simplest(None)),
max_size,
None,
)
}
pub fn for_choices(
choices: &[ChoiceValue],
prefix_nodes: Option<&[ChoiceNode]>,
observer: Option<Box<dyn DataObserver>>,
) -> Self {
Self::for_choices_and_template(choices, prefix_nodes, None, choices.len(), observer)
}
pub fn for_probe(prefix: &[ChoiceValue], rng: EngineRng, max_size: usize) -> Self {
Self::for_choices_and_template(prefix, None, None, max_size, None).with_random(rng)
}
fn with_random(mut self, rng: EngineRng) -> Self {
self.rng = Some(rng);
self
}
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<T: Into<BigInt> + TryFrom<BigInt>>(
&mut self,
min_value: T,
max_value: T,
) -> Result<T, EngineError> {
let min_value = min_value.into();
let max_value = max_value.into();
assert!(
min_value <= max_value,
"Invalid range [{min_value:?}, {max_value:?}]"
);
let kind = IntegerChoice {
min_value,
max_value,
shrink_towards: BigInt::zero(),
};
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")
};
if let Some(ref mut obs) = self.observer {
obs.draw_integer(&v, was_forced);
}
self.nodes.push(ChoiceNode::new(
ChoiceKind::Integer(kind),
ChoiceValue::Integer(v.clone()),
was_forced,
));
Ok(T::try_from(v)
.ok()
.expect("validated value fits the requested width"))
}
pub fn draw_float(
&mut self,
min_value: f64,
max_value: f64,
allow_nan: bool,
allow_infinity: bool,
smallest_nonzero_magnitude: f64,
) -> Result<f64, EngineError> {
let kind = FloatChoice {
min_value,
max_value,
allow_nan,
allow_infinity,
smallest_nonzero_magnitude,
};
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::new(
ChoiceKind::Float(kind),
ChoiceValue::Float(v),
was_forced,
));
if let Some(ref mut obs) = self.observer {
obs.draw_float(v, was_forced);
}
Ok(v)
}
pub fn draw_bytes(&mut self, min_size: usize, max_size: usize) -> Result<Vec<u8>, EngineError> {
assert!(
min_size <= max_size,
"min_size ({min_size}) must be <= max_size ({max_size})"
);
let kind = BytesChoice { min_size, max_size };
let (value, was_forced) = self.resolve_choice(
&ChoiceKind::Bytes(kind.clone()),
|| ChoiceValue::Bytes(kind.simplest()),
|| ChoiceValue::Bytes(kind.unit()),
|v| matches!(v, ChoiceValue::Bytes(b) if kind.validate(b)),
|rng| ChoiceValue::Bytes(biased_bytes_sample(&kind, rng)),
)?;
let ChoiceValue::Bytes(v) = value else {
unreachable!("kind/value invariant violated: outer match guaranteed this variant")
};
self.nodes.push(ChoiceNode::new(
ChoiceKind::Bytes(kind),
ChoiceValue::Bytes(v.clone()),
was_forced,
));
if let Some(ref mut obs) = self.observer {
obs.draw_bytes(&v, was_forced);
}
Ok(v)
}
pub fn draw_string(
&mut self,
intervals: IntervalSet,
min_size: usize,
max_size: usize,
) -> Result<String, EngineError> {
assert!(min_size <= max_size);
assert!(
!intervals.is_empty() || max_size == 0,
"draw_string with empty alphabet must have max_size == 0"
);
let kind = StringChoice {
intervals,
min_size,
max_size,
};
let (value, was_forced) = self.resolve_choice(
&ChoiceKind::String(kind.clone()),
|| ChoiceValue::String(kind.simplest()),
|| ChoiceValue::String(kind.unit()),
|v| matches!(v, ChoiceValue::String(s) if kind.validate(s)),
|rng| ChoiceValue::String(biased_string_sample(&kind, rng)),
)?;
let ChoiceValue::String(v) = value else {
unreachable!("kind/value invariant violated: outer match guaranteed this variant")
};
self.nodes.push(ChoiceNode::new(
ChoiceKind::String(kind),
ChoiceValue::String(v.clone()),
was_forced,
));
let s = codepoints_to_string(&v);
if let Some(ref mut obs) = self.observer {
obs.draw_string(&s, was_forced);
}
Ok(s)
}
pub fn weighted(&mut self, p: f64, forced: Option<bool>) -> Result<bool, EngineError> {
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(weighted_boolean_sample(p, rng)),
)?
};
let ChoiceValue::Boolean(v) = value else {
unreachable!("kind/value invariant violated: outer match guaranteed this variant")
};
self.nodes.push(ChoiceNode::new(
ChoiceKind::Boolean(kind),
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<(), EngineError> {
if let Some(status) = self.status {
return Err(match status {
Status::Invalid => EngineError::InvalidTestCase,
_ => EngineError::Overrun,
});
}
if self.nodes.len() >= self.max_size {
self.status = Some(Status::EarlyStop);
return Err(EngineError::Overrun);
}
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 EngineRng) -> ChoiceValue,
) -> Result<(ChoiceValue, bool), EngineError> {
self.pre_choice()?;
let idx = self.nodes.len();
if idx < self.prefix.len() {
let prefix_value = &self.prefix[idx];
if validate(prefix_value) {
return Ok((prefix_value.clone(), false));
}
let is_simplest = self
.prefix_nodes
.as_ref()
.and_then(|pn| pn.get(idx))
.is_some_and(|pn| *prefix_value == pn.kind.simplest());
return Ok((if is_simplest { simplest() } else { unit() }, false));
}
if let Some(template) = self.trailing_template.as_mut() {
if matches!(template.count, Some(0)) {
self.status = Some(Status::EarlyStop);
return Err(EngineError::Overrun);
}
let value = match template.kind {
ChoiceTemplateKind::Simplest => simplest(),
};
if let Some(c) = template.count.as_mut() {
*c -= 1;
}
return Ok((value, false));
}
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;