Refactor FuzzTestOptions into a standalone sub-crate

Refactor FuzzTestOptions, and common configuration types out of the main fuzztest crate into a new dedicated fuzztest_options sub-crate, so that it can be shared between fuzztest and cargo-fuzztest tool.

Changes:
- Extract FuzzTestOptions, TimeBudgetType, FuzzOptions, FuzzFor, ReplayCrashOptions, and ReplayCorpusOptions into third_party/googlefuzztest/rust/options.
- Re-export options types from fuzztest_options in fuzztest::options for backwards compatibility.
- Add ExecutionModeExt extension trait in fuzztest::options for runtime action preparation.
PiperOrigin-RevId: 964070742
diff --git a/Cargo.lock b/Cargo.lock
index 033c775..3d7faa8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -244,6 +244,7 @@
  "coverage",
  "engine-ffi",
  "fuzztest-macro",
+ "fuzztest-options",
  "googletest",
  "humantime",
  "inventory",
@@ -269,6 +270,16 @@
 ]
 
 [[package]]
+name = "fuzztest-options"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "clap",
+ "googletest",
+ "humantime",
+]
+
+[[package]]
 name = "getrandom"
 version = "0.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/rust/BUILD b/rust/BUILD
index 49c0df7..ef68440 100644
--- a/rust/BUILD
+++ b/rust/BUILD
@@ -18,6 +18,7 @@
     deps = [
         "@com_google_fuzztest//rust/coverage",
         "@com_google_fuzztest//rust/engine",
+        "@com_google_fuzztest//rust/options:fuzztest_options",
         "@crate_index//:anyhow",  # v1
         "@crate_index//:clap",  # v4
         "@crate_index//:humantime",  # v2
diff --git a/rust/Cargo.toml b/rust/Cargo.toml
index 31ce402..8a414e0 100644
--- a/rust/Cargo.toml
+++ b/rust/Cargo.toml
@@ -13,6 +13,7 @@
 coverage = { path = "coverage" }
 engine = { path = "engine", package = "engine-ffi" }
 fuzztest-macro = { path = "fuzztest_macro" }
+fuzztest-options = { path = "options" }
 humantime.workspace = true
 inventory.workspace = true
 num-traits.workspace = true
@@ -23,7 +24,7 @@
 tempfile.workspace = true
 
 [dev-dependencies]
-googletest = "0.14.3"
+googletest.workspace = true
 
 [dev-dependencies.trybuild]
 version = "1.0.103"
diff --git a/rust/e2e_tests/replay_test.rs b/rust/e2e_tests/replay_test.rs
index ba512c8..ca65490 100644
--- a/rust/e2e_tests/replay_test.rs
+++ b/rust/e2e_tests/replay_test.rs
@@ -25,26 +25,27 @@
     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("corpus_db");
+    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");
 
-    let workdir_root_dir = fixture.tmp_dir_path.join("workdir_root");
+    let workdir_root_dir =
+        fixture.tmp_dir_path.join("replay_by_id_reproduces_panic").join("workdir_root");
     fs::create_dir_all(&workdir_root_dir).expect("Failed to workdir_root directory");
 
     // 1. Run Centipede to fuzz the target and populate the corpus database.
     Command::new(&target_binary_path)
         .arg(test_name)
         .arg("--exact")
-        .env("FUZZTEST_FUZZ_FOR", "60s")
+        .env("FUZZTEST_FUZZ_FOR", "15s")
         .env("FUZZTEST_CORPUS_DB", &db_dir)
         .env("FUZZTEST_WORKDIR_ROOT", &workdir_root_dir)
         .env("FUZZTEST_CENTIPEDE_BINARY_PATH", &fixture.centipede_path)
-        .env("FUZZTEST_PRINT_SUBPROCESS_LOG", "true")
         .status()
         .expect("Failed to spawn binary");
 
     // 2. Retrieve the crash IDs from the corpus database using Centipede's --list_crash_ids flag.
-    let crash_ids_file = fixture.tmp_dir_path.join("crash_ids.txt");
+    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),
@@ -102,23 +103,23 @@
 
     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("corpus_db_all");
