Skip to content

Commit 50828bc

Browse files
FuzzTest Teamcopybara-github
authored andcommitted
No public description
PiperOrigin-RevId: 991788346
1 parent 88f66c0 commit 50828bc

4 files changed

Lines changed: 286 additions & 0 deletions

File tree

‎rust/src/domains.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
pub mod arbitrary;
1616
pub mod containers;
17+
pub mod option_of;
1718
pub mod range;
1819
pub mod tuple_of;
1920
pub mod utility;

‎rust/src/domains/arbitrary.rs‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,34 @@ impl Domain for Arbitrary<()> {
328328
}
329329
}
330330

331+
impl<T> Domain for Arbitrary<Option<T>>
332+
where
333+
Arbitrary<T>: Domain,
334+
{
335+
type CorpusValue = Option<<Arbitrary<T> as Domain>::CorpusValue>;
336+
type UserValue<'user> = Option<<Arbitrary<T> as Domain>::UserValue<'user>>;
337+
338+
fn init(&self, rng: &mut dyn rand::Rng) -> anyhow::Result<Self::CorpusValue> {
339+
super::option_of::OptionOf::new(Arbitrary::<T>::default()).init(rng)
340+
}
341+
342+
fn mutate(
343+
&self,
344+
val: &mut Self::CorpusValue,
345+
rng: &mut dyn rand::Rng,
346+
only_shrink: bool,
347+
) -> anyhow::Result<()> {
348+
super::option_of::OptionOf::new(Arbitrary::<T>::default()).mutate(val, rng, only_shrink)
349+
}
350+
351+
fn corpus_to_user_value<'a>(
352+
&self,
353+
corpus_value: &'a Self::CorpusValue,
354+
) -> anyhow::Result<Self::UserValue<'a>> {
355+
super::option_of::OptionOf::new(Arbitrary::<T>::default()).corpus_to_user_value(corpus_value)
356+
}
357+
}
358+
331359
#[cfg(test)]
332360
mod tests {
333361
use std::collections::HashSet;
@@ -783,4 +811,27 @@ mod tests {
783811
);
784812
}
785813
}
814+
815+
#[test]
816+
fn test_arbitrary_option() {
817+
let domain = Arbitrary::<Option<i32>>::default();
818+
let mut rng = get_rng();
819+
820+
let mut has_none = false;
821+
let mut has_some = false;
822+
for _ in 0..100 {
823+
let sample = domain.init(&mut rng).unwrap();
824+
if sample.is_none() {
825+
has_none = true;
826+
} else {
827+
has_some = true;
828+
}
829+
}
830+
assert!(has_none);
831+
assert!(has_some);
832+
833+
let mut val: Option<i32> = None;
834+
domain.mutate(&mut val, &mut rng, false).unwrap();
835+
assert!(val.is_some());
836+
}
786837
}

