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
diff --git a/.github/workflows/cargo_test.yml b/.github/workflows/cargo_test.yml
index 36524fc..4d463e2 100644
--- a/.github/workflows/cargo_test.yml
+++ b/.github/workflows/cargo_test.yml
@@ -69,7 +69,7 @@
       - 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 5e6254d..daafbf8 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 @@
     /// 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<String>,
+
+    /// Optional path to the Centipede binary executable.
+    ///
+    /// When specified via CLI `--centipede-binary-path <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<String>,
 }
 
 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<ExecutionMode> {
         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 @@
     }
 
     /// 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<Command> {
         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 @@
             }
         }
 
-        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 @@
             .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 @@
     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<String> = cmd.get_args().map(|a| a.to_string_lossy().to_string()).collect();
         assert_eq!(args, &["__fuzztest_mod__"]);
     }
@@ -258,7 +332,8 @@
     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<String> = cmd.get_args().map(|a| a.to_string_lossy().to_string()).collect();
         assert_eq!(args, &["__fuzztest_mod__", "--list"]);
     }
@@ -266,13 +341,19 @@
     #[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 @@
         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 @@
         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 9eb4214..1caaaf6 100644
--- a/rust/cargo_fuzztest/tests/e2e_cli_test.rs
+++ b/rust/cargo_fuzztest/tests/e2e_cli_test.rs
@@ -54,9 +54,9 @@
     // 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 @@
     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 344f1e5..15b98f8 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,7 +13,9 @@
     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");
 
@@ -20,6 +23,96 @@
 }
 
 #[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<String> = 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<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_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<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_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<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_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 @@
 
     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<String> = cmd.get_args().map(|a| a.to_string_lossy().to_string()).collect();
     expect_eq!(args, &["__fuzztest_mod__", "--list"]);