rvtest 0.3.0

A Next Level Testing Library for Rust — BDD specs, property-based testing, parametrized tests, rich reporting, and code coverage. Just a library, not a framework.
use std::fmt::Debug;
use std::marker::PhantomData;

use rand::{Rng, RngExt};

/// A strategy for generating random values of type `T`.
///
/// Implement this trait to define how values are produced and optionally
/// shrunk when a failing counterexample is found.
///
/// # Shrinking
///
/// Shrinking tries to simplify a counterexample to its minimal form,
/// making failures easier to diagnose. The default implementation returns
/// an empty vector (no shrinking).
pub trait Strategy<T>: Send + Sync {
    /// Generate a random value of type `T`.
    fn generate(&self, rng: &mut dyn Rng) -> T;

    /// Produce a list of simpler candidates from a given value, used for
    /// shrinking counterexamples.
    fn shrink(&self, _value: &T) -> Vec<T> {
        Vec::new()
    }
}

// ---------------------------------------------------------------------------
// Built-in strategies for common types
// ---------------------------------------------------------------------------

macro_rules! impl_range_strategy {
    ($ty:ty, $range:expr) => {
        impl Strategy<$ty> for RangeStrategy<$ty> {
            fn generate(&self, rng: &mut dyn Rng) -> $ty {
                rng.random_range($range)
            }
        }
    };
}

/// Strategy that produces values in a given range.
#[derive(Debug, Clone)]
pub struct RangeStrategy<T> {
    _marker: PhantomData<T>,
}

impl_range_strategy!(i8, i8::MIN..=i8::MAX);
impl_range_strategy!(i16, i16::MIN..=i16::MAX);
impl_range_strategy!(i32, i32::MIN..=i32::MAX);
impl_range_strategy!(i64, i64::MIN..=i64::MAX);
impl_range_strategy!(u8, u8::MIN..=u8::MAX);
impl_range_strategy!(u16, u16::MIN..=u16::MAX);
impl_range_strategy!(u32, u32::MIN..=u32::MAX);
impl_range_strategy!(u64, u64::MIN..=u64::MAX);
impl_range_strategy!(usize, usize::MIN..=usize::MAX);

impl Strategy<bool> for RangeStrategy<bool> {
    fn generate(&self, rng: &mut dyn Rng) -> bool {
        rng.random_bool(0.5)
    }

    fn shrink(&self, value: &bool) -> Vec<bool> {
        if *value { vec![false] } else { vec![] }
    }
}

/// Return a strategy for any value of type `T`.
///
/// Supported types: all standard integer types, `bool`, and types composed
/// via combinators.
///
/// # Example
///
/// ```ignore
/// use rvtest::property::{check, any};
///
/// check("addition is commutative", any::<i32>(), |v: &i32| true);
/// ```
pub fn any<T: StrategyProvider>() -> T::Strategy {
    T::strategy()
}

/// Trait mapping a type to its default `Strategy`.
///
/// Types that implement [`StrategyProvider`] can be used with [`any`] to
/// obtain a default strategy for property-based testing.
pub trait StrategyProvider: Sized {
    /// The strategy type used to generate values of `Self`.
    type Strategy: Strategy<Self> + Default + Send + Sync;

    /// Returns the default strategy for this type.
    fn strategy() -> Self::Strategy {
        Self::Strategy::default()
    }
}

macro_rules! impl_provider {
    ($ty:ty) => {
        impl StrategyProvider for $ty {
            type Strategy = RangeStrategy<$ty>;

            fn strategy() -> Self::Strategy {
                RangeStrategy { _marker: PhantomData }
            }
        }

        impl Default for RangeStrategy<$ty> {
            fn default() -> Self {
                RangeStrategy { _marker: PhantomData }
            }
        }
    };
}

impl_provider!(i8);
impl_provider!(i16);
impl_provider!(i32);
impl_provider!(i64);
impl_provider!(u8);
impl_provider!(u16);
impl_provider!(u32);
impl_provider!(u64);
impl_provider!(usize);
impl_provider!(bool);

// ---------------------------------------------------------------------------
// Vec strategy
// ---------------------------------------------------------------------------

/// Strategy for generating `Vec<T>` values.
#[derive(Debug, Clone)]
pub struct VecStrategy<S> {
    element_strategy: S,
    min_len: usize,
    max_len: usize,
}

