diff --git a/crates/forge_config/src/reader.rs b/crates/forge_config/src/reader.rs index cc1342aaf1..3e82ecdcbc 100644 --- a/crates/forge_config/src/reader.rs +++ b/crates/forge_config/src/reader.rs @@ -30,6 +30,9 @@ static LOAD_DOT_ENV: LazyLock<()> = LazyLock::new(|| { } }); +/// Caches base-path resolution for the process lifetime. +static BASE_PATH: LazyLock = LazyLock::new(ConfigReader::resolve_base_path); + /// Merges [`ForgeConfig`] from layered sources using a builder pattern. #[derive(Default)] pub struct ConfigReader { @@ -58,6 +61,10 @@ impl ConfigReader { /// existing directory without disruption. /// 3. `~/.forge` as the default path. pub fn base_path() -> PathBuf { + BASE_PATH.clone() + } + + fn resolve_base_path() -> PathBuf { if let Ok(path) = std::env::var("FORGE_CONFIG") { return PathBuf::from(path); } @@ -199,7 +206,7 @@ mod tests { #[test] fn test_base_path_uses_forge_config_env_var() { let _guard = EnvGuard::set(&[("FORGE_CONFIG", "/custom/forge/dir")]); - let actual = ConfigReader::base_path(); + let actual = ConfigReader::resolve_base_path(); let expected = PathBuf::from("/custom/forge/dir"); assert_eq!(actual, expected); } @@ -210,7 +217,7 @@ mod tests { // cannot race with test_base_path_uses_forge_config_env_var. let _guard = EnvGuard::set_and_remove(&[], &["FORGE_CONFIG"]); - let actual = ConfigReader::base_path(); + let actual = ConfigReader::resolve_base_path(); // Without FORGE_CONFIG set the path must be either "forge" (legacy, // preferred when ~/forge exists) or ".forge" (default new path). let name = actual.file_name().unwrap(); diff --git a/crates/forge_infra/src/env.rs b/crates/forge_infra/src/env.rs index d281eabd52..7a42705e51 100644 --- a/crates/forge_infra/src/env.rs +++ b/crates/forge_infra/src/env.rs @@ -158,43 +158,12 @@ impl EnvironmentInfra for ForgeEnvironmentInfra { #[cfg(test)] mod tests { use std::path::PathBuf; - use std::sync::{Mutex, MutexGuard}; use forge_config::ForgeConfig; use pretty_assertions::assert_eq; use super::*; - /// Serializes tests that mutate environment variables to prevent races. - static ENV_MUTEX: Mutex<()> = Mutex::new(()); - - /// Holds env vars set for a test's duration and removes them on drop, - /// while holding [`ENV_MUTEX`]. - struct EnvGuard { - keys: Vec<&'static str>, - _lock: MutexGuard<'static, ()>, - } - - impl EnvGuard { - #[must_use] - fn set(pairs: &[(&'static str, &str)]) -> Self { - let lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner()); - let keys = pairs.iter().map(|(k, _)| *k).collect(); - for (key, value) in pairs { - unsafe { std::env::set_var(key, value) }; - } - Self { keys, _lock: lock } - } - } - - impl Drop for EnvGuard { - fn drop(&mut self) { - for key in &self.keys { - unsafe { std::env::remove_var(key) }; - } - } - } - #[test] fn test_to_environment_sets_cwd() { let fixture_cwd = PathBuf::from("/test/cwd"); @@ -203,11 +172,22 @@ mod tests { } #[test] - fn test_to_environment_uses_forge_config_env_var() { - let _guard = EnvGuard::set(&[("FORGE_CONFIG", "/custom/config/dir")]); - let actual = to_environment(PathBuf::from("/any/cwd")); - let expected = PathBuf::from("/custom/config/dir"); - assert_eq!(actual.base_path, expected); + fn test_to_environment_base_path_is_stable_after_env_var_change() { + let fixture_cwd = PathBuf::from("/any/cwd"); + let expected = to_environment(fixture_cwd.clone()).base_path; + + let previous = std::env::var("FORGE_CONFIG").ok(); + unsafe { std::env::set_var("FORGE_CONFIG", "/custom/config/dir") }; + + let actual = to_environment(fixture_cwd).base_path; + + if let Some(value) = previous { + unsafe { std::env::set_var("FORGE_CONFIG", value) }; + } else { + unsafe { std::env::remove_var("FORGE_CONFIG") }; + } + + assert_eq!(actual, expected); } #[test]