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));
 }