No public description PiperOrigin-RevId: 991788346
diff --git a/rust/src/domains.rs b/rust/src/domains.rs index 550fa2f..c6d5dd1 100644 --- a/rust/src/domains.rs +++ b/rust/src/domains.rs
@@ -14,6 +14,7 @@ pub mod arbitrary; pub mod containers; +pub mod option_of; pub mod range; pub mod tuple_of; pub mod utility;
diff --git a/rust/src/domains/arbitrary.rs b/rust/src/domains/arbitrary.rs index 303d751..d9b59c9 100644 --- a/rust/src/domains/arbitrary.rs +++ b/rust/src/domains/arbitrary.rs
@@ -328,6 +328,34 @@ } } +impl<T> Domain for Arbitrary<Option<T>> +where + Arbitrary<T>: Domain, +{ + type CorpusValue = Option<<Arbitrary<T> as Domain>::CorpusValue>; + type UserValue<'user> = Option<<Arbitrary<T> as Domain>::UserValue<'user>>; + + fn init(&self, rng: &mut dyn rand::Rng) -> anyhow::Result<Self::CorpusValue> { + super::option_of::OptionOf::new(Arbitrary::<T>::default()).init(rng) + } + + fn mutate( + &self, + val: &mut Self::CorpusValue, + rng: &mut dyn rand::Rng, + only_shrink: bool, + ) -> anyhow::Result<()> { + super::option_of::OptionOf::new(Arbitrary::<T>::default()).mutate(val, rng, only_shrink) + } + + fn corpus_to_user_value<'a>( + &self, + corpus_value: &'a Self::CorpusValue, + ) -> anyhow::Result<Self::UserValue<'a>> { + super::option_of::OptionOf::new(Arbitrary::<T>::default()).corpus_to_user_value(corpus_value) + } +} + #[cfg(test)] mod tests { use std::collections::HashSet; @@ -783,4 +811,27 @@ ); } } + + #[test] + fn test_arbitrary_option() { + let domain = Arbitrary::<Option<i32>>::default(); + let mut rng = get_rng(); + + let mut has_none = false; + let mut has_some = false; + for _ in 0..100 { + let sample = domain.init(&mut rng).unwrap(); + if sample.is_none() { + has_none = true; + } else { + has_some = true; + } + } + assert!(has_none); + assert!(has_some); + + let mut val: Option<i32> = None; + domain.mutate(&mut val, &mut rng, false).unwrap(); + assert!(val.is_some()); + } }
diff --git a/rust/src/domains/option_of.rs b/rust/src/domains/option_of.rs new file mode 100644 index 0000000..736b733 --- /dev/null +++ b/rust/src/domains/option_of.rs
@@ -0,0 +1,225 @@ +// 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. + +use rand::RngExt; +use std::fmt; + +use super::Domain; + +/// Generates optional values of type `Option<T>` using an inner domain. +/// +/// An `OptionOf` domain produces either `None` or `Some(value)`, where `value` +/// is drawn from the wrapped inner domain. +/// +/// Example usage: +/// ``` +/// # use fuzztest::domains::Domain; +/// # use fuzztest::domains::arbitrary::Arbitrary; +/// # use fuzztest::domains::option_of::OptionOf; +/// # use rand::rngs::SmallRng; +/// # use rand::SeedableRng; +/// +/// let domain = OptionOf::new(Arbitrary::<i32>::default()); +/// let mut rng = SmallRng::seed_from_u64(42); +/// +/// let sample = domain.init(&mut rng); +/// assert!(sample.is_ok()); +/// ``` +pub struct OptionOf<T> { + inner: T, +} + +impl<T: Clone> Clone for OptionOf<T> { + fn clone(&self) -> Self { + Self { inner: self.inner.clone() } + } +} + +impl<T: fmt::Debug> fmt::Debug for OptionOf<T> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("OptionOf").field("inner", &self.inner).finish() + } +} + +impl<T> OptionOf<T> { + /// Creates a new `OptionOf` domain wrapping the specified inner domain. + pub fn new(inner: T) -> Self { + Self { inner } + } +} + +impl<T> Domain for OptionOf<T> +where + T: Domain, +{ + type CorpusValue = Option<T::CorpusValue>; + type UserValue<'user> = Option<T::UserValue<'user>>; + + fn init(&self, rng: &mut dyn rand::Rng) -> anyhow::Result<Self::CorpusValue> { + // 50% chance of returning None, 50% chance of Some(inner). + if rng.random_bool(0.5) { + Ok(None) + } else { + Ok(Some(self.inner.init(rng)?)) + } + } + + fn mutate( + &self, + val: &mut Self::CorpusValue, + rng: &mut dyn rand::Rng, + only_shrink: bool, + ) -> anyhow::Result<()> { + match val { + None => { + if !only_shrink { + *val = Some(self.inner.init(rng)?); + } + } + Some(inner_val) => { + if only_shrink { + // When shrinking, either drop to None or shrink the inner value. + if rng.random_bool(0.25) { + *val = None; + } else { + self.inner.mutate(inner_val, rng, true)?; + } + } else { + // When mutating without shrinking: 1/100 chance to set to None (matching + // C++ FuzzTest), otherwise mutate the inner value. + if rng.random_bool(0.01) { + *val = None; + } else { + self.inner.mutate(inner_val, rng, false)?; + } + } + } + } + Ok(()) + } + + fn corpus_to_user_value<'a>( + &self, + corpus_value: &'a Self::CorpusValue, + ) -> anyhow::Result<Self::UserValue<'a>> { + match corpus_value { + None => Ok(None), + Some(v) => Ok(Some(self.inner.corpus_to_user_value(v)?)), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::domains::arbitrary::Arbitrary; + use googletest::prelude::*; + use rand::rngs::{SmallRng, SysRng}; + use rand::SeedableRng; + + fn get_rng() -> SmallRng { + SmallRng::try_from_rng(&mut SysRng).expect("Failed to create RNG") + } + + #[gtest] + fn test_option_of_init_produces_both_none_and_some() { + let domain = OptionOf::new(Arbitrary::<i32>::default()); + let mut rng = get_rng(); + + let mut has_none = false; + let mut has_some = false; + + for _ in 0..200 { + let sample = domain.init(&mut rng).unwrap(); + if sample.is_none() { + has_none = true; + } else { + has_some = true; + } + if has_none && has_some { + break; + } + } + + expect_that!(has_none, eq(true), "OptionOf::init should produce None"); + expect_that!(has_some, eq(true), "OptionOf::init should produce Some"); + } + + #[gtest] + fn test_option_of_mutate_from_none_can_become_some() { + let domain = OptionOf::new(Arbitrary::<i32>::default()); + let mut rng = get_rng(); + + let mut val: Option<i32> = None; + domain.mutate(&mut val, &mut rng, false).unwrap(); + expect_that!(val.is_some(), eq(true), "Mutating None without shrinking should produce Some"); + } + + #[gtest] + fn test_option_of_shrink_from_none_stays_none() { + let domain = OptionOf::new(Arbitrary::<i32>::default()); + let mut rng = get_rng(); + + let mut val: Option<i32> = None; + for _ in 0..50 { + domain.mutate(&mut val, &mut rng, true).unwrap(); + expect_that!(val.is_none(), eq(true), "Shrinking None must stay None"); + } + } + + #[gtest] + fn test_option_of_shrink_from_some_eventually_becomes_none() { + let domain = OptionOf::new(Arbitrary::<i32>::default()); + let mut rng = get_rng(); + + let mut val: Option<i32> = Some(1000); + let mut became_none = false; + + for _ in 0..200 { + domain.mutate(&mut val, &mut rng, true).unwrap(); + if val.is_none() { + became_none = true; + break; + } + } + + expect_that!(became_none, eq(true), "Shrinking Some should eventually produce None"); + } + + #[gtest] + fn test_option_of_corpus_to_user_value() { + let domain = OptionOf::new(Arbitrary::<i32>::default()); + + let none_val: Option<i32> = None; + let some_val: Option<i32> = Some(42); + + expect_that!(domain.corpus_to_user_value(&none_val).unwrap(), eq(None)); + expect_that!(domain.corpus_to_user_value(&some_val).unwrap(), eq(Some(42))); + } + + #[gtest] + fn test_option_of_serde_roundtrip() { + let domain = OptionOf::new(Arbitrary::<i32>::default()); + + let none_corpus: Option<i32> = None; + let bytes = domain.serialize_corpus(&none_corpus).unwrap(); + let parsed = domain.parse_corpus(&bytes).unwrap(); + expect_that!(parsed, eq(None)); + + let some_corpus: Option<i32> = Some(12345); + let bytes = domain.serialize_corpus(&some_corpus).unwrap(); + let parsed = domain.parse_corpus(&bytes).unwrap(); + expect_that!(parsed, eq(Some(12345))); + } +}
diff --git a/rust/tests/macro_compiles.rs b/rust/tests/macro_compiles.rs index 04c245e..e326502 100644 --- a/rust/tests/macro_compiles.rs +++ b/rust/tests/macro_compiles.rs
@@ -15,6 +15,7 @@ use fuzztest::domains::arbitrary::Arbitrary; use fuzztest::domains::containers::ContainerDomain; use fuzztest::domains::containers::VecOf; +use fuzztest::domains::option_of::OptionOf; use fuzztest::fuzztest; #[fuzztest(_a = Arbitrary::<i32>::default())] @@ -26,8 +27,16 @@ #[fuzztest(_a = VecOf::new(Arbitrary::<i32>::default()).with_max_len(10))] fn fuzztest_macro_compiles_with_vec(_a: Vec<i32>) {} +#[fuzztest(_a = OptionOf::new(Arbitrary::<i32>::default()))] +fn fuzztest_macro_compiles_with_option_of(_a: Option<i32>) {} + +#[fuzztest(_a = Arbitrary::<Option<i32>>::default())] +fn fuzztest_macro_compiles_with_arbitrary_option(_a: Option<i32>) {} + fn main() { // The property function is defined in the scope where the macro is // invoked, so it can be called directly, e.g. from a regression test. fuzztest_macro_compiles(0); + fuzztest_macro_compiles_with_option_of(Some(0)); + fuzztest_macro_compiles_with_arbitrary_option(None); }