| // 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. |
| |
| //! This module provides the core command-line flag and environment variable options |
| //! structure (`FuzzTestOptions`) and domain execution modes (`ExecutionMode`). |
| |
| #![deny(clippy::absolute_paths)] |
| #![deny(unused_imports)] |
| |
| use anyhow::Context; |
| use clap::{Parser, ValueEnum}; |
| use std::env; |
| use std::fmt; |
| use std::fmt::Display; |
| use std::fmt::Formatter; |
| use std::path::{Path, PathBuf}; |
| use std::str::FromStr; |
| |
| /// If a relative path is provided while executing inside a Bazel/Blaze test sandbox |
| /// (detected via the `TEST_SRCDIR` environment variable), the path is automatically |
| /// resolved relative to the runfiles root directory. |
| fn parse_corpus_db_path(raw: &str) -> Result<PathBuf, String> { |
| if raw.is_empty() { |
| return Err("corpus_db path cannot be empty".to_string()); |
| } |
| |
| let path = PathBuf::from(raw); |
| if path.is_absolute() { |
| Ok(path) |
| } else if let Ok(test_src_dir) = env::var("TEST_SRCDIR") { |
| Ok(Path::new(&test_src_dir).join(path)) |
| } else { |
| Ok(path) |
| } |
| } |
| |
| /// Time budget calculation type for replay corpus mode. |
| #[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Default)] |
| pub enum TimeBudgetType { |
| #[default] |
| PerTest, |
| Total, |
| } |
| |
| /// Command-line and environment variable options parsed for the FuzzTest harness. |
| #[derive(Parser, Debug, Clone, Default)] |
| pub struct FuzzTestOptions { |
| /// The working root directory. |
| #[arg(env = "FUZZTEST_WORKDIR_ROOT", long)] |
| pub workdir_root: Option<String>, |
| |
| /// The duration for which each test should be fuzzed. |
| /// |
| /// 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)] |
| pub fuzz_for: Option<RunDuration>, |
| |
| /// If true, subprocess logs are printed after every batch. Note that crash logs are always |
| /// printed regardless of this flag's value. |
| #[arg(env = "FUZZTEST_PRINT_SUBPROCESS_LOG", long)] |
| pub print_subprocess_log: bool, |
| |
| /// Number of parallel jobs to run. |
| #[arg(env = "FUZZTEST_JOBS", long)] |
| pub jobs: Option<usize>, |
| |
| /// List all crash IDs stored in the corpus database. |
| #[arg( |
| env = "FUZZTEST_LIST_CRASH_IDS", |
| long, |
| requires = "corpus_db", |
| requires = "list_crash_ids_file" |
| )] |
| pub list_crash_ids: bool, |
| |
| /// The output file path where listed crash IDs will be written. |
| #[arg( |
| env = "FUZZTEST_LIST_CRASH_IDS_FILE", |
| long, |
| requires = "corpus_db", |
| requires = "list_crash_ids" |
| )] |
| pub list_crash_ids_file: Option<String>, |
| |
| /// The crash ID to be replayed from the corpus database. |
| /// |
| /// If set, `corpus_db` must also be specified. This mode retrieves the crashing input |
| /// associated with the given ID from the database and executes the property function. |
| #[arg(env = "FUZZTEST_REPLAY_ID", long, requires = "corpus_db")] |
| pub replay_id: Option<String>, |
| |
| /// Replay all crashing inputs from the corpus database. |
| #[arg(env = "FUZZTEST_REPLAY_FINDINGS", long, requires = "corpus_db")] |
| 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<RunDuration>, |
| |
| /// Time budget calculation type for replay corpus mode. |
| #[arg(env = "FUZZTEST_TIME_BUDGET_TYPE", long, value_enum, default_value_t = TimeBudgetType::PerTest)] |
| pub time_budget_type: TimeBudgetType, |
| |
| /// The path to the corpus database. |
| /// |
| /// If set to non-empty, updates/queries the corpus database that contains coverage, |
| /// regression, and crashing inputs for each test binary and fuzz test. |
| #[arg(env = "FUZZTEST_CORPUS_DB", long, value_parser = parse_corpus_db_path)] |
| pub corpus_db: Option<PathBuf>, |
| |
| /// Controls the fuzzing and corpus replaying behavior when a crashing input is found. |
| /// |
| /// If set to false (default), the test execution stops upon finding the first crashing input, |
| /// and the test fails. If set to true, the execution logs any crash inputs found and continues |
| /// until reaching the time limit or manually stopped. |
| /// |
| /// Note that this does not affect crash replaying: |
| /// - Replaying a single crash (`--replay-id`) always stops immediately when a crash occurs. |
| /// - Replaying all crashes (`--replay-findings`) always continues replaying all remaining |
| /// crashing inputs regardless of this flag. |
| #[arg(env = "FUZZTEST_CONTINUE_AFTER_CRASH", long)] |
| pub continue_after_crash: bool, |
| |
| /// The execution identifier for the fuzz test run. |
| /// |
| /// When specified with `corpus_db`, allows resuming interrupted fuzzing or corpus replay |
| /// sessions, or skipping tests that already finished with the same execution ID. |
| #[arg(env = "FUZZTEST_EXECUTION_ID", long, requires = "corpus_db")] |
| pub execution_id: Option<String>, |
| } |
| |
| /// Strongly-typed domain execution mode for test runs. |
| /// |
| /// This enum represents the parsed high-level user intent derived from `FuzzTestOptions`. |
| #[derive(Debug, Clone, PartialEq, Eq)] |
| pub enum ExecutionMode { |
| /// Smoke test execution (regular unit test fallback). |
| SmokeTest, |
| |
| /// Fuzzing mode. |
| Fuzz(FuzzOptions), |
| |
| /// Replay a specific crash ID from the corpus database. |
| ReplayCrash(ReplayCrashOptions), |
| |
| /// Replay all crashing inputs stored in the corpus database. |
| ReplayAllCrashes, |
| |
| /// Replay corpus inputs for a specified duration. |
| ReplayCorpus(ReplayCorpusOptions), |
| |
| /// List crash IDs stored in the corpus database. |
| ListCrashIds(ListCrashIdsOptions), |
| |
| /// List all discovered fuzz tests without running them. |
| /// Currently only supported via `cargo-fuzztest`. |
| ListFuzzTests, |
| } |
| |
| impl ExecutionMode { |
| /// Evaluates the raw options and maps them to a strongly-typed domain `ExecutionMode`. |
| /// |
| /// This decouples raw environment/CLI option parsing from execution mode validation. |
| pub fn from_fuzztest_options(options: &FuzzTestOptions) -> ExecutionMode { |
| if options.list_crash_ids { |
| return ExecutionMode::ListCrashIds(ListCrashIdsOptions { |
| // TODO(the-shank): If a crash-ids file is not provided, write to stdout. |
| list_crash_ids_file: options |
| .list_crash_ids_file |
| .clone() |
| .expect("list_crash_ids_file should be set when list_crash_ids"), |
| }); |
| } |
| |
| if let Some(replay_corpus_for) = options.replay_corpus_for { |
| return ExecutionMode::ReplayCorpus(ReplayCorpusOptions { |
| replay_corpus_for, |
| time_budget_type: options.time_budget_type, |
| jobs: options.jobs, |
| continue_after_crash: options.continue_after_crash, |
| execution_id: options.execution_id.clone(), |
| }); |
| } |
| |
| if options.replay_findings { |
| return ExecutionMode::ReplayAllCrashes; |
| } |
| |
| if let Some(replay_id) = &options.replay_id { |
| return ExecutionMode::ReplayCrash(ReplayCrashOptions { replay_id: replay_id.clone() }); |
| } |
| |
| // 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, |
| continue_after_crash: options.continue_after_crash, |
| execution_id: options.execution_id.clone(), |
| }); |
| } |
| |
| ExecutionMode::SmokeTest |
| } |
| } |
| |
| /// Mode-specific options for continuous fuzzing. |
| #[derive(Debug, Clone, PartialEq, Eq)] |
| pub struct FuzzOptions { |
| 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<usize>, |
| |
| pub continue_after_crash: bool, |
| |
| pub execution_id: Option<String>, |
| } |
| |
| /// The duration or limit for fuzzing or replaying corpus. |
| #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| pub enum RunDuration { |
| /// Run indefinitely until manually stopped or a crash is found. |
| Indefinitely, |
| |
| /// Run for a specific fixed duration. |
| Fixed(humantime::Duration), |
| } |
| |
| impl FromStr for RunDuration { |
| type Err = anyhow::Error; |
| |
| fn from_str(s: &str) -> anyhow::Result<Self> { |
| 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. |
| #[derive(Debug, Clone, PartialEq, Eq)] |
| pub struct ReplayCrashOptions { |
| pub replay_id: String, |
| } |
| |
| /// Mode-specific options for replaying corpus for a duration. |
| #[derive(Debug, Clone, PartialEq, Eq)] |
| pub struct ReplayCorpusOptions { |
| 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. |
| pub jobs: Option<usize>, |
| pub continue_after_crash: bool, |
| pub execution_id: Option<String>, |
| } |
| |
| /// Mode-specific options for listing crash IDs from the database. |
| #[derive(Debug, Clone, PartialEq, Eq)] |
| pub struct ListCrashIdsOptions { |
| pub list_crash_ids_file: String, |
| } |
| |
| #[doc(hidden)] |
| pub mod env_vars; |
| |
| #[cfg(test)] |
| mod tests { |
| use super::env_vars::*; |
| use super::*; |
| 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<RunDuration, _> = "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() { |
| assert_single_threaded_test_environment(); |
| // SAFETY: Single-threaded test environment and no background threads. |
| let result = unsafe { |
| with_env_vars( |
| [("FUZZTEST_REPLAY_ID", Some("my_crash_123")), ("FUZZTEST_CORPUS_DB", None)], |
| || FuzzTestOptions::try_parse_from(std::iter::empty::<OsString>()), |
| ) |
| }; |
| |
| let err = result.expect_err("parsing should fail when corpus_db is missing"); |
| expect_that!(err.kind(), eq(clap::error::ErrorKind::MissingRequiredArgument)); |
| } |
| |
| #[gtest] |
| fn test_replay_id_with_corpus_db_succeeds() { |
| assert_single_threaded_test_environment(); |
| // SAFETY: Single-threaded test environment and no background threads. |
| let options = unsafe { |
| with_env_vars( |
| [ |
| ("FUZZTEST_REPLAY_ID", Some("my_crash_123")), |
| ("FUZZTEST_CORPUS_DB", Some("/tmp/corpus_db")), |
| ], |
| || FuzzTestOptions::try_parse_from(std::iter::empty::<OsString>()), |
| ) |
| } |
| .expect("parsing should succeed when both replay_id and corpus_db are present"); |
| |
| expect_that!(options.replay_id.as_deref(), eq(Some("my_crash_123"))); |
| expect_that!(options.corpus_db.as_deref(), eq(Some(Path::new("/tmp/corpus_db")))); |
| } |
| |
| #[gtest] |
| fn test_jobs_options_parsing_env() { |
| assert_single_threaded_test_environment(); |
| // SAFETY: Single-threaded test environment and no background threads. |
| let options = unsafe { |
| with_env_var("FUZZTEST_JOBS", "4", || { |
| FuzzTestOptions::parse_from(std::iter::empty::<OsString>()) |
| }) |
| }; |
| |
| 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)); |
| } |
| |
| #[gtest] |
| fn test_jobs_with_fuzz_for_parsing_env() { |
| assert_single_threaded_test_environment(); |
| // SAFETY: Single-threaded test environment and no background threads. |
| let options = unsafe { |
| with_env_vars([("FUZZTEST_JOBS", Some("4")), ("FUZZTEST_FUZZ_FOR", Some("5s"))], || { |
| FuzzTestOptions::parse_from(std::iter::empty::<OsString>()) |
| }) |
| }; |
| |
| expect_that!(options.jobs, eq(Some(4))); |
| let expected_duration = "5s".parse().expect("valid duration"); |
| expect_that!( |
| ExecutionMode::from_fuzztest_options(&options), |
| eq(&ExecutionMode::Fuzz(FuzzOptions { |
| fuzz_for: RunDuration::Fixed(expected_duration), |
| jobs: Some(4), |
| continue_after_crash: false, |
| execution_id: None, |
| })) |
| ); |
| } |
| |
| #[gtest] |
| fn test_jobs_with_replay_corpus_parsing_env() { |
| assert_single_threaded_test_environment(); |
| // SAFETY: Single-threaded test environment and no background threads. |
| let options = unsafe { |
| with_env_vars( |
| [ |
| ("FUZZTEST_JOBS", Some("4")), |
| ("FUZZTEST_REPLAY_CORPUS_FOR", Some("10s")), |
| ("FUZZTEST_CORPUS_DB", Some("/tmp/corpus_db")), |
| ], |
| || FuzzTestOptions::parse_from(std::iter::empty::<OsString>()), |
| ) |
| }; |
| |
| expect_that!(options.jobs, eq(Some(4))); |
| let expected_duration = "10s".parse().expect("valid duration string"); |
| expect_that!( |
| ExecutionMode::from_fuzztest_options(&options), |
| eq(&ExecutionMode::ReplayCorpus(ReplayCorpusOptions { |
| replay_corpus_for: expected_duration, |
| time_budget_type: TimeBudgetType::PerTest, |
| jobs: Some(4), |
| continue_after_crash: false, |
| execution_id: None, |
| })) |
| ); |
| } |
| |
| #[gtest] |
| fn test_continue_after_crash_with_fuzz_for_parsing_env() { |
| assert_single_threaded_test_environment(); |
| // SAFETY: Single-threaded test environment and no background threads. |
| let options = unsafe { |
| with_env_vars( |
| [ |
| ("FUZZTEST_FUZZ_FOR", Some("5s")), |
| ("FUZZTEST_CONTINUE_AFTER_CRASH", Some("true")), |
| ], |
| || FuzzTestOptions::parse_from(std::iter::empty::<OsString>()), |
| ) |
| }; |
| |
| expect_true!(options.continue_after_crash); |
| let expected_duration = "5s".parse().expect("valid duration"); |
| expect_that!( |
| ExecutionMode::from_fuzztest_options(&options), |
| eq(&ExecutionMode::Fuzz(FuzzOptions { |
| fuzz_for: RunDuration::Fixed(expected_duration), |
| jobs: None, |
| continue_after_crash: true, |
| execution_id: None, |
| })) |
| ); |
| } |
| |
| #[gtest] |
| fn test_continue_after_crash_with_replay_corpus_parsing_env() { |
| assert_single_threaded_test_environment(); |
| // SAFETY: Single-threaded test environment and no background threads. |
| let options = unsafe { |
| with_env_vars( |
| [ |
| ("FUZZTEST_REPLAY_CORPUS_FOR", Some("10s")), |
| ("FUZZTEST_CORPUS_DB", Some("/tmp/corpus_db")), |
| ("FUZZTEST_CONTINUE_AFTER_CRASH", Some("true")), |
| ], |
| || FuzzTestOptions::parse_from(std::iter::empty::<OsString>()), |
| ) |
| }; |
| |
| expect_true!(options.continue_after_crash); |
| let expected_duration = "10s".parse().expect("valid duration string"); |
| expect_that!( |
| ExecutionMode::from_fuzztest_options(&options), |
| eq(&ExecutionMode::ReplayCorpus(ReplayCorpusOptions { |
| replay_corpus_for: expected_duration, |
| time_budget_type: TimeBudgetType::PerTest, |
| jobs: None, |
| continue_after_crash: true, |
| execution_id: None, |
| })) |
| ); |
| } |
| |
| #[gtest] |
| fn test_replay_findings_requires_corpus_db() { |
| assert_single_threaded_test_environment(); |
| // SAFETY: Single-threaded test environment and no background threads. |
| let result = unsafe { |
| with_env_vars( |
| [("FUZZTEST_REPLAY_FINDINGS", Some("true")), ("FUZZTEST_CORPUS_DB", None)], |
| || FuzzTestOptions::try_parse_from(std::iter::empty::<OsString>()), |
| ) |
| }; |
| |
| let err = result.expect_err("parsing should fail when corpus_db is missing"); |
| expect_that!(err.kind(), eq(clap::error::ErrorKind::MissingRequiredArgument)); |
| } |
| |
| #[gtest] |
| fn test_replay_findings_with_corpus_db_succeeds() { |
| assert_single_threaded_test_environment(); |
| // SAFETY: Single-threaded test environment and no background threads. |
| let options = unsafe { |
| with_env_vars( |
| [ |
| ("FUZZTEST_REPLAY_FINDINGS", Some("true")), |
| ("FUZZTEST_CORPUS_DB", Some("/tmp/corpus_db")), |
| ], |
| || FuzzTestOptions::try_parse_from(std::iter::empty::<OsString>()), |
| ) |
| } |
| .expect("parsing should succeed when both replay_findings and corpus_db are present"); |
| |
| expect_that!(options.replay_findings, eq(true)); |
| expect_that!(options.corpus_db.as_deref(), eq(Some(Path::new("/tmp/corpus_db")))); |
| } |
| |
| #[gtest] |
| fn test_replay_corpus_for_requires_corpus_db() { |
| assert_single_threaded_test_environment(); |
| // SAFETY: Single-threaded test environment and no background threads. |
| let result = unsafe { |
| with_env_vars( |
| [("FUZZTEST_REPLAY_CORPUS_FOR", Some("10s")), ("FUZZTEST_CORPUS_DB", None)], |
| || FuzzTestOptions::try_parse_from(std::iter::empty::<OsString>()), |
| ) |
| }; |
| |
| let err = result.expect_err("parsing should fail when corpus_db is missing"); |
| expect_that!(err.kind(), eq(clap::error::ErrorKind::MissingRequiredArgument)); |
| } |
| |
| #[gtest] |
| fn test_replay_corpus_for_with_corpus_db_succeeds() { |
| assert_single_threaded_test_environment(); |
| // SAFETY: Single-threaded test environment and no background threads. |
| let options = unsafe { |
| with_env_vars( |
| [ |
| ("FUZZTEST_REPLAY_CORPUS_FOR", Some("10s")), |
| ("FUZZTEST_CORPUS_DB", Some("/tmp/corpus_db")), |
| ], |
| || FuzzTestOptions::try_parse_from(std::iter::empty::<OsString>()), |
| ) |
| } |
| .expect("parsing should succeed when both replay_corpus_for and corpus_db are present"); |
| |
| expect_that!( |
| options.replay_corpus_for, |
| eq(Some("10s".parse().expect("valid duration string"))) |
| ); |
| expect_that!(options.corpus_db.as_deref(), eq(Some(Path::new("/tmp/corpus_db")))); |
| expect_that!(options.time_budget_type, eq(TimeBudgetType::PerTest)); |
| } |
| |
| #[gtest] |
| fn test_replay_corpus_for_inf_env_succeeds() { |
| assert_single_threaded_test_environment(); |
| // SAFETY: Single-threaded test environment and no background threads. |
| let options = unsafe { |
| with_env_vars( |
| [ |
| ("FUZZTEST_REPLAY_CORPUS_FOR", Some("inf")), |
| ("FUZZTEST_CORPUS_DB", Some("/tmp/corpus_db")), |
| ], |
| || FuzzTestOptions::try_parse_from(std::iter::empty::<OsString>()), |
| ) |
| } |
| .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(Path::new("/tmp/corpus_db")))); |
| } |
| |
| #[gtest] |
| fn test_replay_corpus_for_infinity_env_succeeds() { |
| assert_single_threaded_test_environment(); |
| // SAFETY: Single-threaded test environment and no background threads. |
| let options = unsafe { |
| with_env_vars( |
| [ |
| ("FUZZTEST_REPLAY_CORPUS_FOR", Some("infinity")), |
| ("FUZZTEST_CORPUS_DB", Some("/tmp/corpus_db")), |
| ], |
| || FuzzTestOptions::try_parse_from(std::iter::empty::<OsString>()), |
| ) |
| } |
| .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(Path::new("/tmp/corpus_db")))); |
| } |
| |
| #[gtest] |
| fn test_replay_corpus_for_with_total_time_budget() { |
| assert_single_threaded_test_environment(); |
| // SAFETY: Single-threaded test environment and no background threads. |
| let options = unsafe { |
| with_env_vars( |
| [ |
| ("FUZZTEST_REPLAY_CORPUS_FOR", Some("10s")), |
| ("FUZZTEST_TIME_BUDGET_TYPE", Some("total")), |
| ("FUZZTEST_CORPUS_DB", Some("/tmp/corpus_db")), |
| ], |
| || FuzzTestOptions::try_parse_from(std::iter::empty::<OsString>()), |
| ) |
| } |
| .expect("parsing should succeed with total time budget type"); |
| |
| expect_that!( |
| options.replay_corpus_for, |
| eq(Some("10s".parse().expect("valid duration string"))) |
| ); |
| expect_that!(options.corpus_db.as_deref(), eq(Some(Path::new("/tmp/corpus_db")))); |
| expect_that!(options.time_budget_type, eq(TimeBudgetType::Total)); |
| } |
| |
| #[gtest] |
| fn test_replay_corpus_for_inf_with_total_time_budget() { |
| assert_single_threaded_test_environment(); |
| // SAFETY: Single-threaded test environment and no background threads. |
| let options = unsafe { |
| with_env_vars( |
| [ |
| ("FUZZTEST_REPLAY_CORPUS_FOR", Some("inf")), |
| ("FUZZTEST_TIME_BUDGET_TYPE", Some("total")), |
| ("FUZZTEST_CORPUS_DB", Some("/tmp/corpus_db")), |
| ], |
| || FuzzTestOptions::try_parse_from(std::iter::empty::<OsString>()), |
| ) |
| } |
| .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(Path::new("/tmp/corpus_db")))); |
| expect_that!(options.time_budget_type, eq(TimeBudgetType::Total)); |
| } |
| |
| #[gtest] |
| fn test_list_crash_ids_requires_corpus_db() { |
| assert_single_threaded_test_environment(); |
| // SAFETY: Single-threaded test environment and no background threads. |
| let result = unsafe { |
| with_env_vars( |
| [ |
| ("FUZZTEST_LIST_CRASH_IDS", Some("true")), |
| ("FUZZTEST_LIST_CRASH_IDS_FILE", Some("/tmp/crashes.txt")), |
| ("FUZZTEST_CORPUS_DB", None), |
| ], |
| || FuzzTestOptions::try_parse_from(std::iter::empty::<OsString>()), |
| ) |
| }; |
| |
| let err = result.expect_err("parsing should fail when corpus_db is missing"); |
| expect_that!(err.kind(), eq(clap::error::ErrorKind::MissingRequiredArgument)); |
| } |
| |
| #[gtest] |
| fn test_list_crash_ids_requires_list_crash_ids_file() { |
| assert_single_threaded_test_environment(); |
| // SAFETY: Single-threaded test environment and no background threads. |
| let result = unsafe { |
| with_env_vars( |
| [ |
| ("FUZZTEST_LIST_CRASH_IDS", Some("true")), |
| ("FUZZTEST_CORPUS_DB", Some("/tmp/corpus_db")), |
| ("FUZZTEST_LIST_CRASH_IDS_FILE", None), |
| ], |
| || FuzzTestOptions::try_parse_from(std::iter::empty::<OsString>()), |
| ) |
| }; |
| |
| let err = result.expect_err("parsing should fail when list_crash_ids_file is missing"); |
| expect_that!(err.kind(), eq(clap::error::ErrorKind::MissingRequiredArgument)); |
| } |
| |
| #[gtest] |
| fn test_list_crash_ids_file_requires_list_crash_ids() { |
| assert_single_threaded_test_environment(); |
| // SAFETY: Single-threaded test environment and no background threads. |
| let result = unsafe { |
| with_env_vars( |
| [ |
| ("FUZZTEST_LIST_CRASH_IDS_FILE", Some("/tmp/crashes.txt")), |
| ("FUZZTEST_CORPUS_DB", Some("/tmp/corpus_db")), |
| ("FUZZTEST_LIST_CRASH_IDS", None), |
| ], |
| || FuzzTestOptions::try_parse_from(std::iter::empty::<OsString>()), |
| ) |
| }; |
| |
| let err = result.expect_err("parsing should fail when list_crash_ids is missing"); |
| expect_that!(err.kind(), eq(clap::error::ErrorKind::MissingRequiredArgument)); |
| } |
| |
| #[gtest] |
| fn test_list_crash_ids_with_corpus_db_and_file_succeeds() { |
| assert_single_threaded_test_environment(); |
| // SAFETY: Single-threaded test environment and no background threads. |
| let options = unsafe { |
| with_env_vars( |
| [ |
| ("FUZZTEST_LIST_CRASH_IDS", Some("true")), |
| ("FUZZTEST_LIST_CRASH_IDS_FILE", Some("/tmp/crashes.txt")), |
| ("FUZZTEST_CORPUS_DB", Some("/tmp/corpus_db")), |
| ], |
| || FuzzTestOptions::try_parse_from(std::iter::empty::<OsString>()), |
| ) |
| } |
| .expect( |
| "parsing should succeed when list_crash_ids, list_crash_ids_file, and corpus_db are \ |
| present", |
| ); |
| |
| expect_that!(options.list_crash_ids, eq(true)); |
| expect_that!(options.list_crash_ids_file.as_deref(), eq(Some("/tmp/crashes.txt"))); |
| expect_that!(options.corpus_db.as_deref(), eq(Some(Path::new("/tmp/corpus_db")))); |
| |
| let mode = ExecutionMode::from_fuzztest_options(&options); |
| expect_that!( |
| mode, |
| eq(&ExecutionMode::ListCrashIds(ListCrashIdsOptions { |
| list_crash_ids_file: "/tmp/crashes.txt".to_string() |
| })) |
| ); |
| } |
| |
| #[gtest] |
| fn test_execution_id_requires_corpus_db() { |
| assert_single_threaded_test_environment(); |
| // SAFETY: Single-threaded test environment and no background threads. |
| let result = unsafe { |
| with_env_vars( |
| [("FUZZTEST_EXECUTION_ID", Some("exec_123")), ("FUZZTEST_CORPUS_DB", None)], |
| || FuzzTestOptions::try_parse_from(std::iter::empty::<OsString>()), |
| ) |
| }; |
| |
| let err = |
| result.expect_err("parsing should fail when corpus_db is missing for execution_id"); |
| expect_that!(err.kind(), eq(clap::error::ErrorKind::MissingRequiredArgument)); |
| } |
| |
| #[gtest] |
| fn test_execution_id_with_corpus_db_and_fuzz_for_succeeds() { |
| assert_single_threaded_test_environment(); |
| // SAFETY: Single-threaded test environment and no background threads. |
| let options = unsafe { |
| with_env_vars( |
| [ |
| ("FUZZTEST_EXECUTION_ID", Some("exec_123")), |
| ("FUZZTEST_CORPUS_DB", Some("/tmp/corpus_db")), |
| ("FUZZTEST_FUZZ_FOR", Some("5s")), |
| ], |
| || FuzzTestOptions::try_parse_from(std::iter::empty::<OsString>()), |
| ) |
| } |
| .expect("parsing should succeed when execution_id, corpus_db, and fuzz_for are present"); |
| |
| expect_that!(options.execution_id.as_deref(), eq(Some("exec_123"))); |
| expect_that!(options.corpus_db.as_deref(), eq(Some(Path::new("/tmp/corpus_db")))); |
| |
| let expected_duration = "5s".parse().expect("valid duration"); |
| expect_that!( |
| ExecutionMode::from_fuzztest_options(&options), |
| eq(&ExecutionMode::Fuzz(FuzzOptions { |
| fuzz_for: RunDuration::Fixed(expected_duration), |
| jobs: None, |
| continue_after_crash: false, |
| execution_id: Some("exec_123".to_string()), |
| })) |
| ); |
| } |
| |
| #[gtest] |
| fn test_execution_id_with_corpus_db_and_replay_corpus_succeeds() { |
| assert_single_threaded_test_environment(); |
| // SAFETY: Single-threaded test environment and no background threads. |
| let options = unsafe { |
| with_env_vars( |
| [ |
| ("FUZZTEST_EXECUTION_ID", Some("exec_123")), |
| ("FUZZTEST_CORPUS_DB", Some("/tmp/corpus_db")), |
| ("FUZZTEST_REPLAY_CORPUS_FOR", Some("10s")), |
| ], |
| || FuzzTestOptions::try_parse_from(std::iter::empty::<OsString>()), |
| ) |
| } |
| .expect( |
| "parsing should succeed when execution_id, corpus_db, and replay_corpus_for are \ |
| present", |
| ); |
| |
| expect_that!(options.execution_id.as_deref(), eq(Some("exec_123"))); |
| expect_that!(options.corpus_db.as_deref(), eq(Some(Path::new("/tmp/corpus_db")))); |
| |
| let expected_duration = "10s".parse().expect("valid duration string"); |
| expect_that!( |
| ExecutionMode::from_fuzztest_options(&options), |
| eq(&ExecutionMode::ReplayCorpus(ReplayCorpusOptions { |
| replay_corpus_for: expected_duration, |
| time_budget_type: TimeBudgetType::PerTest, |
| jobs: None, |
| continue_after_crash: false, |
| execution_id: Some("exec_123".to_string()), |
| })) |
| ); |
| } |
| } |