blob: d84bb4a36d9642c8afba3660f054eb8c2989b8e8 [file] [edit]
// 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()),
}))
);
}
}