‎rust/src/domains/option_of.rs‎

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
use rand::RngExt;
16+
use std::fmt;
17+
18+
use super::Domain;
19+
20+
/// Generates optional values of type `Option<T>` using an inner domain.
21+
///
22+
/// An `OptionOf` domain produces either `None` or `Some(value)`, where `value`
23+
/// is drawn from the wrapped inner domain.
24+
///
25+
/// Example usage:
26+
/// ```
27+
/// # use fuzztest::domains::Domain;
28+
/// # use fuzztest::domains::arbitrary::Arbitrary;
29+
/// # use fuzztest::domains::option_of::OptionOf;
30+
/// # use rand::rngs::SmallRng;
31+
/// # use rand::SeedableRng;
32+
///
33+
/// let domain = OptionOf::new(Arbitrary::<i32>::default());
34+
/// let mut rng = SmallRng::seed_from_u64(42);
35+
///
36+
/// let sample = domain.init(&mut rng);
37+
/// assert!(sample.is_ok());
38+
/// ```
39+
pub struct OptionOf<T> {
40+
inner: T,
41+
}
42+
43+
impl<T: Clone> Clone for OptionOf<T> {
44+
fn clone(&self) -> Self {
45+
Self { inner: self.inner.clone() }
46+
}
47+
}
48+
49+
impl<T: fmt::Debug> fmt::Debug for OptionOf<T> {
50+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51+
f.debug_struct("OptionOf").field("inner", &self.inner).finish()
52+
}
53+
}
54+
55+
impl<T> OptionOf<T> {
56+
/// Creates a new `OptionOf` domain wrapping the specified inner domain.
57+
pub fn new(inner: T) -> Self {
58+
Self { inner }
59+
}
60+
}
61+
62+
impl<T> Domain for OptionOf<T>
63+
where
64+
T: Domain,
65+
{
66+
type CorpusValue = Option<T::CorpusValue>;
67+
type UserValue<'user> = Option<T::UserValue<'user>>;
68+
69+
fn init(&self, rng: &mut dyn rand::Rng) -> anyhow::Result<Self::CorpusValue> {
70+
// 50% chance of returning None, 50% chance of Some(inner).
71+
if rng.random_bool(0.5) {
72+
Ok(None)
73+
} else {
74+
Ok(Some(self.inner.init(rng)?))
75+
}
76+
}
77+
78+
fn mutate(
79+
&self,
80+
val: &mut Self::CorpusValue,
81+
rng: &mut dyn rand::Rng,
82+
only_shrink: bool,
83+
) -> anyhow::Result<()> {
84+
match val {
85+
None => {
86+
if !only_shrink {
87+
*val = Some(self.inner.init(rng)?);
88+
}
89+
}
90+
Some(inner_val) => {
91+
if only_shrink {
92+
// When shrinking, either drop to None or shrink the inner value.
93+
if rng.random_bool(0.25) {
94+
*val = None;
95+
} else {
96+
self.inner.mutate(inner_val, rng, true)?;
97+
}
98+
} else {
99+
// When mutating without shrinking: 1/100 chance to set to None (matching
100+
// C++ FuzzTest), otherwise mutate the inner value.
101+
if rng.random_bool(0.01) {
102+
*val = None;
103+
} else {
104+
self.inner.mutate(inner_val, rng, false)?;
105+
}
106+
}
107+
}
108+
}
109+
Ok(())
110+
}
111+
112+
fn corpus_to_user_value<'a>(
113+
&self,
114+
corpus_value: &'a Self::CorpusValue,
115+
) -> anyhow::Result<Self::UserValue<'a>> {
116+
match corpus_value {
117+
None => Ok(None),
118+
Some(v) => Ok(Some(self.inner.corpus_to_user_value(v)?)),
119+
}
120+
}
121+
}
122+
123+
#[cfg(test)]
124+
mod tests {
125+
use super::*;
126+
use crate::domains::arbitrary::Arbitrary;
127+
use googletest::prelude::*;
128+
use rand::rngs::{SmallRng, SysRng};
129+
use rand::SeedableRng;
130+
131+
fn get_rng() -> SmallRng {
132+
SmallRng::try_from_rng(&mut SysRng).expect("Failed to create RNG")
133+
}
134+
135+
#[gtest]
136+
fn test_option_of_init_produces_both_none_and_some() {
137+
let domain = OptionOf::new(Arbitrary::<i32>::default());
138+
let mut rng = get_rng();
139+
140+
let mut has_none = false;
141+
let mut has_some = false;
142+
143+
for _ in 0..200 {
144+
let sample = domain.init(&mut rng).unwrap();
145+
if sample.is_none() {
146+
has_none = true;
147+
} else {
148+
has_some = true;
149+
}
150+
if has_none && has_some {
151+
break;
152+
}
153+
}
154+
155+
expect_that!(has_none, eq(true), "OptionOf::init should produce None");
156+
expect_that!(has_some, eq(true), "OptionOf::init should produce Some");
157+
}
158+
159+
#[gtest]
160+
fn test_option_of_mutate_from_none_can_become_some() {
161+
let domain = OptionOf::new(Arbitrary::<i32>::default());
162+
let mut rng = get_rng();
163+
164+
let mut val: Option<i32> = None;
165+
domain.mutate(&mut val, &mut rng, false).unwrap();
166+
expect_that!(val.is_some(), eq(true), "Mutating None without shrinking should produce Some");
167+
}
168+
169+
#[gtest]
170+
fn test_option_of_shrink_from_none_stays_none() {
171+
let domain = OptionOf::new(Arbitrary::<i32>::default());
172+
let mut rng = get_rng();
173+
174+
let mut val: Option<i32> = None;
175+
for _ in 0..50 {
176+
domain.mutate(&mut val, &mut rng, true).unwrap();
177+
expect_that!(val.is_none(), eq(true), "Shrinking None must stay None");
178+
}
179+
}
180+
181+
#[gtest]
182+
fn test_option_of_shrink_from_some_eventually_becomes_none() {
183+
let domain = OptionOf::new(Arbitrary::<i32>::default());
184+
let mut rng = get_rng();
185+
186+
let mut val: Option<i32> = Some(1000);
187+
let mut became_none = false;
188+
189+
for _ in 0..200 {
190+
domain.mutate(&mut val, &mut rng, true).unwrap();
191+
if val.is_none() {
192+
became_none = true;
193+
break;
194+
}
195+
}
196+
197+
expect_that!(became_none, eq(true), "Shrinking Some should eventually produce None");
198+
}
199+
200+
#[gtest]
201+
fn test_option_of_corpus_to_user_value() {
202+
let domain = OptionOf::new(Arbitrary::<i32>::default());
203+
204+
let none_val: Option<i32> = None;
205+
let some_val: Option<i32> = Some(42);
206+
207+
expect_that!(domain.corpus_to_user_value(&none_val).unwrap(), eq(None));
208+
expect_that!(domain.corpus_to_user_value(&some_val).unwrap(), eq(Some(42)));
209+
}
210+
211+
#[gtest]
212+
fn test_option_of_serde_roundtrip() {
213+
let domain = OptionOf::new(Arbitrary::<i32>::default());
214+
215+
let none_corpus: Option<i32> = None;
216+
let bytes = domain.serialize_corpus(&none_corpus).unwrap();
217+
let parsed = domain.parse_corpus(&bytes).unwrap();
218+
expect_that!(parsed, eq(None));
219+
220+
let some_corpus: Option<i32> = Some(12345);
221+
let bytes = domain.serialize_corpus(&some_corpus).unwrap();
222+
let parsed = domain.parse_corpus(&bytes).unwrap();
223+
expect_that!(parsed, eq(Some(12345)));
224+
}
225+
}

