cargo-fuzztest: support replaying a crash by id - Validate that --centipede-binary-path and --corpus-db are provided when replaying a crash. - Pass FUZZTEST_REPLAY_ID and FUZZTEST_CORPUS_DB environment variables to the target binary. - Add unit and integration tests for crash replay configuration. PiperOrigin-RevId: 965984007
diff --git a/rust/cargo_fuzztest/BUILD b/rust/cargo_fuzztest/BUILD index 2b2fae5..d9ff6a5 100644 --- a/rust/cargo_fuzztest/BUILD +++ b/rust/cargo_fuzztest/BUILD
@@ -62,7 +62,7 @@ ) rust_test( - name = "sample_fuzz_test_bin", + name = "sample_fuzz_crate_bin", srcs = ["test_crates/sample_fuzz_crate/src/lib.rs"], edition = "2024", tags = [ @@ -77,9 +77,12 @@ rust_test( name = "runner_test", - srcs = ["tests/runner_test.rs"], + srcs = [ + "tests/common/mod.rs", + "tests/runner_test.rs", + ], data = [ - ":sample_fuzz_test_bin", + ":sample_fuzz_crate_bin", ], edition = "2024", deps = [
diff --git a/rust/cargo_fuzztest/Cargo.toml b/rust/cargo_fuzztest/Cargo.toml index 990523c..0a663e1 100644 --- a/rust/cargo_fuzztest/Cargo.toml +++ b/rust/cargo_fuzztest/Cargo.toml
@@ -30,11 +30,24 @@ tempfile = "3.27.0" # Compiled as a [[test]] target so Cargo builds it with the test harness enabled -# (`rustc --test`) and exposes `CARGO_BIN_EXE_sample_fuzz_test_bin` to `runner_test`. +# (`rustc --test`) and exposes `CARGO_BIN_EXE_sample_fuzz_crate_bin` to `runner_test`. [[test]] -name = "sample_fuzz_test_bin" +name = "sample_fuzz_crate_bin" path = "test_crates/sample_fuzz_crate/src/lib.rs" +test = false + +# Compiled as a [[test]] target so Cargo builds it with the test harness enabled +# (`rustc --test`) and exposes `CARGO_BIN_EXE_another_sample_fuzz_crate_bin` to `runner_test`. +[[test]] +name = "another_sample_fuzz_crate_bin" +path = "test_crates/another_sample_fuzz_crate/src/lib.rs" +test = false [[test]] name = "runner_test" path = "tests/runner_test.rs" + +[[test]] +name = "e2e_cli_test" +path = "tests/e2e_cli_test.rs" +
diff --git a/rust/cargo_fuzztest/src/lib.rs b/rust/cargo_fuzztest/src/lib.rs index 6f61bcf..78b6942 100644 --- a/rust/cargo_fuzztest/src/lib.rs +++ b/rust/cargo_fuzztest/src/lib.rs
@@ -17,7 +17,9 @@ use anyhow::{Context, Result}; use clap::Parser; -pub use fuzztest_options::{ExecutionMode, FuzzFor, FuzzOptions, FuzzTestOptions}; +pub use fuzztest_options::{ + ExecutionMode, FuzzFor, FuzzOptions, FuzzTestOptions, ReplayCrashOptions, +}; use std::env; use std::ffi::OsString; use std::path::{Path, PathBuf}; @@ -54,7 +56,15 @@ fn check_centipede_binary_path_is_set(&self) -> Result<()> { anyhow::ensure!( self.centipede_binary_path.is_some(), - "fuzzing mode requires `--centipede-binary-path` to be specified" + "`--centipede-binary-path` needs to be specified" + ); + Ok(()) + } + + fn check_corpus_db_is_set(&self) -> Result<()> { + anyhow::ensure!( + self.fuzztest_options.corpus_db.is_some(), + "`--corpus-db` needs to be specified" ); Ok(()) } @@ -72,14 +82,18 @@ self.check_centipede_binary_path_is_set()?; mode } + ExecutionMode::ReplayCrash(_) => { + self.check_centipede_binary_path_is_set()?; + self.check_corpus_db_is_set()?; + mode + } ExecutionMode::SmokeTest => { if self.test_path.is_some() { self.check_centipede_binary_path_is_set()?; - return Ok(ExecutionMode::Fuzz(FuzzOptions { - fuzz_for: FuzzFor::Indefinitely, - // TODO(the-shank): support parallel jobs - jobs: None, - })); + ExecutionMode::Fuzz(FuzzOptions { + fuzz_for: self.fuzztest_options.fuzz_for.unwrap_or(FuzzFor::Indefinitely), + jobs: self.fuzztest_options.jobs, + }) } else { mode } @@ -241,6 +255,10 @@ } } + ExecutionMode::ReplayCrash(replay_options) => { + cmd.env("FUZZTEST_REPLAY_ID", replay_options.replay_id); + } + ExecutionMode::SmokeTest => { // nothing to be done } @@ -249,6 +267,14 @@ } } + if let Some(corpus_db) = &self.options.fuzztest_options.corpus_db { + cmd.env("FUZZTEST_CORPUS_DB", corpus_db); + } + + if let Some(workdir_root) = &self.options.fuzztest_options.workdir_root { + cmd.env("FUZZTEST_WORKDIR_ROOT", workdir_root); + } + // If `--centipede-binary-path` was passed to `cargo-fuzztest`, forward it to the // child test executable via `FUZZTEST_CENTIPEDE_BINARY_PATH` environment variable so the // fuzzer runtime can locate and run Centipede during continuous fuzzing. @@ -256,6 +282,11 @@ cmd.env("FUZZTEST_CENTIPEDE_BINARY_PATH", centipede_binary_path); } + if self.options.fuzztest_options.print_subprocess_log { + cmd.env("FUZZTEST_PRINT_SUBPROCESS_LOG", "true"); + cmd.arg("--nocapture"); + } + Ok(cmd) } @@ -379,4 +410,88 @@ ExecutionMode::SmokeTest ); } + + #[gtest] + fn test_cli_option_parsing_replay_id_success() { + let parsed = CargoFuzzTestOptions::try_parse_from([ + "cargo-fuzztest", + "--replay-id", + "crash_12345", + "--corpus-db", + "/tmp/corpus_db", + "--centipede-binary-path", + "/custom/centipede", + ]) + .expect("valid replay options should parse successfully"); + + assert_eq!(parsed.fuzztest_options.replay_id.as_deref(), Some("crash_12345")); + 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::ReplayCrash(ReplayCrashOptions { replay_id: "crash_12345".to_string() }) + ); + } + + #[gtest] + fn test_execution_mode_replay_id_missing_corpus_db_errors() { + let options = CargoFuzzTestOptions { + fuzztest_options: FuzzTestOptions { + replay_id: Some("crash_12345".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_replay_id_missing_centipede_binary_path_errors() { + let options = CargoFuzzTestOptions { + fuzztest_options: FuzzTestOptions { + replay_id: Some("crash_12345".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_replay_crash() { + let options = CargoFuzzTestOptions { + fuzztest_options: FuzzTestOptions { + replay_id: Some("crash_12345".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<String>)> = 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_REPLAY_ID".to_string(), Some("crash_12345".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/test_crates/another_sample_fuzz_crate/Cargo.toml b/rust/cargo_fuzztest/test_crates/another_sample_fuzz_crate/Cargo.toml new file mode 100644 index 0000000..d863aea --- /dev/null +++ b/rust/cargo_fuzztest/test_crates/another_sample_fuzz_crate/Cargo.toml
@@ -0,0 +1,11 @@ +[package] +name = "another_sample_fuzz_crate" +version = "0.1.0" +edition = "2024" + +[dependencies] +fuzztest = { path = "../../.." } +googletest = "0.14.3" + +[workspace] +
diff --git a/rust/cargo_fuzztest/test_crates/another_sample_fuzz_crate/src/lib.rs b/rust/cargo_fuzztest/test_crates/another_sample_fuzz_crate/src/lib.rs new file mode 100644 index 0000000..0995410 --- /dev/null +++ b/rust/cargo_fuzztest/test_crates/another_sample_fuzz_crate/src/lib.rs
@@ -0,0 +1,24 @@ +use fuzztest::domains::arbitrary::Arbitrary; +use fuzztest::fuzztest; + +#[fuzztest(data = Arbitrary::<i32>::default())] +fn sample_fuzztest_target(data: i32) { + let _ = data; +} + +#[fuzztest(data = Arbitrary::<i32>::default())] +fn another_sample_fuzztest_target(data: i32) { + let _ = data; +} + +#[fuzztest(data = Arbitrary::<i32>::default())] +fn jobs_fuzztest_target(_data: i32) { + println!("JOBS_TEST_PID: {}", std::process::id()); +} + +#[fuzztest(data = Arbitrary::<u8>::default())] +fn crashing_fuzztest_target(data: u8) { + if data == 10 { + panic!("Crashing bug found!"); + } +}
diff --git a/rust/cargo_fuzztest/tests/common/mod.rs b/rust/cargo_fuzztest/tests/common/mod.rs new file mode 100644 index 0000000..820ce7b --- /dev/null +++ b/rust/cargo_fuzztest/tests/common/mod.rs
@@ -0,0 +1,45 @@ +use cargo_fuzztest::FuzztestRunner; +use std::env; +use std::path::PathBuf; + +// Helper to locate compiled sample fuzz test executables across different build environments +// (Cargo tests vs Blaze/Bazel tests). +pub fn get_sample_test_bin_path(crate_name: &str) -> PathBuf { + let test_bin_name = format!("{crate_name}_bin"); + + // 1. Cargo test: Check if CARGO_BIN_EXE_<name> is set by Cargo when running integration tests. + if let Ok(cargo_bin) = env::var(format!("CARGO_BIN_EXE_{test_bin_name}")) { + return PathBuf::from(cargo_bin); + } + + // 2. Bazel/Blaze test: Fall back to TEST_SRCDIR and TEST_WORKSPACE. + if let Ok(src_dir) = env::var("TEST_SRCDIR") { + let test_workspace = env::var("TEST_WORKSPACE").unwrap_or_else(|_| { + "_main".to_string() + }); + + let relative_binary_path = + format!("rust/cargo_fuzztest/{test_bin_name}"); + + let blaze_path = PathBuf::from(src_dir).join(test_workspace).join(relative_binary_path); + assert!(blaze_path.exists()); + return blaze_path; + } + + // 3. Cargo test fallback: Query compiled test executable path via Cargo JSON compiler messages. + if env::var("CARGO_MANIFEST_DIR").is_ok() { + let cargo_bin = env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()); + let output = std::process::Command::new(cargo_bin) + .args(["test", "--no-run", "--message-format=json", "--test", &test_bin_name]) + .output() + .expect("cargo test compilation command should execute successfully"); + assert!(output.status.success()); + + let json_stdout = String::from_utf8_lossy(&output.stdout); + if let Ok(exe) = FuzztestRunner::parse_compiler_messages(&json_stdout) { + return exe; + } + } + + panic!("Could not locate {test_bin_name} via CARGO_BIN_EXE, TEST_SRCDIR, or Cargo compiler messages"); +}
diff --git a/rust/cargo_fuzztest/tests/e2e_cli_test.rs b/rust/cargo_fuzztest/tests/e2e_cli_test.rs index 00119a5..221a21d 100644 --- a/rust/cargo_fuzztest/tests/e2e_cli_test.rs +++ b/rust/cargo_fuzztest/tests/e2e_cli_test.rs
@@ -1,5 +1,6 @@ use googletest::prelude::*; use std::env; +use std::fs; use std::path::{Path, PathBuf}; use std::process::Command; use tempfile::TempDir; @@ -140,3 +141,135 @@ } expect_that!(pids.len(), eq(4)); } + +#[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"); + fs::create_dir_all(&temp_target_dir).expect("Failed to corpus db 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 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 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 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); + + // 2. Run Centipede 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()); + + // 3. Get list of 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 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(); + + expect_true!(!crash_ids.is_empty()); + let crash_id = crash_ids[0]; + + // 4. Run cargo-fuzztest CLI with --replay-id <crash_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") + .arg(crash_id) + .arg("--corpus-db") + .arg(temp_db_dir.path()) + .arg("--workdir-root") + .arg(workdir_root_dir.path()) + .arg("--centipede-binary-path") + .arg(¢ipede_bin); + + let output = cmd.output().expect("Failed to run cargo-fuzztest in replay mode"); + + let stderr_str = String::from_utf8_lossy(&output.stderr); + let stdout_str = String::from_utf8_lossy(&output.stdout); + expect_false!(output.status.success()); + expect_true!( + stderr_str.contains("FuzzTest controller reported failure") + || stderr_str.contains("Crashing bug found!") + || stdout_str.contains("FuzzTest controller reported failure") + ); +} + +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"); + + String::from_utf8_lossy(&process.stderr).to_string() +}
diff --git a/rust/cargo_fuzztest/tests/runner_test.rs b/rust/cargo_fuzztest/tests/runner_test.rs index ee2dce5..b45852c 100644 --- a/rust/cargo_fuzztest/tests/runner_test.rs +++ b/rust/cargo_fuzztest/tests/runner_test.rs
@@ -1,12 +1,13 @@ +mod common; + use cargo_fuzztest::{CargoFuzzTestOptions, FuzztestRunner}; +use common::get_sample_test_bin_path; use fuzztest_options::{FuzzFor, FuzzTestOptions}; use googletest::prelude::*; -use std::env; -use std::path::PathBuf; #[gtest] fn test_runner_execution() { - let binary_path = get_sample_fuzz_test_bin_path(); + let binary_path = get_sample_test_bin_path("sample_fuzz_crate"); assert!(binary_path.exists(), "Sample fuzz binary does not exist at {}", binary_path.display()); @@ -24,7 +25,7 @@ #[gtest] fn test_runner_build_run_command_with_target() { - let binary_path = get_sample_fuzz_test_bin_path(); + let binary_path = get_sample_test_bin_path("sample_fuzz_crate"); let options = CargoFuzzTestOptions { test_path: Some( "__fuzztest_mod__sample_fuzztest_target::sample_fuzztest_target".to_string(), @@ -43,7 +44,7 @@ #[gtest] fn test_runner_build_run_command_with_duration() { - let binary_path = get_sample_fuzz_test_bin_path(); + let binary_path = get_sample_test_bin_path("sample_fuzz_crate"); let fuzztest_options = FuzzTestOptions { fuzz_for: Some(FuzzFor::Duration("5s".parse().unwrap())), ..Default::default() @@ -65,7 +66,7 @@ #[gtest] fn test_runner_build_run_command_with_indefinitely() { - let binary_path = get_sample_fuzz_test_bin_path(); + let binary_path = get_sample_test_bin_path("sample_fuzz_crate"); let fuzztest_options = FuzzTestOptions { fuzz_for: Some(FuzzFor::Indefinitely), ..Default::default() }; let options = CargoFuzzTestOptions { @@ -85,7 +86,7 @@ #[gtest] fn test_runner_build_run_command_with_centipede_binary_path() { - let binary_path = get_sample_fuzz_test_bin_path(); + let binary_path = get_sample_test_bin_path("sample_fuzz_crate"); let options = CargoFuzzTestOptions { centipede_binary_path: Some("/custom/path/to/centipede".to_string()), ..Default::default() @@ -105,7 +106,7 @@ #[gtest] fn test_runner_build_run_command_with_jobs() { - let binary_path = get_sample_fuzz_test_bin_path(); + let binary_path = get_sample_test_bin_path("sample_fuzz_crate"); let fuzztest_options = FuzzTestOptions { jobs: Some(4), fuzz_for: Some(FuzzFor::Duration("10s".parse().expect("static valid duration string"))), @@ -129,7 +130,7 @@ #[gtest] fn test_runner_build_run_command_with_jobs_only() { - let binary_path = get_sample_fuzz_test_bin_path(); + let binary_path = get_sample_test_bin_path("sample_fuzz_crate"); let fuzztest_options = FuzzTestOptions { jobs: Some(4), ..Default::default() }; let options = CargoFuzzTestOptions { fuzztest_options, ..Default::default() }; let runner = FuzztestRunner::new("x86_64-unknown-linux-gnu".to_string(), options); @@ -144,17 +145,70 @@ } #[gtest] -fn test_execution_mode_without_centipede_binary_path_errors() { +fn test_runner_build_run_command_with_replay_id() { + let binary_path = get_sample_test_bin_path("sample_fuzz_crate"); + let fuzztest_options = FuzzTestOptions { + replay_id: Some("crash_12345".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<String>)> = 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_REPLAY_ID".to_string(), Some("crash_12345".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_replay_id_missing_corpus_db_errors() { let fuzztest_options = - FuzzTestOptions { fuzz_for: Some(FuzzFor::Indefinitely), ..Default::default() }; + FuzzTestOptions { replay_id: Some("crash_12345".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_replay_id_missing_centipede_binary_path_errors() { + let fuzztest_options = FuzzTestOptions { + replay_id: Some("crash_12345".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")); } #[gtest] fn test_runner_list_command() { - let binary_path = get_sample_fuzz_test_bin_path(); + let binary_path = get_sample_test_bin_path("sample_fuzz_crate"); assert!(binary_path.exists(), "Sample fuzz binary does not exist at {}", binary_path.display()); @@ -167,43 +221,3 @@ let args: Vec<String> = cmd.get_args().map(|a| a.to_string_lossy().to_string()).collect(); expect_eq!(args, &["__fuzztest_mod__", "--list"]); } - -fn get_sample_fuzz_test_bin_path() -> PathBuf { - // 1. Cargo test: Check if CARGO_BIN_EXE_<name> is set - if let Ok(cargo_bin) = env::var("CARGO_BIN_EXE_sample_fuzz_test_bin") { - return PathBuf::from(cargo_bin); - } - - // 2. Bazel/Blaze test: Fall back to TEST_SRCDIR and TEST_WORKSPACE - if let Ok(src_dir) = env::var("TEST_SRCDIR") { - let test_workspace = env::var("TEST_WORKSPACE").unwrap_or_else(|_| { - "_main".to_string() - }); - - const RELATIVE_BINARY_PATH: &str = - "rust/cargo_fuzztest/sample_fuzz_test_bin"; - - let blaze_path = PathBuf::from(src_dir).join(test_workspace).join(RELATIVE_BINARY_PATH); - if blaze_path.exists() { - return blaze_path; - } - } - - // 2. Cargo test fallback: Query compiled test executable path via Cargo JSON compiler messages - if env::var("CARGO_MANIFEST_DIR").is_ok() { - let cargo_bin = env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()); - let output = std::process::Command::new(cargo_bin) - .args(["test", "--no-run", "--message-format=json", "--test", "sample_fuzz_test_bin"]) - .output() - .expect("Failed to execute `cargo test --no-run --message-format=json` to locate test binary"); - - if output.status.success() { - let json_stdout = String::from_utf8_lossy(&output.stdout); - if let Ok(exe) = FuzztestRunner::parse_compiler_messages(&json_stdout) { - return exe; - } - } - } - - panic!("Could not locate sample_fuzz_test_bin via CARGO_BIN_EXE, TEST_SRCDIR, or Cargo compiler messages"); -}