impl<T, S> Strategy<Vec<T>> for VecStrategy<S>
where
    S: Strategy<T> + Send + Sync,
    T: Send + Clone,
{
    fn generate(&self, rng: &mut dyn Rng) -> Vec<T> {
        let len = rng.random_range(self.min_len..=self.max_len);
        (0..len).map(|_| self.element_strategy.generate(rng)).collect()
    }

    fn shrink(&self, value: &Vec<T>) -> Vec<Vec<T>> {
        let mut candidates = Vec::new();
        if !value.is_empty() {
            let mut smaller = (*value).clone();
            smaller.pop();
            candidates.push(smaller);
        }
        candidates
    }
}

/// Create a strategy that generates `Vec<T>`s with elements from `strategy`.
///
/// The vector length ranges from `min_len` to `max_len` (inclusive).
pub fn vec<T, S>(strategy: S, min_len: usize, max_len: usize) -> VecStrategy<S>
where
    S: Strategy<T>,
{
    VecStrategy { element_strategy: strategy, min_len, max_len }
}

// ---------------------------------------------------------------------------
// Mapping combinator
// ---------------------------------------------------------------------------

/// A strategy that transforms generated values with a mapping function.
pub struct MapStrategy<S, F, T, U> {
    inner: S,
    f: F,
    _phantom: PhantomData<(T, U)>,
}

impl<S: Clone, F: Clone, T, U> Clone for MapStrategy<S, F, T, U> {
    fn clone(&self) -> Self {
        MapStrategy {
            inner: self.inner.clone(),
            f: self.f.clone(),
            _phantom: PhantomData,
        }
    }
}

impl<S: Debug, F, T, U> Debug for MapStrategy<S, F, T, U> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("MapStrategy").field("inner", &self.inner).finish()
    }
}

impl<T, U, S, F> Strategy<U> for MapStrategy<S, F, T, U>
where
    S: Strategy<T>,
    F: Fn(T) -> U + Send + Sync,
    T: Send + Sync,
    U: Send + Sync,
{
    fn generate(&self, rng: &mut dyn Rng) -> U {
        (self.f)(self.inner.generate(rng))
    }

    fn shrink(&self, value: &U) -> Vec<U> {
        // Mapping makes shrinking complex; skip for now.
        let _ = value;
        Vec::new()
    }
}

/// Transform a strategy's output with a mapping function.
pub fn map<T, U, S, F>(strategy: S, f: F) -> MapStrategy<S, F, T, U>
where
    S: Strategy<T>,
    F: Fn(T) -> U,
{
    MapStrategy { inner: strategy, f, _phantom: PhantomData }
}

// ---------------------------------------------------------------------------
// Filter combinator
// ---------------------------------------------------------------------------

/// A strategy that only produces values satisfying a predicate.
pub struct FilterStrategy<S, P, T> {
    inner: S,
    predicate: P,
    max_attempts: u32,
    _phantom: PhantomData<T>,
}

impl<S: Clone, P: Clone, T> Clone for FilterStrategy<S, P, T> {
    fn clone(&self) -> Self {
        FilterStrategy {
            inner: self.inner.clone(),
            predicate: self.predicate.clone(),
            max_attempts: self.max_attempts,
            _phantom: PhantomData,
        }
    }
}

impl<S: Debug, P, T> Debug for FilterStrategy<S, P, T> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("FilterStrategy").field("inner", &self.inner).finish()
    }
}

impl<T, S, P> Strategy<T> for FilterStrategy<S, P, T>
where
    S: Strategy<T>,
    P: Fn(&T) -> bool + Send + Sync,
    T: Send + Sync,
{
    fn generate(&self, rng: &mut dyn Rng) -> T {
        for _ in 0..self.max_attempts {
            let value = self.inner.generate(rng);
            if (self.predicate)(&value) {
                return value;
            }
        }
        self.inner.generate(rng)
    }

    fn shrink(&self, value: &T) -> Vec<T> {
        self.inner
            .shrink(value)
            .into_iter()
            .filter(|v| (self.predicate)(v))
            .collect()
    }
}

/// Create a strategy that only generates values satisfying a predicate.
pub fn filter<T, S, P>(strategy: S, predicate: P) -> FilterStrategy<S, P, T>
where
    S: Strategy<T>,
    P: Fn(&T) -> bool,
{
    FilterStrategy { inner: strategy, predicate, max_attempts: 100, _phantom: PhantomData }
}