From 810d993fa2c8fe02650d8e2388246a5b67341022 Mon Sep 17 00:00:00 2001 From: Shashank Sharma Date: Fri, 14 Aug 2026 13:07:43 -0700 Subject: [PATCH] cargo-fuzztest: support fuzzing a fuzztest. - Add an optional test_path field to CargoFuzzTestOptions and implement test_filter() to support running specific fuzztest targets. - Add --centipede-binary-path CLI flag to CargoFuzzTestOptions and forward it via FUZZTEST_CENTIPEDE_BINARY_PATH environment variable to compiled test processes. - Update FuzztestRunner::build_run_command in cargo-fuzztest to append exact matching arguments when a specific test target is selected. - Forward configured fuzzing durations (both finite durations and indefinite fuzzing via inf) to child test executables using environment variables. - Add unit and e2e integration tests for targeted execution and command flag propagation. PiperOrigin-RevId: 964850072 --- .github/workflows/cargo_test.yml | 2 +- rust/cargo_fuzztest/src/lib.rs | 121 +++++++++++++++++++--- rust/cargo_fuzztest/tests/e2e_cli_test.rs | 30 +++++- rust/cargo_fuzztest/tests/runner_test.rs | 97 ++++++++++++++++- 4 files changed, 229 insertions(+), 21 deletions(-) diff --git a/.github/workflows/cargo_test.yml b/.github/workflows/cargo_test.yml index 36524fcf5..4d463e232 100644 --- a/.github/workflows/cargo_test.yml +++ b/.github/workflows/cargo_test.yml @@ -69,7 +69,7 @@ jobs: - name: Run Cargo workspace tests env: FUZZTEST_LIB_PATH: ${{ github.workspace }}/bazel-bin/centipede - CENTIPEDE_BINARY_PATH: ${{ github.workspace }}/bazel-bin/centipede/centipede + FUZZTEST_CENTIPEDE_BINARY_PATH: ${{ github.workspace }}/bazel-bin/centipede/centipede run: | cargo test --locked --workspace --no-fail-fast -- --test-threads=1 diff --git a/rust/cargo_fuzztest/src/lib.rs b/rust/cargo_fuzztest/src/lib.rs index 5e6254d16..daafbf8c3 100644 --- a/rust/cargo_fuzztest/src/lib.rs +++ b/rust/cargo_fuzztest/src/lib.rs @@ -17,7 +17,7 @@ use anyhow::{Context, Result}; use clap::Parser; -pub use fuzztest_options::{ExecutionMode, FuzzTestOptions}; +pub use fuzztest_options::{ExecutionMode, FuzzFor, FuzzOptions, FuzzTestOptions}; use std::env; use std::ffi::OsString; use std::path::{Path, PathBuf}; @@ -35,17 +35,61 @@ pub struct CargoFuzzTestOptions { /// List all fuzz tests in the crate without running them. #[arg(long)] pub list: bool, + + /// Optional target test path to run (for example, `__fuzztest_mod__my_test::my_test`). + /// + /// If omitted, all generated fuzz tests in the binary are run. + #[arg()] + pub test_path: Option, + + /// Optional path to the Centipede binary executable. + /// + /// When specified via CLI `--centipede-binary-path `, this is forwarded to + /// the compiled test executable via the `FUZZTEST_CENTIPEDE_BINARY_PATH` environment variable. + #[arg(env = "FUZZTEST_CENTIPEDE_BINARY_PATH", long)] + pub centipede_binary_path: Option, } impl CargoFuzzTestOptions { + 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" + ); + Ok(()) + } + /// Returns the `ExecutionMode` derived from CLI options. - pub fn execution_mode(&self) -> ExecutionMode { + pub fn execution_mode(&self) -> Result { if self.list { - return ExecutionMode::ListFuzzTests; + return Ok(ExecutionMode::ListFuzzTests); } - // TODO(the-shank): add support for other modes. - ExecutionMode::SmokeTest + let mode = ExecutionMode::from_fuzztest_options(&self.fuzztest_options); + + let mode = match &mode { + ExecutionMode::Fuzz(_) => { + self.check_centipede_binary_path_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, + })); + } else { + mode + } + } + _ => { + anyhow::bail!("mode not yet supported"); + } + }; + + Ok(mode) } } @@ -166,15 +210,35 @@ impl FuzztestRunner { } /// Construct the direct binary invocation command. - pub fn build_run_command(&self, test_binary: &Path) -> Command { + pub fn build_run_command(&self, test_binary: &Path) -> Result { let mut cmd = Command::new(test_binary); - cmd.arg("__fuzztest_mod__"); - let mode = self.options.execution_mode(); + if let Some(test_path) = &self.options.test_path { + cmd.arg(test_path); + cmd.arg("--exact"); + } else { + cmd.arg("__fuzztest_mod__"); + } + + let mode = self.options.execution_mode()?; match mode { ExecutionMode::ListFuzzTests => { cmd.arg("--list"); } + + ExecutionMode::Fuzz(fuzz_options) => { + let FuzzOptions { fuzz_for, jobs: _ } = fuzz_options; + match fuzz_for { + FuzzFor::Indefinitely => { + cmd.env("FUZZTEST_FUZZ_FOR", "inf"); + } + FuzzFor::Duration(duration) => { + cmd.env("FUZZTEST_FUZZ_FOR", duration.to_string()); + } + } + // TODO(the-shank): support parallel jobs + } + ExecutionMode::SmokeTest => { // nothing to be done } @@ -183,7 +247,14 @@ impl FuzztestRunner { } } - cmd + // 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. + if let Some(centipede_binary_path) = &self.options.centipede_binary_path { + cmd.env("FUZZTEST_CENTIPEDE_BINARY_PATH", centipede_binary_path); + } + + Ok(cmd) } /// Runs the tool in two steps: @@ -209,8 +280,10 @@ impl FuzztestRunner { .context("while attempting to parse compilation JSON output as UTF-8")?; let test_binary = Self::parse_compiler_messages(&json_stdout)?; - // 2. execute command according to the selected execution mode - let mut run_cmd = self.build_run_command(&test_binary); + // 2. run the test binary + let mut run_cmd = self + .build_run_command(&test_binary) + .context("while attempting to construct test binary run command")?; let status = run_cmd.status().context("while attempting to execute compiled test binary")?; @@ -249,7 +322,8 @@ mod tests { fn test_build_run_command_default() { let options = CargoFuzzTestOptions::default(); let runner = FuzztestRunner::new("sample-host-triple".to_string(), options); - let cmd = runner.build_run_command(Path::new("/tmp/test_bin")); + let cmd = + runner.build_run_command(Path::new("/tmp/test_bin")).expect("should build run command"); let args: Vec = cmd.get_args().map(|a| a.to_string_lossy().to_string()).collect(); assert_eq!(args, &["__fuzztest_mod__"]); } @@ -258,7 +332,8 @@ mod tests { fn test_build_run_command_list() { let options = CargoFuzzTestOptions { list: true, ..Default::default() }; let runner = FuzztestRunner::new("sample-host-triple".to_string(), options); - let cmd = runner.build_run_command(Path::new("/tmp/test_bin")); + let cmd = + runner.build_run_command(Path::new("/tmp/test_bin")).expect("should build run command"); let args: Vec = cmd.get_args().map(|a| a.to_string_lossy().to_string()).collect(); assert_eq!(args, &["__fuzztest_mod__", "--list"]); } @@ -266,13 +341,19 @@ mod tests { #[gtest] fn test_execution_mode_smoke_test() { let options = CargoFuzzTestOptions { list: false, ..Default::default() }; - assert_eq!(options.execution_mode(), ExecutionMode::SmokeTest); + assert_eq!( + options.execution_mode().expect("valid execution mode"), + ExecutionMode::SmokeTest + ); } #[gtest] fn test_execution_mode_list_fuzz_tests() { let options = CargoFuzzTestOptions { list: true, ..Default::default() }; - assert_eq!(options.execution_mode(), ExecutionMode::ListFuzzTests); + assert_eq!( + options.execution_mode().expect("valid execution mode"), + ExecutionMode::ListFuzzTests + ); } #[gtest] @@ -280,7 +361,10 @@ mod tests { let parsed = CargoFuzzTestOptions::try_parse_from(["cargo-fuzztest", "--list"]) .expect("--list argument should be valid CLI option"); assert!(parsed.list); - assert_eq!(parsed.execution_mode(), ExecutionMode::ListFuzzTests); + assert_eq!( + parsed.execution_mode().expect("valid execution mode"), + ExecutionMode::ListFuzzTests + ); } #[gtest] @@ -288,6 +372,9 @@ mod tests { let parsed = CargoFuzzTestOptions::try_parse_from(["cargo-fuzztest"]) .expect("empty CLI arguments should be valid"); assert!(!parsed.list); - assert_eq!(parsed.execution_mode(), ExecutionMode::SmokeTest); + assert_eq!( + parsed.execution_mode().expect("valid execution mode"), + ExecutionMode::SmokeTest + ); } } diff --git a/rust/cargo_fuzztest/tests/e2e_cli_test.rs b/rust/cargo_fuzztest/tests/e2e_cli_test.rs index 9eb421479..1caaaf6cb 100644 --- a/rust/cargo_fuzztest/tests/e2e_cli_test.rs +++ b/rust/cargo_fuzztest/tests/e2e_cli_test.rs @@ -54,9 +54,9 @@ fn test_cargo_fuzztest_e2e_list() { // Resolve the absolute path of the sample crate let sample_crate_path = get_sample_crate_path("sample_fuzz_crate"); - // Create a temporary directory for Cargo build outputs to avoid polluting workspace let temp_target_dir = TempDir::new().expect("Failed to create temporary target directory"); + // Invoke: `cargo-fuzztest --list` inside the sample crate directory let output = setup_cargo_fuzztest_command(&sample_crate_path, temp_target_dir.path()) .arg("--list") .output() @@ -73,4 +73,32 @@ fn test_cargo_fuzztest_e2e_list() { expect_true!(stdout_str.contains( "__fuzztest_mod__another_sample_fuzztest_target::another_sample_fuzztest_target" )); + let stdout_str = String::from_utf8_lossy(&output.stdout); + expect_true!(stdout_str.contains("sample_fuzztest_target")); + expect_true!(stdout_str.contains("another_sample_fuzztest_target")); +} + +#[gtest] +fn test_cargo_fuzztest_e2e_specific_target() { + // Resolve the absolute path of the sample crate + let sample_crate_path = get_sample_crate_path("sample_fuzz_crate"); + + let temp_target_dir = TempDir::new().expect("Failed to create temporary target directory"); + + let mut cmd = setup_cargo_fuzztest_command(&sample_crate_path, temp_target_dir.path()); + + let centipede_bin = env::var("FUZZTEST_CENTIPEDE_BINARY_PATH") + .expect("FUZZTEST_CENTIPEDE_BINARY_PATH needs to be set for the test"); + + cmd.arg("__fuzztest_mod__sample_fuzztest_target::sample_fuzztest_target") + .arg("--fuzz-for=2s") + .env_remove("FUZZTEST_CENTIPEDE_BINARY_PATH") + .arg("--centipede-binary-path") + .arg(centipede_bin); + + let output = cmd.output().expect("Failed to run cargo-fuzztest command"); + + let stdout_str = String::from_utf8_lossy(&output.stdout); + expect_true!(stdout_str.contains("sample_fuzztest_target")); + expect_false!(stdout_str.contains("another_sample_fuzztest_target")); } diff --git a/rust/cargo_fuzztest/tests/runner_test.rs b/rust/cargo_fuzztest/tests/runner_test.rs index 344f1e53a..15b98f8a1 100644 --- a/rust/cargo_fuzztest/tests/runner_test.rs +++ b/rust/cargo_fuzztest/tests/runner_test.rs @@ -1,4 +1,5 @@ use cargo_fuzztest::{CargoFuzzTestOptions, FuzztestRunner}; +use fuzztest_options::{FuzzFor, FuzzTestOptions}; use googletest::prelude::*; use std::env; use std::path::PathBuf; @@ -12,13 +13,105 @@ fn test_runner_execution() { let options = CargoFuzzTestOptions::default(); let runner = FuzztestRunner::new("sample-host-triple".to_string(), options); - let mut cmd = runner.build_run_command(&binary_path); + let mut cmd = runner + .build_run_command(&binary_path) + .expect("building run command in smoke test mode should succeed"); let status = cmd.status().expect("Failed to execute test binary command"); expect_true!(status.success()); } +#[gtest] +fn test_runner_build_run_command_with_target() { + let binary_path = get_sample_fuzz_test_bin_path(); + let options = CargoFuzzTestOptions { + test_path: Some( + "__fuzztest_mod__sample_fuzztest_target::sample_fuzztest_target".to_string(), + ), + 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 args: Vec = cmd.get_args().map(|s| s.to_string_lossy().to_string()).collect(); + expect_true!(args + .contains(&"__fuzztest_mod__sample_fuzztest_target::sample_fuzztest_target".to_string())); + expect_true!(args.contains(&"--exact".to_string())); +} + +#[gtest] +fn test_runner_build_run_command_with_duration() { + let binary_path = get_sample_fuzz_test_bin_path(); + let fuzztest_options = FuzzTestOptions { + fuzz_for: Some(FuzzFor::Duration("5s".parse().unwrap())), + ..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_FUZZ_FOR".to_string(), Some("5s".to_string())))); +} + +#[gtest] +fn test_runner_build_run_command_with_indefinitely() { + let binary_path = get_sample_fuzz_test_bin_path(); + let fuzztest_options = + FuzzTestOptions { fuzz_for: Some(FuzzFor::Indefinitely), ..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_FUZZ_FOR".to_string(), Some("inf".to_string())))); +} + +#[gtest] +fn test_runner_build_run_command_with_centipede_binary_path() { + let binary_path = get_sample_fuzz_test_bin_path(); + let options = CargoFuzzTestOptions { + 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_CENTIPEDE_BINARY_PATH".to_string(), + Some("/custom/path/to/centipede".to_string()) + ))); +} + +#[gtest] +fn test_execution_mode_without_centipede_binary_path_errors() { + let fuzztest_options = + FuzzTestOptions { fuzz_for: Some(FuzzFor::Indefinitely), ..Default::default() }; + let options = CargoFuzzTestOptions { fuzztest_options, ..Default::default() }; + let result = options.execution_mode(); + expect_true!(result.is_err()); +} + #[gtest] fn test_runner_list_command() { let binary_path = get_sample_fuzz_test_bin_path(); @@ -29,7 +122,7 @@ fn test_runner_list_command() { let runner = FuzztestRunner::new("sample-host-triple".to_string(), options); - let cmd = runner.build_run_command(&binary_path); + let cmd = runner.build_run_command(&binary_path).expect("should build run command"); let args: Vec = cmd.get_args().map(|a| a.to_string_lossy().to_string()).collect(); expect_eq!(args, &["__fuzztest_mod__", "--list"]);