From e86373492e2b5e0cb2a95b9a239cde2542e9c86f Mon Sep 17 00:00:00 2001 From: Shashank Sharma Date: Mon, 3 Aug 2026 14:04:00 -0700 Subject: [PATCH] fuzztest-rust | support replaying corpus for inf - Introduce a Duration enum with Indefinitely and Fixed variants in fuzztest_options. - Replace FuzzFor with Duration across fuzztest_options, fuzztest, and cargo-fuzztest. - Implement FromStr and Display on Duration to parse "inf", "infinite", and human-readable durations. - Update replay_corpus_for option to support indefinite replay. - In Centipede argument builder, omit time limit flag when replaying or fuzzing indefinitely. - Add unit tests for Duration parsing, formatting, Centipede arguments, and cargo-fuzztest commands. PiperOrigin-RevId: 958566576 --- rust/cargo_fuzztest/src/lib.rs | 109 ++++++++++-- rust/cargo_fuzztest/tests/e2e_cli_test.rs | 11 +- rust/cargo_fuzztest/tests/runner_test.rs | 49 +++++- rust/options/src/lib.rs | 191 ++++++++++++++++++---- rust/src/domains/arbitrary.rs | 7 +- rust/src/domains/containers.rs | 7 +- rust/src/options.rs | 133 ++++++++++----- 7 files changed, 401 insertions(+), 106 deletions(-) diff --git a/rust/cargo_fuzztest/src/lib.rs b/rust/cargo_fuzztest/src/lib.rs index b7c57ebc2..e2af0905d 100644 --- a/rust/cargo_fuzztest/src/lib.rs +++ b/rust/cargo_fuzztest/src/lib.rs @@ -18,8 +18,8 @@ use anyhow::{Context, Result}; use clap::Parser; pub use fuzztest_options::{ - ExecutionMode, FuzzFor, FuzzOptions, FuzzTestOptions, ListCrashIdsOptions, ReplayCorpusOptions, - ReplayCrashOptions, TimeBudgetType, + ExecutionMode, FuzzOptions, FuzzTestOptions, ListCrashIdsOptions, ReplayCorpusOptions, + ReplayCrashOptions, RunDuration, TimeBudgetType, }; use std::env; use std::ffi::OsString; @@ -95,7 +95,10 @@ impl CargoFuzzTestOptions { if self.test_path.is_some() { self.check_centipede_binary_path_is_set()?; ExecutionMode::Fuzz(FuzzOptions { - fuzz_for: self.fuzztest_options.fuzz_for.unwrap_or(FuzzFor::Indefinitely), + fuzz_for: self + .fuzztest_options + .fuzz_for + .unwrap_or(RunDuration::Indefinitely), jobs: self.fuzztest_options.jobs, }) } else { @@ -246,14 +249,7 @@ impl FuzztestRunner { ExecutionMode::Fuzz(fuzz_options) => { let FuzzOptions { fuzz_for, jobs } = fuzz_options; - match fuzz_for { - FuzzFor::Indefinitely => { - cmd.env("FUZZTEST_FUZZ_FOR", "inf"); - } - FuzzFor::Duration(duration) => { - cmd.env("FUZZTEST_FUZZ_FOR", duration.to_string()); - } - } + cmd.env("FUZZTEST_FUZZ_FOR", fuzz_for.to_string()); if let Some(jobs) = jobs { cmd.env("FUZZTEST_JOBS", jobs.to_string()); } @@ -610,7 +606,60 @@ mod tests { assert_eq!( mode, ExecutionMode::ReplayCorpus(ReplayCorpusOptions { - replay_corpus_for: "10s".parse().unwrap(), + replay_corpus_for: "10s".parse().expect("valid duration string"), + time_budget_type: TimeBudgetType::PerTest, + jobs: None, + }) + ); + } + + #[gtest] + fn test_cli_option_parsing_replay_corpus_for_inf() { + let parsed = CargoFuzzTestOptions::try_parse_from([ + "cargo-fuzztest", + "--replay-corpus-for", + "inf", + "--corpus-db", + "/tmp/corpus_db", + "--centipede-binary-path", + "/custom/centipede", + ]) + .expect("valid replay-corpus-for inf should parse successfully"); + + assert_eq!(parsed.fuzztest_options.replay_corpus_for, Some(RunDuration::Indefinitely)); + assert_eq!(parsed.fuzztest_options.time_budget_type, TimeBudgetType::PerTest); + assert_eq!(parsed.fuzztest_options.corpus_db.as_deref(), Some("/tmp/corpus_db")); + + let mode = parsed.execution_mode().expect("valid execution mode"); + assert_eq!( + mode, + ExecutionMode::ReplayCorpus(ReplayCorpusOptions { + replay_corpus_for: RunDuration::Indefinitely, + time_budget_type: TimeBudgetType::PerTest, + jobs: None, + }) + ); + } + + #[gtest] + fn test_cli_option_parsing_replay_corpus_for_infinity() { + let parsed = CargoFuzzTestOptions::try_parse_from([ + "cargo-fuzztest", + "--replay-corpus-for", + "infinity", + "--corpus-db", + "/tmp/corpus_db", + "--centipede-binary-path", + "/custom/centipede", + ]) + .expect("valid replay-corpus-for infinity should parse successfully"); + + assert_eq!(parsed.fuzztest_options.replay_corpus_for, Some(RunDuration::Indefinitely)); + let mode = parsed.execution_mode().expect("valid execution mode"); + assert_eq!( + mode, + ExecutionMode::ReplayCorpus(ReplayCorpusOptions { + replay_corpus_for: RunDuration::Indefinitely, time_budget_type: TimeBudgetType::PerTest, jobs: None, }) @@ -794,4 +843,40 @@ mod tests { Some("/custom/centipede".to_string()) ))); } + + #[gtest] + fn test_build_run_command_replay_corpus_indefinite() { + let options = CargoFuzzTestOptions { + fuzztest_options: FuzzTestOptions { + replay_corpus_for: Some(RunDuration::Indefinitely), + time_budget_type: TimeBudgetType::Total, + corpus_db: Some("/tmp/corpus_db".to_string()), + ..Default::default() + }, + centipede_binary_path: Some("/custom/centipede".to_string()), + ..Default::default() + }; + let runner = FuzztestRunner::new("x86_64-unknown-linux-gnu".to_string(), options); + let cmd = + runner.build_run_command(Path::new("/tmp/test_bin")).expect("should build run command"); + + let envs: Vec<(String, Option)> = cmd + .get_envs() + .map(|(k, v)| { + (k.to_string_lossy().to_string(), v.map(|s| s.to_string_lossy().to_string())) + }) + .collect(); + + assert!(envs.contains(&("FUZZTEST_REPLAY_CORPUS_FOR".to_string(), Some("inf".to_string())))); + assert!( + envs.contains(&("FUZZTEST_TIME_BUDGET_TYPE".to_string(), Some("total".to_string()))) + ); + assert!( + envs.contains(&("FUZZTEST_CORPUS_DB".to_string(), Some("/tmp/corpus_db".to_string()))) + ); + assert!(envs.contains(&( + "FUZZTEST_CENTIPEDE_BINARY_PATH".to_string(), + Some("/custom/centipede".to_string()) + ))); + } } diff --git a/rust/cargo_fuzztest/tests/e2e_cli_test.rs b/rust/cargo_fuzztest/tests/e2e_cli_test.rs index b77256f6a..04ed647ab 100644 --- a/rust/cargo_fuzztest/tests/e2e_cli_test.rs +++ b/rust/cargo_fuzztest/tests/e2e_cli_test.rs @@ -365,7 +365,7 @@ fn test_cargo_fuzztest_e2e_replay_corpus_total_budget() { // 2. Run cargo-fuzztest CLI with --replay-corpus-for and --time-budget-type total. let mut cmd = setup_cargo_fuzztest_command(&sample_crate_path, temp_target_dir.path()); cmd.arg("__fuzztest_mod__sample_fuzztest_target::sample_fuzztest_target") - .arg("--replay-corpus-for=3s") + .arg("--replay-corpus-for=4.5s") .arg("--time-budget-type=total") .arg("--corpus-db") .arg(temp_db_dir.path()) @@ -376,12 +376,15 @@ fn test_cargo_fuzztest_e2e_replay_corpus_total_budget() { let stderr_str = String::from_utf8_lossy(&output.stderr); let stdout_str = String::from_utf8_lossy(&output.stdout); + eprintln!("tmp:: stderr:\n{stderr_str}"); + eprintln!("tmp:: stdout:\n{stdout_str}"); + expect_true!(output.status.success()); expect_true!( stderr_str.contains( - "Replaying __fuzztest_mod__sample_fuzztest_target.sample_fuzztest_target for 1s" + "Replaying __fuzztest_mod__sample_fuzztest_target.sample_fuzztest_target for 1.5s" ) || stdout_str.contains( - "Replaying __fuzztest_mod__sample_fuzztest_target.sample_fuzztest_target for 1s" + "Replaying __fuzztest_mod__sample_fuzztest_target.sample_fuzztest_target for 1.5s" ) ); } @@ -409,7 +412,7 @@ fn test_cargo_fuzztest_e2e_list_crash_ids() { let mut cmd = setup_cargo_fuzztest_command(&sample_crate_path, temp_target_dir.path()); cmd.arg(test_target) .arg("--fuzz-for=5s") - .env_remove("CENTIPEDE_BINARY_PATH") + .env_remove("FUZZTEST_CENTIPEDE_BINARY_PATH") .arg("--centipede-binary-path") .arg(¢ipede_bin) .arg("--corpus-db") diff --git a/rust/cargo_fuzztest/tests/runner_test.rs b/rust/cargo_fuzztest/tests/runner_test.rs index b865b1d4d..14bd430b0 100644 --- a/rust/cargo_fuzztest/tests/runner_test.rs +++ b/rust/cargo_fuzztest/tests/runner_test.rs @@ -2,7 +2,7 @@ mod common; use cargo_fuzztest::{CargoFuzzTestOptions, FuzztestRunner}; use common::get_sample_test_bin_path; -use fuzztest_options::{FuzzFor, FuzzTestOptions, TimeBudgetType}; +use fuzztest_options::{FuzzTestOptions, RunDuration, TimeBudgetType}; use googletest::prelude::*; #[gtest] @@ -45,10 +45,8 @@ fn test_runner_build_run_command_with_target() { #[gtest] fn test_runner_build_run_command_with_duration() { let binary_path = get_sample_test_bin_path("sample_fuzz_crate"); - let fuzztest_options = FuzzTestOptions { - fuzz_for: Some(FuzzFor::Duration("5s".parse().unwrap())), - ..Default::default() - }; + let fuzztest_options = + FuzzTestOptions { fuzz_for: Some("5s".parse().unwrap()), ..Default::default() }; let options = CargoFuzzTestOptions { fuzztest_options, centipede_binary_path: Some("/custom/path/to/centipede".to_string()), @@ -68,7 +66,7 @@ fn test_runner_build_run_command_with_duration() { fn test_runner_build_run_command_with_indefinitely() { let binary_path = get_sample_test_bin_path("sample_fuzz_crate"); let fuzztest_options = - FuzzTestOptions { fuzz_for: Some(FuzzFor::Indefinitely), ..Default::default() }; + FuzzTestOptions { fuzz_for: Some(RunDuration::Indefinitely), ..Default::default() }; let options = CargoFuzzTestOptions { fuzztest_options, centipede_binary_path: Some("/custom/path/to/centipede".to_string()), @@ -109,7 +107,7 @@ fn test_runner_build_run_command_with_jobs() { let binary_path = get_sample_test_bin_path("sample_fuzz_crate"); let fuzztest_options = FuzzTestOptions { jobs: Some(4), - fuzz_for: Some(FuzzFor::Duration("10s".parse().expect("static valid duration string"))), + fuzz_for: Some("10s".parse().expect("static valid duration string")), ..Default::default() }; let options = CargoFuzzTestOptions { @@ -288,6 +286,43 @@ fn test_runner_build_run_command_with_replay_corpus() { ))); } +#[gtest] +fn test_runner_build_run_command_with_replay_corpus_indefinitely() { + let binary_path = get_sample_test_bin_path("sample_fuzz_crate"); + let fuzztest_options = FuzzTestOptions { + replay_corpus_for: Some(RunDuration::Indefinitely), + time_budget_type: TimeBudgetType::Total, + corpus_db: Some("/custom/path/to/corpus_db".to_string()), + ..Default::default() + }; + let options = CargoFuzzTestOptions { + fuzztest_options, + centipede_binary_path: Some("/custom/path/to/centipede".to_string()), + ..Default::default() + }; + let runner = FuzztestRunner::new("x86_64-unknown-linux-gnu".to_string(), options); + let cmd = runner.build_run_command(&binary_path).expect("valid run command"); + + let envs: Vec<(String, Option)> = cmd + .get_envs() + .map(|(k, v)| (k.to_string_lossy().to_string(), v.map(|s| s.to_string_lossy().to_string()))) + .collect(); + expect_true!( + envs.contains(&("FUZZTEST_REPLAY_CORPUS_FOR".to_string(), Some("inf".to_string()))) + ); + expect_true!( + envs.contains(&("FUZZTEST_TIME_BUDGET_TYPE".to_string(), Some("total".to_string()))) + ); + expect_true!(envs.contains(&( + "FUZZTEST_CORPUS_DB".to_string(), + Some("/custom/path/to/corpus_db".to_string()) + ))); + expect_true!(envs.contains(&( + "FUZZTEST_CENTIPEDE_BINARY_PATH".to_string(), + Some("/custom/path/to/centipede".to_string()) + ))); +} + #[gtest] fn test_execution_mode_replay_corpus_missing_centipede_binary_path_errors() { let fuzztest_options = FuzzTestOptions { diff --git a/rust/options/src/lib.rs b/rust/options/src/lib.rs index 2b4c18f9b..93f9cc40c 100644 --- a/rust/options/src/lib.rs +++ b/rust/options/src/lib.rs @@ -18,8 +18,12 @@ #![deny(clippy::absolute_paths)] #![deny(unused_imports)] +use anyhow::Context; use clap::{Parser, ValueEnum}; -use humantime::Duration; +use std::fmt; +use std::fmt::Display; +use std::fmt::Formatter; +use std::str::FromStr; /// Time budget calculation type for replay corpus mode. #[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Default)] @@ -29,20 +33,6 @@ pub enum TimeBudgetType { Total, } -/// Parses a fuzzing duration string from `FUZZTEST_FUZZ_FOR`. -/// -/// Matches `"inf"` or `"infinity"` to [`FuzzFor::Indefinitely`]. All other values -/// are parsed as standard human-readable durations (for example, `"5s"` or `"10m"`). -fn parse_fuzz_for(s: &str) -> anyhow::Result { - let s_lower = s.trim().to_lowercase(); - if s_lower == "inf" || s_lower == "infinity" { - Ok(FuzzFor::Indefinitely) - } else { - let duration = s.parse()?; - Ok(FuzzFor::Duration(duration)) - } -} - /// Command-line and environment variable options parsed for the FuzzTest harness. #[derive(Parser, Debug, Clone, Default)] pub struct FuzzTestOptions { @@ -54,8 +44,8 @@ pub struct FuzzTestOptions { /// /// Accepts a human-readable duration (e.g., `5s`, `10m`, `1h`) or `inf` / `infinity` /// to fuzz indefinitely until a crash is found or it is stopped manually. - #[arg(env = "FUZZTEST_FUZZ_FOR", long, value_parser = parse_fuzz_for)] - pub fuzz_for: Option, + #[arg(env = "FUZZTEST_FUZZ_FOR", long)] + pub fuzz_for: Option, /// If true, subprocess logs are printed after every batch. Note that crash logs are always /// printed regardless of this flag's value. @@ -96,8 +86,11 @@ pub struct FuzzTestOptions { pub replay_findings: bool, /// Replay the corpus for a specified duration. + /// + /// Accepts a human-readable duration (e.g., `5s`, `10m`, `1h`) or `inf` / `infinity` + /// to replay indefinitely until stopped manually. #[arg(env = "FUZZTEST_REPLAY_CORPUS_FOR", long, requires = "corpus_db")] - pub replay_corpus_for: Option, + pub replay_corpus_for: Option, /// Time budget calculation type for replay corpus mode. #[arg(env = "FUZZTEST_TIME_BUDGET_TYPE", long, value_enum, default_value_t = TimeBudgetType::PerTest)] @@ -158,7 +151,7 @@ impl ExecutionMode { return ExecutionMode::ReplayCorpus(ReplayCorpusOptions { replay_corpus_for, time_budget_type: options.time_budget_type, - jobs: options.jobs.clone(), + jobs: options.jobs, }); } @@ -172,10 +165,7 @@ impl ExecutionMode { // Continuous fuzzing mode is selected if an explicit duration/budget (`fuzz_for`) is specified. if let Some(fuzz_for) = &options.fuzz_for { - return ExecutionMode::Fuzz(FuzzOptions { - fuzz_for: *fuzz_for, - jobs: options.jobs.clone(), - }); + return ExecutionMode::Fuzz(FuzzOptions { fuzz_for: *fuzz_for, jobs: options.jobs }); } ExecutionMode::SmokeTest @@ -185,21 +175,46 @@ impl ExecutionMode { /// Mode-specific options for continuous fuzzing. #[derive(Debug, Clone, PartialEq, Eq)] pub struct FuzzOptions { - pub fuzz_for: FuzzFor, + pub fuzz_for: RunDuration, /// If `jobs` is `None`, we won't specify the number of jobs while invoking Centipede and it /// will use its own default value. pub jobs: Option, } -/// The duration or limit for fuzzing. +/// The duration or limit for fuzzing or replaying corpus. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum FuzzFor { - /// Fuzz indefinitely until it is manually stopped or a crash is found. +pub enum RunDuration { + /// Run indefinitely until manually stopped or a crash is found. Indefinitely, - /// Fuzz for a specific duration. - Duration(Duration), + /// Run for a specific fixed duration. + Fixed(humantime::Duration), +} + +impl FromStr for RunDuration { + type Err = anyhow::Error; + + fn from_str(s: &str) -> anyhow::Result { + let s_lower = s.trim().to_lowercase(); + if s_lower == "inf" || s_lower == "infinity" { + Ok(RunDuration::Indefinitely) + } else { + let duration: humantime::Duration = s + .parse() + .with_context(|| format!("while attempting to parse duration string '{s}'"))?; + Ok(RunDuration::Fixed(duration)) + } + } +} + +impl Display for RunDuration { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + RunDuration::Indefinitely => write!(f, "inf"), + RunDuration::Fixed(duration) => write!(f, "{duration}"), + } + } } /// Mode-specific options for replaying a specific crashing input from the corpus database. @@ -211,7 +226,7 @@ pub struct ReplayCrashOptions { /// Mode-specific options for replaying corpus for a duration. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ReplayCorpusOptions { - pub replay_corpus_for: Duration, + pub replay_corpus_for: RunDuration, pub time_budget_type: TimeBudgetType, /// If `jobs` is `None`, we won't specify the number of jobs while invoking Centipede and it /// will use its own default value. @@ -230,6 +245,39 @@ mod tests { use googletest::prelude::*; use std::ffi::OsString; + #[gtest] + fn test_duration_from_str_inf() { + let duration: RunDuration = "inf".parse().expect("failed to parse 'inf'"); + expect_that!(duration, eq(RunDuration::Indefinitely)); + } + + #[gtest] + fn test_duration_from_str_infinity() { + let duration: RunDuration = "infinity".parse().expect("failed to parse 'infinity'"); + expect_that!(duration, eq(RunDuration::Indefinitely)); + } + + #[gtest] + fn test_duration_from_str_fixed() { + let duration: RunDuration = "10s".parse().expect("failed to parse '10s'"); + let expected_fixed = humantime::Duration::from(std::time::Duration::from_secs(10)); + expect_that!(duration, eq(RunDuration::Fixed(expected_fixed))); + } + + #[gtest] + fn test_duration_from_str_invalid() { + let result: Result = "invalid_duration".parse(); + expect_true!(result.is_err()); + } + + #[gtest] + fn test_duration_display() { + expect_that!(RunDuration::Indefinitely.to_string(), eq("inf")); + let fixed = + RunDuration::Fixed(humantime::Duration::from(std::time::Duration::from_secs(10))); + expect_that!(fixed.to_string(), eq("10s")); + } + #[gtest] fn test_replay_id_requires_corpus_db() { // SAFETY: Testing environment parsing in single-threaded context. @@ -281,6 +329,7 @@ mod tests { let options = FuzzTestOptions::parse_from(std::iter::empty::()); expect_that!(options.jobs, eq(Some(4))); + // Setting jobs alone should not enter fuzzing mode; it defaults to smoke test mode. expect_that!(ExecutionMode::from_fuzztest_options(&options), eq(&ExecutionMode::SmokeTest)); @@ -305,7 +354,7 @@ mod tests { expect_that!( ExecutionMode::from_fuzztest_options(&options), eq(&ExecutionMode::Fuzz(FuzzOptions { - fuzz_for: FuzzFor::Duration(expected_duration), + fuzz_for: RunDuration::Fixed(expected_duration), jobs: Some(4), })) ); @@ -425,11 +474,60 @@ mod tests { let options = result .expect("parsing should succeed when both replay_corpus_for and corpus_db are present"); - expect_that!(options.replay_corpus_for, eq(Some("10s".parse().unwrap()))); + expect_that!( + options.replay_corpus_for, + eq(Some("10s".parse().expect("valid duration string"))) + ); expect_that!(options.corpus_db.as_deref(), eq(Some("/tmp/corpus_db"))); expect_that!(options.time_budget_type, eq(TimeBudgetType::PerTest)); } + #[gtest] + fn test_replay_corpus_for_inf_env_succeeds() { + // SAFETY: Testing environment parsing in single-threaded context. + unsafe { + std::env::set_var("FUZZTEST_REPLAY_CORPUS_FOR", "inf"); + std::env::set_var("FUZZTEST_CORPUS_DB", "/tmp/corpus_db"); + } + + let result = FuzzTestOptions::try_parse_from(std::iter::empty::()); + + // SAFETY: Cleaning up environment variables. + unsafe { + std::env::remove_var("FUZZTEST_REPLAY_CORPUS_FOR"); + std::env::remove_var("FUZZTEST_CORPUS_DB"); + } + + let options = result.expect( + "parsing should succeed when replay_corpus_for is inf and corpus_db is present", + ); + expect_that!(options.replay_corpus_for, eq(Some(RunDuration::Indefinitely))); + expect_that!(options.corpus_db.as_deref(), eq(Some("/tmp/corpus_db"))); + } + + #[gtest] + fn test_replay_corpus_for_infinity_env_succeeds() { + // SAFETY: Testing environment parsing in single-threaded context. + unsafe { + std::env::set_var("FUZZTEST_REPLAY_CORPUS_FOR", "infinity"); + std::env::set_var("FUZZTEST_CORPUS_DB", "/tmp/corpus_db"); + } + + let result = FuzzTestOptions::try_parse_from(std::iter::empty::()); + + // SAFETY: Cleaning up environment variables. + unsafe { + std::env::remove_var("FUZZTEST_REPLAY_CORPUS_FOR"); + std::env::remove_var("FUZZTEST_CORPUS_DB"); + } + + let options = result.expect( + "parsing should succeed when replay_corpus_for is infinity and corpus_db is present", + ); + expect_that!(options.replay_corpus_for, eq(Some(RunDuration::Indefinitely))); + expect_that!(options.corpus_db.as_deref(), eq(Some("/tmp/corpus_db"))); + } + #[gtest] fn test_replay_corpus_for_with_total_time_budget() { // SAFETY: Testing environment parsing in single-threaded context. @@ -449,7 +547,34 @@ mod tests { } let options = result.expect("parsing should succeed with total time budget type"); - expect_that!(options.replay_corpus_for, eq(Some("10s".parse().unwrap()))); + expect_that!( + options.replay_corpus_for, + eq(Some("10s".parse().expect("valid duration string"))) + ); + expect_that!(options.corpus_db.as_deref(), eq(Some("/tmp/corpus_db"))); + expect_that!(options.time_budget_type, eq(TimeBudgetType::Total)); + } + + #[gtest] + fn test_replay_corpus_for_inf_with_total_time_budget() { + // SAFETY: Testing environment parsing in single-threaded context. + unsafe { + std::env::set_var("FUZZTEST_REPLAY_CORPUS_FOR", "inf"); + std::env::set_var("FUZZTEST_TIME_BUDGET_TYPE", "total"); + std::env::set_var("FUZZTEST_CORPUS_DB", "/tmp/corpus_db"); + } + + let result = FuzzTestOptions::try_parse_from(std::iter::empty::()); + + // SAFETY: Cleaning up environment variables. + unsafe { + std::env::remove_var("FUZZTEST_REPLAY_CORPUS_FOR"); + std::env::remove_var("FUZZTEST_TIME_BUDGET_TYPE"); + std::env::remove_var("FUZZTEST_CORPUS_DB"); + } + + let options = result.expect("parsing should succeed with total time budget type and inf"); + expect_that!(options.replay_corpus_for, eq(Some(RunDuration::Indefinitely))); expect_that!(options.corpus_db.as_deref(), eq(Some("/tmp/corpus_db"))); expect_that!(options.time_budget_type, eq(TimeBudgetType::Total)); } diff --git a/rust/src/domains/arbitrary.rs b/rust/src/domains/arbitrary.rs index 33abb2b38..e2167bf55 100644 --- a/rust/src/domains/arbitrary.rs +++ b/rust/src/domains/arbitrary.rs @@ -17,6 +17,7 @@ use super::utility::mutate_integer; use super::utility::shrink_towards; use super::Domain; use std::char; +use std::fmt; use std::marker::PhantomData; use anyhow; @@ -47,12 +48,12 @@ pub struct Arbitrary { impl Clone for Arbitrary { fn clone(&self) -> Self { - Self { _phantom: std::marker::PhantomData } + Self { _phantom: PhantomData } } } -impl std::fmt::Debug for Arbitrary { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl fmt::Debug for Arbitrary { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Arbitrary").field("_phantom", &self._phantom).finish() } } diff --git a/rust/src/domains/containers.rs b/rust/src/domains/containers.rs index 3c34565c9..a15e28ca2 100644 --- a/rust/src/domains/containers.rs +++ b/rust/src/domains/containers.rs @@ -1,4 +1,5 @@ use rand::RngExt; +use std::fmt; use super::Domain; @@ -99,8 +100,8 @@ impl Clone for VecOf { } } -impl std::fmt::Debug for VecOf { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl fmt::Debug for VecOf { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("VecOf") .field("inner", &self.inner) .field("min_len", &self.min_len) @@ -201,7 +202,7 @@ impl ContainerDomain for VecOf { fn with_min_len(self, min_len: usize) -> Self { assert!( - self.max_len.map_or(true, |max| min_len <= max), + self.max_len.is_none_or(|max| min_len <= max), "Minimum length {} cannot be greater than the maximum length {}", min_len, self.max_len.unwrap() diff --git a/rust/src/options.rs b/rust/src/options.rs index 5b659dacb..41abaf5a4 100644 --- a/rust/src/options.rs +++ b/rust/src/options.rs @@ -25,8 +25,8 @@ use std::sync::OnceLock; use tempfile::{NamedTempFile, TempDir}; pub use fuzztest_options::{ - ExecutionMode, FuzzFor, FuzzOptions, FuzzTestOptions, ListCrashIdsOptions, ReplayCorpusOptions, - ReplayCrashOptions, TimeBudgetType, + ExecutionMode, FuzzOptions, FuzzTestOptions, ReplayCorpusOptions, ReplayCrashOptions, + RunDuration, TimeBudgetType, }; /// Returns a lazily-initialized static reference to the global `FuzzTestOptions`. @@ -214,10 +214,10 @@ impl CentipedeArgs { match mode_opts { ExecutionMode::Fuzz(fuzz_opts) => { match &fuzz_opts.fuzz_for { - FuzzFor::Indefinitely => { + RunDuration::Indefinitely => { // not specifying `--stop_after` means to run indefinitely. } - FuzzFor::Duration(duration) => { + RunDuration::Fixed(duration) => { let duration_secs = duration.as_secs_f64(); if opt_corpusdb.is_some() { add_arg(format!("--fuzztest_time_limit_per_test={duration_secs}s"))?; @@ -242,22 +242,31 @@ impl CentipedeArgs { add_arg(format!("--list_crash_ids_file={}", path.display()))?; } ExecutionMode::ReplayCorpus(replay_corpus_opts) => { - let time_limit = match replay_corpus_opts.time_budget_type { - TimeBudgetType::PerTest => replay_corpus_opts.replay_corpus_for, - TimeBudgetType::Total => { - let num_tests = inventory::iter::().count(); - if num_tests == 0 { - replay_corpus_opts.replay_corpus_for - } else { - (*replay_corpus_opts.replay_corpus_for.as_ref() / (num_tests as u32)) - .into() - } - } - }; add_arg("--fuzztest_only_replay=true".to_string())?; add_arg("--fuzztest_replay_coverage_inputs=true".to_string())?; add_arg("--load_shards_only=true".to_string())?; - add_arg(format!("--fuzztest_time_limit_per_test={time_limit}"))?; + match replay_corpus_opts.replay_corpus_for { + RunDuration::Indefinitely => { + // Not specifying `--fuzztest_time_limit_per_test` means to run indefinitely + } + RunDuration::Fixed(duration) => { + let time_limit: humantime::Duration = match replay_corpus_opts + .time_budget_type + { + TimeBudgetType::PerTest => duration, + TimeBudgetType::Total => { + let num_tests = inventory::iter::().count(); + if num_tests == 0 { + duration + } else { + (*duration.as_ref() / (num_tests as u32)).into() + } + } + }; + let time_limit_secs = time_limit.as_secs_f64(); + add_arg(format!("--fuzztest_time_limit_per_test={time_limit_secs}s"))?; + } + } if let Some(jobs) = &replay_corpus_opts.jobs { add_arg(format!("--j={jobs}"))?; } @@ -397,12 +406,12 @@ mod tests { let options = FuzzTestOptions::parse_from(std::iter::empty::()); - expect_that!(options.fuzz_for, eq(Some(FuzzFor::Indefinitely))); + expect_that!(options.fuzz_for, eq(Some(RunDuration::Indefinitely))); let mode = ExecutionMode::from_fuzztest_options(&options); let ExecutionMode::Fuzz(fuzz_opts) = mode else { panic!("Expected ExecutionMode::Fuzz"); }; - expect_that!(fuzz_opts.fuzz_for, eq(FuzzFor::Indefinitely)); + expect_that!(fuzz_opts.fuzz_for, eq(RunDuration::Indefinitely)); // SAFETY: Cleaning up environment variables. unsafe { @@ -413,7 +422,7 @@ mod tests { #[gtest] fn test_determine_execution_action_standalone_indefinite() { let options = - FuzzTestOptions { fuzz_for: Some(FuzzFor::Indefinitely), ..Default::default() }; + FuzzTestOptions { fuzz_for: Some(RunDuration::Indefinitely), ..Default::default() }; let action = determine_execution_action_internal(&options, "my_mod::my_test"); let ExecutionAction::Standalone(args) = action else { @@ -437,11 +446,8 @@ mod tests { #[gtest] fn test_determine_execution_action_standalone() { - let expected_duration = "10s".parse().unwrap(); - let options = FuzzTestOptions { - fuzz_for: Some(FuzzFor::Duration(expected_duration)), - ..Default::default() - }; + let expected_duration: RunDuration = "10s".parse().unwrap(); + let options = FuzzTestOptions { fuzz_for: Some(expected_duration), ..Default::default() }; let action = determine_execution_action_internal(&options, "my_mod::my_test"); let ExecutionAction::Standalone(args) = action else { @@ -471,10 +477,7 @@ mod tests { #[gtest] fn test_centipede_args_binary_identifier() { let expected_duration = "1s".parse().unwrap(); - let options = FuzzTestOptions { - fuzz_for: Some(FuzzFor::Duration(expected_duration)), - ..Default::default() - }; + let options = FuzzTestOptions { fuzz_for: Some(expected_duration), ..Default::default() }; let action = determine_execution_action_internal(&options, "my_mod::my_test"); let ExecutionAction::Standalone(args) = action else { @@ -494,10 +497,8 @@ mod tests { #[gtest] fn test_centipede_args_jobs() { let expected_duration = "1s".parse().expect("failed to parse duration"); - let options_no_jobs = FuzzTestOptions { - fuzz_for: Some(FuzzFor::Duration(expected_duration)), - ..Default::default() - }; + let options_no_jobs = + FuzzTestOptions { fuzz_for: Some(expected_duration), ..Default::default() }; let action_no_jobs = determine_execution_action_internal(&options_no_jobs, "my_mod::my_test"); let ExecutionAction::Standalone(args_no_jobs) = action_no_jobs else { @@ -508,7 +509,7 @@ mod tests { assert!(!args_str_no_jobs.iter().any(|s| s.starts_with("--j="))); let options_with_jobs = FuzzTestOptions { - fuzz_for: Some(FuzzFor::Duration(expected_duration)), + fuzz_for: Some(expected_duration), jobs: Some(4), ..Default::default() }; @@ -525,8 +526,7 @@ mod tests { #[gtest] fn test_default_env_diff_set() { let duration = "1s".parse().unwrap(); - let options = - FuzzTestOptions { fuzz_for: Some(FuzzFor::Duration(duration)), ..Default::default() }; + let options = FuzzTestOptions { fuzz_for: Some(duration), ..Default::default() }; let action = determine_execution_action_internal(&options, "my_mod::my_test"); let ExecutionAction::Standalone(args) = action else { panic!("Expected Standalone action"); @@ -640,6 +640,31 @@ mod tests { assert!(args_str.contains(&"--fuzztest_time_limit_per_test=10s")); } + #[gtest] + fn test_determine_execution_action_replay_corpus_indefinite() { + let options = FuzzTestOptions { + replay_corpus_for: Some(RunDuration::Indefinitely), + time_budget_type: TimeBudgetType::PerTest, + ..Default::default() + }; + let action = determine_execution_action_internal(&options, "my_mod::my_test"); + + let ExecutionAction::Standalone(args) = action else { + panic!("Expected Standalone action"); + }; + + let args_str: Vec<&str> = + args._c_strings.iter().map(|s| s.to_str().expect("invalid utf8")).collect(); + + assert!(args_str + .iter() + .any(|s| s.starts_with("--binary=") && s.contains("my_mod::my_test --exact"))); + assert!(args_str.contains(&"--fuzztest_only_replay=true")); + assert!(args_str.contains(&"--fuzztest_replay_coverage_inputs=true")); + assert!(args_str.contains(&"--load_shards_only=true")); + assert!(!args_str.iter().any(|s| s.starts_with("--fuzztest_time_limit_per_test="))); + } + #[gtest] fn test_determine_execution_action_replay_corpus_total_budget() { let expected_duration = "10s".parse().expect("failed to parse duration"); @@ -658,16 +683,39 @@ mod tests { args._c_strings.iter().map(|s| s.to_str().expect("invalid utf8")).collect(); let num_tests = inventory::iter::().count(); + let RunDuration::Fixed(fixed_duration) = expected_duration else { + panic!("expected Fixed duration"); + }; let expected_limit = if num_tests == 0 { - expected_duration + fixed_duration } else { - (*expected_duration.as_ref() / (num_tests as u32)).into() + (*fixed_duration.as_ref() / (num_tests as u32)).into() }; let expected_limit_str = format!("--fuzztest_time_limit_per_test={}", expected_limit); assert!(args_str.contains(&expected_limit_str.as_str())); } + #[gtest] + fn test_determine_execution_action_replay_corpus_indefinite_total_budget() { + let options = FuzzTestOptions { + replay_corpus_for: Some(RunDuration::Indefinitely), + time_budget_type: TimeBudgetType::Total, + ..Default::default() + }; + let action = determine_execution_action_internal(&options, "my_mod::my_test"); + + let ExecutionAction::Standalone(args) = action else { + panic!("Expected Standalone action"); + }; + + let args_str: Vec<&str> = + args._c_strings.iter().map(|s| s.to_str().expect("invalid utf8")).collect(); + + assert!(args_str.contains(&"--fuzztest_only_replay=true")); + assert!(!args_str.iter().any(|s| s.starts_with("--fuzztest_time_limit_per_test="))); + } + #[gtest] fn test_get_corpusdb_and_workdir_default_creates_temp_workdir() -> Result<()> { let options = FuzzTestOptions::default(); @@ -752,11 +800,8 @@ mod tests { // When corpus_db is not provided, fuzzing for a fixed duration should pass --stop_after // to Centipede so it stops fuzzing after the specified duration. let duration = "1s".parse().expect("fixed test string should parse as duration"); - let options = FuzzTestOptions { - fuzz_for: Some(FuzzFor::Duration(duration)), - corpus_db: None, - ..Default::default() - }; + let options = + FuzzTestOptions { fuzz_for: Some(duration), corpus_db: None, ..Default::default() }; let action = determine_execution_action_internal(&options, "my_mod::my_test"); let ExecutionAction::Standalone(args) = action else { @@ -780,7 +825,7 @@ mod tests { // a corpus database. let duration = "1s".parse().expect("fixed test string should parse as duration"); let options = FuzzTestOptions { - fuzz_for: Some(FuzzFor::Duration(duration)), + fuzz_for: Some(duration), corpus_db: Some("/tmp/corpus_db".to_string()), ..Default::default() };