No public description PiperOrigin-RevId: 1004960422
diff --git a/rust/src/domains.rs b/rust/src/domains.rs index 3857424..f8493dd 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/option_of.rs b/rust/src/domains/option_of.rs new file mode 100644 index 0000000..4d9d535 --- /dev/null +++ b/rust/src/domains/option_of.rs
@@ -0,0 +1,279 @@ +// 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; + +const NONE_ON_INIT_PROBABILITY: f64 = 0.5; +const NONE_ON_MUTATE_PROBABILITY: f64 = 0.01; + +/// 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 mut domain = OptionOf::new(Arbitrary::<i32>::default()); +/// let mut rng = SmallRng::seed_from_u64(42); +/// +/// let sample = domain.init(&mut rng); +/// assert!(sample.is_ok()); +/// ``` +// TODO(b/568396509): Support null policies (equivalent to C++ `NonNull` and `NullOpt`). +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(&mut self, rng: &mut dyn rand::Rng) -> anyhow::Result<Self::CorpusValue> { + // 50% chance of returning None, 50% chance of Some(inner). + if rng.random_bool(NONE_ON_INIT_PROBABILITY) { + Ok(None) + } else { + Ok(Some(self.inner.init(rng)?)) + } + } + + fn mutate( + &mut 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) => { + // 1/100 chance to set to None (matching C++ OptionalOfImpl::Mutate), + // otherwise mutate the inner value. + if rng.random_bool(NONE_ON_MUTATE_PROBABILITY) { + *val = None; + } else { + self.inner.mutate(inner_val, rng, only_shrink)?; + } + } + } + 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)?)), + } + } + + fn validate_corpus_value(&self, corpus_value: &Self::CorpusValue) -> anyhow::Result<()> { + match corpus_value { + None => Ok(()), + Some(v) => self.inner.validate_corpus_value(v), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::domains::arbitrary::Arbitrary; + use crate::domains::range::InRange; + use crate::domains::utility::iterations_to_hit_all; + 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 mut domain = OptionOf::new(Arbitrary::<i32>::default()); + let mut rng = get_rng(); + + let mut has_none = false; + let mut has_some = false; + + for _ in 0..iterations_to_hit_all(2, NONE_ON_INIT_PROBABILITY) { + 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_true!(has_none, "OptionOf::init should produce None"); + expect_true!(has_some, "OptionOf::init should produce Some"); + } + + #[gtest] + fn test_option_of_mutate_from_none_can_become_some() { + let mut 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_true!(val.is_some(), "Mutating None without shrinking should produce Some"); + } + + #[gtest] + fn test_option_of_shrink_from_none_stays_none() { + let mut 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_true!(val.is_none(), "Shrinking None must stay None"); + } + } + + #[gtest] + fn test_option_of_shrink_from_some() { + let mut domain = OptionOf::new(Arbitrary::<i32>::default()); + let mut rng = get_rng(); + + let mut none_count = 0; + let mut shrunk_some_count = 0; + + for _ in 0..iterations_to_hit_all(2, NONE_ON_MUTATE_PROBABILITY) { + let mut val: Option<i32> = Some(1000); + domain.mutate(&mut val, &mut rng, true).unwrap(); + match val { + None => none_count += 1, + Some(v) => { + expect_that!(v, all!(ge(0), lt(1000))); + shrunk_some_count += 1; + } + } + } + + expect_that!(none_count, gt(0), "Shrinking Some should occasionally produce None"); + expect_that!( + shrunk_some_count, + gt(none_count), + "Shrinking Some should shrink the inner value (99%) more often than dropping to None (1%)" + ); + } + + #[gtest] + fn test_option_of_mutate_from_some() { + let mut domain = OptionOf::new(Arbitrary::<i32>::default()); + let mut rng = get_rng(); + + let mut none_count = 0; + let mut mutated_some_count = 0; + + for _ in 0..iterations_to_hit_all(2, NONE_ON_MUTATE_PROBABILITY) { + let mut val: Option<i32> = Some(0); + domain.mutate(&mut val, &mut rng, false).unwrap(); + match val { + None => none_count += 1, + Some(v) => { + expect_that!(v, not(eq(0))); + mutated_some_count += 1; + } + } + } + + expect_that!( + none_count, + gt(0), + "Mutating Some without shrinking should occasionally produce None" + ); + expect_that!( + mutated_some_count, + gt(none_count), + "Mutating Some without shrinking should mutate the inner value (99%) more often than dropping to None (1%)" + ); + } + + #[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_validate_corpus_value() { + let domain = OptionOf::new(InRange::new(10i32, 20)); + + expect_that!(domain.validate_corpus_value(&None), ok(anything())); + expect_that!(domain.validate_corpus_value(&Some(15)), ok(anything())); + expect_that!(domain.validate_corpus_value(&Some(5)), err(anything())); + } + + #[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/src/domains/utility.rs b/rust/src/domains/utility.rs index cf70f56..7df51c8 100644 --- a/rust/src/domains/utility.rs +++ b/rust/src/domains/utility.rs
@@ -377,6 +377,21 @@ Ok(()) } +/// Returns the number of iterations needed to hit each of `num_cases` cases, where each case is +/// hit with probability at least `hit_probability` per iteration, such that the probability of +/// missing some case is at most 10^-15 (by the union bound). +#[cfg(test)] +pub(crate) fn iterations_to_hit_all(num_cases: usize, hit_probability: f64) -> usize { + assert!(num_cases > 0, "iterations_to_hit_all: num_cases must be > 0"); + assert!( + 0.0 < hit_probability && hit_probability < 1.0, + "iterations_to_hit_all: hit_probability must be in (0.0, 1.0), got {hit_probability}" + ); + const CONFIDENCE_LEVEL: f64 = 15.0; + let numerator = CONFIDENCE_LEVEL * std::f64::consts::LN_10 + (num_cases as f64).ln(); + (numerator / -(-hit_probability).ln_1p()).ceil() as usize +} + #[cfg(test)] mod tests { use super::*; @@ -590,4 +605,12 @@ assert_eq!(map_char_to_int('\u{10FFFF}'), NUM_VALID_CODEPOINTS - 1); assert_eq!(map_int_to_char(NUM_VALID_CODEPOINTS - 1), '\u{10FFFF}'); } + + #[test] + fn test_iterations_to_hit_all() { + assert_eq!(iterations_to_hit_all(1, 0.5), 50); + assert_eq!(iterations_to_hit_all(2, 0.5), 51); + assert_eq!(iterations_to_hit_all(1, 0.01), 3437); + assert_eq!(iterations_to_hit_all(2, 0.01), 3506); + } }
diff --git a/rust/tests/macro_compiles.rs b/rust/tests/macro_compiles.rs index 04c245e..cdf6f13 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,12 @@ #[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>) {} + 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)); }