From 7c5c7e584d212564ec7e44fd5fb273393fbe636b Mon Sep 17 00:00:00 2001 From: Shashank Sharma Date: Sat, 1 Aug 2026 08:50:30 -0700 Subject: [PATCH] No public description PiperOrigin-RevId: 957642803 --- rust/BUILD | 9 +- rust/cargo_fuzztest/src/lib.rs | 109 ++++++++++-- rust/cargo_fuzztest/tests/e2e_cli_test.rs | 11 +- rust/cargo_fuzztest/tests/runner_test.rs | 49 +++++- rust/coverage/BUILD | 9 +- rust/engine/BUILD | 9 +- rust/engine/src/engine_ffi.rs | 5 +- rust/engine/src/lib.rs | 3 + rust/options/BUILD | 9 +- rust/options/src/lib.rs | 198 ++++++++++++++++++---- rust/src/domains.rs | 4 +- rust/src/domains/arbitrary.rs | 34 ++-- rust/src/domains/containers.rs | 7 +- rust/src/domains/utility.rs | 24 ++- rust/src/internal.rs | 3 +- rust/src/lib.rs | 2 + rust/src/options.rs | 139 ++++++++++----- rust/src/worker.rs | 24 ++- 18 files changed, 490 insertions(+), 158 deletions(-) diff --git a/rust/BUILD b/rust/BUILD index eb43c1107..abbffd102 100644 --- a/rust/BUILD +++ b/rust/BUILD @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") +load("@rules_rust//rust:defs.bzl", "rust_clippy", "rust_library", "rust_test") licenses(["notice"]) @@ -57,3 +57,10 @@ rust_test( "@crate_index//:googletest", ], ) + +rust_clippy( + name = "fuzztest_clippy", + deps = [ + ":fuzztest", + ], +) diff --git a/rust/cargo_fuzztest/src/lib.rs b/rust/cargo_fuzztest/src/lib.rs index b7c57ebc2..e2af0905d 100644 --- a/rust/cargo_fuzztest/src/lib.rs +++ b/rust/cargo_fuzztest/src/lib.rs @@ -18,8 +18,8 @@ use anyhow::{Context, Result}; use clap::Parser; pub use fuzztest_options::{ - ExecutionMode, FuzzFor, FuzzOptions, FuzzTestOptions, ListCrashIdsOptions, ReplayCorpusOptions, - ReplayCrashOptions, TimeBudgetType, + ExecutionMode, FuzzOptions, FuzzTestOptions, ListCrashIdsOptions, ReplayCorpusOptions, + ReplayCrashOptions, RunDuration, TimeBudgetType, }; use std::env; use std::ffi::OsString; @@ -95,7 +95,10 @@ impl CargoFuzzTestOptions { if self.test_path.is_some() { self.check_centipede_binary_path_is_set()?; ExecutionMode::Fuzz(FuzzOptions { - fuzz_for: self.fuzztest_options.fuzz_for.unwrap_or(FuzzFor::Indefinitely), + fuzz_for: self + .fuzztest_options + .fuzz_for + .unwrap_or(RunDuration::Indefinitely), jobs: self.fuzztest_options.jobs, }) } else { @@ -246,14 +249,7 @@ impl FuzztestRunner { 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()); - } - } + cmd.env("FUZZTEST_FUZZ_FOR", fuzz_for.to_string()); if let Some(jobs) = jobs { cmd.env("FUZZTEST_JOBS", jobs.to_string()); } @@ -610,7 +606,60 @@ mod tests { assert_eq!( mode, ExecutionMode::ReplayCorpus(ReplayCorpusOptions { - replay_corpus_for: "10s".parse().unwrap(), + replay_corpus_for: "10s".parse().expect("valid duration string"), + time_budget_type: TimeBudgetType::PerTest, + jobs: None, + }) + ); + } + + #[gtest] + fn test_cli_option_parsing_replay_corpus_for_inf() { + let parsed = CargoFuzzTestOptions::try_parse_from([ + "cargo-fuzztest", + "--replay-corpus-for", + "inf", + "--corpus-db", + "/tmp/corpus_db", + "--centipede-binary-path", + "/custom/centipede", + ]) + .expect("valid replay-corpus-for inf should parse successfully"); + + assert_eq!(parsed.fuzztest_options.replay_corpus_for, Some(RunDuration::Indefinitely)); + assert_eq!(parsed.fuzztest_options.time_budget_type, TimeBudgetType::PerTest); + 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::ReplayCorpus(ReplayCorpusOptions { + replay_corpus_for: RunDuration::Indefinitely, + time_budget_type: TimeBudgetType::PerTest, + jobs: None, + }) + ); + } + + #[gtest] + fn test_cli_option_parsing_replay_corpus_for_infinity() { + let parsed = CargoFuzzTestOptions::try_parse_from([ + "cargo-fuzztest", + "--replay-corpus-for", + "infinity", + "--corpus-db", + "/tmp/corpus_db", + "--centipede-binary-path", + "/custom/centipede", + ]) + .expect("valid replay-corpus-for infinity should parse successfully"); + + assert_eq!(parsed.fuzztest_options.replay_corpus_for, Some(RunDuration::Indefinitely)); + let mode = parsed.execution_mode().expect("valid execution mode"); + assert_eq!( + mode, + ExecutionMode::ReplayCorpus(ReplayCorpusOptions { + replay_corpus_for: RunDuration::Indefinitely, time_budget_type: TimeBudgetType::PerTest, jobs: None, }) @@ -794,4 +843,40 @@ mod tests { Some("/custom/centipede".to_string()) ))); } + + #[gtest] + fn test_build_run_command_replay_corpus_indefinite() { + let options = CargoFuzzTestOptions { + fuzztest_options: FuzzTestOptions { + replay_corpus_for: Some(RunDuration::Indefinitely), + time_budget_type: TimeBudgetType::Total, + 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: 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(); + + assert!(envs.contains(&("FUZZTEST_REPLAY_CORPUS_FOR".to_string(), Some("inf".to_string())))); + assert!( + envs.contains(&("FUZZTEST_TIME_BUDGET_TYPE".to_string(), Some("total".to_string()))) + ); + assert!( + envs.contains(&("FUZZTEST_CORPUS_DB".to_string(), Some("/tmp/corpus_db".to_string()))) + ); + assert!(envs.contains(&( + "FUZZTEST_CENTIPEDE_BINARY_PATH".to_string(), + Some("/custom/centipede".to_string()) + ))); + } } diff --git a/rust/cargo_fuzztest/tests/e2e_cli_test.rs b/rust/cargo_fuzztest/tests/e2e_cli_test.rs index b77256f6a..04ed647ab 100644 --- a/rust/cargo_fuzztest/tests/e2e_cli_test.rs +++ b/rust/cargo_fuzztest/tests/e2e_cli_test.rs @@ -365,7 +365,7 @@ fn test_cargo_fuzztest_e2e_replay_corpus_total_budget() { // 2. Run cargo-fuzztest CLI with --replay-corpus-for and --time-budget-type total. let mut cmd = setup_cargo_fuzztest_command(&sample_crate_path, temp_target_dir.path()); cmd.arg("__fuzztest_mod__sample_fuzztest_target::sample_fuzztest_target") - .arg("--replay-corpus-for=3s") + .arg("--replay-corpus-for=4.5s") .arg("--time-budget-type=total") .arg("--corpus-db") .arg(temp_db_dir.path()) @@ -376,12 +376,15 @@ fn test_cargo_fuzztest_e2e_replay_corpus_total_budget() { let stderr_str = String::from_utf8_lossy(&output.stderr); let stdout_str = String::from_utf8_lossy(&output.stdout); + eprintln!("tmp:: stderr:\n{stderr_str}"); + eprintln!("tmp:: stdout:\n{stdout_str}"); + expect_true!(output.status.success()); expect_true!( stderr_str.contains( - "Replaying __fuzztest_mod__sample_fuzztest_target.sample_fuzztest_target for 1s" + "Replaying __fuzztest_mod__sample_fuzztest_target.sample_fuzztest_target for 1.5s" ) || stdout_str.contains( - "Replaying __fuzztest_mod__sample_fuzztest_target.sample_fuzztest_target for 1s" + "Replaying __fuzztest_mod__sample_fuzztest_target.sample_fuzztest_target for 1.5s" ) ); } @@ -409,7 +412,7 @@ fn test_cargo_fuzztest_e2e_list_crash_ids() { 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(¢ipede_bin) .arg("--corpus-db") diff --git a/rust/cargo_fuzztest/tests/runner_test.rs b/rust/cargo_fuzztest/tests/runner_test.rs index b865b1d4d..14bd430b0 100644 --- a/rust/cargo_fuzztest/tests/runner_test.rs +++ b/rust/cargo_fuzztest/tests/runner_test.rs @@ -2,7 +2,7 @@ mod common; use cargo_fuzztest::{CargoFuzzTestOptions, FuzztestRunner}; use common::get_sample_test_bin_path; -use fuzztest_options::{FuzzFor, FuzzTestOptions, TimeBudgetType}; +use fuzztest_options::{FuzzTestOptions, RunDuration, TimeBudgetType}; use googletest::prelude::*; #[gtest] @@ -45,10 +45,8 @@ fn test_runner_build_run_command_with_target() { #[gtest] fn test_runner_build_run_command_with_duration() { let binary_path = get_sample_test_bin_path("sample_fuzz_crate"); - let fuzztest_options = FuzzTestOptions { - fuzz_for: Some(FuzzFor::Duration("5s".parse().unwrap())), - ..Default::default() - }; + let fuzztest_options = + FuzzTestOptions { fuzz_for: Some("5s".parse().unwrap()), ..Default::default() }; let options = CargoFuzzTestOptions { fuzztest_options, centipede_binary_path: Some("/custom/path/to/centipede".to_string()), @@ -68,7 +66,7 @@ fn test_runner_build_run_command_with_duration() { fn test_runner_build_run_command_with_indefinitely() { let binary_path = get_sample_test_bin_path("sample_fuzz_crate"); let fuzztest_options = - FuzzTestOptions { fuzz_for: Some(FuzzFor::Indefinitely), ..Default::default() }; + FuzzTestOptions { fuzz_for: Some(RunDuration::Indefinitely), ..Default::default() }; let options = CargoFuzzTestOptions { fuzztest_options, centipede_binary_path: Some("/custom/path/to/centipede".to_string()), @@ -109,7 +107,7 @@ fn test_runner_build_run_command_with_jobs() { let binary_path = get_sample_test_bin_path("sample_fuzz_crate"); let fuzztest_options = FuzzTestOptions { jobs: Some(4), - fuzz_for: Some(FuzzFor::Duration("10s".parse().expect("static valid duration string"))), + fuzz_for: Some("10s".parse().expect("static valid duration string")), ..Default::default() }; let options = CargoFuzzTestOptions { @@ -288,6 +286,43 @@ fn test_runner_build_run_command_with_replay_corpus() { ))); } +#[gtest] +fn test_runner_build_run_command_with_replay_corpus_indefinitely() { + let binary_path = get_sample_test_bin_path("sample_fuzz_crate"); + let fuzztest_options = FuzzTestOptions { + replay_corpus_for: Some(RunDuration::Indefinitely), + time_budget_type: TimeBudgetType::Total, + 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)> = 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_CORPUS_FOR".to_string(), Some("inf".to_string()))) + ); + expect_true!( + envs.contains(&("FUZZTEST_TIME_BUDGET_TYPE".to_string(), Some("total".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_corpus_missing_centipede_binary_path_errors() { let fuzztest_options = FuzzTestOptions { diff --git a/rust/coverage/BUILD b/rust/coverage/BUILD index 38f65f7b3..3344a71c9 100644 --- a/rust/coverage/BUILD +++ b/rust/coverage/BUILD @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") +load("@rules_rust//rust:defs.bzl", "rust_clippy", "rust_library", "rust_test") licenses(["notice"]) @@ -48,3 +48,10 @@ rust_test( "@crate_index//:googletest", ], ) + +rust_clippy( + name = "coverage_clippy", + deps = [ + ":coverage", + ], +) diff --git a/rust/engine/BUILD b/rust/engine/BUILD index 86036582d..42dc85354 100644 --- a/rust/engine/BUILD +++ b/rust/engine/BUILD @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -load("@rules_rust//rust:defs.bzl", "rust_library") +load("@rules_rust//rust:defs.bzl", "rust_clippy", "rust_library") licenses(["notice"]) @@ -30,3 +30,10 @@ rust_library( "@com_google_fuzztest//centipede:engine_worker", ], ) + +rust_clippy( + name = "engine_clippy", + deps = [ + ":engine", + ], +) diff --git a/rust/engine/src/engine_ffi.rs b/rust/engine/src/engine_ffi.rs index 74badf48f..276b3e116 100644 --- a/rust/engine/src/engine_ffi.rs +++ b/rust/engine/src/engine_ffi.rs @@ -185,10 +185,7 @@ impl FuzzTestUint64sView { if self.data.is_null() { &[] } else { - ptr::slice_from_raw_parts( - self.data as *const u8, - self.size * core::mem::size_of::(), - ) + ptr::slice_from_raw_parts(self.data as *const u8, self.size * size_of::()) } } } diff --git a/rust/engine/src/lib.rs b/rust/engine/src/lib.rs index 58f9be3c8..f6e0bf838 100644 --- a/rust/engine/src/lib.rs +++ b/rust/engine/src/lib.rs @@ -12,6 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +#![deny(clippy::absolute_paths)] +#![deny(unused_imports)] + pub mod engine_ffi; use std::marker::PhantomData; diff --git a/rust/options/BUILD b/rust/options/BUILD index 2fc95f343..b56f9a655 100644 --- a/rust/options/BUILD +++ b/rust/options/BUILD @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") +load("@rules_rust//rust:defs.bzl", "rust_clippy", "rust_library", "rust_test") licenses(["notice"]) @@ -42,3 +42,10 @@ rust_test( "@crate_index//:googletest", ], ) + +rust_clippy( + name = "fuzztest_options_clippy", + deps = [ + ":fuzztest_options", + ], +) diff --git a/rust/options/src/lib.rs b/rust/options/src/lib.rs index 070b00dae..93f9cc40c 100644 --- a/rust/options/src/lib.rs +++ b/rust/options/src/lib.rs @@ -12,11 +12,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -// This module provides the core command-line flag and environment variable options -// structure (`FuzzTestOptions`) and domain execution modes (`ExecutionMode`). +//! This module provides the core command-line flag and environment variable options +//! structure (`FuzzTestOptions`) and domain execution modes (`ExecutionMode`). +#![deny(clippy::absolute_paths)] +#![deny(unused_imports)] + +use anyhow::Context; use clap::{Parser, ValueEnum}; -use humantime::Duration; +use std::fmt; +use std::fmt::Display; +use std::fmt::Formatter; +use std::str::FromStr; /// Time budget calculation type for replay corpus mode. #[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Default)] @@ -26,20 +33,6 @@ pub enum TimeBudgetType { 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 { - 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 { @@ -51,8 +44,8 @@ pub struct FuzzTestOptions { /// /// 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, + #[arg(env = "FUZZTEST_FUZZ_FOR", long)] + pub fuzz_for: Option, /// If true, subprocess logs are printed after every batch. Note that crash logs are always /// printed regardless of this flag's value. @@ -93,8 +86,11 @@ pub struct FuzzTestOptions { pub replay_findings: bool, /// Replay the corpus for a specified duration. + /// + /// Accepts a human-readable duration (e.g., `5s`, `10m`, `1h`) or `inf` / `infinity` + /// to replay indefinitely until stopped manually. #[arg(env = "FUZZTEST_REPLAY_CORPUS_FOR", long, requires = "corpus_db")] - pub replay_corpus_for: Option, + pub replay_corpus_for: Option, /// Time budget calculation type for replay corpus mode. #[arg(env = "FUZZTEST_TIME_BUDGET_TYPE", long, value_enum, default_value_t = TimeBudgetType::PerTest)] @@ -155,7 +151,7 @@ impl ExecutionMode { return ExecutionMode::ReplayCorpus(ReplayCorpusOptions { replay_corpus_for, time_budget_type: options.time_budget_type, - jobs: options.jobs.clone(), + jobs: options.jobs, }); } @@ -169,10 +165,7 @@ impl ExecutionMode { // Continuous fuzzing mode is selected if an explicit duration/budget (`fuzz_for`) is specified. if let Some(fuzz_for) = &options.fuzz_for { - return ExecutionMode::Fuzz(FuzzOptions { - fuzz_for: *fuzz_for, - jobs: options.jobs.clone(), - }); + return ExecutionMode::Fuzz(FuzzOptions { fuzz_for: *fuzz_for, jobs: options.jobs }); } ExecutionMode::SmokeTest @@ -182,21 +175,46 @@ impl ExecutionMode { /// Mode-specific options for continuous fuzzing. #[derive(Debug, Clone, PartialEq, Eq)] pub struct FuzzOptions { - pub fuzz_for: FuzzFor, + pub fuzz_for: RunDuration, /// 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, } -/// The duration or limit for fuzzing. +/// The duration or limit for fuzzing or replaying corpus. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum FuzzFor { - /// Fuzz indefinitely until it is manually stopped or a crash is found. +pub enum RunDuration { + /// Run indefinitely until manually stopped or a crash is found. Indefinitely, - /// Fuzz for a specific duration. - Duration(Duration), + /// Run for a specific fixed duration. + Fixed(humantime::Duration), +} + +impl FromStr for RunDuration { + type Err = anyhow::Error; + + fn from_str(s: &str) -> anyhow::Result { + let s_lower = s.trim().to_lowercase(); + if s_lower == "inf" || s_lower == "infinity" { + Ok(RunDuration::Indefinitely) + } else { + let duration: humantime::Duration = s + .parse() + .with_context(|| format!("while attempting to parse duration string '{s}'"))?; + Ok(RunDuration::Fixed(duration)) + } + } +} + +impl Display for RunDuration { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + RunDuration::Indefinitely => write!(f, "inf"), + RunDuration::Fixed(duration) => write!(f, "{duration}"), + } + } } /// Mode-specific options for replaying a specific crashing input from the corpus database. @@ -208,7 +226,7 @@ pub struct ReplayCrashOptions { /// Mode-specific options for replaying corpus for a duration. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ReplayCorpusOptions { - pub replay_corpus_for: Duration, + pub replay_corpus_for: RunDuration, pub time_budget_type: TimeBudgetType, /// If `jobs` is `None`, we won't specify the number of jobs while invoking Centipede and it /// will use its own default value. @@ -227,6 +245,39 @@ mod tests { use googletest::prelude::*; use std::ffi::OsString; + #[gtest] + fn test_duration_from_str_inf() { + let duration: RunDuration = "inf".parse().expect("failed to parse 'inf'"); + expect_that!(duration, eq(RunDuration::Indefinitely)); + } + + #[gtest] + fn test_duration_from_str_infinity() { + let duration: RunDuration = "infinity".parse().expect("failed to parse 'infinity'"); + expect_that!(duration, eq(RunDuration::Indefinitely)); + } + + #[gtest] + fn test_duration_from_str_fixed() { + let duration: RunDuration = "10s".parse().expect("failed to parse '10s'"); + let expected_fixed = humantime::Duration::from(std::time::Duration::from_secs(10)); + expect_that!(duration, eq(RunDuration::Fixed(expected_fixed))); + } + + #[gtest] + fn test_duration_from_str_invalid() { + let result: Result = "invalid_duration".parse(); + expect_true!(result.is_err()); + } + + #[gtest] + fn test_duration_display() { + expect_that!(RunDuration::Indefinitely.to_string(), eq("inf")); + let fixed = + RunDuration::Fixed(humantime::Duration::from(std::time::Duration::from_secs(10))); + expect_that!(fixed.to_string(), eq("10s")); + } + #[gtest] fn test_replay_id_requires_corpus_db() { // SAFETY: Testing environment parsing in single-threaded context. @@ -278,6 +329,7 @@ mod tests { let options = FuzzTestOptions::parse_from(std::iter::empty::()); expect_that!(options.jobs, eq(Some(4))); + // Setting jobs alone should not enter fuzzing mode; it defaults to smoke test mode. expect_that!(ExecutionMode::from_fuzztest_options(&options), eq(&ExecutionMode::SmokeTest)); @@ -302,7 +354,7 @@ mod tests { expect_that!( ExecutionMode::from_fuzztest_options(&options), eq(&ExecutionMode::Fuzz(FuzzOptions { - fuzz_for: FuzzFor::Duration(expected_duration), + fuzz_for: RunDuration::Fixed(expected_duration), jobs: Some(4), })) ); @@ -422,11 +474,60 @@ mod tests { let options = result .expect("parsing should succeed when both replay_corpus_for and corpus_db are present"); - expect_that!(options.replay_corpus_for, eq(Some("10s".parse().unwrap()))); + expect_that!( + options.replay_corpus_for, + eq(Some("10s".parse().expect("valid duration string"))) + ); expect_that!(options.corpus_db.as_deref(), eq(Some("/tmp/corpus_db"))); expect_that!(options.time_budget_type, eq(TimeBudgetType::PerTest)); } + #[gtest] + fn test_replay_corpus_for_inf_env_succeeds() { + // SAFETY: Testing environment parsing in single-threaded context. + unsafe { + std::env::set_var("FUZZTEST_REPLAY_CORPUS_FOR", "inf"); + std::env::set_var("FUZZTEST_CORPUS_DB", "/tmp/corpus_db"); + } + + let result = FuzzTestOptions::try_parse_from(std::iter::empty::()); + + // SAFETY: Cleaning up environment variables. + unsafe { + std::env::remove_var("FUZZTEST_REPLAY_CORPUS_FOR"); + std::env::remove_var("FUZZTEST_CORPUS_DB"); + } + + let options = result.expect( + "parsing should succeed when replay_corpus_for is inf and corpus_db is present", + ); + expect_that!(options.replay_corpus_for, eq(Some(RunDuration::Indefinitely))); + expect_that!(options.corpus_db.as_deref(), eq(Some("/tmp/corpus_db"))); + } + + #[gtest] + fn test_replay_corpus_for_infinity_env_succeeds() { + // SAFETY: Testing environment parsing in single-threaded context. + unsafe { + std::env::set_var("FUZZTEST_REPLAY_CORPUS_FOR", "infinity"); + std::env::set_var("FUZZTEST_CORPUS_DB", "/tmp/corpus_db"); + } + + let result = FuzzTestOptions::try_parse_from(std::iter::empty::()); + + // SAFETY: Cleaning up environment variables. + unsafe { + std::env::remove_var("FUZZTEST_REPLAY_CORPUS_FOR"); + std::env::remove_var("FUZZTEST_CORPUS_DB"); + } + + let options = result.expect( + "parsing should succeed when replay_corpus_for is infinity and corpus_db is present", + ); + expect_that!(options.replay_corpus_for, eq(Some(RunDuration::Indefinitely))); + expect_that!(options.corpus_db.as_deref(), eq(Some("/tmp/corpus_db"))); + } + #[gtest] fn test_replay_corpus_for_with_total_time_budget() { // SAFETY: Testing environment parsing in single-threaded context. @@ -446,7 +547,34 @@ mod tests { } let options = result.expect("parsing should succeed with total time budget type"); - expect_that!(options.replay_corpus_for, eq(Some("10s".parse().unwrap()))); + expect_that!( + options.replay_corpus_for, + eq(Some("10s".parse().expect("valid duration string"))) + ); + expect_that!(options.corpus_db.as_deref(), eq(Some("/tmp/corpus_db"))); + expect_that!(options.time_budget_type, eq(TimeBudgetType::Total)); + } + + #[gtest] + fn test_replay_corpus_for_inf_with_total_time_budget() { + // SAFETY: Testing environment parsing in single-threaded context. + unsafe { + std::env::set_var("FUZZTEST_REPLAY_CORPUS_FOR", "inf"); + std::env::set_var("FUZZTEST_TIME_BUDGET_TYPE", "total"); + std::env::set_var("FUZZTEST_CORPUS_DB", "/tmp/corpus_db"); + } + + let result = FuzzTestOptions::try_parse_from(std::iter::empty::()); + + // SAFETY: Cleaning up environment variables. + unsafe { + std::env::remove_var("FUZZTEST_REPLAY_CORPUS_FOR"); + std::env::remove_var("FUZZTEST_TIME_BUDGET_TYPE"); + std::env::remove_var("FUZZTEST_CORPUS_DB"); + } + + let options = result.expect("parsing should succeed with total time budget type and inf"); + expect_that!(options.replay_corpus_for, eq(Some(RunDuration::Indefinitely))); expect_that!(options.corpus_db.as_deref(), eq(Some("/tmp/corpus_db"))); expect_that!(options.time_budget_type, eq(TimeBudgetType::Total)); } diff --git a/rust/src/domains.rs b/rust/src/domains.rs index 0447752a4..a873c5990 100644 --- a/rust/src/domains.rs +++ b/rust/src/domains.rs @@ -17,6 +17,8 @@ pub mod containers; pub mod range; pub mod tuple_of; pub mod utility; +use ::serde::de::DeserializeOwned; +use ::serde::Serialize; use anyhow; use anyhow::Context; @@ -118,7 +120,7 @@ pub trait Domain { /// the CorpusValue could the owned data structured that the `&str` points to (eg: String). /// The CorpusValue type should implement `serde::Serialize`, `serde::de::DeserializeOwned` and /// `Clone`. - type CorpusValue: ::serde::Serialize + ::serde::de::DeserializeOwned + Clone; + type CorpusValue: Serialize + DeserializeOwned + Clone; /// Initializes a new value drawn from the domain. fn init(&self, rng: &mut dyn rand::Rng) -> anyhow::Result; diff --git a/rust/src/domains/arbitrary.rs b/rust/src/domains/arbitrary.rs index 3ad8bdbad..e2167bf55 100644 --- a/rust/src/domains/arbitrary.rs +++ b/rust/src/domains/arbitrary.rs @@ -16,6 +16,9 @@ use super::utility::choose_value; use super::utility::mutate_integer; use super::utility::shrink_towards; use super::Domain; +use std::char; +use std::fmt; +use std::marker::PhantomData; use anyhow; use rand::RngExt; @@ -39,19 +42,18 @@ use rand::RngExt; /// let sample = arbitrary_i32.init(&mut rng); /// assert!(sample.is_ok()); /// ``` - pub struct Arbitrary { - _phantom: std::marker::PhantomData, + _phantom: PhantomData, } impl Clone for Arbitrary { fn clone(&self) -> Self { - Self { _phantom: std::marker::PhantomData } + Self { _phantom: PhantomData } } } -impl std::fmt::Debug for Arbitrary { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl fmt::Debug for Arbitrary { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Arbitrary").field("_phantom", &self._phantom).finish() } } @@ -59,14 +61,14 @@ impl std::fmt::Debug for Arbitrary { // We cannot just use `#[derive(Default)]` because `T` might not be `Default`. impl Default for Arbitrary { fn default() -> Self { - Self { _phantom: std::marker::PhantomData } + Self { _phantom: PhantomData } } } impl Arbitrary { /// Creates a new `Arbitrary` domain for the given type `T`. pub fn new() -> Self { - Self { _phantom: std::marker::PhantomData } + Self { _phantom: PhantomData } } } @@ -254,7 +256,7 @@ fn map_int_to_char(u: u32) -> char { NUM_VALID_CODEPOINTS ); let val = if u >= SURROGATE_START { u + (SURROGATE_END - SURROGATE_START + 1) } else { u }; - std::char::from_u32(val).unwrap() + char::from_u32(val).unwrap() } impl Domain for Arbitrary { @@ -745,19 +747,13 @@ mod tests { assert_eq!(map_int_to_char(0), '\u{0000}'); let before_surrogate = SURROGATE_START - 1; - assert_eq!( - map_char_to_int(std::char::from_u32(before_surrogate).unwrap()), - before_surrogate - ); - assert_eq!( - map_int_to_char(before_surrogate), - std::char::from_u32(before_surrogate).unwrap() - ); + assert_eq!(map_char_to_int(char::from_u32(before_surrogate).unwrap()), before_surrogate); + assert_eq!(map_int_to_char(before_surrogate), char::from_u32(before_surrogate).unwrap()); let after_surrogate = SURROGATE_END + 1; - let mapped_after_surrogate = map_char_to_int(std::char::from_u32(after_surrogate).unwrap()); + let mapped_after_surrogate = map_char_to_int(char::from_u32(after_surrogate).unwrap()); assert_eq!(mapped_after_surrogate, SURROGATE_START); - assert_eq!(map_int_to_char(SURROGATE_START), std::char::from_u32(after_surrogate).unwrap()); + assert_eq!(map_int_to_char(SURROGATE_START), char::from_u32(after_surrogate).unwrap()); assert_eq!(map_char_to_int('\u{10FFFF}'), NUM_VALID_CODEPOINTS - 1); assert_eq!(map_int_to_char(NUM_VALID_CODEPOINTS - 1), '\u{10FFFF}'); @@ -776,7 +772,7 @@ mod tests { while value != '\0' && iterations < MAX_ITERATIONS { domain.mutate(&mut value, &mut rng, true).unwrap(); // Ensure that the value is always a valid char after mutation. - assert!(std::char::from_u32(value as u32).is_some()); + assert!(char::from_u32(value as u32).is_some()); iterations += 1; } assert_eq!( diff --git a/rust/src/domains/containers.rs b/rust/src/domains/containers.rs index 3c34565c9..a15e28ca2 100644 --- a/rust/src/domains/containers.rs +++ b/rust/src/domains/containers.rs @@ -1,4 +1,5 @@ use rand::RngExt; +use std::fmt; use super::Domain; @@ -99,8 +100,8 @@ impl Clone for VecOf { } } -impl std::fmt::Debug for VecOf { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl fmt::Debug for VecOf { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("VecOf") .field("inner", &self.inner) .field("min_len", &self.min_len) @@ -201,7 +202,7 @@ impl ContainerDomain for VecOf { fn with_min_len(self, min_len: usize) -> Self { assert!( - self.max_len.map_or(true, |max| min_len <= max), + self.max_len.is_none_or(|max| min_len <= max), "Minimum length {} cannot be greater than the maximum length {}", min_len, self.max_len.unwrap() diff --git a/rust/src/domains/utility.rs b/rust/src/domains/utility.rs index 2ce15fa32..e14bcef02 100644 --- a/rust/src/domains/utility.rs +++ b/rust/src/domains/utility.rs @@ -18,6 +18,7 @@ use num_traits::PrimInt; use rand::distr::uniform::SampleUniform; use rand::distr::{Distribution, StandardUniform}; use rand::RngExt; +use std::fmt::Display; /// Shrinks a `val` towards a `target` value. /// @@ -32,7 +33,7 @@ use rand::RngExt; /// * `target`: The value to shrink towards. pub fn shrink_towards(rng: &mut R, val: T, target: T) -> T where - T: SampleUniform + PartialOrd + Copy + std::fmt::Display, + T: SampleUniform + PartialOrd + Copy + Display, { match val.partial_cmp(&target) { Some(Ordering::Equal) => val, @@ -81,7 +82,7 @@ pub fn mutate_integer( max_value: Option, ) -> T where - T: PrimInt + SampleUniform + std::fmt::Display, + T: PrimInt + SampleUniform + Display, { assert!(range > T::zero(), "mutate_integer: range value cannot be <= 0: {range}"); @@ -106,7 +107,7 @@ where } 1 => { // 1/3 chance: Flip a random bit - let num_bits = std::mem::size_of::() * 8; + let num_bits = size_of::() * 8; let bit_index = rng.random_range(0..num_bits); let mask = T::one() << bit_index; let result = val ^ mask; @@ -194,7 +195,7 @@ impl SpecialValues for char { /// /// # Type Parameters /// * `T`: The type of the value to choose. Must implement `SpecialValues` and -/// `StandardUniform` must be able to generate values of type `T`. +/// `StandardUniform` must be able to generate values of type `T`. pub fn choose_value(rng: &mut R) -> T where T: SpecialValues + 'static, @@ -217,7 +218,7 @@ pub fn mutate_float( _range: Option<(T, T)>, // TODO: Implement range support for floats. ) -> anyhow::Result<()> where - T: num_traits::Float + SampleUniform + std::fmt::Display + Copy + SpecialValues + 'static, + T: num_traits::Float + SampleUniform + Display + Copy + SpecialValues + 'static, StandardUniform: Distribution, { if only_shrink { @@ -259,6 +260,7 @@ mod tests { rngs::{SmallRng, SysRng}, SeedableRng, }; + use std::fmt::Debug; fn get_rng() -> SmallRng { SmallRng::try_from_rng(&mut SysRng).unwrap() @@ -266,7 +268,7 @@ mod tests { fn check_shrink_towards(smaller: T, larger: T) where - T: SampleUniform + PartialOrd + Copy + std::fmt::Display + std::fmt::Debug + PartialEq, + T: SampleUniform + PartialOrd + Copy + Display + Debug + PartialEq, { let mut rng = get_rng(); @@ -317,7 +319,7 @@ mod tests { fn check_mutate_integer() where - T: PrimInt + SampleUniform + std::fmt::Display + std::fmt::Debug + SpecialValues + 'static, + T: PrimInt + SampleUniform + Display + Debug + SpecialValues + 'static, StandardUniform: Distribution, { let mut rng = get_rng(); @@ -369,13 +371,7 @@ mod tests { fn check_mutate_float() where - T: num_traits::Float - + SampleUniform - + std::fmt::Display - + std::fmt::Debug - + Copy - + SpecialValues - + 'static, + T: num_traits::Float + SampleUniform + Display + Debug + Copy + SpecialValues + 'static, StandardUniform: Distribution, { let mut rng = get_rng(); diff --git a/rust/src/internal.rs b/rust/src/internal.rs index c83c1575b..213e66aa3 100644 --- a/rust/src/internal.rs +++ b/rust/src/internal.rs @@ -33,7 +33,7 @@ pub trait FuzzTest { /// (will attempt to downcast to actual user values). /// /// Returns `true` if the property function holds, `false` if it crashes. - fn execute<'a>(&self, args: &'a GenericCorpusValue) -> bool; + fn execute(&self, args: &GenericCorpusValue) -> bool; fn print_finding_report(&self); fn domains(&self) -> &dyn GenericDomain; } @@ -57,6 +57,7 @@ pub struct FuzzTestRegistration { inventory::collect!(FuzzTestRegistration); +#[allow(clippy::type_complexity)] pub static FUZZ_TEST_NAME_TO_FACTORY: LazyLock BoxedFuzzTest>> = LazyLock::new(|| { inventory::iter diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 6a8d7a11b..bc663ef5b 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +#![deny(clippy::absolute_paths)] +#![deny(unused_imports)] #![feature(cfg_sanitize)] mod crash_handler; diff --git a/rust/src/options.rs b/rust/src/options.rs index 0d7984336..41abaf5a4 100644 --- a/rust/src/options.rs +++ b/rust/src/options.rs @@ -16,15 +16,17 @@ use crate::internal::FuzzTestRegistration; use ::engine::engine_ffi; use anyhow::Context; use clap::Parser; +use std::env; use std::ffi::CString; use std::ffi::OsString; +use std::iter; use std::path::Path; use std::sync::OnceLock; use tempfile::{NamedTempFile, TempDir}; pub use fuzztest_options::{ - ExecutionMode, FuzzFor, FuzzOptions, FuzzTestOptions, ListCrashIdsOptions, ReplayCorpusOptions, - ReplayCrashOptions, TimeBudgetType, + ExecutionMode, FuzzOptions, FuzzTestOptions, ReplayCorpusOptions, ReplayCrashOptions, + RunDuration, TimeBudgetType, }; /// Returns a lazily-initialized static reference to the global `FuzzTestOptions`. @@ -34,7 +36,7 @@ pub fn get_fuzztest_options() -> &'static FuzzTestOptions { // from environment variables (like `FUZZTEST_FUZZ_FOR` etc.). We (currently) do not envisage // support for passing flags on cli as the Rust's libtest harness does not support custom // flags. - OPTIONS.get_or_init(|| FuzzTestOptions::parse_from(std::iter::empty::())) + OPTIONS.get_or_init(|| FuzzTestOptions::parse_from(iter::empty::())) } trait ExecutionModeExt { @@ -155,7 +157,7 @@ impl CentipedeArgs { // ============================================================================== // 1. Common Base Arguments (Required across all Centipede executions) // ============================================================================== - let argv0 = std::env::args().next().context("while attempting to get argv[0]")?; + let argv0 = env::args().next().context("while attempting to get argv[0]")?; add_arg(format!("--binary={argv0} {current_test_name} --exact --nocapture"))?; let normalized_test_name = current_test_name.replace("::", "."); @@ -212,10 +214,10 @@ impl CentipedeArgs { match mode_opts { ExecutionMode::Fuzz(fuzz_opts) => { match &fuzz_opts.fuzz_for { - FuzzFor::Indefinitely => { + RunDuration::Indefinitely => { // not specifying `--stop_after` means to run indefinitely. } - FuzzFor::Duration(duration) => { + RunDuration::Fixed(duration) => { let duration_secs = duration.as_secs_f64(); if opt_corpusdb.is_some() { add_arg(format!("--fuzztest_time_limit_per_test={duration_secs}s"))?; @@ -240,22 +242,31 @@ impl CentipedeArgs { add_arg(format!("--list_crash_ids_file={}", path.display()))?; } ExecutionMode::ReplayCorpus(replay_corpus_opts) => { - let time_limit = match replay_corpus_opts.time_budget_type { - TimeBudgetType::PerTest => replay_corpus_opts.replay_corpus_for, - TimeBudgetType::Total => { - let num_tests = inventory::iter::().count(); - if num_tests == 0 { - replay_corpus_opts.replay_corpus_for - } else { - (*replay_corpus_opts.replay_corpus_for.as_ref() / (num_tests as u32)) - .into() - } - } - }; add_arg("--fuzztest_only_replay=true".to_string())?; add_arg("--fuzztest_replay_coverage_inputs=true".to_string())?; add_arg("--load_shards_only=true".to_string())?; - add_arg(format!("--fuzztest_time_limit_per_test={time_limit}"))?; + match replay_corpus_opts.replay_corpus_for { + RunDuration::Indefinitely => { + // Not specifying `--fuzztest_time_limit_per_test` means to run indefinitely + } + RunDuration::Fixed(duration) => { + let time_limit: humantime::Duration = match replay_corpus_opts + .time_budget_type + { + TimeBudgetType::PerTest => duration, + TimeBudgetType::Total => { + let num_tests = inventory::iter::().count(); + if num_tests == 0 { + duration + } else { + (*duration.as_ref() / (num_tests as u32)).into() + } + } + }; + let time_limit_secs = time_limit.as_secs_f64(); + add_arg(format!("--fuzztest_time_limit_per_test={time_limit_secs}s"))?; + } + } if let Some(jobs) = &replay_corpus_opts.jobs { add_arg(format!("--j={jobs}"))?; } @@ -395,12 +406,12 @@ mod tests { let options = FuzzTestOptions::parse_from(std::iter::empty::()); - expect_that!(options.fuzz_for, eq(Some(FuzzFor::Indefinitely))); + expect_that!(options.fuzz_for, eq(Some(RunDuration::Indefinitely))); let mode = ExecutionMode::from_fuzztest_options(&options); let ExecutionMode::Fuzz(fuzz_opts) = mode else { panic!("Expected ExecutionMode::Fuzz"); }; - expect_that!(fuzz_opts.fuzz_for, eq(FuzzFor::Indefinitely)); + expect_that!(fuzz_opts.fuzz_for, eq(RunDuration::Indefinitely)); // SAFETY: Cleaning up environment variables. unsafe { @@ -411,7 +422,7 @@ mod tests { #[gtest] fn test_determine_execution_action_standalone_indefinite() { let options = - FuzzTestOptions { fuzz_for: Some(FuzzFor::Indefinitely), ..Default::default() }; + FuzzTestOptions { fuzz_for: Some(RunDuration::Indefinitely), ..Default::default() }; let action = determine_execution_action_internal(&options, "my_mod::my_test"); let ExecutionAction::Standalone(args) = action else { @@ -435,11 +446,8 @@ mod tests { #[gtest] fn test_determine_execution_action_standalone() { - let expected_duration = "10s".parse().unwrap(); - let options = FuzzTestOptions { - fuzz_for: Some(FuzzFor::Duration(expected_duration)), - ..Default::default() - }; + let expected_duration: RunDuration = "10s".parse().unwrap(); + let options = FuzzTestOptions { fuzz_for: Some(expected_duration), ..Default::default() }; let action = determine_execution_action_internal(&options, "my_mod::my_test"); let ExecutionAction::Standalone(args) = action else { @@ -469,10 +477,7 @@ mod tests { #[gtest] fn test_centipede_args_binary_identifier() { let expected_duration = "1s".parse().unwrap(); - let options = FuzzTestOptions { - fuzz_for: Some(FuzzFor::Duration(expected_duration)), - ..Default::default() - }; + let options = FuzzTestOptions { fuzz_for: Some(expected_duration), ..Default::default() }; let action = determine_execution_action_internal(&options, "my_mod::my_test"); let ExecutionAction::Standalone(args) = action else { @@ -492,10 +497,8 @@ mod tests { #[gtest] fn test_centipede_args_jobs() { let expected_duration = "1s".parse().expect("failed to parse duration"); - let options_no_jobs = FuzzTestOptions { - fuzz_for: Some(FuzzFor::Duration(expected_duration)), - ..Default::default() - }; + let options_no_jobs = + FuzzTestOptions { fuzz_for: Some(expected_duration), ..Default::default() }; let action_no_jobs = determine_execution_action_internal(&options_no_jobs, "my_mod::my_test"); let ExecutionAction::Standalone(args_no_jobs) = action_no_jobs else { @@ -506,7 +509,7 @@ mod tests { assert!(!args_str_no_jobs.iter().any(|s| s.starts_with("--j="))); let options_with_jobs = FuzzTestOptions { - fuzz_for: Some(FuzzFor::Duration(expected_duration)), + fuzz_for: Some(expected_duration), jobs: Some(4), ..Default::default() }; @@ -523,8 +526,7 @@ mod tests { #[gtest] fn test_default_env_diff_set() { let duration = "1s".parse().unwrap(); - let options = - FuzzTestOptions { fuzz_for: Some(FuzzFor::Duration(duration)), ..Default::default() }; + let options = FuzzTestOptions { fuzz_for: Some(duration), ..Default::default() }; let action = determine_execution_action_internal(&options, "my_mod::my_test"); let ExecutionAction::Standalone(args) = action else { panic!("Expected Standalone action"); @@ -638,6 +640,31 @@ mod tests { assert!(args_str.contains(&"--fuzztest_time_limit_per_test=10s")); } + #[gtest] + fn test_determine_execution_action_replay_corpus_indefinite() { + let options = FuzzTestOptions { + replay_corpus_for: Some(RunDuration::Indefinitely), + time_budget_type: TimeBudgetType::PerTest, + ..Default::default() + }; + let action = determine_execution_action_internal(&options, "my_mod::my_test"); + + let ExecutionAction::Standalone(args) = action else { + panic!("Expected Standalone action"); + }; + + let args_str: Vec<&str> = + args._c_strings.iter().map(|s| s.to_str().expect("invalid utf8")).collect(); + + assert!(args_str + .iter() + .any(|s| s.starts_with("--binary=") && s.contains("my_mod::my_test --exact"))); + assert!(args_str.contains(&"--fuzztest_only_replay=true")); + assert!(args_str.contains(&"--fuzztest_replay_coverage_inputs=true")); + assert!(args_str.contains(&"--load_shards_only=true")); + assert!(!args_str.iter().any(|s| s.starts_with("--fuzztest_time_limit_per_test="))); + } + #[gtest] fn test_determine_execution_action_replay_corpus_total_budget() { let expected_duration = "10s".parse().expect("failed to parse duration"); @@ -656,16 +683,39 @@ mod tests { args._c_strings.iter().map(|s| s.to_str().expect("invalid utf8")).collect(); let num_tests = inventory::iter::().count(); + let RunDuration::Fixed(fixed_duration) = expected_duration else { + panic!("expected Fixed duration"); + }; let expected_limit = if num_tests == 0 { - expected_duration + fixed_duration } else { - (*expected_duration.as_ref() / (num_tests as u32)).into() + (*fixed_duration.as_ref() / (num_tests as u32)).into() }; let expected_limit_str = format!("--fuzztest_time_limit_per_test={}", expected_limit); assert!(args_str.contains(&expected_limit_str.as_str())); } + #[gtest] + fn test_determine_execution_action_replay_corpus_indefinite_total_budget() { + let options = FuzzTestOptions { + replay_corpus_for: Some(RunDuration::Indefinitely), + time_budget_type: TimeBudgetType::Total, + ..Default::default() + }; + let action = determine_execution_action_internal(&options, "my_mod::my_test"); + + let ExecutionAction::Standalone(args) = action else { + panic!("Expected Standalone action"); + }; + + let args_str: Vec<&str> = + args._c_strings.iter().map(|s| s.to_str().expect("invalid utf8")).collect(); + + assert!(args_str.contains(&"--fuzztest_only_replay=true")); + assert!(!args_str.iter().any(|s| s.starts_with("--fuzztest_time_limit_per_test="))); + } + #[gtest] fn test_get_corpusdb_and_workdir_default_creates_temp_workdir() -> Result<()> { let options = FuzzTestOptions::default(); @@ -750,11 +800,8 @@ mod tests { // When corpus_db is not provided, fuzzing for a fixed duration should pass --stop_after // to Centipede so it stops fuzzing after the specified duration. let duration = "1s".parse().expect("fixed test string should parse as duration"); - let options = FuzzTestOptions { - fuzz_for: Some(FuzzFor::Duration(duration)), - corpus_db: None, - ..Default::default() - }; + let options = + FuzzTestOptions { fuzz_for: Some(duration), corpus_db: None, ..Default::default() }; let action = determine_execution_action_internal(&options, "my_mod::my_test"); let ExecutionAction::Standalone(args) = action else { @@ -778,7 +825,7 @@ mod tests { // a corpus database. let duration = "1s".parse().expect("fixed test string should parse as duration"); let options = FuzzTestOptions { - fuzz_for: Some(FuzzFor::Duration(duration)), + fuzz_for: Some(duration), corpus_db: Some("/tmp/corpus_db".to_string()), ..Default::default() }; diff --git a/rust/src/worker.rs b/rust/src/worker.rs index 20ac83ff8..8eae57fa8 100644 --- a/rust/src/worker.rs +++ b/rust/src/worker.rs @@ -20,10 +20,16 @@ use ::engine::{ BytesSink, CoverageDomainRegistry, DiagnosticSink, ExecuteContext, FeedbackSink, InputSink, }; use spin::Mutex; +use std::env; +use std::ffi::c_int; use std::ffi::CString; +use std::fs; use std::path::Path; +use std::process; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::LazyLock; +use std::time::Duration; +use std::time::Instant; /// The DiagnosticSink provided by the engine while creating the adapter. /// @@ -59,7 +65,9 @@ pub(crate) fn clear_diagnostic_sink() { /// This function blocks till it can get the lock on the global DiagnosticSink and is not /// signal-safe. pub(crate) fn emit_error(message: &str) { - DIAGNOSTIC_SINK.lock().as_ref().map(|sink| sink.emit_error(message)); + if let Some(sink) = DIAGNOSTIC_SINK.lock().as_ref() { + sink.emit_error(message) + } } /// Emits a finding into the DiagnosticSink if the DiagnosticSink is set. @@ -99,7 +107,7 @@ pub(crate) fn try_emit_finding(description: &str, signature: &str) -> bool { return false; }; sink.emit_finding(token, description, signature); - return true; + true } // We double-box the input because `GenericCorpusValue` is a fat pointer (`Box`), @@ -252,7 +260,7 @@ impl RustFuzzTestAdapterManager { pub fn get_binary_id(&self, sink: &mut BytesSink) { static ARGV0: LazyLock = LazyLock::new(|| { CString::new( - Path::new(&std::env::args().nth(0).unwrap()) + Path::new(&env::args().next().unwrap()) .file_name() .and_then(|f| f.to_str()) .unwrap_or(""), @@ -428,7 +436,7 @@ pub unsafe extern "C" fn get_random_seed_input_callback( pub unsafe extern "C" fn mutate_callback( ctx: *mut engine_ffi::FuzzTestAdapterCtx, origin: engine_ffi::FuzzTestInputHandle, - shrink: std::ffi::c_int, + shrink: c_int, sink: *const engine_ffi::FuzzTestInputSink, ) { // SAFETY: The engine guarantees `ctx` is a valid pointer to the `RustFuzzTestAdapter` @@ -620,10 +628,10 @@ pub unsafe extern "C" fn free_ctx_callback(ctx: *mut engine_ffi::FuzzTestAdapter } pub fn run_smoke_test(fuzztest: &dyn FuzzTest) { - let start_time = std::time::Instant::now(); + let start_time = Instant::now(); // TODO(the-shank): these should be configurable externally. - let smoke_test_duration = std::time::Duration::from_secs(1); + let smoke_test_duration = Duration::from_secs(1); let only_shrink = false; // TODO(the-shank): the rng seed should be configurable @@ -681,7 +689,7 @@ pub fn process(manager: RustFuzzTestAdapterManager) { return; } WorkerStatus::Failure => { - std::process::exit(1); + process::exit(1); } } } @@ -701,7 +709,7 @@ pub fn process(manager: RustFuzzTestAdapterManager) { } // Now read the file and replay each crash - if let Ok(contents) = std::fs::read_to_string(list_file.path()) { + if let Ok(contents) = fs::read_to_string(list_file.path()) { let options = options::get_fuzztest_options(); for crash_id in contents.lines() { let crash_id = crash_id.trim();