+    let db_dir =
+        fixture.tmp_dir_path.join("replay_all_reproduces_all_failures").join("corpus_db_all");
     fs::create_dir_all(&db_dir).expect("Failed to create db directory");
 
-    let workdir_root_dir = fixture.tmp_dir_path.join("workdir_root_all");
+    let workdir_root_dir =
+        fixture.tmp_dir_path.join("replay_all_reproduces_all_failures").join("workdir_root_all");
     fs::create_dir_all(&workdir_root_dir).expect("Failed to workdir_root_all directory");
 
     // 1. Run Centipede to fuzz the target and populate the corpus database.
     Command::new(&target_binary_path)
         .arg(test_name)
         .arg("--exact")
-        .env("FUZZTEST_FUZZ_FOR", "60s")
+        .env("FUZZTEST_FUZZ_FOR", "15s")
         .env("FUZZTEST_CORPUS_DB", &db_dir)
         .env("FUZZTEST_WORKDIR_ROOT", &workdir_root_dir)
         .env("FUZZTEST_CENTIPEDE_BINARY_PATH", &fixture.centipede_path)
-        .env("FUZZTEST_PRINT_SUBPROCESS_LOG", "true")
         .status()
         .expect("Failed to spawn binary");
 
diff --git a/rust/e2e_tests/standalone_mode_test.rs b/rust/e2e_tests/standalone_mode_test.rs
index 98cede1..20ba005 100644
--- a/rust/e2e_tests/standalone_mode_test.rs
+++ b/rust/e2e_tests/standalone_mode_test.rs
@@ -130,8 +130,14 @@
     let target_binary_path =
         fixture.target_binary_path.parent().unwrap().join("standalone_fuzz_tests_bin");
 
-    let corpus_db = fixture.tmp_dir_path.join("corpus_db_per_test");
-    let workdir_root = fixture.tmp_dir_path.join("workdir_root_per_test");
+    let corpus_db = fixture
+        .tmp_dir_path
+        .join("standalone_mode_replay_corpus_per_test_budget")
+        .join("corpus_db_per_test");
+    let workdir_root = fixture
+        .tmp_dir_path
+        .join("standalone_mode_replay_corpus_per_test_budget")
+        .join("workdir_root_per_test");
 
     // Centipede appends the binary identifier (relative path) to the corpus database path.
     let identifier = target_binary_path.to_str().unwrap();
@@ -215,8 +221,14 @@
     let target_binary_path =
         fixture.target_binary_path.parent().unwrap().join("standalone_fuzz_tests_bin");
 
-    let corpus_db = fixture.tmp_dir_path.join("corpus_db_total");
-    let workdir_root = fixture.tmp_dir_path.join("workdir_root_total");
+    let corpus_db = fixture
+        .tmp_dir_path
+        .join("standalone_mode_replay_corpus_per_test_budget")
+        .join("corpus_db_total");
+    let workdir_root = fixture
+        .tmp_dir_path
+        .join("standalone_mode_replay_corpus_per_test_budget")
+        .join("workdir_root_total");
 
     // Centipede appends the binary identifier (relative path) to the corpus database path.
     let identifier = target_binary_path.to_str().unwrap();
diff --git a/rust/e2e_tests/testdata/replay_fuzz_tests.rs b/rust/e2e_tests/testdata/replay_fuzz_tests.rs
index 7d664da..b4a1027 100644
--- a/rust/e2e_tests/testdata/replay_fuzz_tests.rs
+++ b/rust/e2e_tests/testdata/replay_fuzz_tests.rs
@@ -10,16 +10,12 @@
 }
 
 #[fuzztest(a = Arbitrary::<i32>::default())]
