cargo-fuzztest: support replaying all crashes from corpus

- Add requires = "corpus_db" to replay_findings in FuzzTestOptions.
- Support ExecutionMode::ReplayAllCrashes in CargoFuzzTestOptions.
- Set FUZZTEST_REPLAY_FINDINGS environment variable in FuzztestRunner.
- Add unit, runner, and end-to-end integration tests for replaying all crashes.

PiperOrigin-RevId: 966003938
diff --git a/rust/cargo_fuzztest/src/lib.rs b/rust/cargo_fuzztest/src/lib.rs
index 78b6942..d3a9c4e 100644
--- a/rust/cargo_fuzztest/src/lib.rs
+++ b/rust/cargo_fuzztest/src/lib.rs
@@ -82,7 +82,7 @@
                 self.check_centipede_binary_path_is_set()?;
                 mode
             }
-            ExecutionMode::ReplayCrash(_) => {
+            ExecutionMode::ReplayAllCrashes | ExecutionMode::ReplayCrash(_) => {
                 self.check_centipede_binary_path_is_set()?;
                 self.check_corpus_db_is_set()?;
                 mode
@@ -259,6 +259,10 @@
                 cmd.env("FUZZTEST_REPLAY_ID", replay_options.replay_id);
             }
 
+            ExecutionMode::ReplayAllCrashes => {
+                cmd.env("FUZZTEST_REPLAY_FINDINGS", "true");
+            }
+
             ExecutionMode::SmokeTest => {
                 // nothing to be done
             }
@@ -328,6 +332,7 @@
 mod tests {
     use super::*;
     use googletest::prelude::*;
+    use std::collections::HashMap;
 
     #[gtest]
     fn test_parse_host_triple_valid() {
@@ -478,20 +483,88 @@
         let cmd =
             runner.build_run_command(Path::new("/tmp/test_bin")).expect("should build run command");
 
-        let envs: Vec<(String, Option<String>)> = cmd
+        let envs: HashMap<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())))
+        expect_eq!(envs.get("FUZZTEST_REPLAY_ID").and_then(|v| v.as_deref()), Some("crash_12345"));
+        expect_eq!(
+            envs.get("FUZZTEST_CORPUS_DB").and_then(|v| v.as_deref()),
+            Some("/tmp/corpus_db")
         );
