| // 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>, | |
| /// If true, carries out fuzzing without coverage guidance. | |
| #[arg(env = "FUZZTEST_UNGUIDED", long)] | |
| pub unguided: bool, | |
| } | |
| /// 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()), | |
| })) | |
| ); | |
| } | |
| #[gtest] | |
| fn test_unguided_parsing_env() { | |
| assert_single_threaded_test_environment(); | |
| // SAFETY: Single-threaded test environment and no background threads. | |
| let options = unsafe { | |
| with_env_var("FUZZTEST_UNGUIDED", "true", || { | |
| FuzzTestOptions::parse_from(std::iter::empty::<OsString>()) | |
| }) | |
| }; | |
| expect_true!(options.unguided); | |
| } | |
| } |