From bce2c8d07bd35b2609aef475b2c7279f8ffa9f73 Mon Sep 17 00:00:00 2001 From: Shashank Sharma Date: Mon, 17 Aug 2026 11:09:56 -0700 Subject: [PATCH] fuzztest-rust | Add support for listing crash IDs from the corpus database Support listing crash IDs stored in the corpus database in fuzztest-rs and cargo-fuzztest. - Add --list-crash-ids and --list-crash-ids-file options to fuzztest-options, requiring corpus-db and each other. - Add ExecutionMode::ListCrashIds and forward --list_crash_ids=true and --list_crash_ids_file= to Centipede. - Support --list-crash-ids and --list-crash-ids-file in cargo-fuzztest CLI and runner. - Update replay integration tests and cargo-fuzztest end-to-end tests to use the new options. PiperOrigin-RevId: 966073502 --- rust/cargo_fuzztest/src/lib.rs | 113 +++++++++++++++++- rust/cargo_fuzztest/tests/e2e_cli_test.rs | 139 ++++++++++++---------- rust/cargo_fuzztest/tests/runner_test.rs | 69 +++++++++++ rust/e2e_tests/replay_test.rs | 25 ++-- rust/options/src/lib.rs | 134 +++++++++++++++++++++ rust/src/options.rs | 35 +++++- 6 files changed, 431 insertions(+), 84 deletions(-) diff --git a/rust/cargo_fuzztest/src/lib.rs b/rust/cargo_fuzztest/src/lib.rs index 241bf1fd3..b7c57ebc2 100644 --- a/rust/cargo_fuzztest/src/lib.rs +++ b/rust/cargo_fuzztest/src/lib.rs @@ -18,8 +18,8 @@ use anyhow::{Context, Result}; use clap::Parser; pub use fuzztest_options::{ - ExecutionMode, FuzzFor, FuzzOptions, FuzzTestOptions, ReplayCorpusOptions, ReplayCrashOptions, - TimeBudgetType, + ExecutionMode, FuzzFor, FuzzOptions, FuzzTestOptions, ListCrashIdsOptions, ReplayCorpusOptions, + ReplayCrashOptions, TimeBudgetType, }; use std::env; use std::ffi::OsString; @@ -85,7 +85,8 @@ impl CargoFuzzTestOptions { } ExecutionMode::ReplayCorpus(_) | ExecutionMode::ReplayAllCrashes - | ExecutionMode::ReplayCrash(_) => { + | ExecutionMode::ReplayCrash(_) + | ExecutionMode::ListCrashIds(_) => { self.check_centipede_binary_path_is_set()?; self.check_corpus_db_is_set()?; mode @@ -278,6 +279,14 @@ impl FuzztestRunner { cmd.env("FUZZTEST_TIME_BUDGET_TYPE", time_budget_str); } + ExecutionMode::ListCrashIds(list_crash_ids_options) => { + cmd.env("FUZZTEST_LIST_CRASH_IDS", "true"); + cmd.env( + "FUZZTEST_LIST_CRASH_IDS_FILE", + &list_crash_ids_options.list_crash_ids_file, + ); + } + ExecutionMode::SmokeTest => { // nothing to be done } @@ -687,4 +696,102 @@ mod tests { Some("/custom/centipede".to_string()) ))); } + + #[gtest] + fn test_cli_option_parsing_list_crash_ids_success() { + let parsed = CargoFuzzTestOptions::try_parse_from([ + "cargo-fuzztest", + "--list-crash-ids", + "--list-crash-ids-file", + "/tmp/crashes.txt", + "--corpus-db", + "/tmp/corpus_db", + "--centipede-binary-path", + "/custom/centipede", + ]) + .expect("valid list-crash-ids options should parse successfully"); + + assert!(parsed.fuzztest_options.list_crash_ids); + assert_eq!( + parsed.fuzztest_options.list_crash_ids_file.as_deref(), + Some("/tmp/crashes.txt") + ); + assert_eq!(parsed.fuzztest_options.corpus_db.as_deref(), Some("/tmp/corpus_db")); + + let mode = parsed.execution_mode().expect("valid execution mode"); + assert_eq!( + mode, + ExecutionMode::ListCrashIds(ListCrashIdsOptions { + list_crash_ids_file: "/tmp/crashes.txt".to_string(), + }) + ); + } + + #[gtest] + fn test_execution_mode_list_crash_ids_missing_corpus_db_errors() { + let options = CargoFuzzTestOptions { + fuzztest_options: FuzzTestOptions { + list_crash_ids: true, + list_crash_ids_file: Some("/tmp/crashes.txt".to_string()), + ..Default::default() + }, + centipede_binary_path: Some("/custom/centipede".to_string()), + ..Default::default() + }; + let err = options.execution_mode().expect_err("missing corpus-db should cause error"); + assert!(err.to_string().contains("`--corpus-db` needs to be specified")); + } + + #[gtest] + fn test_execution_mode_list_crash_ids_missing_centipede_binary_path_errors() { + let options = CargoFuzzTestOptions { + fuzztest_options: FuzzTestOptions { + list_crash_ids: true, + list_crash_ids_file: Some("/tmp/crashes.txt".to_string()), + corpus_db: Some("/tmp/corpus_db".to_string()), + ..Default::default() + }, + ..Default::default() + }; + let err = + options.execution_mode().expect_err("missing centipede-binary-path should cause error"); + assert!(err.to_string().contains("`--centipede-binary-path` needs to be specified")); + } + + #[gtest] + fn test_build_run_command_list_crash_ids() { + let options = CargoFuzzTestOptions { + fuzztest_options: FuzzTestOptions { + list_crash_ids: true, + list_crash_ids_file: Some("/tmp/crashes.txt".to_string()), + corpus_db: Some("/tmp/corpus_db".to_string()), + ..Default::default() + }, + centipede_binary_path: Some("/custom/centipede".to_string()), + ..Default::default() + }; + let runner = FuzztestRunner::new("x86_64-unknown-linux-gnu".to_string(), options); + let cmd = + runner.build_run_command(Path::new("/tmp/test_bin")).expect("should build run command"); + + let envs: Vec<(String, Option)> = cmd + .get_envs() + .map(|(k, v)| { + (k.to_string_lossy().to_string(), v.map(|s| s.to_string_lossy().to_string())) + }) + .collect(); + + assert!(envs.contains(&("FUZZTEST_LIST_CRASH_IDS".to_string(), Some("true".to_string())))); + assert!(envs.contains(&( + "FUZZTEST_LIST_CRASH_IDS_FILE".to_string(), + Some("/tmp/crashes.txt".to_string()) + ))); + assert!( + envs.contains(&("FUZZTEST_CORPUS_DB".to_string(), Some("/tmp/corpus_db".to_string()))) + ); + assert!(envs.contains(&( + "FUZZTEST_CENTIPEDE_BINARY_PATH".to_string(), + Some("/custom/centipede".to_string()) + ))); + } } diff --git a/rust/cargo_fuzztest/tests/e2e_cli_test.rs b/rust/cargo_fuzztest/tests/e2e_cli_test.rs index fa73f9944..b77256f6a 100644 --- a/rust/cargo_fuzztest/tests/e2e_cli_test.rs +++ b/rust/cargo_fuzztest/tests/e2e_cli_test.rs @@ -144,9 +144,6 @@ fn test_cargo_fuzztest_e2e_parallel_jobs() { #[gtest] fn test_cargo_fuzztest_e2e_replay_by_id() { - let centipede_bin = env::var("FUZZTEST_CENTIPEDE_BINARY_PATH") - .expect("FUZZTEST_CENTIPEDE_BINARY_PATH needs to be set for the test"); - let sample_crate_path = get_sample_crate_path("another_sample_fuzz_crate"); let temp_target_dir = TempDir::new().expect("Failed to create temporary target directory"); @@ -159,30 +156,12 @@ fn test_cargo_fuzztest_e2e_replay_by_id() { TempDir::new().expect("Failed to create temporary workdir_root directory"); fs::create_dir_all(&workdir_root_dir).expect("Failed to workdir_root directory"); - let test_target = "__fuzztest_mod__crashing_fuzztest_target::crashing_fuzztest_target"; - let normalized_test_name = test_target.replace("::", "."); - - // 1. Retrieve the target binary path. - let host_triple = cargo_fuzztest::get_host_target_triple() - .expect("Failed to get host target triple for compilation"); - let runner = cargo_fuzztest::FuzztestRunner::new( - host_triple, - cargo_fuzztest::CargoFuzzTestOptions::default(), - ); - let mut compile_cmd = runner.build_compile_command(); - compile_cmd.current_dir(&sample_crate_path).env("CARGO_TARGET_DIR", temp_target_dir.path()); - - let compile_output = compile_cmd.output().expect("Failed to execute cargo compilation command"); - assert!(compile_output.status.success()); - let json_stdout = String::from_utf8(compile_output.stdout) - .expect("Cargo compilation stdout must be valid UTF-8"); - let target_binary_path = cargo_fuzztest::FuzztestRunner::parse_compiler_messages(&json_stdout) - .expect("Failed to parse target binary path from cargo JSON output"); + let centipede_bin = env::var("FUZZTEST_CENTIPEDE_BINARY_PATH") + .expect("FUZZTEST_CENTIPEDE_BINARY_PATH needs to be set for the test"); - let target_binary_str = target_binary_path.to_str().expect("Valid binary path string"); - let binary_id = target_binary_str.strip_prefix('/').unwrap_or(target_binary_str); + let test_target = "__fuzztest_mod__crashing_fuzztest_target::crashing_fuzztest_target"; - // 2. Run Centipede to fuzz the target and populate the corpus database. + // 1. Run Centipede via cargo-fuzztest to fuzz the target and populate the corpus database. let mut cmd = setup_cargo_fuzztest_command(&sample_crate_path, temp_target_dir.path()); cmd.arg(test_target) .arg("--fuzz-for=5s") @@ -197,20 +176,22 @@ fn test_cargo_fuzztest_e2e_replay_by_id() { let output = cmd.output().expect("Failed to run cargo-fuzztest to fuzz target"); assert!(output.status.success()); - // 3. Get list of crash ids + // 2. Get list of crash ids via cargo-fuzztest --list-crash-ids let list_temp_dir = TempDir::new().expect("Failed to create temporary list directory"); let crash_ids_file = list_temp_dir.path().join("crash_ids.txt"); - let list_args = [ - format!("--binary={}", target_binary_path.display()), - format!("--fuzztest_binary_identifier={}", binary_id), - format!("--test_name={}", normalized_test_name), - format!("--fuzztest_corpus_database={}", temp_db_dir.path().display()), - "--list_crash_ids=1".to_string(), - format!("--list_crash_ids_file={}", crash_ids_file.display()), - ]; - let list_args_refs: Vec<&str> = list_args.iter().map(|s| s.as_str()).collect(); - run_centipede_with_args_expect_termination(¢ipede_bin, &list_args_refs); + let mut cmd = setup_cargo_fuzztest_command(&sample_crate_path, temp_target_dir.path()); + cmd.arg(test_target) + .arg("--list-crash-ids") + .arg("--list-crash-ids-file") + .arg(&crash_ids_file) + .arg("--corpus-db") + .arg(temp_db_dir.path()) + .arg("--centipede-binary-path") + .arg(¢ipede_bin); + + let output = cmd.output().expect("Failed to run cargo-fuzztest to list crash IDs"); + assert!(output.status.success()); let crash_ids_contents = fs::read_to_string(&crash_ids_file).expect("Failed to read crash IDs file"); @@ -220,7 +201,7 @@ fn test_cargo_fuzztest_e2e_replay_by_id() { expect_true!(!crash_ids.is_empty()); let crash_id = crash_ids[0]; - // 4. Run cargo-fuzztest CLI with --replay-id to verify it replays the crash. + // 3. Run cargo-fuzztest CLI with --replay-id to verify it replays the crash. let mut cmd = setup_cargo_fuzztest_command(&sample_crate_path, temp_target_dir.path()); cmd.arg(test_target) .arg("--replay-id") @@ -405,32 +386,62 @@ fn test_cargo_fuzztest_e2e_replay_corpus_total_budget() { ); } -fn run_centipede_with_args_expect_termination(centipede_bin: &str, args: &[&str]) -> String { - // Disable interference from Bazel environment variables. - let env_diff = [ - "-TEST_DIAGNOSTICS_OUTPUT_DIR", - "-TEST_INFRASTRUCTURE_FAILURE_FILE", - "-TEST_LOGSPLITTER_OUTPUT_FILE", - "-TEST_PREMATURE_EXIT_FILE", - "-TEST_RANDOM_SEED", - "-TEST_RUN_NUMBER", - "-TEST_SHARD_INDEX", - "-TEST_SHARD_STATUS_FILE", - "-TEST_TOTAL_SHARDS", - "-TEST_UNDECLARED_OUTPUTS_ANNOTATIONS_DIR", - "-TEST_UNDECLARED_OUTPUTS_DIR", - "-TEST_WARNINGS_OUTPUT_FILE", - "-GTEST_OUTPUT", - "-XML_OUTPUT_FILE", - ]; - let process = Command::new(centipede_bin) - .arg("--populate_binary_info=0") - .arg("--fork_server=0") - .arg("--persistent_mode=0") - .arg(format!("--env_diff_for_binaries={}", env_diff.join(","))) - .args(args) - .output() - .expect("Centipede should have executed"); +#[gtest] +fn test_cargo_fuzztest_e2e_list_crash_ids() { + let centipede_bin = env::var("FUZZTEST_CENTIPEDE_BINARY_PATH") + .expect("FUZZTEST_CENTIPEDE_BINARY_PATH needs to be provided"); + + let sample_crate_path = get_sample_crate_path("another_sample_fuzz_crate"); + + let temp_target_dir = TempDir::new().expect("Failed to create temporary target directory"); + fs::create_dir_all(&temp_target_dir).expect("Failed to create target directory"); + + let temp_db_dir = TempDir::new().expect("Failed to create temporary corpus db directory"); + fs::create_dir_all(&temp_db_dir).expect("Failed to create corpus db directory"); + + let workdir_root_dir = + TempDir::new().expect("Failed to create temporary workdir_root directory"); + fs::create_dir_all(&workdir_root_dir).expect("Failed to create workdir_root directory"); - String::from_utf8_lossy(&process.stderr).to_string() + let test_target = "__fuzztest_mod__crashing_fuzztest_target::crashing_fuzztest_target"; + + // 1. Run Centipede via cargo-fuzztest to fuzz the target and populate the corpus database. + let mut cmd = setup_cargo_fuzztest_command(&sample_crate_path, temp_target_dir.path()); + cmd.arg(test_target) + .arg("--fuzz-for=5s") + .env_remove("CENTIPEDE_BINARY_PATH") + .arg("--centipede-binary-path") + .arg(¢ipede_bin) + .arg("--corpus-db") + .arg(temp_db_dir.path()) + .arg("--workdir-root") + .arg(workdir_root_dir.path()); + let output = cmd.output().expect("Failed to run cargo-fuzztest to fuzz target"); + assert!(output.status.success()); + + // 2. Run cargo-fuzztest CLI with --list-crash-ids and --list-crash-ids-file to list crashes from corpus db. + let list_temp_dir = TempDir::new().expect("Failed to create temporary list directory"); + let crash_ids_file = list_temp_dir.path().join("crash_ids.txt"); + + let mut cmd = setup_cargo_fuzztest_command(&sample_crate_path, temp_target_dir.path()); + cmd.arg(test_target) + .arg("--list-crash-ids") + .arg("--list-crash-ids-file") + .arg(&crash_ids_file) + .arg("--corpus-db") + .arg(temp_db_dir.path()) + .arg("--centipede-binary-path") + .arg(¢ipede_bin); + + let output = cmd.output().expect("Failed to run cargo-fuzztest in list-crash-ids mode"); + assert!(output.status.success()); + + let crash_ids_contents = + fs::read_to_string(&crash_ids_file).expect("Failed to read crash IDs file"); + let crash_ids: Vec<&str> = + crash_ids_contents.lines().filter(|line| !line.trim().is_empty()).collect(); + + // DISCUSS: We probably should be checking for something more specific, but what would be a + // good check here? + expect_true!(!crash_ids.is_empty()); } diff --git a/rust/cargo_fuzztest/tests/runner_test.rs b/rust/cargo_fuzztest/tests/runner_test.rs index 57aaba889..b865b1d4d 100644 --- a/rust/cargo_fuzztest/tests/runner_test.rs +++ b/rust/cargo_fuzztest/tests/runner_test.rs @@ -317,3 +317,72 @@ fn test_runner_list_command() { let args: Vec = cmd.get_args().map(|a| a.to_string_lossy().to_string()).collect(); expect_eq!(args, &["__fuzztest_mod__", "--list"]); } + +#[gtest] +fn test_runner_build_run_command_with_list_crash_ids() { + let binary_path = get_sample_test_bin_path("sample_fuzz_crate"); + let fuzztest_options = FuzzTestOptions { + list_crash_ids: true, + list_crash_ids_file: Some("/custom/path/to/crash_ids.txt".to_string()), + corpus_db: Some("/custom/path/to/corpus_db".to_string()), + ..Default::default() + }; + let options = CargoFuzzTestOptions { + fuzztest_options, + centipede_binary_path: Some("/custom/path/to/centipede".to_string()), + ..Default::default() + }; + let runner = FuzztestRunner::new("x86_64-unknown-linux-gnu".to_string(), options); + let cmd = runner.build_run_command(&binary_path).expect("valid run command"); + + let envs: Vec<(String, Option)> = cmd + .get_envs() + .map(|(k, v)| (k.to_string_lossy().to_string(), v.map(|s| s.to_string_lossy().to_string()))) + .collect(); + expect_true!(envs.contains(&("FUZZTEST_LIST_CRASH_IDS".to_string(), Some("true".to_string())))); + expect_true!(envs.contains(&( + "FUZZTEST_LIST_CRASH_IDS_FILE".to_string(), + Some("/custom/path/to/crash_ids.txt".to_string()) + ))); + expect_true!(envs.contains(&( + "FUZZTEST_CORPUS_DB".to_string(), + Some("/custom/path/to/corpus_db".to_string()) + ))); + expect_true!(envs.contains(&( + "FUZZTEST_CENTIPEDE_BINARY_PATH".to_string(), + Some("/custom/path/to/centipede".to_string()) + ))); +} + +#[gtest] +fn test_execution_mode_list_crash_ids_missing_corpus_db_errors() { + let fuzztest_options = FuzzTestOptions { + list_crash_ids: true, + list_crash_ids_file: Some("/custom/path/to/crash_ids.txt".to_string()), + ..Default::default() + }; + let options = CargoFuzzTestOptions { + fuzztest_options, + centipede_binary_path: Some("/custom/path/to/centipede".to_string()), + ..Default::default() + }; + let result = options.execution_mode(); + expect_true!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + expect_true!(err_msg.contains("`--corpus-db` needs to be specified")); +} + +#[gtest] +fn test_execution_mode_list_crash_ids_missing_centipede_binary_path_errors() { + let fuzztest_options = FuzzTestOptions { + list_crash_ids: true, + list_crash_ids_file: Some("/custom/path/to/crash_ids.txt".to_string()), + corpus_db: Some("/custom/path/to/corpus_db".to_string()), + ..Default::default() + }; + let options = CargoFuzzTestOptions { fuzztest_options, ..Default::default() }; + let result = options.execution_mode(); + expect_true!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + expect_true!(err_msg.contains("`--centipede-binary-path` needs to be specified")); +} diff --git a/rust/e2e_tests/replay_test.rs b/rust/e2e_tests/replay_test.rs index 767038afd..d14d7722c 100644 --- a/rust/e2e_tests/replay_test.rs +++ b/rust/e2e_tests/replay_test.rs @@ -33,11 +33,8 @@ fn get_target_binary_path(fixture: &EnvVars) -> PathBuf { #[gtest] fn replay_by_id_reproduces_panic(fixture: &EnvVars) { let test_name = "__fuzztest_mod__find_two_bugs_fuzz_test::find_two_bugs_fuzz_test"; - let normalized_test_name = test_name.replace("::", "."); let target_binary_path = get_target_binary_path(fixture); - let target_binary_str = target_binary_path.to_str().expect("valid path string"); - let binary_id = target_binary_str.strip_prefix('/').unwrap_or(target_binary_str); let db_dir = fixture.tmp_dir_path.join("replay_by_id_reproduces_panic").join("corpus_db"); fs::create_dir_all(&db_dir).expect("Failed to create db directory"); @@ -57,19 +54,19 @@ fn replay_by_id_reproduces_panic(fixture: &EnvVars) { .status() .expect("Failed to spawn binary"); - // 2. Retrieve the crash IDs from the corpus database using Centipede's --list_crash_ids flag. + // 2. Retrieve the crash IDs from the corpus database using FUZZTEST_LIST_CRASH_IDS. let crash_ids_file = fixture.tmp_dir_path.join("replay_by_id_reproduces_panic").join("crash_ids.txt"); - let list_args = [ - format!("--binary={}", target_binary_path.display()), - format!("--fuzztest_binary_identifier={}", binary_id), - format!("--test_name={}", normalized_test_name), - format!("--fuzztest_corpus_database={}", db_dir.display()), - "--list_crash_ids=1".to_string(), - format!("--list_crash_ids_file={}", crash_ids_file.display()), - ]; - let list_args_refs: Vec<&str> = list_args.iter().map(|s| s.as_str()).collect(); - test_utils::run_centipede_with_args_expect_termination(fixture, &list_args_refs); + let status = Command::new(&target_binary_path) + .arg(test_name) + .arg("--exact") + .env("FUZZTEST_LIST_CRASH_IDS", "true") + .env("FUZZTEST_LIST_CRASH_IDS_FILE", &crash_ids_file) + .env("FUZZTEST_CORPUS_DB", &db_dir) + .env("FUZZTEST_CENTIPEDE_BINARY_PATH", &fixture.centipede_path) + .status() + .expect("Failed to execute target binary to list crash IDs"); + expect_true!(status.success()); let crash_ids_contents = fs::read_to_string(&crash_ids_file).expect("Failed to read crash IDs file"); diff --git a/rust/options/src/lib.rs b/rust/options/src/lib.rs index d072ceff3..070b00dae 100644 --- a/rust/options/src/lib.rs +++ b/rust/options/src/lib.rs @@ -63,6 +63,24 @@ pub struct FuzzTestOptions { #[arg(env = "FUZZTEST_JOBS", long)] pub jobs: Option, + /// 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, + /// The crash ID to be replayed from the corpus database. /// /// If set, `corpus_db` must also be specified. This mode retrieves the crashing input @@ -110,6 +128,9 @@ pub enum ExecutionMode { /// 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, @@ -120,6 +141,16 @@ impl 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, @@ -184,6 +215,12 @@ pub struct ReplayCorpusOptions { pub jobs: Option, } +/// Mode-specific options for listing crash IDs from the database. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ListCrashIdsOptions { + pub list_crash_ids_file: String, +} + #[cfg(test)] mod tests { use super::*; @@ -413,4 +450,101 @@ mod tests { expect_that!(options.corpus_db.as_deref(), eq(Some("/tmp/corpus_db"))); expect_that!(options.time_budget_type, eq(TimeBudgetType::Total)); } + + #[gtest] + fn test_list_crash_ids_requires_corpus_db() { + // SAFETY: Testing environment parsing in single-threaded context. + unsafe { + std::env::set_var("FUZZTEST_LIST_CRASH_IDS", "true"); + std::env::set_var("FUZZTEST_LIST_CRASH_IDS_FILE", "/tmp/crashes.txt"); + std::env::remove_var("FUZZTEST_CORPUS_DB"); + } + + let result = FuzzTestOptions::try_parse_from(std::iter::empty::()); + + // SAFETY: Cleaning up environment variables. + unsafe { + std::env::remove_var("FUZZTEST_LIST_CRASH_IDS"); + std::env::remove_var("FUZZTEST_LIST_CRASH_IDS_FILE"); + } + + 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() { + // SAFETY: Testing environment parsing in single-threaded context. + unsafe { + std::env::set_var("FUZZTEST_LIST_CRASH_IDS", "true"); + std::env::set_var("FUZZTEST_CORPUS_DB", "/tmp/corpus_db"); + std::env::remove_var("FUZZTEST_LIST_CRASH_IDS_FILE"); + } + + let result = FuzzTestOptions::try_parse_from(std::iter::empty::()); + + // SAFETY: Cleaning up environment variables. + unsafe { + std::env::remove_var("FUZZTEST_LIST_CRASH_IDS"); + std::env::remove_var("FUZZTEST_CORPUS_DB"); + } + + 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() { + // SAFETY: Testing environment parsing in single-threaded context. + unsafe { + std::env::set_var("FUZZTEST_LIST_CRASH_IDS_FILE", "/tmp/crashes.txt"); + std::env::set_var("FUZZTEST_CORPUS_DB", "/tmp/corpus_db"); + std::env::remove_var("FUZZTEST_LIST_CRASH_IDS"); + } + + let result = FuzzTestOptions::try_parse_from(std::iter::empty::()); + + // SAFETY: Cleaning up environment variables. + unsafe { + std::env::remove_var("FUZZTEST_LIST_CRASH_IDS_FILE"); + std::env::remove_var("FUZZTEST_CORPUS_DB"); + } + + 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() { + // SAFETY: Testing environment parsing in single-threaded context. + unsafe { + std::env::set_var("FUZZTEST_LIST_CRASH_IDS", "true"); + std::env::set_var("FUZZTEST_LIST_CRASH_IDS_FILE", "/tmp/crashes.txt"); + std::env::set_var("FUZZTEST_CORPUS_DB", "/tmp/corpus_db"); + } + + let result = FuzzTestOptions::try_parse_from(std::iter::empty::()); + + // SAFETY: Cleaning up environment variables. + unsafe { + std::env::remove_var("FUZZTEST_LIST_CRASH_IDS"); + std::env::remove_var("FUZZTEST_LIST_CRASH_IDS_FILE"); + std::env::remove_var("FUZZTEST_CORPUS_DB"); + } + + let options = result.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("/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() + })) + ); + } } diff --git a/rust/src/options.rs b/rust/src/options.rs index 2c8cbcf0b..0d7984336 100644 --- a/rust/src/options.rs +++ b/rust/src/options.rs @@ -23,8 +23,8 @@ use std::sync::OnceLock; use tempfile::{NamedTempFile, TempDir}; pub use fuzztest_options::{ - ExecutionMode, FuzzFor, FuzzOptions, FuzzTestOptions, ReplayCorpusOptions, ReplayCrashOptions, - TimeBudgetType, + ExecutionMode, FuzzFor, FuzzOptions, FuzzTestOptions, ListCrashIdsOptions, ReplayCorpusOptions, + ReplayCrashOptions, TimeBudgetType, }; /// Returns a lazily-initialized static reference to the global `FuzzTestOptions`. @@ -59,7 +59,8 @@ impl ExecutionModeExt for ExecutionMode { ExecutionMode::SmokeTest => Ok(ExecutionAction::SmokeTest), ExecutionMode::Fuzz(_) | ExecutionMode::ReplayCrash(_) - | ExecutionMode::ReplayCorpus(_) => { + | ExecutionMode::ReplayCorpus(_) + | ExecutionMode::ListCrashIds(_) => { let centipede_args = CentipedeArgs::from_execution_mode(self, options, current_test_name, None)? .context( @@ -259,6 +260,10 @@ impl CentipedeArgs { add_arg(format!("--j={jobs}"))?; } } + ExecutionMode::ListCrashIds(list_opts) => { + add_arg("--list_crash_ids=true".to_string())?; + add_arg(format!("--list_crash_ids_file={}", list_opts.list_crash_ids_file))?; + } ExecutionMode::SmokeTest => unreachable!(), ExecutionMode::ListFuzzTests => unreachable!(), } @@ -793,4 +798,28 @@ mod tests { expect_true!(args_str.contains(&"--fuzztest_corpus_database=/tmp/corpus_db")); expect_false!(args_str.iter().any(|s| s.starts_with("--stop_after="))); } + + #[gtest] + fn test_determine_execution_action_list_crash_ids() { + let options = FuzzTestOptions { + list_crash_ids: true, + list_crash_ids_file: Some("/tmp/custom_crashes.txt".to_string()), + corpus_db: Some("/tmp/corpus_db".to_string()), + ..Default::default() + }; + let action = determine_execution_action_internal(&options, "my_mod::my_test"); + + let ExecutionAction::Standalone(args) = action else { + panic!("Expected Standalone action for ListCrashIds"); + }; + + let args_str: Vec<&str> = + args._c_strings.iter().map(|s| s.to_str().expect("invalid utf8")).collect(); + + expect_true!(args_str.contains(&"--list_crash_ids=true")); + expect_true!(args_str.contains(&"--list_crash_ids_file=/tmp/custom_crashes.txt")); + expect_true!(args_str.contains(&"--fuzztest_corpus_database=/tmp/corpus_db")); + expect_true!(args_str.iter().any(|s| s.starts_with("--workdir="))); + expect_true!(args_str.contains(&"--test_name=my_mod.my_test")); + } }