diff --git a/rust/options/src/env_vars.rs b/rust/options/src/env_vars.rs new file mode 100644 index 000000000..01a06047e --- /dev/null +++ b/rust/options/src/env_vars.rs @@ -0,0 +1,276 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Testing utilities for safely modifying environment variables in +//! single-threaded tests. +//! +//! # Safety & Limitations +//! Modifying process environment variables (`std::env::set_var` / +//! `std::env::remove_var`) is inherently `unsafe` because it affects the +//! global process environment. +//! +//! The functions [`with_env_var`] and [`with_env_vars`] use the scoped closure +//! RAII pattern: they apply modifications for the duration of a closure and +//! restore original environment variables in reverse order when the closure +//! finishes or panics. +//! +//! Callers must ensure that tests run single-threaded (which can be +//! validated via [`assert_single_threaded_test_environment`]) and that no +//! other threads concurrently access the environment during execution. + +use std::env; +use std::ffi::{OsStr, OsString}; + +/// Asserts that the test process is executing in single-threaded mode. +/// +/// Verifies that at least one single-threaded test harness indicator +/// (`RUST_TEST_THREADS=1` or `--test-threads=1`) is present and that no +/// conflicting multi-threaded settings exist. +/// +/// # Panics +/// Panics if called outside a single-threaded test environment or if +/// conflicting/multi-threaded thread settings are detected. +pub fn assert_single_threaded_test_environment() { + let mut has_single_threaded_indicator = false; + + if let Ok(val) = env::var("RUST_TEST_THREADS") { + if val.trim() == "1" { + has_single_threaded_indicator = true; + } else { + panic!( + "EnvVars detected multi-threaded setting \ + RUST_TEST_THREADS={val:?}. Single-threaded execution is \ + required to prevent environment data races." + ); + } + } + + let args: Vec = env::args().collect(); + let mut i = 0; + while i < args.len() { + let arg = &args[i]; + + if let Some(val) = arg.strip_prefix("--test-threads=") { + if val == "1" { + has_single_threaded_indicator = true; + } else { + panic!( + "EnvVars detected multi-threaded argument \ + --test-threads={val}. Single-threaded execution is \ + required to prevent environment data races." + ); + } + } else if arg == "--test-threads" { + if i + 1 < args.len() { + let val = &args[i + 1]; + if val == "1" { + has_single_threaded_indicator = true; + i += 1; + } else { + panic!( + "EnvVars detected multi-threaded argument \ + --test-threads {val}. Single-threaded execution is \ + required to prevent environment data races." + ); + } + } else { + panic!("EnvVars detected incomplete argument --test-threads."); + } + } + + i += 1; + } + + if !has_single_threaded_indicator { + panic!( + "Environment modification tests can only be run single-threaded \ + (e.g., --test-threads=1 or RUST_TEST_THREADS=1) to prevent \ + environment data races." + ); + } +} + +/// Executes a closure with a temporary environment variable modification. +/// +/// Restores the original environment variable value when the closure +/// completes or panics. +/// +/// # Safety +/// The caller must ensure that no other threads are concurrently reading or +/// writing environment variables during the execution of `f`. +pub unsafe fn with_env_var(key: K, val: V, f: F) -> R +where + K: AsRef, + V: AsRef, + F: FnOnce() -> R, +{ + // SAFETY: Forwarded to with_env_vars. + unsafe { with_env_vars([(key.as_ref(), Some(val.as_ref()))], f) } +} + +/// Executes a closure with multiple temporary environment variable +/// modifications. +/// +/// For each entry `(key, value)`: +/// - If `value` is `Some(v)`, the environment variable `key` is set to `v`. +/// - If `value` is `None`, the environment variable `key` is unset. +/// +/// Restores all original environment variable values in reverse order when the +/// closure completes or panics. +/// +/// # Safety +/// The caller must ensure that no other threads are concurrently reading or +/// writing environment variables during the execution of `f`. +pub unsafe fn with_env_vars(vars: I, f: F) -> R +where + K: AsRef, + V: AsRef, + I: IntoIterator)>, + F: FnOnce() -> R, +{ + struct EnvRestorer { + saved_vars: Vec<(OsString, Option)>, + } + + impl Drop for EnvRestorer { + fn drop(&mut self) { + // Restore in reverse order of modification to handle repeated + // keys correctly. + for (key, original_val) in self.saved_vars.iter().rev() { + // SAFETY: Restoring original environment values within the + // caller's unsafe scope. + unsafe { + match original_val { + Some(val) => env::set_var(key, val), + None => env::remove_var(key), + } + } + } + } + } + + let mut restorer = EnvRestorer { saved_vars: Vec::new() }; + + for (key, val) in vars { + let key_ref = key.as_ref(); + let key_os = key_ref.to_os_string(); + let original_val = env::var_os(key_ref); + restorer.saved_vars.push((key_os, original_val)); + + // SAFETY: Upheld by caller contract. + unsafe { + match val { + Some(v) => env::set_var(key_ref, v.as_ref()), + None => env::remove_var(key_ref), + } + } + } + + f() +} + +#[cfg(test)] +mod tests { + use super::*; + use googletest::prelude::*; + use std::panic; + + #[gtest] + fn test_with_env_var_sets_and_restores() { + const KEY: &str = "FUZZTEST_TEST_SCOPED_ENV_VAR"; + assert_single_threaded_test_environment(); + + // Ensure initially unset + // SAFETY: Test runs in verified single-threaded context. + unsafe { + env::remove_var(KEY); + } + + // SAFETY: Verified single-threaded execution and no concurrent + // threads. + let value_during_scope = unsafe { with_env_var(KEY, "scoped_val", || env::var(KEY)) }; + + expect_that!(value_during_scope, ok(eq("scoped_val"))); + expect_true!(env::var(KEY).is_err()); + } + + #[gtest] + fn test_with_env_vars_multiple_and_reverse_order_restoration() { + const KEY1: &str = "FUZZTEST_TEST_KEY1"; + const KEY2: &str = "FUZZTEST_TEST_KEY2"; + assert_single_threaded_test_environment(); + + // Setup pre-existing state + // SAFETY: Test runs in verified single-threaded context. + unsafe { + env::set_var(KEY1, "init1"); + env::set_var(KEY2, "init2"); + } + + // Modify KEY1 twice and unset KEY2 + // SAFETY: Verified single-threaded execution and no concurrent + // threads. + let (val1_during_scope, val2_during_scope) = unsafe { + with_env_vars( + [(KEY1, Some("intermediate")), (KEY1, Some("final")), (KEY2, None)], + || (env::var(KEY1), env::var(KEY2)), + ) + }; + + expect_that!(val1_during_scope, ok(eq("final"))); + expect_true!(val2_during_scope.is_err()); + + // Original values must be restored in reverse order + expect_that!(env::var(KEY1), ok(eq("init1"))); + expect_that!(env::var(KEY2), ok(eq("init2"))); + + // Cleanup + // SAFETY: Test runs in verified single-threaded context. + unsafe { + env::remove_var(KEY1); + env::remove_var(KEY2); + } + } + + #[gtest] + fn test_with_env_vars_restores_on_panic() { + const KEY: &str = "FUZZTEST_TEST_PANIC_RECOVERY"; + assert_single_threaded_test_environment(); + + // SAFETY: Test runs in verified single-threaded context. + unsafe { + env::set_var(KEY, "before_panic"); + } + + let panic_result = panic::catch_unwind(|| { + // SAFETY: Verified single-threaded execution and no concurrent + // threads. + unsafe { + with_env_var(KEY, "during_panic", || { + panic!("simulated test failure"); + }); + } + }); + + expect_true!(panic_result.is_err()); + // Environment must be restored despite the panic + expect_that!(env::var(KEY), ok(eq("before_panic"))); + + // Cleanup + // SAFETY: Test runs in verified single-threaded context. + unsafe { + env::remove_var(KEY); + } + } +} diff --git a/rust/options/src/lib.rs b/rust/options/src/lib.rs index 52d5a3b69..d84bb4a36 100644 --- a/rust/options/src/lib.rs +++ b/rust/options/src/lib.rs @@ -292,8 +292,12 @@ pub struct ListCrashIdsOptions { pub list_crash_ids_file: String, } +#[doc(hidden)] +pub mod env_vars; + #[cfg(test)] mod tests { + use super::env_vars::*; use super::*; use googletest::prelude::*; use std::ffi::OsString; @@ -333,18 +337,14 @@ mod tests { #[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::()); - - // SAFETY: Cleaning up environment variables. - unsafe { - std::env::remove_var("FUZZTEST_REPLAY_ID"); - } + assert_single_threaded_test_environment(); + // SAFETY: Single-threaded test environment and no background threads. + let result = unsafe { + with_env_vars( + [("FUZZTEST_REPLAY_ID", Some("my_crash_123")), ("FUZZTEST_CORPUS_DB", None)], + || FuzzTestOptions::try_parse_from(std::iter::empty::()), + ) + }; let err = result.expect_err("parsing should fail when corpus_db is missing"); expect_that!(err.kind(), eq(clap::error::ErrorKind::MissingRequiredArgument)); @@ -352,55 +352,47 @@ mod tests { #[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"); - } + assert_single_threaded_test_environment(); + // SAFETY: Single-threaded test environment and no background threads. + let options = unsafe { + with_env_vars( + [ + ("FUZZTEST_REPLAY_ID", Some("my_crash_123")), + ("FUZZTEST_CORPUS_DB", Some("/tmp/corpus_db")), + ], + || FuzzTestOptions::try_parse_from(std::iter::empty::()), + ) + } + .expect("parsing should succeed when both replay_id and corpus_db are present"); - let result = FuzzTestOptions::try_parse_from(std::iter::empty::()); - - // 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(Path::new("/tmp/corpus_db")))); } #[gtest] fn test_jobs_options_parsing_env() { - // SAFETY: Testing environment parsing in single-threaded context. - unsafe { - std::env::set_var("FUZZTEST_JOBS", "4"); - } - - let options = FuzzTestOptions::parse_from(std::iter::empty::()); + assert_single_threaded_test_environment(); + // SAFETY: Single-threaded test environment and no background threads. + let options = unsafe { + with_env_var("FUZZTEST_JOBS", "4", || { + 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)); - - // SAFETY: Cleaning up environment variables. - unsafe { - std::env::remove_var("FUZZTEST_JOBS"); - } } #[gtest] fn test_jobs_with_fuzz_for_parsing_env() { - // SAFETY: Testing environment parsing in single-threaded context. - unsafe { - std::env::set_var("FUZZTEST_JOBS", "4"); - std::env::set_var("FUZZTEST_FUZZ_FOR", "5s"); - } - - let options = FuzzTestOptions::parse_from(std::iter::empty::()); + assert_single_threaded_test_environment(); + // SAFETY: Single-threaded test environment and no background threads. + let options = unsafe { + with_env_vars([("FUZZTEST_JOBS", Some("4")), ("FUZZTEST_FUZZ_FOR", Some("5s"))], || { + FuzzTestOptions::parse_from(std::iter::empty::()) + }) + }; expect_that!(options.jobs, eq(Some(4))); let expected_duration = "5s".parse().expect("valid duration"); @@ -413,24 +405,22 @@ mod tests { execution_id: None, })) ); - - // SAFETY: Cleaning up environment variables. - unsafe { - std::env::remove_var("FUZZTEST_JOBS"); - std::env::remove_var("FUZZTEST_FUZZ_FOR"); - } } #[gtest] fn test_jobs_with_replay_corpus_parsing_env() { - // SAFETY: Testing environment parsing in single-threaded context. - unsafe { - std::env::set_var("FUZZTEST_JOBS", "4"); - std::env::set_var("FUZZTEST_REPLAY_CORPUS_FOR", "10s"); - std::env::set_var("FUZZTEST_CORPUS_DB", "/tmp/corpus_db"); - } - - let options = FuzzTestOptions::parse_from(std::iter::empty::()); + assert_single_threaded_test_environment(); + // SAFETY: Single-threaded test environment and no background threads. + let options = unsafe { + with_env_vars( + [ + ("FUZZTEST_JOBS", Some("4")), + ("FUZZTEST_REPLAY_CORPUS_FOR", Some("10s")), + ("FUZZTEST_CORPUS_DB", Some("/tmp/corpus_db")), + ], + || FuzzTestOptions::parse_from(std::iter::empty::()), + ) + }; expect_that!(options.jobs, eq(Some(4))); let expected_duration = "10s".parse().expect("valid duration string"); @@ -444,24 +434,22 @@ mod tests { execution_id: None, })) ); - - // SAFETY: Cleaning up environment variables. - unsafe { - std::env::remove_var("FUZZTEST_JOBS"); - std::env::remove_var("FUZZTEST_REPLAY_CORPUS_FOR"); - std::env::remove_var("FUZZTEST_CORPUS_DB"); - } } #[gtest] fn test_continue_after_crash_with_fuzz_for_parsing_env() { - // SAFETY: Testing environment parsing in single-threaded context. - unsafe { - std::env::set_var("FUZZTEST_FUZZ_FOR", "5s"); - std::env::set_var("FUZZTEST_CONTINUE_AFTER_CRASH", "true"); - } + assert_single_threaded_test_environment(); + // SAFETY: Single-threaded test environment and no background threads. + let options = unsafe { + with_env_vars( + [ + ("FUZZTEST_FUZZ_FOR", Some("5s")), + ("FUZZTEST_CONTINUE_AFTER_CRASH", Some("true")), + ], + || FuzzTestOptions::parse_from(std::iter::empty::()), + ) + }; - let options = FuzzTestOptions::parse_from(std::iter::empty::()); expect_true!(options.continue_after_crash); let expected_duration = "5s".parse().expect("valid duration"); expect_that!( @@ -473,24 +461,23 @@ mod tests { execution_id: None, })) ); - - // SAFETY: Cleaning up environment variables. - unsafe { - std::env::remove_var("FUZZTEST_FUZZ_FOR"); - std::env::remove_var("FUZZTEST_CONTINUE_AFTER_CRASH"); - } } #[gtest] fn test_continue_after_crash_with_replay_corpus_parsing_env() { - // SAFETY: Testing environment parsing in single-threaded context. - unsafe { - std::env::set_var("FUZZTEST_REPLAY_CORPUS_FOR", "10s"); - std::env::set_var("FUZZTEST_CORPUS_DB", "/tmp/corpus_db"); - std::env::set_var("FUZZTEST_CONTINUE_AFTER_CRASH", "true"); - } + assert_single_threaded_test_environment(); + // SAFETY: Single-threaded test environment and no background threads. + let options = unsafe { + with_env_vars( + [ + ("FUZZTEST_REPLAY_CORPUS_FOR", Some("10s")), + ("FUZZTEST_CORPUS_DB", Some("/tmp/corpus_db")), + ("FUZZTEST_CONTINUE_AFTER_CRASH", Some("true")), + ], + || FuzzTestOptions::parse_from(std::iter::empty::()), + ) + }; - let options = FuzzTestOptions::parse_from(std::iter::empty::()); expect_true!(options.continue_after_crash); let expected_duration = "10s".parse().expect("valid duration string"); expect_that!( @@ -503,29 +490,18 @@ mod tests { execution_id: None, })) ); - - // SAFETY: Cleaning up environment variables. - unsafe { - std::env::remove_var("FUZZTEST_REPLAY_CORPUS_FOR"); - std::env::remove_var("FUZZTEST_CORPUS_DB"); - std::env::remove_var("FUZZTEST_CONTINUE_AFTER_CRASH"); - } } #[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::()); - - // SAFETY: Cleaning up environment variables. - unsafe { - std::env::remove_var("FUZZTEST_REPLAY_FINDINGS"); - } + assert_single_threaded_test_environment(); + // SAFETY: Single-threaded test environment and no background threads. + let result = unsafe { + with_env_vars( + [("FUZZTEST_REPLAY_FINDINGS", Some("true")), ("FUZZTEST_CORPUS_DB", None)], + || FuzzTestOptions::try_parse_from(std::iter::empty::()), + ) + }; let err = result.expect_err("parsing should fail when corpus_db is missing"); expect_that!(err.kind(), eq(clap::error::ErrorKind::MissingRequiredArgument)); @@ -533,40 +509,33 @@ mod tests { #[gtest] fn test_replay_findings_with_corpus_db_succeeds() { - // SAFETY: Testing environment parsing in single-threaded context. - unsafe { - env::set_var("FUZZTEST_REPLAY_FINDINGS", "true"); - 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_FINDINGS"); - std::env::remove_var("FUZZTEST_CORPUS_DB"); - } + assert_single_threaded_test_environment(); + // SAFETY: Single-threaded test environment and no background threads. + let options = unsafe { + with_env_vars( + [ + ("FUZZTEST_REPLAY_FINDINGS", Some("true")), + ("FUZZTEST_CORPUS_DB", Some("/tmp/corpus_db")), + ], + || FuzzTestOptions::try_parse_from(std::iter::empty::()), + ) + } + .expect("parsing should succeed when both replay_findings and corpus_db are present"); - 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(Path::new("/tmp/corpus_db")))); } #[gtest] fn test_replay_corpus_for_requires_corpus_db() { - // SAFETY: Testing environment parsing in single-threaded context. - unsafe { - std::env::set_var("FUZZTEST_REPLAY_CORPUS_FOR", "10s"); - std::env::remove_var("FUZZTEST_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"); - } + assert_single_threaded_test_environment(); + // SAFETY: Single-threaded test environment and no background threads. + let result = unsafe { + with_env_vars( + [("FUZZTEST_REPLAY_CORPUS_FOR", Some("10s")), ("FUZZTEST_CORPUS_DB", None)], + || FuzzTestOptions::try_parse_from(std::iter::empty::()), + ) + }; let err = result.expect_err("parsing should fail when corpus_db is missing"); expect_that!(err.kind(), eq(clap::error::ErrorKind::MissingRequiredArgument)); @@ -574,22 +543,19 @@ mod tests { #[gtest] fn test_replay_corpus_for_with_corpus_db_succeeds() { - // SAFETY: Testing environment parsing in single-threaded context. - unsafe { - std::env::set_var("FUZZTEST_REPLAY_CORPUS_FOR", "10s"); - std::env::set_var("FUZZTEST_CORPUS_DB", "/tmp/corpus_db"); - } - - let result = FuzzTestOptions::try_parse_from(std::iter::empty::()); + assert_single_threaded_test_environment(); + // SAFETY: Single-threaded test environment and no background threads. + let options = unsafe { + with_env_vars( + [ + ("FUZZTEST_REPLAY_CORPUS_FOR", Some("10s")), + ("FUZZTEST_CORPUS_DB", Some("/tmp/corpus_db")), + ], + || FuzzTestOptions::try_parse_from(std::iter::empty::()), + ) + } + .expect("parsing should succeed when both replay_corpus_for and corpus_db are present"); - // 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 both replay_corpus_for and corpus_db are present"); expect_that!( options.replay_corpus_for, eq(Some("10s".parse().expect("valid duration string"))) @@ -600,69 +566,60 @@ mod tests { #[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"); - } + assert_single_threaded_test_environment(); + // SAFETY: Single-threaded test environment and no background threads. + let options = unsafe { + with_env_vars( + [ + ("FUZZTEST_REPLAY_CORPUS_FOR", Some("inf")), + ("FUZZTEST_CORPUS_DB", Some("/tmp/corpus_db")), + ], + || FuzzTestOptions::try_parse_from(std::iter::empty::()), + ) + } + .expect("parsing should succeed when replay_corpus_for is inf and corpus_db is present"); - 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(Path::new("/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( + assert_single_threaded_test_environment(); + // SAFETY: Single-threaded test environment and no background threads. + let options = unsafe { + with_env_vars( + [ + ("FUZZTEST_REPLAY_CORPUS_FOR", Some("infinity")), + ("FUZZTEST_CORPUS_DB", Some("/tmp/corpus_db")), + ], + || FuzzTestOptions::try_parse_from(std::iter::empty::()), + ) + } + .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(Path::new("/tmp/corpus_db")))); } #[gtest] fn test_replay_corpus_for_with_total_time_budget() { - // SAFETY: Testing environment parsing in single-threaded context. - unsafe { - std::env::set_var("FUZZTEST_REPLAY_CORPUS_FOR", "10s"); - 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::()); + assert_single_threaded_test_environment(); + // SAFETY: Single-threaded test environment and no background threads. + let options = unsafe { + with_env_vars( + [ + ("FUZZTEST_REPLAY_CORPUS_FOR", Some("10s")), + ("FUZZTEST_TIME_BUDGET_TYPE", Some("total")), + ("FUZZTEST_CORPUS_DB", Some("/tmp/corpus_db")), + ], + || FuzzTestOptions::try_parse_from(std::iter::empty::()), + ) + } + .expect("parsing should succeed with total time budget type"); - // 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"); expect_that!( options.replay_corpus_for, eq(Some("10s".parse().expect("valid duration string"))) @@ -673,23 +630,20 @@ mod tests { #[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"); - } + assert_single_threaded_test_environment(); + // SAFETY: Single-threaded test environment and no background threads. + let options = unsafe { + with_env_vars( + [ + ("FUZZTEST_REPLAY_CORPUS_FOR", Some("inf")), + ("FUZZTEST_TIME_BUDGET_TYPE", Some("total")), + ("FUZZTEST_CORPUS_DB", Some("/tmp/corpus_db")), + ], + || FuzzTestOptions::try_parse_from(std::iter::empty::()), + ) + } + .expect("parsing should succeed with total time budget type and inf"); - 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(Path::new("/tmp/corpus_db")))); expect_that!(options.time_budget_type, eq(TimeBudgetType::Total)); @@ -697,20 +651,18 @@ mod tests { #[gtest] fn test_list_crash_ids_requires_corpus_db() { - // SAFETY: Testing environment parsing in single-threaded context. - unsafe { - std::env::set_var("FUZZTEST_LIST_CRASH_IDS", "true"); - std::env::set_var("FUZZTEST_LIST_CRASH_IDS_FILE", "/tmp/crashes.txt"); - std::env::remove_var("FUZZTEST_CORPUS_DB"); - } - - let result = FuzzTestOptions::try_parse_from(std::iter::empty::()); - - // SAFETY: Cleaning up environment variables. - unsafe { - std::env::remove_var("FUZZTEST_LIST_CRASH_IDS"); - std::env::remove_var("FUZZTEST_LIST_CRASH_IDS_FILE"); - } + assert_single_threaded_test_environment(); + // SAFETY: Single-threaded test environment and no background threads. + let result = unsafe { + with_env_vars( + [ + ("FUZZTEST_LIST_CRASH_IDS", Some("true")), + ("FUZZTEST_LIST_CRASH_IDS_FILE", Some("/tmp/crashes.txt")), + ("FUZZTEST_CORPUS_DB", None), + ], + || FuzzTestOptions::try_parse_from(std::iter::empty::()), + ) + }; let err = result.expect_err("parsing should fail when corpus_db is missing"); expect_that!(err.kind(), eq(clap::error::ErrorKind::MissingRequiredArgument)); @@ -718,20 +670,18 @@ mod tests { #[gtest] fn test_list_crash_ids_requires_list_crash_ids_file() { - // SAFETY: Testing environment parsing in single-threaded context. - unsafe { - std::env::set_var("FUZZTEST_LIST_CRASH_IDS", "true"); - std::env::set_var("FUZZTEST_CORPUS_DB", "/tmp/corpus_db"); - std::env::remove_var("FUZZTEST_LIST_CRASH_IDS_FILE"); - } - - let result = FuzzTestOptions::try_parse_from(std::iter::empty::()); - - // SAFETY: Cleaning up environment variables. - unsafe { - std::env::remove_var("FUZZTEST_LIST_CRASH_IDS"); - std::env::remove_var("FUZZTEST_CORPUS_DB"); - } + assert_single_threaded_test_environment(); + // SAFETY: Single-threaded test environment and no background threads. + let result = unsafe { + with_env_vars( + [ + ("FUZZTEST_LIST_CRASH_IDS", Some("true")), + ("FUZZTEST_CORPUS_DB", Some("/tmp/corpus_db")), + ("FUZZTEST_LIST_CRASH_IDS_FILE", None), + ], + || FuzzTestOptions::try_parse_from(std::iter::empty::()), + ) + }; let err = result.expect_err("parsing should fail when list_crash_ids_file is missing"); expect_that!(err.kind(), eq(clap::error::ErrorKind::MissingRequiredArgument)); @@ -739,20 +689,18 @@ mod tests { #[gtest] fn test_list_crash_ids_file_requires_list_crash_ids() { - // SAFETY: Testing environment parsing in single-threaded context. - unsafe { - std::env::set_var("FUZZTEST_LIST_CRASH_IDS_FILE", "/tmp/crashes.txt"); - std::env::set_var("FUZZTEST_CORPUS_DB", "/tmp/corpus_db"); - std::env::remove_var("FUZZTEST_LIST_CRASH_IDS"); - } - - let result = FuzzTestOptions::try_parse_from(std::iter::empty::()); - - // SAFETY: Cleaning up environment variables. - unsafe { - std::env::remove_var("FUZZTEST_LIST_CRASH_IDS_FILE"); - std::env::remove_var("FUZZTEST_CORPUS_DB"); - } + assert_single_threaded_test_environment(); + // SAFETY: Single-threaded test environment and no background threads. + let result = unsafe { + with_env_vars( + [ + ("FUZZTEST_LIST_CRASH_IDS_FILE", Some("/tmp/crashes.txt")), + ("FUZZTEST_CORPUS_DB", Some("/tmp/corpus_db")), + ("FUZZTEST_LIST_CRASH_IDS", None), + ], + || FuzzTestOptions::try_parse_from(std::iter::empty::()), + ) + }; let err = result.expect_err("parsing should fail when list_crash_ids is missing"); expect_that!(err.kind(), eq(clap::error::ErrorKind::MissingRequiredArgument)); @@ -760,25 +708,23 @@ mod tests { #[gtest] fn test_list_crash_ids_with_corpus_db_and_file_succeeds() { - // SAFETY: Testing environment parsing in single-threaded context. - unsafe { - std::env::set_var("FUZZTEST_LIST_CRASH_IDS", "true"); - std::env::set_var("FUZZTEST_LIST_CRASH_IDS_FILE", "/tmp/crashes.txt"); - 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_LIST_CRASH_IDS"); - std::env::remove_var("FUZZTEST_LIST_CRASH_IDS_FILE"); - std::env::remove_var("FUZZTEST_CORPUS_DB"); - } - - let options = result.expect( - "parsing should succeed when list_crash_ids, list_crash_ids_file, and corpus_db are present", + assert_single_threaded_test_environment(); + // SAFETY: Single-threaded test environment and no background threads. + let options = unsafe { + with_env_vars( + [ + ("FUZZTEST_LIST_CRASH_IDS", Some("true")), + ("FUZZTEST_LIST_CRASH_IDS_FILE", Some("/tmp/crashes.txt")), + ("FUZZTEST_CORPUS_DB", Some("/tmp/corpus_db")), + ], + || FuzzTestOptions::try_parse_from(std::iter::empty::()), + ) + } + .expect( + "parsing should succeed when list_crash_ids, list_crash_ids_file, and corpus_db are \ + present", ); + expect_that!(options.list_crash_ids, eq(true)); expect_that!(options.list_crash_ids_file.as_deref(), eq(Some("/tmp/crashes.txt"))); expect_that!(options.corpus_db.as_deref(), eq(Some(Path::new("/tmp/corpus_db")))); @@ -794,18 +740,14 @@ mod tests { #[gtest] fn test_execution_id_requires_corpus_db() { - // SAFETY: Testing environment parsing in single-threaded context. - unsafe { - std::env::set_var("FUZZTEST_EXECUTION_ID", "exec_123"); - std::env::remove_var("FUZZTEST_CORPUS_DB"); - } - - let result = FuzzTestOptions::try_parse_from(std::iter::empty::()); - - // SAFETY: Cleaning up environment variables. - unsafe { - std::env::remove_var("FUZZTEST_EXECUTION_ID"); - } + assert_single_threaded_test_environment(); + // SAFETY: Single-threaded test environment and no background threads. + let result = unsafe { + with_env_vars( + [("FUZZTEST_EXECUTION_ID", Some("exec_123")), ("FUZZTEST_CORPUS_DB", None)], + || FuzzTestOptions::try_parse_from(std::iter::empty::()), + ) + }; let err = result.expect_err("parsing should fail when corpus_db is missing for execution_id"); @@ -814,25 +756,20 @@ mod tests { #[gtest] fn test_execution_id_with_corpus_db_and_fuzz_for_succeeds() { - // SAFETY: Testing environment parsing in single-threaded context. - unsafe { - std::env::set_var("FUZZTEST_EXECUTION_ID", "exec_123"); - std::env::set_var("FUZZTEST_CORPUS_DB", "/tmp/corpus_db"); - std::env::set_var("FUZZTEST_FUZZ_FOR", "5s"); - } - - let result = FuzzTestOptions::try_parse_from(std::iter::empty::()); + assert_single_threaded_test_environment(); + // SAFETY: Single-threaded test environment and no background threads. + let options = unsafe { + with_env_vars( + [ + ("FUZZTEST_EXECUTION_ID", Some("exec_123")), + ("FUZZTEST_CORPUS_DB", Some("/tmp/corpus_db")), + ("FUZZTEST_FUZZ_FOR", Some("5s")), + ], + || FuzzTestOptions::try_parse_from(std::iter::empty::()), + ) + } + .expect("parsing should succeed when execution_id, corpus_db, and fuzz_for are present"); - // SAFETY: Cleaning up environment variables. - unsafe { - std::env::remove_var("FUZZTEST_EXECUTION_ID"); - std::env::remove_var("FUZZTEST_CORPUS_DB"); - std::env::remove_var("FUZZTEST_FUZZ_FOR"); - } - - let options = result.expect( - "parsing should succeed when execution_id, corpus_db, and fuzz_for are present", - ); expect_that!(options.execution_id.as_deref(), eq(Some("exec_123"))); expect_that!(options.corpus_db.as_deref(), eq(Some(Path::new("/tmp/corpus_db")))); @@ -850,25 +787,23 @@ mod tests { #[gtest] fn test_execution_id_with_corpus_db_and_replay_corpus_succeeds() { - // SAFETY: Testing environment parsing in single-threaded context. - unsafe { - std::env::set_var("FUZZTEST_EXECUTION_ID", "exec_123"); - std::env::set_var("FUZZTEST_CORPUS_DB", "/tmp/corpus_db"); - std::env::set_var("FUZZTEST_REPLAY_CORPUS_FOR", "10s"); - } - - let result = FuzzTestOptions::try_parse_from(std::iter::empty::()); - - // SAFETY: Cleaning up environment variables. - unsafe { - std::env::remove_var("FUZZTEST_EXECUTION_ID"); - std::env::remove_var("FUZZTEST_CORPUS_DB"); - std::env::remove_var("FUZZTEST_REPLAY_CORPUS_FOR"); - } - - let options = result.expect( - "parsing should succeed when execution_id, corpus_db, and replay_corpus_for are present", + assert_single_threaded_test_environment(); + // SAFETY: Single-threaded test environment and no background threads. + let options = unsafe { + with_env_vars( + [ + ("FUZZTEST_EXECUTION_ID", Some("exec_123")), + ("FUZZTEST_CORPUS_DB", Some("/tmp/corpus_db")), + ("FUZZTEST_REPLAY_CORPUS_FOR", Some("10s")), + ], + || FuzzTestOptions::try_parse_from(std::iter::empty::()), + ) + } + .expect( + "parsing should succeed when execution_id, corpus_db, and replay_corpus_for are \ + present", ); + expect_that!(options.execution_id.as_deref(), eq(Some("exec_123"))); expect_that!(options.corpus_db.as_deref(), eq(Some(Path::new("/tmp/corpus_db")))); diff --git a/rust/src/options.rs b/rust/src/options.rs index cb636a139..82415571d 100644 --- a/rust/src/options.rs +++ b/rust/src/options.rs @@ -393,38 +393,36 @@ fn normalize_binary_identifier(binary_path: &str) -> &str { #[cfg(test)] mod tests { use super::*; + use fuzztest_options::env_vars::*; use googletest::prelude::*; use std::ffi::OsString; #[gtest] fn test_fuzz_options_parsing_env() { - // SAFETY: Testing environment parsing in single-threaded context. - unsafe { - std::env::set_var("FUZZTEST_FUZZ_FOR", "5s"); - } - - let options = FuzzTestOptions::parse_from(std::iter::empty::()); + assert_single_threaded_test_environment(); + // SAFETY: Single-threaded test environment and no background threads. + let options = unsafe { + with_env_var("FUZZTEST_FUZZ_FOR", "5s", || { + FuzzTestOptions::parse_from(std::iter::empty::()) + }) + }; expect_true!(options.fuzz_for.is_some()); expect_that!( ExecutionMode::from_fuzztest_options(&options), matches_pattern!(ExecutionMode::Fuzz(_)) ); - - // SAFETY: Cleaning up environment variables. - unsafe { - std::env::remove_var("FUZZTEST_FUZZ_FOR"); - } } #[gtest] fn test_fuzz_options_parsing_indefinite_env() { - // SAFETY: Testing environment parsing in single-threaded context. - unsafe { - std::env::set_var("FUZZTEST_FUZZ_FOR", "inf"); - } - - let options = FuzzTestOptions::parse_from(std::iter::empty::()); + assert_single_threaded_test_environment(); + // SAFETY: Single-threaded test environment and no background threads. + let options = unsafe { + with_env_var("FUZZTEST_FUZZ_FOR", "inf", || { + FuzzTestOptions::parse_from(std::iter::empty::()) + }) + }; expect_that!(options.fuzz_for, eq(Some(RunDuration::Indefinitely))); let mode = ExecutionMode::from_fuzztest_options(&options); @@ -432,11 +430,6 @@ mod tests { panic!("Expected ExecutionMode::Fuzz"); }; expect_that!(fuzz_opts.fuzz_for, eq(RunDuration::Indefinitely)); - - // SAFETY: Cleaning up environment variables. - unsafe { - std::env::remove_var("FUZZTEST_FUZZ_FOR"); - } } #[gtest] @@ -775,18 +768,14 @@ mod tests { #[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::()); - - // SAFETY: Cleaning up environment variables. - unsafe { - std::env::remove_var("FUZZTEST_REPLAY_ID"); - } + assert_single_threaded_test_environment(); + // SAFETY: Single-threaded test environment and no background threads. + let result = unsafe { + with_env_vars( + [("FUZZTEST_REPLAY_ID", Some("my_crash_123")), ("FUZZTEST_CORPUS_DB", None)], + || FuzzTestOptions::try_parse_from(std::iter::empty::()), + ) + }; let err = result.expect_err("parsing should fail when corpus_db is missing"); expect_that!(err.kind(), eq(clap::error::ErrorKind::MissingRequiredArgument)); @@ -794,22 +783,19 @@ mod tests { #[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"); + assert_single_threaded_test_environment(); + // SAFETY: Single-threaded test environment and no background threads. + let options = unsafe { + with_env_vars( + [ + ("FUZZTEST_REPLAY_ID", Some("my_crash_123")), + ("FUZZTEST_CORPUS_DB", Some("/tmp/corpus_db")), + ], + || FuzzTestOptions::try_parse_from(std::iter::empty::()), + ) } + .expect("parsing should succeed when both replay_id and corpus_db are present"); - let result = FuzzTestOptions::try_parse_from(std::iter::empty::()); - - // 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(Path::new("/tmp/corpus_db")))); }