-        assert!(envs.contains(&(
-            "FUZZTEST_CENTIPEDE_BINARY_PATH".to_string(),
-            Some("/custom/centipede".to_string())
-        )));
+        expect_eq!(
+            envs.get("FUZZTEST_CENTIPEDE_BINARY_PATH").and_then(|v| v.as_deref()),
+            Some("/custom/centipede")
+        );
+    }
+
+    #[gtest]
+    fn test_cli_option_parsing_replay_findings_success() {
+        let parsed = CargoFuzzTestOptions::try_parse_from([
+            "cargo-fuzztest",
+            "--replay-findings",
+            "--corpus-db",
+            "/tmp/corpus_db",
+            "--centipede-binary-path",
+            "/custom/centipede",
+        ])
+        .expect("valid replay-findings options should parse successfully");
+
+        assert!(parsed.fuzztest_options.replay_findings);
+        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::ReplayAllCrashes);
+    }
+
+    #[gtest]
+    fn test_execution_mode_replay_findings_missing_centipede_binary_path_errors() {
+        let options = CargoFuzzTestOptions {
+            fuzztest_options: FuzzTestOptions {
+                replay_findings: true,
+                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_all_crashes() {
+        let options = CargoFuzzTestOptions {
+            fuzztest_options: FuzzTestOptions {
+                replay_findings: true,
+                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: HashMap<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_eq!(envs.get("FUZZTEST_REPLAY_FINDINGS").and_then(|v| v.as_deref()), Some("true"));
+        expect_eq!(
+            envs.get("FUZZTEST_CORPUS_DB").and_then(|v| v.as_deref()),
+            Some("/tmp/corpus_db")
+        );
+        expect_eq!(
+            envs.get("FUZZTEST_CENTIPEDE_BINARY_PATH").and_then(|v| v.as_deref()),
+            Some("/custom/centipede")
+        );
     }
 }
diff --git a/rust/cargo_fuzztest/tests/e2e_cli_test.rs b/rust/cargo_fuzztest/tests/e2e_cli_test.rs
index 221a21d..7541d5e 100644
--- a/rust/cargo_fuzztest/tests/e2e_cli_test.rs
+++ b/rust/cargo_fuzztest/tests/e2e_cli_test.rs
@@ -186,7 +186,7 @@
     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")
+        .env_remove("FUZZTEST_CENTIPEDE_BINARY_PATH")
         .arg("--centipede-binary-path")
         .arg(&centipede_bin)
         .arg("--corpus-db")
@@ -244,6 +244,58 @@
     );
 }
 
+#[gtest]
+fn test_cargo_fuzztest_e2e_replay_all_crashes() {
+    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");
+
+    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("FUZZTEST_CENTIPEDE_BINARY_PATH")
+        .arg("--centipede-binary-path")
+        .arg(&centipede_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 --replay-findings to verify it replays all crashes from corpus db.
+    let mut cmd = setup_cargo_fuzztest_command(&sample_crate_path, temp_target_dir.path());
+    cmd.arg(test_target)
+        .arg("--replay-findings")
+        .arg("--corpus-db")
+        .arg(temp_db_dir.path())
+        .arg("--centipede-binary-path")
+        .arg(&centipede_bin);
+
+    let output = cmd.output().expect("Failed to run cargo-fuzztest in replay-findings mode");
+
+    let stderr_str = String::from_utf8_lossy(&output.stderr);
+    let stdout_str = String::from_utf8_lossy(&output.stdout);
+    expect_true!(output.status.success());
+    expect_true!(
+        stderr_str.contains("Crashing bug found!") || stdout_str.contains("Crashing bug found!")
+    );
+}
+
 fn run_centipede_with_args_expect_termination(centipede_bin: &str, args: &[&str]) -> String {
     // Disable interference from Bazel environment variables.
     let env_diff = [
diff --git a/rust/cargo_fuzztest/tests/runner_test.rs b/rust/cargo_fuzztest/tests/runner_test.rs
index b45852c..8498c43 100644
--- a/rust/cargo_fuzztest/tests/runner_test.rs
+++ b/rust/cargo_fuzztest/tests/runner_test.rs
@@ -207,6 +207,51 @@
 }
 
 #[gtest]
+fn test_runner_build_run_command_with_replay_findings() {
+    let binary_path = get_sample_test_bin_path("sample_fuzz_crate");
+    let fuzztest_options = FuzzTestOptions {
+        replay_findings: true,
+        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_FINDINGS".to_string(), Some("true".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_findings_missing_centipede_binary_path_errors() {
+    let fuzztest_options = FuzzTestOptions {
+        replay_findings: true,
+        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_test_bin_path("sample_fuzz_crate");
 
diff --git a/rust/options/src/lib.rs b/rust/options/src/lib.rs
index 78145e5..94c1b58 100644
--- a/rust/options/src/lib.rs
+++ b/rust/options/src/lib.rs
@@ -71,7 +71,7 @@
     pub replay_id: Option<String>,
 
     /// Replay all crashing inputs from the corpus database.
-    #[arg(env = "FUZZTEST_REPLAY_FINDINGS", long)]
+    #[arg(env = "FUZZTEST_REPLAY_FINDINGS", long, requires = "corpus_db")]
     pub replay_findings: bool,
 
     /// Replay the corpus for a specified duration.
@@ -304,4 +304,45 @@
             std::env::remove_var("FUZZTEST_REPLAY_CORPUS_FOR");
         }
     }
+
+    #[gtest]
+    fn test_replay_findings_requires_corpus_db() {
+        // SAFETY: Testing environment parsing in single-threaded context.
+        unsafe {
+            std::env::set_var("FUZZTEST_REPLAY_FINDINGS", "true");
+            std::env::remove_var("FUZZTEST_CORPUS_DB");
+        }
+
+        let result = FuzzTestOptions::try_parse_from(std::iter::empty::<OsString>());
+
+        // SAFETY: Cleaning up environment variables.
+        unsafe {
+            std::env::remove_var("FUZZTEST_REPLAY_FINDINGS");
+        }
+
+        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() {
+        // SAFETY: Testing environment parsing in single-threaded context.
+        unsafe {
+            std::env::set_var("FUZZTEST_REPLAY_FINDINGS", "true");
+            std::env::set_var("FUZZTEST_CORPUS_DB", "/tmp/corpus_db");
+        }
+
+        let result = FuzzTestOptions::try_parse_from(std::iter::empty::<OsString>());
+
+        // SAFETY: Cleaning up environment variables.
+        unsafe {
+            std::env::remove_var("FUZZTEST_REPLAY_FINDINGS");
+            std::env::remove_var("FUZZTEST_CORPUS_DB");
+        }
+
+        let options = result
+            .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("/tmp/corpus_db")));
+    }
 }