| // 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<String> = 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<K, V, R, F>(key: K, val: V, f: F) -> R | |
| where | |
| K: AsRef<OsStr>, | |
| V: AsRef<OsStr>, | |
| 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<K, V, I, R, F>(vars: I, f: F) -> R | |
| where | |
| K: AsRef<OsStr>, | |
| V: AsRef<OsStr>, | |
| I: IntoIterator<Item = (K, Option<V>)>, | |
| F: FnOnce() -> R, | |
| { | |
| struct EnvRestorer { | |
| saved_vars: Vec<(OsString, Option<OsString>)>, | |
| } | |
| 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); | |
| } | |
| } | |
| } |