blob: ecb497a2e1ee117f51f102b3eba9b94661c37454 [file]
// This module provides the core command-line flag and environment variable options
// structure (`FuzzTestOptions`) and domain execution modes (`ExecutionMode`).
// Both the test harness runtime and host tools (e.g. `cargo-fuzztest`) can share option parsing
// logic.
use clap::{Parser, ValueEnum};
use humantime::Duration;
/// Time budget calculation type for replay corpus mode.
#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TimeBudgetType {
#[default]
PerTest,
Total,
}
/// Parses a fuzzing duration string from `FUZZTEST_FUZZ_FOR`.
///
/// Matches `"inf"` or `"infinite"` 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<FuzzFor> {
let s_lower = s.trim().to_lowercase();
if s_lower == "inf" || s_lower == "infinite" {
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 {
/// 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` / `infinite`
/// 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<FuzzFor>,
/// 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>,
/// 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)]
pub replay_findings: bool,
/// Replay the corpus for a specified duration.
#[arg(env = "FUZZTEST_REPLAY_CORPUS_FOR", long)]
pub replay_corpus_for: Option<Duration>,
/// 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)]
pub corpus_db: 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),
}
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 let Some(replay_corpus_for) = options.replay_corpus_for {
return ExecutionMode::ReplayCorpus(ReplayCorpusOptions {
replay_corpus_for,
time_budget_type: options.time_budget_type,
});
}
if options.replay_findings {
return ExecutionMode::ReplayAllCrashes;
}
if let Some(replay_id) = &options.replay_id {
return ExecutionMode::ReplayCrash(ReplayCrashOptions { replay_id: replay_id.clone() });
}
if let Some(fuzz_for) = options.fuzz_for {
return ExecutionMode::Fuzz(FuzzOptions { fuzz_for, jobs: options.jobs.clone() });
}
ExecutionMode::SmokeTest
}
}
/// Mode-specific options for continuous fuzzing.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FuzzOptions {
pub fuzz_for: FuzzFor,
/// 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>,
}
/// The duration or limit for fuzzing.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FuzzFor {
/// Fuzz indefinitely until it is manually stopped or a crash is found.
Indefinitely,
/// Fuzz for a specific duration.
Duration(Duration),
}
/// Mode-specific options for replaying a specific database crash.
#[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: Duration,
pub time_budget_type: TimeBudgetType,
}
#[cfg(test)]
mod tests {
use super::*;
use googletest::prelude::*;
use std::ffi::OsString;
#[gtest]
fn test_replay_id_requires_corpus_db() {
// SAFETY: Testing environment parsing in single-threaded context.
unsafe {
std::env::set_var("FUZZTEST_REPLAY_ID", "my_crash_123");
std::env::remove_var("FUZZTEST_CORPUS_DB");
}
let result = FuzzTestOptions::try_parse_from(std::iter::empty::<OsString>());
// SAFETY: Cleaning up environment variables.
unsafe {
std::env::remove_var("FUZZTEST_REPLAY_ID");
}
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() {
// SAFETY: Testing environment parsing in single-threaded context.
unsafe {
std::env::set_var("FUZZTEST_REPLAY_ID", "my_crash_123");
std::env::set_var("FUZZTEST_CORPUS_DB", "/tmp/corpus_db");
}
let result = FuzzTestOptions::try_parse_from(std::iter::empty::<OsString>());
// SAFETY: Cleaning up environment variables.
unsafe {
std::env::remove_var("FUZZTEST_REPLAY_ID");
std::env::remove_var("FUZZTEST_CORPUS_DB");
}
let options =
result.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("/tmp/corpus_db")));
}
}