‎rust/tests/macro_compiles.rs‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use fuzztest::domains::arbitrary::Arbitrary;
1616
use fuzztest::domains::containers::ContainerDomain;
1717
use fuzztest::domains::containers::VecOf;
18+
use fuzztest::domains::option_of::OptionOf;
1819
use fuzztest::fuzztest;
1920

2021
#[fuzztest(_a = Arbitrary::<i32>::default())]
@@ -26,8 +27,16 @@ fn fuzztest_macro_compiles_with_two_args(_a: i32, _b: i32) {}
2627
#[fuzztest(_a = VecOf::new(Arbitrary::<i32>::default()).with_max_len(10))]
2728
fn fuzztest_macro_compiles_with_vec(_a: Vec<i32>) {}
2829

30+
#[fuzztest(_a = OptionOf::new(Arbitrary::<i32>::default()))]
31+
fn fuzztest_macro_compiles_with_option_of(_a: Option<i32>) {}
32+
33+
#[fuzztest(_a = Arbitrary::<Option<i32>>::default())]
34+
fn fuzztest_macro_compiles_with_arbitrary_option(_a: Option<i32>) {}
35+
2936
fn main() {
3037
// The property function is defined in the scope where the macro is
3138
// invoked, so it can be called directly, e.g. from a regression test.
3239
fuzztest_macro_compiles(0);
40+
fuzztest_macro_compiles_with_option_of(Some(0));
41+
fuzztest_macro_compiles_with_arbitrary_option(None);
3342
}

0 commit comments

Comments
 (0)