blob: 2d63441ccc08bbf5b072443e9ecc00c47760ce12 [file]
use std::cmp::Ordering;
use num_traits::PrimInt;
use rand::distr::uniform::SampleUniform;
use rand::distr::{Distribution, StandardUniform};
use rand::RngExt;
/// Shrinks a `val` towards a `target` value.
///
/// The new value is chosen uniformly from the range between `val` and `target`.
/// - If `val > target`, the range is `[target, val)`.
/// - If `val < target`, the range is `(val, target]`.
/// - If `val == target`, `val` is returned.
///
/// # Arguments
/// * `rng`: Random number generator.
/// * `val`: The current value to shrink.
/// * `target`: The value to shrink towards.
pub fn shrink_towards<T, R: rand::Rng + ?Sized>(rng: &mut R, val: T, target: T) -> T
where
T: SampleUniform + PartialOrd + Copy + std::fmt::Display,
{
match val.partial_cmp(&target) {
Some(Ordering::Equal) => val,
Some(Ordering::Less) => {
// val < target
// Sample from (val, target].
// To do that, we sample from [val, target) and return target if we draw val.
let sample = rng.random_range(val..target);
if sample == val {
target
} else {
sample
}
}
Some(Ordering::Greater) => {
// val > target
// Sample from [target, val).
rng.random_range(target..val)
}
None => val, // Cannot compare, return original
}
}
/// Mutates an integer value using one of several strategies.
///
/// The mutation strategies are:
/// 1. Random Walk: Add/subtract a small value within the given `range`.
/// 2. Bit Flip: Flip a single random bit.
/// 3. Random Choice: Replace with a new completely random value.
///
/// # Arguments
/// * `rng`: Random number generator.
/// * `val`: The integer value to mutate.
/// * `range`: The maximum +/- delta for the random walk.
/// * `min_value`: Optional minimum boundary for mutation. If `None`, `T::min_value()` is used.
/// * `max_value`: Optional maximum boundary for mutation. If `None`, `T::max_value()` is used.
// REQUIRES: range > 0
// REQUIRES: min_value <= max_value (after unwrapping)
// TODO(b/405382579): Add dictionary-based mutation strategies similar to C++ implementation
// (See `fuzztest/internal/domains/value_mutation_helpers.h`)
pub fn mutate_integer<T, R: rand::Rng + ?Sized>(
rng: &mut R,
val: T,
range: T,
min_value: Option<T>,
max_value: Option<T>,
) -> T
where
T: PrimInt + SampleUniform + std::fmt::Display,
{
assert!(range > T::zero(), "mutate_integer: range value cannot be <= 0: {range}");
let min_val = min_value.unwrap_or(T::min_value());
let max_val = max_value.unwrap_or(T::max_value());
assert!(
min_val < max_val,
"mutate_integer: min_val must be strictly smaller than max_val: min={min_val}, max={max_val}"
);
match rng.random_range(0..3) {
0 => {
// 1/3 chance: Random Walk
// Establish a window around the current value. If val > max_val, we should walk
// around max_val instead to bring the value back into a valid range.
let center = val.clamp(min_val, max_val);
let lo = center.saturating_sub(range).max(min_val);
let hi = center.saturating_add(range).min(max_val);
// Sample uniformly within the clamped window [lo, hi].
rng.random_range(lo..=hi)
}
1 => {
// 1/3 chance: Flip a random bit
let num_bits = std::mem::size_of::<T>() * 8;
let bit_index = rng.random_range(0..num_bits);
let mask = T::one() << bit_index;
let result = val ^ mask;
if result < min_val || result > max_val {
val
} else {
result
}
}
_ => {
// 1/3 chance: Replace with a new random value
rng.random_range(min_val..=max_val)
}
}
}
pub trait SpecialValues: Sized + Copy {
fn get() -> &'static [Self];
}
macro_rules! impl_special_values_signed {
($($ty:ty),*) => {
$(
impl SpecialValues for $ty {
fn get() -> &'static [Self] {
&[0, 1, -1, <$ty>::MIN, <$ty>::MAX]
}
}
)*
};
}
macro_rules! impl_special_values_unsigned {
($($ty:ty),*) => {
$(
impl SpecialValues for $ty {
fn get() -> &'static [Self] {
&[0, 1, <$ty>::MAX, <$ty>::MAX >> 1]
}
}
)*
};
}
impl_special_values_signed!(i8, i16, i32, i64, i128);
impl_special_values_unsigned!(u8, u16, u32, u64, u128);
macro_rules! impl_special_values_float {
($($ty:ty),*) => {
$(
impl SpecialValues for $ty {
fn get() -> &'static [Self] {
&[0.0, -0.0, 1.0, -1.0, <$ty>::MIN, <$ty>::MAX, <$ty>::INFINITY, <$ty>::NEG_INFINITY, <$ty>::NAN]
}
}
)*
};
}
impl_special_values_float!(f32, f64);
impl SpecialValues for char {
fn get() -> &'static [Self] {
&[
'\0', // Null
'a', // Common ASCII
'~', // Last printable ASCII
'\n', // Newline
' ', // Space
'\u{0080}', // Start of multi-byte UTF-8
'\u{D7FF}', // Before surrogate range
'\u{E000}', // After surrogate range
'\u{10FFFF}', // Max valid Unicode scalar value
]
}
}
/// Chooses a value of type `T`, potentially from a set of "special values".
///
/// This function has a higher chance of returning one of the predefined "special values"
/// for the type `T` (as defined by the `SpecialValues` trait). Otherwise, it generates
/// a completely random value of type `T`.
///
/// # Arguments
/// * `rng`: Random number generator.
///
/// # Type Parameters
/// * `T`: The type of the value to choose. Must implement `SpecialValues` and
/// `StandardUniform` must be able to generate values of type `T`.
pub fn choose_value<T, R: rand::Rng + ?Sized>(rng: &mut R) -> T
where
T: SpecialValues + 'static,
StandardUniform: Distribution<T>,
{
let special_values = T::get();
let index = rng.random_range(0..=special_values.len());
if index < special_values.len() {
special_values[index]
} else {
rng.random()
}
}
/// Mutates a float value.
pub fn mutate_float<T, R: rand::Rng + ?Sized>(
rng: &mut R,
val: &mut T,
only_shrink: bool,
_range: Option<(T, T)>, // TODO: Implement range support for floats.
) -> anyhow::Result<()>
where
T: num_traits::Float + SampleUniform + std::fmt::Display + Copy + SpecialValues + 'static,
StandardUniform: Distribution<T>,
{
if only_shrink {
// Shrink only finite, non-terminal values.
if val.is_finite() && *val != T::zero() {
*val = shrink_towards(rng, *val, T::zero());
}
return Ok(());
}
// Non-shrinking mutation.
let original_val = *val;
loop {
// Loop to ensure mutation
if !val.is_finite() {
// NaN or Infinity
*val = choose_value(rng);
} else {
match rng.random_range(0..4) {
0 => *val = *val / (T::one() + T::one()),
1 => *val = -*val,
2 => *val = *val + T::one(),
3 => *val = *val * (T::one() + T::one() + T::one()),
_ => unreachable!(),
}
}
if (val.is_nan() && original_val.is_nan()) || *val == original_val {
continue;
}
break;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use rand::{
rngs::{SmallRng, SysRng},
SeedableRng,
};
fn get_rng() -> SmallRng {
SmallRng::try_from_rng(&mut SysRng).unwrap()
}
fn check_shrink_towards<T>(smaller: T, larger: T)
where
T: SampleUniform + PartialOrd + Copy + std::fmt::Display + std::fmt::Debug + PartialEq,
{
let mut rng = get_rng();
// val > target (e.g. larger -> smaller)
// Range should be [smaller, larger)
let mut results = Vec::new();
for _ in 0..100 {
let sample = shrink_towards(&mut rng, larger, smaller);
assert!(sample >= smaller && sample < larger);
if !results.contains(&sample) {
results.push(sample);
}
}
assert!(results.len() > 1);
// val < target (e.g. smaller -> larger)
// Range should be (smaller, larger]
let mut results = Vec::new();
for _ in 0..100 {
let sample = shrink_towards(&mut rng, smaller, larger);
assert!(sample > smaller && sample <= larger);
if !results.contains(&sample) {
results.push(sample);
}
}
assert!(results.len() > 1);
// identical target
let sample = shrink_towards(&mut rng, smaller, smaller);
assert_eq!(sample, smaller);
}
#[test]
fn test_shrink_towards_all_types() {
check_shrink_towards::<i8>(5, 10);
check_shrink_towards::<u8>(5, 10);
check_shrink_towards::<i16>(5, 10);
check_shrink_towards::<u16>(5, 10);
check_shrink_towards::<i32>(5, 10);
check_shrink_towards::<u32>(5, 10);
check_shrink_towards::<i64>(5, 10);
check_shrink_towards::<u64>(5, 10);
check_shrink_towards::<i128>(5, 10);
check_shrink_towards::<u128>(5, 10);
check_shrink_towards::<f32>(5.0, 10.0);
check_shrink_towards::<f64>(5.0, 10.0);
}
fn check_mutate_integer<T>()
where
T: PrimInt + SampleUniform + std::fmt::Display + std::fmt::Debug + SpecialValues + 'static,
StandardUniform: Distribution<T>,
{
let mut rng = get_rng();
let min_val = T::zero();
let max_val = T::one() + T::one() + T::one() + T::one() + T::one();
let range = T::one() + T::one();
// Bounded mutation stays in bounds
for _ in 0..100 {
let val: T = choose_value(&mut rng).clamp(min_val, max_val);
let next = mutate_integer(&mut rng, val, range, Some(min_val), Some(max_val));
assert!(
next >= min_val && next <= max_val,
"mutate_integer bounded range check failed for value {} not in [{}, {}]",
next,
min_val,
max_val
);
}
// Unbounded mutation changes value
let mut val = T::zero();
let mut changed = false;
for _ in 0..100 {
let next = mutate_integer(&mut rng, val, range, None, None);
if next != val {
changed = true;
break;
}
val = next;
}
assert!(changed, "mutate_integer did not change value");
}
#[test]
fn test_mutate_integer_all_types() {
check_mutate_integer::<i8>();
check_mutate_integer::<u8>();
check_mutate_integer::<i16>();
check_mutate_integer::<u16>();
check_mutate_integer::<i32>();
check_mutate_integer::<u32>();
check_mutate_integer::<i64>();
check_mutate_integer::<u64>();
check_mutate_integer::<i128>();
check_mutate_integer::<u128>();
}
fn check_mutate_float<T>()
where
T: num_traits::Float
+ SampleUniform
+ std::fmt::Display
+ std::fmt::Debug
+ Copy
+ SpecialValues
+ 'static,
StandardUniform: Distribution<T>,
{
let mut rng = get_rng();
// Mutation changes value
let mut val = T::one();
let mut changed = false;
for _ in 0..100 {
let mut next = val;
mutate_float(&mut rng, &mut next, false, None).unwrap();
// Floats might mutate to NaN
if next != val || (next.is_nan() && !val.is_nan()) {
changed = true;
if next.is_nan() {
val = T::one();
} else {
val = next;
}
}
}
assert!(changed, "mutate_float did not change value");
// Shrinking
let mut val = T::one() + T::one() + T::one();
let original_abs = val.abs();
for _ in 0..10 {
let mut next = val;
mutate_float(&mut rng, &mut next, true, None).unwrap();
assert!(next.abs() <= val.abs(), "mutate_float shrink increased absolute value");
val = next;
}
assert!(val.abs() < original_abs, "mutate_float did not shrink value");
}
#[test]
fn test_mutate_float_all_types() {
check_mutate_float::<f32>();
check_mutate_float::<f64>();
}
}