blob: 54d4cc73cb59cd9ad8d62f43ca21212f54858d65 [file] [edit]
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Domain seeding support for FuzzTest.
//!
//! Seeding lets a domain occasionally return user-chosen values from
//! [`Domain::init`](crate::domains::Domain::init). This mirrors the C++ FuzzTest
//! `.WithSeeds(...)` functionality.
//!
//! When seeds are present, [`Domain::init`](crate::domains::Domain::init) returns a uniformly
//! chosen seed with probability 1/2; otherwise, it delegates to the inner domain. Other domain
//! operations—such as [`Domain::mutate`](crate::domains::Domain::mutate), serialization,
//! parsing, and validation—always delegate directly to the inner domain.
//!
//! # Eager Seeds vs. Lazy Providers
//!
//! - **Eager seeds** ([`SeedableDomainExt::with_seeds`]) are converted and validated immediately
//! when added, panicking if any seed is invalid for the domain.
//! - **Lazy seed providers** ([`SeedableDomainExt::with_seed_provider`]) run once, on the first
//! call to [`Domain::init`](crate::domains::Domain::init), and panic if any produced seed is
//! invalid.
//!
//! # Compile-time Opt-in via [`ReversibleDomain`]
//!
//! Seeding is opt-in per domain via the [`ReversibleDomain`] trait. Domains whose corpus
//! value cannot be reconstructed from a user value (such as one-way maps or irreversible
//! transformations) cannot be seeded. This provides a compile-time guarantee against seeding
//! incompatible domains, in contrast to the runtime failure in C++ FuzzTest `WithSeeds`.
//!
//! # Order of Configuration
//!
//! Configure inner-domain constraints (such as `VecOf::with_max_len`) *before* seeding. Seeds
//! are validated against the domain at the time they are added (or upon first initialization for
//! providers), and [`Seeded`] does not forward domain-specific constraint setters.
//!
//! # Examples
//!
//! ```rust
//! use fuzztest::domains::{Domain, ReversibleDomain, SeedableDomainExt};
//! use fuzztest::reexports::anyhow;
//! use fuzztest::reexports::rand::rngs::SmallRng;
//! use fuzztest::reexports::rand::RngExt;
//! use fuzztest::reexports::rand::SeedableRng;
//!
//! #[derive(Clone, Debug)]
//! struct Percent;
//!
//! impl Domain for Percent {
//! type UserValue<'user> = u8;
//! type CorpusValue = u8;
//!
//! fn init(
//! &mut self,
//! rng: &mut dyn fuzztest::reexports::rand::Rng,
//! ) -> anyhow::Result<Self::CorpusValue> {
//! Ok(rng.random_range(0..=100))
//! }
//!
//! fn mutate(
//! &mut self,
//! corpus_value: &mut Self::CorpusValue,
//! rng: &mut dyn fuzztest::reexports::rand::Rng,
//! _only_shrink: bool,
//! ) -> anyhow::Result<()> {
//! *corpus_value = self.init(rng)?;
//! Ok(())
//! }
//!
//! fn corpus_to_user_value<'a>(
//! &self,
//! corpus_value: &'a Self::CorpusValue,
//! ) -> anyhow::Result<Self::UserValue<'a>> {
//! Ok(*corpus_value)
//! }
//!
//! fn validate_corpus_value(&self, corpus_value: &Self::CorpusValue) -> anyhow::Result<()> {
//! anyhow::ensure!(*corpus_value <= 100, "percent {corpus_value} exceeds 100");
//! Ok(())
//! }
//! }
//!
//! impl ReversibleDomain<u8> for Percent {
//! fn to_unvalidated_corpus_value(&self, value: u8) -> anyhow::Result<Self::CorpusValue> {
//! Ok(value)
//! }
//! }
//!
//! let mut domain = Percent
//! .with_seeds([0, 100])
//! .with_seed_provider(|| vec![50]);
//! let mut rng = SmallRng::seed_from_u64(73);
//! let value = domain.init(&mut rng).unwrap();
//! assert!(value <= 100);
//! ```
use super::Domain;
use anyhow::Context;
use rand::RngExt;
use std::fmt;
use std::mem;
use std::sync::Arc;
use std::sync::Mutex;
use std::sync::OnceLock;
/// Allows constructing domain corpus values from values of type `T`.
///
/// Implementing this trait for a seed type `T` (typically the domain's `UserValue`, or an owned
/// counterpart for domains whose `UserValue` borrows from the corpus) makes the domain seedable
/// with values of type `T` via the blanket [`SeedableDomainExt`] implementation. It represents
/// the inverse of [`Domain::corpus_to_user_value`](super::Domain::corpus_to_user_value).
///
/// Implementations must only convert the value and must **not** perform validation;
/// callers validate the converted corpus value via
/// [`Domain::validate_corpus_value`](super::Domain::validate_corpus_value).
///
/// # Errors
///
/// Returns an error if the value cannot be represented as a corpus value for
/// this domain.
pub trait ReversibleDomain<T>: Domain {
/// Converts `value` into an unvalidated [`Domain::CorpusValue`].
///
/// Do not validate domain constraints here; implement them in
/// [`Domain::validate_corpus_value`] so they apply to both seeds and
/// deserialized corpus entries.
fn to_unvalidated_corpus_value(&self, value: T) -> anyhow::Result<Self::CorpusValue>;
}
mod sealed {
pub trait Sealed {}
impl<D: super::Domain> Sealed for D {}
}
/// Extension trait providing seeding methods for domains.
///
/// Blanket-implemented for all types that implement [`Domain`]. The seeding methods
/// require `Self: ReversibleDomain<T>` for the chosen seed type `T` and wrap the domain
/// in a [`Seeded<Self>`].
pub trait SeedableDomainExt: Domain + Sized + sealed::Sealed {
/// Converts a seed `value` into a [`Domain::CorpusValue`] and validates it
/// against [`Domain::validate_corpus_value`].
///
/// Because `SeedableDomainExt` is blanket-implemented, this pipeline cannot
/// be overridden to skip validation.
fn into_corpus_value<T>(&self, value: T) -> anyhow::Result<Self::CorpusValue>
where
Self: ReversibleDomain<T>,
{
let corpus_val = self
.to_unvalidated_corpus_value(value)
.context("Failed to convert seed to corpus value")?;
self.validate_corpus_value(&corpus_val)
.context("Failed to validate seed against domain constraints")?;
Ok(corpus_val)
}
/// Adds pre-defined seeds to the domain.
///
/// Seeds are converted and validated immediately against the domain.
///
/// # Panics
///
/// Panics if any seed fails conversion or validation for this domain. The panic message
/// includes the index of the failing seed.
fn with_seeds<T>(self, seeds: impl IntoIterator<Item = T>) -> Seeded<Self>
where
Self: ReversibleDomain<T>,
{
Seeded::new(self).with_seeds(seeds)
}
/// Adds a lazy seed provider evaluated on first sampling.
///
/// The provider is evaluated at most once on the first call to
/// [`Domain::init`](super::Domain::init). Multiple providers can be added, and all will
/// contribute their seeds.
///
/// The provider closure requires `F: FnOnce() -> I + Send + 'static`. The `FnOnce` bound
/// allows moving non-`Clone` captured data out of the closure. If a [`Seeded`] instance is
/// cloned before its first [`Domain::init`](super::Domain::init), the lazy provider is shared
/// via `Arc` and evaluated at most once across all clones.
///
/// # Panics
///
/// Panics on the first call to [`Domain::init`](super::Domain::init) if the provider yields
/// a seed that fails conversion or validation.
fn with_seed_provider<T, F, I>(self, seed_provider: F) -> Seeded<Self>
where
Self: ReversibleDomain<T>,
F: FnOnce() -> I + Send + 'static,
I: IntoIterator<Item = T>,
{
Seeded::new(self).with_seed_provider(seed_provider)
}
}
impl<D: Domain> SeedableDomainExt for D {}
type BoxedSeedProvider<D> =
Box<dyn FnOnce(&D) -> anyhow::Result<Vec<<D as Domain>::CorpusValue>> + Send>;
/// A shared, lazily-evaluated `FnOnce` seed provider.
///
/// Wrapping the provider in an `Arc` allows `Seeded<D>` to implement `Clone` without requiring
/// the user's `FnOnce` closure (or its captured variables) to implement `Clone`.
struct OnceSeedProvider<D: Domain> {
provider: Mutex<Option<BoxedSeedProvider<D>>>,
resolved: OnceLock<Result<Vec<D::CorpusValue>, Arc<anyhow::Error>>>,
}
impl<D: Domain> OnceSeedProvider<D> {
fn new<T, F, I>(seed_provider: F) -> Arc<Self>
where
D: ReversibleDomain<T>,
F: FnOnce() -> I + Send + 'static,
I: IntoIterator<Item = T>,
{
Arc::new(Self {
provider: Mutex::new(Some(Box::new(move |domain: &D| {
validate_and_convert_seeds(domain, seed_provider())
}))),
resolved: OnceLock::new(),
})
}
fn resolve(&self, domain: &D) -> Result<&[D::CorpusValue], &Arc<anyhow::Error>> {
self.resolved
.get_or_init(|| {
let provider = self
.provider
.lock()
.expect("Seed provider mutex should not be poisoned")
.take()
.expect("OnceLock initializes at most once");
provider(domain).map_err(Arc::new)
})
.as_deref()
}
}
/// A domain wrapper that occasionally samples from user-provided seeds during initialization.
///
/// Produced by [`SeedableDomainExt::with_seeds`] or [`SeedableDomainExt::with_seed_provider`].
/// All domain operations delegate to the inner domain `D`, except for
/// [`Domain::init`](super::Domain::init), which returns a seed with probability 1/2 when seeds
/// are present.
///
/// Implements [`Clone`] when `D: Clone`, and [`fmt::Debug`] to display existing seeds and whether
/// seed providers are currently pending.
#[derive(Clone)]
#[must_use]
pub struct Seeded<D: Domain> {
domain: D,
seeds: Vec<D::CorpusValue>,
seed_providers: Vec<Arc<OnceSeedProvider<D>>>,
}
impl<D: Domain> Seeded<D> {
fn new(domain: D) -> Self {
Seeded { domain, seeds: Vec::new(), seed_providers: Vec::new() }
}
/// Returns a reference to the inner domain.
pub fn inner(&self) -> &D {
&self.domain
}
/// Consumes the seeded wrapper and returns the inner domain.
pub fn into_inner(self) -> D {
self.domain
}
/// Adds pre-defined seeds, appending them to existing seeds.
///
/// Calling this method on an already seeded domain appends seeds directly without
/// nesting [`Seeded`] wrappers. Seeds are converted and validated immediately.
///
/// # Panics
///
/// Panics if any seed fails conversion or validation for this domain. The panic message
/// includes the index of the failing seed.
pub fn with_seeds<T>(mut self, seeds: impl IntoIterator<Item = T>) -> Self
where
D: ReversibleDomain<T>,
{
let validated_seeds = validate_and_convert_seeds(&self.domain, seeds)
.unwrap_or_else(|e| panic!("Failed to add seeds: {e:#}"));
self.seeds.extend(validated_seeds);
self
}
/// Adds a lazy seed provider evaluated on first sampling.
///
/// Calling this method on an already seeded domain appends the provider without
/// nesting [`Seeded`] wrappers. The provider is evaluated at most once on the first
/// call to [`Domain::init`](super::Domain::init). Multiple providers can be added, and all will
/// contribute their seeds.
///
/// The provider closure requires `F: FnOnce() -> I + Send + 'static`. The `FnOnce` bound
/// allows moving non-`Clone` captured data out of the closure. If a [`Seeded`] instance is
/// cloned before its first [`Domain::init`](super::Domain::init), the lazy provider is shared
/// via `Arc` and evaluated at most once across all clones.
///
/// # Panics
///
/// Panics on the first call to [`Domain::init`](super::Domain::init) if the provider yields
/// a seed that fails conversion or validation.
pub fn with_seed_provider<T, F, I>(mut self, seed_provider: F) -> Self
where
D: ReversibleDomain<T>,
F: FnOnce() -> I + Send + 'static,
I: IntoIterator<Item = T>,
{
self.seed_providers.push(OnceSeedProvider::new(seed_provider));
self
}
}
/// Converts and validates each user seed against the domain.
///
/// Each seed is converted and validated via [`SeedableDomainExt::into_corpus_value`] (which calls
/// [`ReversibleDomain::to_unvalidated_corpus_value`] and [`Domain::validate_corpus_value`]). If
/// either step fails, the returned error context includes the index of the offending seed.
fn validate_and_convert_seeds<D, T>(
domain: &D,
seeds: impl IntoIterator<Item = T>,
) -> anyhow::Result<Vec<D::CorpusValue>>
where
D: ReversibleDomain<T>,
{
seeds
.into_iter()
.enumerate()
.map(|(idx, s)| {
domain
.into_corpus_value(s)
.with_context(|| format!("Failed to convert seed at index {idx} to corpus value."))
})
.collect()
}
impl<D> fmt::Debug for Seeded<D>
where
D: Domain + fmt::Debug,
D::CorpusValue: fmt::Debug,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Seeded")
.field("domain", &self.domain)
.field("seeds", &self.seeds)
.field("has_seed_providers", &!self.seed_providers.is_empty())
.finish()
}
}
impl<D: Domain> Domain for Seeded<D> {
type UserValue<'user> = D::UserValue<'user>;
type CorpusValue = D::CorpusValue;
/// Initializes a value from the domain, evaluating pending seed providers on first call.
///
/// On the first call to `init`, all pending seed providers are evaluated, converted, and
/// validated; their seeds are appended to the domain's seed pool.
///
/// If seeds are present, returns a uniformly chosen seed with probability 1/2.
/// Otherwise (or if no seeds were provided), delegates to the inner domain's `init`.
///
/// # Panics
///
/// Panics if any seed provider yields a seed that fails conversion or validation.
fn init(&mut self, rng: &mut dyn rand::Rng) -> anyhow::Result<Self::CorpusValue> {
for provider in mem::take(&mut self.seed_providers) {
let seeds = provider
.resolve(&self.domain)
.unwrap_or_else(|e| panic!("Failed to retrieve seeds from seed provider: {e:#}"));
self.seeds.extend_from_slice(seeds);
}
if !self.seeds.is_empty() && rng.random_bool(0.5) {
let idx = rng.random_range(0..self.seeds.len());
Ok(self.seeds[idx].clone())
} else {
self.domain.init(rng)
}
}
/// Delegates to the inner domain.
fn mutate(
&mut self,
corpus_value: &mut Self::CorpusValue,
rng: &mut dyn rand::Rng,
only_shrink: bool,
) -> anyhow::Result<()> {
self.domain.mutate(corpus_value, rng, only_shrink)
}
/// Delegates to the inner domain.
fn parse_corpus(&self, data: &[u8]) -> anyhow::Result<Self::CorpusValue> {
self.domain.parse_corpus(data)
}
/// Delegates to the inner domain.
fn serialize_corpus(&self, corpus_value: &Self::CorpusValue) -> anyhow::Result<Vec<u8>> {
self.domain.serialize_corpus(corpus_value)
}
/// Delegates to the inner domain.
fn validate_corpus_value(&self, corpus_value: &Self::CorpusValue) -> anyhow::Result<()> {
self.domain.validate_corpus_value(corpus_value)
}
/// Delegates to the inner domain.
fn corpus_to_user_value<'a>(
&self,
corpus_value: &'a Self::CorpusValue,
) -> anyhow::Result<Self::UserValue<'a>> {
self.domain.corpus_to_user_value(corpus_value)
}
}
/// Delegates seed conversion to the inner domain.
///
/// This implementation enables outer container or combinator domains (such as
/// `VectorOf<Seeded<D>>`) to construct corpus values when seeding the composite domain.
///
/// When adding more seeds directly to an existing `Seeded<D>`, callers should call the inherent
/// [`Seeded::with_seeds`] or [`Seeded::with_seed_provider`] methods instead of trait-based
/// wrapping, ensuring seeds are appended into a single pool without introducing nested
/// `Seeded<Seeded<D>>` wrappers.
impl<D, T> ReversibleDomain<T> for Seeded<D>
where
D: ReversibleDomain<T>,
{
fn to_unvalidated_corpus_value(&self, value: T) -> anyhow::Result<D::CorpusValue> {
self.domain.to_unvalidated_corpus_value(value)
}
}
#[cfg(test)]
mod tests {
use super::*;
use googletest::prelude::*;
use rand::rngs::{SmallRng, SysRng};
use rand::RngExt;
use rand::SeedableRng;
use std::collections::HashSet;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
fn get_rng() -> SmallRng {
SmallRng::try_from_rng(&mut SysRng).expect("Failed to create RNG")
}
#[derive(Clone, Debug)]
struct BoundedI32 {
max: i32,
}
impl Domain for BoundedI32 {
type UserValue<'user> = i32;
type CorpusValue = i32;
fn init(&mut self, _rng: &mut dyn rand::Rng) -> anyhow::Result<Self::CorpusValue> {
Ok(0)
}
fn mutate(
&mut self,
corpus_value: &mut Self::CorpusValue,
_rng: &mut dyn rand::Rng,
_only_shrink: bool,
) -> anyhow::Result<()> {
*corpus_value = (*corpus_value + 1).min(self.max);
Ok(())
}
fn corpus_to_user_value<'a>(
&self,
corpus_value: &'a Self::CorpusValue,
) -> anyhow::Result<Self::UserValue<'a>> {
Ok(*corpus_value)
}
fn validate_corpus_value(&self, corpus_value: &Self::CorpusValue) -> anyhow::Result<()> {
anyhow::ensure!(
*corpus_value <= self.max,
"value {} exceeds max {}",
corpus_value,
self.max
);
Ok(())
}
}
impl ReversibleDomain<i32> for BoundedI32 {
fn to_unvalidated_corpus_value(&self, value: i32) -> anyhow::Result<Self::CorpusValue> {
Ok(value)
}
}
#[derive(Clone, Debug)]
struct NonNegativeOnly;
impl Domain for NonNegativeOnly {
type UserValue<'user> = u32;
type CorpusValue = u32;
fn init(&mut self, _rng: &mut dyn rand::Rng) -> anyhow::Result<Self::CorpusValue> {
Ok(0)
}
fn mutate(
&mut self,
_corpus_value: &mut Self::CorpusValue,
_rng: &mut dyn rand::Rng,
_only_shrink: bool,
) -> anyhow::Result<()> {
Ok(())
}
fn corpus_to_user_value<'a>(
&self,
corpus_value: &'a Self::CorpusValue,
) -> anyhow::Result<Self::UserValue<'a>> {
Ok(*corpus_value)
}
fn validate_corpus_value(&self, _corpus_value: &Self::CorpusValue) -> anyhow::Result<()> {
Ok(())
}
}
impl ReversibleDomain<i32> for NonNegativeOnly {
fn to_unvalidated_corpus_value(&self, value: i32) -> anyhow::Result<Self::CorpusValue> {
u32::try_from(value)
.map_err(|e| anyhow::anyhow!("Failed to convert {} to u32: {e}", value))
}
}
#[derive(Clone, Debug)]
struct Percent;
impl Domain for Percent {
type UserValue<'user> = u8;
type CorpusValue = u8;
fn init(&mut self, rng: &mut dyn rand::Rng) -> anyhow::Result<Self::CorpusValue> {
Ok(rng.random_range(0..=100))
}
fn mutate(
&mut self,
corpus_value: &mut Self::CorpusValue,
rng: &mut dyn rand::Rng,
_only_shrink: bool,
) -> anyhow::Result<()> {
*corpus_value = self.init(rng)?;
Ok(())
}
fn corpus_to_user_value<'a>(
&self,
corpus_value: &'a Self::CorpusValue,
) -> anyhow::Result<Self::UserValue<'a>> {
Ok(*corpus_value)
}
fn validate_corpus_value(&self, corpus_value: &Self::CorpusValue) -> anyhow::Result<()> {
anyhow::ensure!(*corpus_value <= 100, "percent {corpus_value} exceeds 100");
Ok(())
}
}
impl ReversibleDomain<u8> for Percent {
fn to_unvalidated_corpus_value(&self, value: u8) -> anyhow::Result<Self::CorpusValue> {
Ok(value)
}
}
#[derive(Clone, Debug)]
struct StrDomain;
impl Domain for StrDomain {
type UserValue<'user> = &'user str;
type CorpusValue = String;
fn init(&mut self, _rng: &mut dyn rand::Rng) -> anyhow::Result<Self::CorpusValue> {
Ok(String::new())
}
fn mutate(
&mut self,
_corpus_value: &mut Self::CorpusValue,
_rng: &mut dyn rand::Rng,
_only_shrink: bool,
) -> anyhow::Result<()> {
Ok(())
}
fn corpus_to_user_value<'a>(
&self,
corpus_value: &'a Self::CorpusValue,
) -> anyhow::Result<Self::UserValue<'a>> {
Ok(corpus_value.as_str())
}
fn validate_corpus_value(&self, _corpus_value: &Self::CorpusValue) -> anyhow::Result<()> {
Ok(())
}
}
impl<'a> ReversibleDomain<&'a str> for StrDomain {
fn to_unvalidated_corpus_value(&self, value: &'a str) -> anyhow::Result<Self::CorpusValue> {
Ok(value.to_owned())
}
}
impl ReversibleDomain<String> for StrDomain {
fn to_unvalidated_corpus_value(&self, value: String) -> anyhow::Result<Self::CorpusValue> {
Ok(value)
}
}
#[gtest]
fn test_with_seeds_samples_all_seeds() {
let mut domain = BoundedI32 { max: i32::MAX }.with_seeds([10, 20, 30]);
let mut rng = get_rng();
let mut observed = HashSet::new();
for _ in 0..200 {
observed.insert(domain.init(&mut rng).unwrap());
}
expect_that!(observed, contains(eq(&10)));
expect_that!(observed, contains(eq(&20)));
expect_that!(observed, contains(eq(&30)));
}
#[gtest]
fn test_with_seeds_falls_back_to_inner_domain() {
let mut domain = BoundedI32 { max: 100 }.with_seeds([42]);
let mut rng = get_rng();
let mut observed = HashSet::new();
for _ in 0..200 {
observed.insert(domain.init(&mut rng).unwrap());
}
expect_that!(observed, contains(eq(&42)));
expect_that!(observed, contains(eq(&0)));
}
#[gtest]
fn test_empty_seeds_and_empty_provider_fall_back_to_inner_domain() {
let mut domain = BoundedI32 { max: 100 }.with_seeds([]).with_seed_provider(Vec::<i32>::new);
let mut rng = get_rng();
let mut observed = HashSet::new();
for _ in 0..200 {
observed.insert(domain.init(&mut rng).unwrap());
}
expect_that!(observed, contains(eq(&0)));
}
#[gtest]
#[should_panic(expected = "Failed to add seeds")]
fn test_with_seeds_panics_on_validation_failure() {
let _ = BoundedI32 { max: 10 }.with_seeds([1, 20]);
}
#[gtest]
#[should_panic(expected = "Failed to add seeds")]
fn test_with_seeds_panics_on_conversion_failure() {
let _ = NonNegativeOnly.with_seeds([-1]);
}
#[gtest]
fn test_chained_with_seeds_appends_without_nesting() {
let mut domain: Seeded<BoundedI32> =
BoundedI32 { max: i32::MAX }.with_seeds([1]).with_seeds([2]);
let mut rng = get_rng();
let mut observed = HashSet::new();
for _ in 0..200 {
observed.insert(domain.init(&mut rng).unwrap());
}
expect_that!(observed, contains(eq(&1)));
expect_that!(observed, contains(eq(&2)));
}
#[gtest]
fn test_seed_provider_lazy_and_called_exactly_once() {
let counter = Arc::new(AtomicUsize::new(0));
let counter_clone = Arc::clone(&counter);
let mut domain = BoundedI32 { max: 100 }.with_seed_provider(move || {
counter_clone.fetch_add(1, Ordering::SeqCst);
vec![7]
});
expect_that!(counter.load(Ordering::SeqCst), eq(0));
let mut rng = get_rng();
let first = domain.init(&mut rng).unwrap();
expect_that!(counter.load(Ordering::SeqCst), eq(1));
let mut observed = HashSet::new();
observed.insert(first);
for _ in 0..100 {
observed.insert(domain.init(&mut rng).unwrap());
}
expect_that!(counter.load(Ordering::SeqCst), eq(1));
expect_that!(observed, contains(eq(&7)));
}
#[gtest]
fn test_multiple_providers_and_seeds_all_contribute() {
let mut domain = BoundedI32 { max: 100 }
.with_seed_provider(|| vec![1])
.with_seed_provider(|| vec![2])
.with_seeds([3]);
let mut rng = get_rng();
let mut observed = HashSet::new();
for _ in 0..200 {
observed.insert(domain.init(&mut rng).unwrap());
}
expect_that!(observed, contains(eq(&1)));
expect_that!(observed, contains(eq(&2)));
expect_that!(observed, contains(eq(&3)));
}
#[gtest]
#[should_panic(expected = "Failed to retrieve seeds from seed provider")]
fn test_provider_yielding_invalid_seed_panics_on_first_init() {
let mut domain = BoundedI32 { max: 10 }.with_seed_provider(|| vec![99]);
let mut rng = get_rng();
let _ = domain.init(&mut rng);
}
#[gtest]
fn test_clone_before_init_with_failing_provider_does_not_poison_mutex() {
let counter = Arc::new(AtomicUsize::new(0));
let counter_clone = Arc::clone(&counter);
let a = BoundedI32 { max: 10 }.with_seed_provider(move || {
counter_clone.fetch_add(1, Ordering::SeqCst);
vec![99]
});
let mut a = a;
let mut b = a.clone();
let res_a = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let mut rng = get_rng();
let _ = a.init(&mut rng);
}));
expect_true!(res_a.is_err());
expect_that!(counter.load(Ordering::SeqCst), eq(1));
let res_b = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let mut rng = get_rng();
let _ = b.init(&mut rng);
}));
expect_true!(res_b.is_err());
expect_that!(counter.load(Ordering::SeqCst), eq(1));
}
#[gtest]
fn test_fnonce_move_out_provider() {
let owned = vec![5];
let mut domain = BoundedI32 { max: 100 }.with_seed_provider(move || owned);
let mut rng = get_rng();
let mut observed = HashSet::new();
for _ in 0..200 {
observed.insert(domain.init(&mut rng).unwrap());
}
expect_that!(observed, contains(eq(&5)));
}
#[gtest]
fn test_clone_before_init_evaluates_provider_once_and_shares_seeds() {
let counter = Arc::new(AtomicUsize::new(0));
let counter_clone = Arc::clone(&counter);
let a = BoundedI32 { max: 100 }.with_seed_provider(move || {
counter_clone.fetch_add(1, Ordering::SeqCst);
vec![7]
});
let mut a = a;
let mut b = a.clone();
expect_that!(counter.load(Ordering::SeqCst), eq(0));
let mut rng = get_rng();
let _ = a.init(&mut rng).unwrap();
expect_that!(counter.load(Ordering::SeqCst), eq(1));
let _ = b.init(&mut rng).unwrap();
expect_that!(counter.load(Ordering::SeqCst), eq(1));
let mut observed_a = HashSet::new();
let mut observed_b = HashSet::new();
for _ in 0..200 {
observed_a.insert(a.init(&mut rng).unwrap());
observed_b.insert(b.init(&mut rng).unwrap());
}
expect_that!(observed_a, contains(eq(&7)));
expect_that!(observed_b, contains(eq(&7)));
}
#[gtest]
fn test_non_clone_capture_in_seed_provider() {
struct NonClone(i32);
let non_clone = NonClone(5);
let a = BoundedI32 { max: 100 }.with_seed_provider(move || vec![non_clone.0]);
let mut a = a;
let mut b = a.clone();
let mut rng = get_rng();
let mut observed_a = HashSet::new();
let mut observed_b = HashSet::new();
for _ in 0..200 {
observed_a.insert(a.init(&mut rng).unwrap());
observed_b.insert(b.init(&mut rng).unwrap());
}
expect_that!(observed_a, contains(eq(&5)));
expect_that!(observed_b, contains(eq(&5)));
}
#[gtest]
fn test_clone_after_init_shares_already_evaluated_seeds() {
let counter = Arc::new(AtomicUsize::new(0));
let counter_clone = Arc::clone(&counter);
let mut a = BoundedI32 { max: 100 }.with_seed_provider(move || {
counter_clone.fetch_add(1, Ordering::SeqCst);
vec![7]
});
let mut rng = get_rng();
let _ = a.init(&mut rng).unwrap();
expect_that!(counter.load(Ordering::SeqCst), eq(1));
let mut b = a.clone();
let mut observed = HashSet::new();
for _ in 0..200 {
observed.insert(b.init(&mut rng).unwrap());
}
expect_that!(counter.load(Ordering::SeqCst), eq(1));
expect_that!(observed, contains(eq(&7)));
}
#[gtest]
fn test_seeded_delegates_to_inner_domain() {
let mut domain = BoundedI32 { max: 10 }.with_seeds([3]);
let mut inner = BoundedI32 { max: 10 };
// validate_corpus_value
expect_that!(domain.validate_corpus_value(&11), err(anything()));
expect_that!(domain.validate_corpus_value(&5), ok(anything()));
// serialize_corpus and parse_corpus round-trip
let serialized = domain.serialize_corpus(&5).unwrap();
let deserialized = domain.parse_corpus(&serialized).unwrap();
expect_that!(deserialized, eq(5));
// parse_corpus validates deserialized value against domain constraints
let invalid_bytes = domain.serialize_corpus(&11).unwrap();
expect_that!(domain.parse_corpus(&invalid_bytes), err(anything()));
// corpus_to_user_value
let user_val = domain.corpus_to_user_value(&5).unwrap();
expect_that!(user_val, eq(5));
// into_corpus_value
let corpus_val = domain.into_corpus_value(5).unwrap();
expect_that!(corpus_val, eq(5));
// mutate
let mut rng = get_rng();
let mut val1 = 5;
let mut val2 = 5;
domain.mutate(&mut val1, &mut rng, false).unwrap();
inner.mutate(&mut val2, &mut rng, false).unwrap();
expect_that!(val1, eq(val2));
// inner and into_inner
expect_that!(domain.inner().max, eq(10));
let inner_domain = domain.into_inner();
expect_that!(inner_domain.max, eq(10));
}
#[gtest]
fn test_seeded_debug_formatting() {
let domain = BoundedI32 { max: 1 }.with_seeds([1]).with_seed_provider(|| vec![0]);
let debug_str = format!("{:?}", domain);
expect_that!(debug_str, contains_substring("seeds: [1]"));
expect_that!(debug_str, contains_substring("has_seed_providers: true"));
}
#[gtest]
fn test_doc_example_percent_domain() {
let mut domain = Percent.with_seeds([0, 100]).with_seed_provider(|| vec![50]);
let mut rng = SmallRng::seed_from_u64(73);
let value = domain.init(&mut rng).unwrap();
expect_that!(value, le(100));
let mut observed = HashSet::new();
for _ in 0..200 {
observed.insert(domain.init(&mut rng).unwrap());
}
expect_that!(observed, contains(eq(&0u8)));
expect_that!(observed, contains(eq(&50u8)));
expect_that!(observed, contains(eq(&100u8)));
}
#[gtest]
fn test_borrowed_user_value_domain_seeding_with_owned_and_borrowed_seeds() {
let local_seed = String::from("borrowed_local");
let mut domain: Seeded<StrDomain> = StrDomain
.with_seeds([local_seed.as_str()])
.with_seed_provider(|| vec![String::from("owned_from_provider")]);
drop(local_seed);
let mut rng = get_rng();
let mut observed = HashSet::new();
for _ in 0..200 {
observed.insert(domain.init(&mut rng).unwrap());
}
expect_that!(observed, contains(eq("borrowed_local")));
expect_that!(observed, contains(eq("owned_from_provider")));
}
}