-fn find_two_bugs_fuzz_test(a: i32) {
-    println!("PROPERTY_FUNCTION_EXECUTED");
-    if a < -1 {
+fn find_two_bugs_fuzz_test(a: i32) { println!("PROPERTY_FUNCTION_EXECUTED");
+    if a == 10 {
         panic!("Bug 1 found!");
-    } else if a > 1 {
+    } else if a == 20 {
         println!("Bug 2 found!");
-        // Safety: We are intentionally causing a segfault to test that the engine would catch it.
-        unsafe {
-            std::ptr::null_mut::<i32>().write_volatile(42);
-        }
+        std::process::exit(99);
     }
 }
 
diff --git a/rust/options/BUILD b/rust/options/BUILD
new file mode 100644
index 0000000..b85746a
--- /dev/null
+++ b/rust/options/BUILD
@@ -0,0 +1,30 @@
+load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test")
+
+licenses(["notice"])
+
+exports_files(["BUILD"])
+
+rust_library(
+    name = "fuzztest_options",
+    srcs = glob([
+        "src/**/*.rs",
+    ]),
+    edition = "2024",
+    visibility = ["@com_google_fuzztest//rust:__subpackages__"],
+    deps = [
+        "@crate_index//:anyhow",  # v1
+        "@crate_index//:clap",  # v4
+        "@crate_index//:humantime",  # v2
+    ],
+)
+
+rust_test(
+    name = "fuzztest_options_test",
+    # Avoid interference when setting/resetting environment variables in tests.
+    args = ["--test-threads=1"],
+    crate = ":fuzztest_options",
+    edition = "2024",
+    deps = [
+        "@crate_index//:googletest",
+    ],
+)
diff --git a/rust/options/Cargo.toml b/rust/options/Cargo.toml
new file mode 100644
index 0000000..2fa2ff4
--- /dev/null
+++ b/rust/options/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "fuzztest-options"
+version = "0.1.0"
+edition = "2024"
+
+[dependencies]
+clap.workspace = true
+humantime.workspace = true
+anyhow.workspace = true
+
+[dev-dependencies]
+googletest.workspace = true
diff --git a/rust/options/src/lib.rs b/rust/options/src/lib.rs
new file mode 100644
index 0000000..1e72d34
--- /dev/null
+++ b/rust/options/src/lib.rs
@@ -0,0 +1,207 @@
+// This module provides the core command-line flag and environment variable options
+// structure (`FuzzTestOptions`) and domain execution modes (`ExecutionMode`).
+
+use clap::{Parser, ValueEnum};
+use humantime::Duration;
+
+/// Time budget calculation type for replay corpus mode.
+#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Default)]
+pub enum TimeBudgetType {
+    #[default]
+    PerTest,
+    Total,
+}
+
+/// Parses a fuzzing duration string from `FUZZTEST_FUZZ_FOR`.
+///
+/// Matches `"inf"` or `"infinity"` to [`FuzzFor::Indefinitely`]. All other values
+/// are parsed as standard human-readable durations (for example, `"5s"` or `"10m"`).
+fn parse_fuzz_for(s: &str) -> anyhow::Result<FuzzFor> {
+    let s_lower = s.trim().to_lowercase();
+    if s_lower == "inf" || s_lower == "infinity" {
+        Ok(FuzzFor::Indefinitely)
+    } else {
+        let duration = s.parse()?;
+        Ok(FuzzFor::Duration(duration))
+    }
+}
+
+/// Command-line and environment variable options parsed for the FuzzTest harness.
+#[derive(Parser, Debug, Clone, Default)]
+pub struct FuzzTestOptions {
+    /// The working root directory.
+    #[arg(env = "FUZZTEST_WORKDIR_ROOT", long)]
+    pub workdir_root: Option<String>,
+
+    /// The duration for which each test should be fuzzed.
+    ///
+    /// Accepts a human-readable duration (e.g., `5s`, `10m`, `1h`) or `inf` / `infinity`
+    /// to fuzz indefinitely until a crash is found or it is stopped manually.
+    #[arg(env = "FUZZTEST_FUZZ_FOR", long, value_parser = parse_fuzz_for)]
+    pub fuzz_for: Option<FuzzFor>,
+
+    /// If true, subprocess logs are printed after every batch. Note that crash logs are always
+    /// printed regardless of this flag's value.
+    #[arg(env = "FUZZTEST_PRINT_SUBPROCESS_LOG", long)]
+    pub print_subprocess_log: bool,
+
+    /// Number of parallel jobs to run.
+    #[arg(env = "FUZZTEST_JOBS", long)]
+    pub jobs: Option<usize>,
+
+    /// The crash ID to be replayed from the corpus database.
+    ///
+    /// If set, `corpus_db` must also be specified. This mode retrieves the crashing input
+    /// associated with the given ID from the database and executes the property function.
+    #[arg(env = "FUZZTEST_REPLAY_ID", long, requires = "corpus_db")]
+    pub replay_id: Option<String>,
+
+    /// Replay all crashing inputs from the corpus database.
+    #[arg(env = "FUZZTEST_REPLAY_FINDINGS", long)]
+    pub replay_findings: bool,
+
+    /// Replay the corpus for a specified duration.
+    #[arg(env = "FUZZTEST_REPLAY_CORPUS_FOR", long)]
+    pub replay_corpus_for: Option<Duration>,
+
+    /// Time budget calculation type for replay corpus mode.
+    #[arg(env = "FUZZTEST_TIME_BUDGET_TYPE", long, value_enum, default_value_t = TimeBudgetType::PerTest)]
+    pub time_budget_type: TimeBudgetType,
+
+    /// The path to the corpus database.
+    ///
+    /// If set to non-empty, updates/queries the corpus database that contains coverage,
+    /// regression, and crashing inputs for each test binary and fuzz test.
+    #[arg(env = "FUZZTEST_CORPUS_DB", long)]
+    pub corpus_db: Option<String>,
+}
+
+/// Strongly-typed domain execution mode for test runs.
+///
+/// This enum represents the parsed high-level user intent derived from `FuzzTestOptions`.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum ExecutionMode {
+    /// Smoke test execution (regular unit test fallback).
+    SmokeTest,
+
+    /// Fuzzing mode.
+    Fuzz(FuzzOptions),
+
+    /// Replay a specific crash ID from the corpus database.
+    ReplayCrash(ReplayCrashOptions),
+
+    /// Replay all crashing inputs stored in the corpus database.
+    ReplayAllCrashes,
+
+    /// Replay corpus inputs for a specified duration.
+    ReplayCorpus(ReplayCorpusOptions),
+}
+
+impl ExecutionMode {
+    /// Evaluates the raw options and maps them to a strongly-typed domain `ExecutionMode`.
+    ///
+    /// This decouples raw environment/CLI option parsing from execution mode validation.
+    pub fn from_fuzztest_options(options: &FuzzTestOptions) -> ExecutionMode {
+        if let Some(replay_corpus_for) = options.replay_corpus_for {
+            return ExecutionMode::ReplayCorpus(ReplayCorpusOptions {
+                replay_corpus_for,
+                time_budget_type: options.time_budget_type,
+            });
+        }
+
+        if options.replay_findings {
+            return ExecutionMode::ReplayAllCrashes;
+        }
+
+        if let Some(replay_id) = &options.replay_id {
+            return ExecutionMode::ReplayCrash(ReplayCrashOptions { replay_id: replay_id.clone() });
+        }
+
+        if let Some(fuzz_for) = options.fuzz_for {
+            return ExecutionMode::Fuzz(FuzzOptions { fuzz_for, jobs: options.jobs.clone() });
+        }
+
+        ExecutionMode::SmokeTest
+    }
+}
+
+/// Mode-specific options for continuous fuzzing.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct FuzzOptions {
+    pub fuzz_for: FuzzFor,
+
+    /// If `jobs` is `None`, we won't specify the number of jobs while invoking Centipede and it
+    /// will use its own default value.
+    pub jobs: Option<usize>,
+}
+
+/// The duration or limit for fuzzing.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum FuzzFor {
+    /// Fuzz indefinitely until it is manually stopped or a crash is found.
+    Indefinitely,
+
+    /// Fuzz for a specific duration.
+    Duration(Duration),
+}
+
+/// Mode-specific options for replaying a specific crashing input from the corpus database.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct ReplayCrashOptions {
+    pub replay_id: String,
+}
+
+/// Mode-specific options for replaying corpus for a duration.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct ReplayCorpusOptions {
+    pub replay_corpus_for: Duration,
+    pub time_budget_type: TimeBudgetType,
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use googletest::prelude::*;
+    use std::ffi::OsString;
+
+    #[gtest]
+    fn test_replay_id_requires_corpus_db() {
+        // SAFETY: Testing environment parsing in single-threaded context.
+        unsafe {
+            std::env::set_var("FUZZTEST_REPLAY_ID", "my_crash_123");
+            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_ID");
+        }
+
+        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_id_with_corpus_db_succeeds() {
+        // SAFETY: Testing environment parsing in single-threaded context.
+        unsafe {
+            std::env::set_var("FUZZTEST_REPLAY_ID", "my_crash_123");
+            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_ID");
+            std::env::remove_var("FUZZTEST_CORPUS_DB");
+        }
+
+        let options =
+            result.expect("parsing should succeed when both replay_id and corpus_db are present");
+        expect_that!(options.replay_id.as_deref(), eq(Some("my_crash_123")));
+        expect_that!(options.corpus_db.as_deref(), eq(Some("/tmp/corpus_db")));
+    }
+}
diff --git a/rust/src/options.rs b/rust/src/options.rs
index c9e46d8..210a535 100644
--- a/rust/src/options.rs
+++ b/rust/src/options.rs
@@ -1,113 +1,17 @@
 use crate::internal::FuzzTestRegistration;
 use ::engine::engine_ffi;
 use anyhow::Context;
