cargo-fuzztest: add fuzztest-options dependency and CLI option parsing

Update cargo-fuzztest to integrate fuzztest-options crate for parsing CLI flags and environment variables.

For this initial minimal version, cargo-fuzztest parses options and executes all fuzz tests in smoke-test mode.

Changes:
- Add fuzztest-options dependency to cargo_fuzztest crate in BUILD and Cargo.toml.
- Add parse_cli_options() to cargo_fuzztest to parse CLI flags and handle the "fuzztest" subcommand prefix.
- Update FuzztestRunner to accept FuzzTestOptions.
PiperOrigin-RevId: 964790856
diff --git a/rust/cargo_fuzztest/BUILD b/rust/cargo_fuzztest/BUILD
index 8152fc7..2b2fae5 100644
--- a/rust/cargo_fuzztest/BUILD
+++ b/rust/cargo_fuzztest/BUILD
@@ -22,6 +22,7 @@
     edition = "2024",
     visibility = ["@com_google_fuzztest//rust:__subpackages__"],
     deps = [
+        "@com_google_fuzztest//rust/options:fuzztest_options",
         "@crate_index//:anyhow",  # v1
         "@crate_index//:clap",  # v4
         "@crate_index//:serde",  # v1
@@ -36,6 +37,7 @@
     visibility = ["@com_google_fuzztest//rust:__subpackages__"],
     deps = [
         ":cargo_fuzztest",
+        "@com_google_fuzztest//rust/options:fuzztest_options",
         "@crate_index//:anyhow",  # v1
         "@crate_index//:clap",  # v4
     ],
@@ -43,6 +45,7 @@
 
 rust_test(
     name = "cargo_fuzztest_lib_test",
+    args = ["--test-threads=1"],
     crate = ":cargo_fuzztest",
     edition = "2024",
     deps = [
@@ -81,6 +84,7 @@
     edition = "2024",
     deps = [
         ":cargo_fuzztest",
+        "@com_google_fuzztest//rust/options:fuzztest_options",
         "@crate_index//:anyhow",  # v1
         "@crate_index//:googletest",
     ],
diff --git a/rust/cargo_fuzztest/src/lib.rs b/rust/cargo_fuzztest/src/lib.rs
index 510cdc0..5e6254d 100644
--- a/rust/cargo_fuzztest/src/lib.rs
+++ b/rust/cargo_fuzztest/src/lib.rs
@@ -17,6 +17,7 @@
 
 use anyhow::{Context, Result};
 use clap::Parser;
+pub use fuzztest_options::{ExecutionMode, FuzzTestOptions};
 use std::env;
 use std::ffi::OsString;
 use std::path::{Path, PathBuf};
@@ -28,31 +29,27 @@
 
 #[derive(Parser, Debug, Clone, Default)]
 pub struct CargoFuzzTestOptions {
+    #[command(flatten)]
+    pub fuzztest_options: FuzzTestOptions,
+
     /// List all fuzz tests in the crate without running them.
     #[arg(long)]
     pub list: bool,
 }
 
 impl CargoFuzzTestOptions {
-    /// Returns the active domain `ExecutionMode` derived from CLI options.
+    /// Returns the `ExecutionMode` derived from CLI options.
     pub fn execution_mode(&self) -> ExecutionMode {
         if self.list {
             return ExecutionMode::ListFuzzTests;
         }
+
+        // TODO(the-shank): add support for other modes.
         ExecutionMode::SmokeTest
     }
 }
 
-/// Domain execution mode for `cargo-fuzztest`.
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum ExecutionMode {
-    /// List all discovered fuzz tests without running them.
-    ListFuzzTests,
-
-    SmokeTest,
-}
-
-/// Queries cargo via `cargo -vV` to discover the default host target triple (e.g. `x86_64-unknown-linux-gnu`).
+/// Queries Cargo via `cargo -vV` to discover the default host target triple (e.g. `x86_64-unknown-linux-gnu`).
 pub fn get_host_target_triple() -> anyhow::Result<String> {
     let cargo_bin = get_cargo_bin();
     let output = Command::new(&cargo_bin)
@@ -141,8 +138,11 @@
                 continue;
             }
             if let Ok(val) = serde_json::from_str::<serde_json::Value>(trimmed) {
-                let is_compiler_artifact = val["reason"] == "compiler-artifact";
-                let is_test_profile = val["profile"]["test"].as_bool().unwrap_or(false);
+                let is_compiler_artifact =
+                    val.get("reason").and_then(|r| r.as_str()) == Some("compiler-artifact");
+                let is_test_profile =
+                    val.get("profile").and_then(|p| p.get("test")).and_then(|t| t.as_bool())
+                        == Some(true);
 
                 if is_compiler_artifact
                     && is_test_profile
@@ -178,6 +178,9 @@
             ExecutionMode::SmokeTest => {
                 // nothing to be done
             }
+            _ => {
+                // TODO(the-shank): add support for other modes.
+            }
         }
 
         cmd
@@ -253,7 +256,7 @@
 
     #[gtest]
     fn test_build_run_command_list() {
-        let options = CargoFuzzTestOptions { list: true };
+        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 args: Vec<String> = cmd.get_args().map(|a| a.to_string_lossy().to_string()).collect();
@@ -262,13 +265,13 @@
 
     #[gtest]
     fn test_execution_mode_smoke_test() {
-        let options = CargoFuzzTestOptions { list: false };
+        let options = CargoFuzzTestOptions { list: false, ..Default::default() };
         assert_eq!(options.execution_mode(), ExecutionMode::SmokeTest);
     }
 
     #[gtest]
     fn test_execution_mode_list_fuzz_tests() {
-        let options = CargoFuzzTestOptions { list: true };
+        let options = CargoFuzzTestOptions { list: true, ..Default::default() };
         assert_eq!(options.execution_mode(), ExecutionMode::ListFuzzTests);
     }
 
diff --git a/rust/cargo_fuzztest/tests/runner_test.rs b/rust/cargo_fuzztest/tests/runner_test.rs
index cad9800..344f1e5 100644
--- a/rust/cargo_fuzztest/tests/runner_test.rs
+++ b/rust/cargo_fuzztest/tests/runner_test.rs
@@ -10,7 +10,6 @@
     assert!(binary_path.exists(), "Sample fuzz binary does not exist at {}", binary_path.display());
 
     let options = CargoFuzzTestOptions::default();
-
     let runner = FuzztestRunner::new("sample-host-triple".to_string(), options);
 
     let mut cmd = runner.build_run_command(&binary_path);
@@ -26,7 +25,7 @@
 
     assert!(binary_path.exists(), "Sample fuzz binary does not exist at {}", binary_path.display());
 
-    let options = CargoFuzzTestOptions { list: true };
+    let options = CargoFuzzTestOptions { list: true, ..Default::default() };
 
     let runner = FuzztestRunner::new("sample-host-triple".to_string(), options);
 
diff --git a/rust/options/src/lib.rs b/rust/options/src/lib.rs
index 2e49538..f211dbc 100644
--- a/rust/options/src/lib.rs
+++ b/rust/options/src/lib.rs
@@ -109,6 +109,10 @@
 
     /// Replay corpus inputs for a specified duration.
     ReplayCorpus(ReplayCorpusOptions),
+
+    /// List all discovered fuzz tests without running them.
+    /// Currently only supported via `cargo-fuzztest`.
+    ListFuzzTests,
 }
 
 impl ExecutionMode {
diff --git a/rust/src/options.rs b/rust/src/options.rs
index f7da7ee..073cde1 100644
--- a/rust/src/options.rs
+++ b/rust/src/options.rs
@@ -79,6 +79,9 @@
                 .context("while attempting to build CentipedeArgs for replay all crashes mode")?;
                 Ok(ExecutionAction::ReplayAllCrashes { args: centipede_args, list_file })
             }
+            ExecutionMode::ListFuzzTests => {
+                anyhow::bail!("ListFuzzTest mode is supported only in cargo-fuzztest");
+            }
         }
     }
 }
@@ -254,6 +257,7 @@
                 add_arg(format!("--fuzztest_time_limit_per_test={time_limit}"))?;
             }
             ExecutionMode::SmokeTest => unreachable!(),
+            ExecutionMode::ListFuzzTests => unreachable!(),
         }
 
         Ok(Some(Self::new(args, opt_workdir)))