-use clap::{Parser, ValueEnum};
-use humantime::Duration;
+use clap::Parser;
 use std::ffi::CString;
 use std::ffi::OsString;
 use std::path::Path;
 use std::sync::OnceLock;
 use tempfile::{NamedTempFile, TempDir};
 
-/// Time budget calculation type for replay corpus mode.
-#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Default)]
-pub enum TimeBudgetType {
-    #[default]
-    PerTest,
-    Total,
-}
-
-/// Parses a fuzzing duration string from `FUZZTEST_FUZZ_FOR`.
-///
-/// Matches `"inf"` or `"infinity"` to [`FuzzFor::Indefinitely`]. All other values
-/// are parsed as standard human-readable durations (for example, `"5s"` or `"10m"`).
-fn parse_fuzz_for(s: &str) -> anyhow::Result<FuzzFor> {
-    let s_lower = s.trim().to_lowercase();
-    if s_lower == "inf" || s_lower == "infinity" {
-        Ok(FuzzFor::Indefinitely)
-    } else {
-        let duration = s.parse()?;
-        Ok(FuzzFor::Duration(duration))
-    }
-}
-
-/// Command-line and environment variable options parsed for the FuzzTest harness.
-#[derive(Parser, Debug, Clone, Default)]
-pub struct FuzzTestOptions {
-    /// The working root directory.
-    #[arg(env = "FUZZTEST_WORKDIR_ROOT", long)]
-    pub workdir_root: Option<String>,
-
-    /// The duration for which each test should be fuzzed.
-    ///
-    /// Accepts a human-readable duration (e.g., `5s`, `10m`, `1h`) or `inf` / `infinite`
-    /// to fuzz indefinitely until a crash is found or it is stopped manually.
-    #[arg(env = "FUZZTEST_FUZZ_FOR", long, value_parser = parse_fuzz_for)]
-    pub fuzz_for: Option<FuzzFor>,
-
-    /// If true, subprocess logs are printed after every batch. Note that crash logs are always printed
-    /// regardless of this flag's value.
-    #[arg(env = "FUZZTEST_PRINT_SUBPROCESS_LOG", long)]
-    pub print_subprocess_log: bool,
-
-    /// Number of parallel jobs to run.
-    #[arg(env = "FUZZTEST_JOBS", long)]
-    pub jobs: Option<usize>,
-
-    /// The crash ID to be replayed from the corpus database.
-    ///
-    /// If set, `corpus_db` must also be specified. This mode retrieves the crashing input
-    /// associated with the given ID from the database and executes the property function.
-    #[arg(env = "FUZZTEST_REPLAY_ID", long, requires = "corpus_db")]
-    pub replay_id: Option<String>,
-
-    /// Replay all crashing inputs from the corpus database.
-    #[arg(env = "FUZZTEST_REPLAY_FINDINGS", long)]
-    pub replay_findings: bool,
-
-    /// Replay the corpus for a specified duration.
-    #[arg(env = "FUZZTEST_REPLAY_CORPUS_FOR", long)]
-    pub replay_corpus_for: Option<Duration>,
-
-    /// Time budget calculation type for replay corpus mode.
-    #[arg(env = "FUZZTEST_TIME_BUDGET_TYPE", long, value_enum, default_value_t = TimeBudgetType::PerTest)]
-    pub time_budget_type: TimeBudgetType,
-
-    /// The path to the corpus database.
-    ///
-    /// If set to non-empty, updates/queries the corpus database that contains coverage,
-    /// regression, and crashing inputs for each test binary and fuzz test.
-    #[arg(env = "FUZZTEST_CORPUS_DB", long)]
-    pub corpus_db: Option<String>,
-}
-
-impl FuzzTestOptions {
-    /// Evaluates the raw options and maps them to a strongly-typed domain `ExecutionMode`.
-    ///
-    /// This decouples raw environment/CLI option parsing from execution mode validation.
-    pub fn execution_mode(&self) -> ExecutionMode {
-        if let Some(replay_corpus_for) = self.replay_corpus_for {
-            return ExecutionMode::ReplayCorpus(ReplayCorpusOptions {
-                replay_corpus_for,
-                time_budget_type: self.time_budget_type,
-            });
-        }
-
-        if self.replay_findings {
-            return ExecutionMode::ReplayAllCrashes;
-        }
-
-        if let Some(replay_id) = &self.replay_id {
-            return ExecutionMode::ReplayCrash(ReplayCrashOptions { replay_id: replay_id.clone() });
-        }
-
-        if let Some(fuzz_for) = self.fuzz_for {
-            return ExecutionMode::Fuzz(FuzzOptions { fuzz_for, jobs: self.jobs.clone() });
-        }
-
-        ExecutionMode::SmokeTest
-    }
-}
+pub use fuzztest_options::{
+    ExecutionMode, FuzzFor, FuzzOptions, FuzzTestOptions, ReplayCorpusOptions, ReplayCrashOptions,
+    TimeBudgetType,
+};
 
 /// Returns a lazily-initialized static reference to the global `FuzzTestOptions`.
 pub fn get_fuzztest_options() -> &'static FuzzTestOptions {
@@ -119,64 +23,20 @@
     OPTIONS.get_or_init(|| FuzzTestOptions::parse_from(std::iter::empty::<OsString>()))
 }
 
-/// Strongly-typed domain execution mode for test runs.
-///
-/// This enum represents the parsed high-level user intent derived from `FuzzTestOptions`.
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum ExecutionMode {
-    /// Smoke test execution (regular unit test fallback).
-    SmokeTest,
-
-    /// Fuzzing mode.
-    Fuzz(FuzzOptions),
-
-    /// Replay a specific crash ID from the corpus database.
-    ReplayCrash(ReplayCrashOptions),
-
-    /// Replay all crashing inputs stored in the corpus database.
-    ReplayAllCrashes,
-
-    /// Replay corpus inputs for a specified duration.
-    ReplayCorpus(ReplayCorpusOptions),
-}
-
-/// Mode-specific options for continuous fuzzing.
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct FuzzOptions {
-    pub fuzz_for: FuzzFor,
-
-    /// If `jobs` is `None`, we won't specify the number of jobs while invoking Centipede and it
-    /// will use its own default value.
-    pub jobs: Option<usize>,
-}
-
-/// The duration or limit for fuzzing.
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum FuzzFor {
-    /// Fuzz indefinitely until it is manually stopped or a crash is found.
-    Indefinitely,
-
-    /// Fuzz for a specific duration.
-    Duration(Duration),
-}
-
-/// Mode-specific options for replaying a specific crashing input from the corpus database.
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct ReplayCrashOptions {
-    pub replay_id: String,
-}
-
-/// Mode-specific options for replaying corpus for a duration.
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct ReplayCorpusOptions {
-    pub replay_corpus_for: Duration,
-    pub time_budget_type: TimeBudgetType,
-}
-
-impl ExecutionMode {
+trait ExecutionModeExt {
     /// Prepares the concrete `ExecutionAction` payload for execution by building any required
     /// `CentipedeArgs` or temporary runtime assets.
-    pub fn prepare_action(
+    fn prepare_action(
+        &self,
+        options: &FuzzTestOptions,
+        current_test_name: &str,
+    ) -> anyhow::Result<ExecutionAction>;
+}
+
+impl ExecutionModeExt for ExecutionMode {
+    /// Prepares the concrete `ExecutionAction` payload for execution by building any required
+    /// `CentipedeArgs` or temporary runtime assets.
+    fn prepare_action(
         &self,
         options: &FuzzTestOptions,
         current_test_name: &str,
@@ -288,6 +148,9 @@
         add_arg(format!("--fuzztest_binary_identifier={binary_id}"))?;
 
         add_arg("--populate_binary_info=false".to_string())?;
+        // TODO(the-shank): provide a way to override this.
+        // allow more crashes to be reported when running with FuzzTest (default is 5)
+        add_arg("--max_num_crash_reports=20".to_string())?;
         add_arg("--fork_server=false".to_string())?;
 
         add_arg(format!("--print_runner_log={}", options.print_subprocess_log))?;
@@ -426,8 +289,7 @@
     options: &FuzzTestOptions,
     current_test_name: &str,
 ) -> ExecutionAction {
-    options
-        .execution_mode()
+    ExecutionMode::from_fuzztest_options(options)
         .prepare_action(options, current_test_name)
         .expect("failed to prepare execution action from fuzztest options")
 }
@@ -486,7 +348,10 @@
         let options = FuzzTestOptions::parse_from(std::iter::empty::<OsString>());
 
         expect_true!(options.fuzz_for.is_some());
-        expect_that!(options.execution_mode(), matches_pattern!(ExecutionMode::Fuzz(_)));
+        expect_that!(
+            ExecutionMode::from_fuzztest_options(&options),
+            matches_pattern!(ExecutionMode::Fuzz(_))
+        );
 
         // SAFETY: Cleaning up environment variables.
         unsafe {
@@ -504,7 +369,7 @@
         let options = FuzzTestOptions::parse_from(std::iter::empty::<OsString>());
 
         expect_that!(options.fuzz_for, eq(Some(FuzzFor::Indefinitely)));
-        let mode = options.execution_mode();
+        let mode = ExecutionMode::from_fuzztest_options(&options);
         let ExecutionMode::Fuzz(fuzz_opts) = mode else {
             panic!("Expected ExecutionMode::Fuzz");
         };