diff --git a/crates/socket-patch-core/src/crawlers/composer_crawler.rs b/crates/socket-patch-core/src/crawlers/composer_crawler.rs index fda0dc0cd..31333d786 100644 --- a/crates/socket-patch-core/src/crawlers/composer_crawler.rs +++ b/crates/socket-patch-core/src/crawlers/composer_crawler.rs @@ -772,10 +772,28 @@ fn global_vendor_dir_sync(composer_home: &Path) -> Option { manifest.as_deref(), true, || read("config.json"), - &|name| std::env::var_os(name), + &process_var, ) } +/// The process environment the vendor-dir lookups read +/// (`COMPOSER_VENDOR_DIR`, and `COMPOSER_HOME` / `HOME` / `APPDATA` for the +/// user-level `config.json`). +#[cfg(not(test))] +fn process_var(name: &str) -> Option { + std::env::var_os(name) +} + +/// In the unit tests, only the vars a test holds with an +/// [`EnvGuard`](crate::test_env::EnvGuard). A developer's +/// `COMPOSER_VENDOR_DIR`, or a `vendor-dir` in their own Composer +/// `config.json` (found through `HOME`), would otherwise move every +/// fixture's vendor tree. +#[cfg(test)] +fn process_var(name: &str) -> Option { + crate::test_env::guarded_var_os(name) +} + /// Resolve the vendor directory of a local project the way Composer does /// (see [`resolve_vendor_dir`] for the cascade and the rules). /// @@ -798,7 +816,7 @@ async fn resolve_local_vendor_dir(cwd: &Path) -> Option { /// a synchronous path after the run, and must name the directory Composer /// actually installed into, not a hardcoded `vendor/`. pub fn resolve_local_vendor_dir_sync(cwd: &Path) -> Option { - let var = |name: &str| std::env::var_os(name); + let var = process_var; let read = |path: &Path| crate::utils::fs::read_regular_to_string_sync(path).ok(); let manifest = read(&cwd.join("composer.json")); let user_config = @@ -819,7 +837,7 @@ pub fn resolve_local_vendor_dir_sync(cwd: &Path) -> Option { /// file exists, or when the directory cannot be resolved at all (a /// placeholder naming nothing). pub fn local_install_unread(cwd: &Path) -> bool { - local_install_unread_with(cwd, &|name| std::env::var_os(name)) + local_install_unread_with(cwd, &process_var) } fn local_install_unread_with(cwd: &Path, var: &impl Fn(&str) -> Option) -> bool { @@ -2514,4 +2532,49 @@ mod tests { ); } } + + /// The unit tests see only the Composer env a test holds: a host + /// `COMPOSER_VENDOR_DIR` must not move a fixture's vendor tree. + #[test] + #[serial_test::serial] + fn vendor_dir_lookups_ignore_the_hosts_composer_env() { + use crate::test_env::EnvGuard; + /// Sets a var raw, as a host would (an `EnvGuard` would make it + /// visible), and puts the previous value back on drop, a failed + /// assertion included. Only the lookups this test covers read + /// `COMPOSER_VENDOR_DIR`. + struct HostVar(&'static str, Option); + impl HostVar { + fn set(key: &'static str, value: &str) -> Self { + let previous = std::env::var_os(key); + std::env::set_var(key, value); + Self(key, previous) + } + } + impl Drop for HostVar { + fn drop(&mut self) { + match self.1.take() { + Some(value) => std::env::set_var(self.0, value), + None => std::env::remove_var(self.0), + } + } + } + let dir = tempfile::tempdir().unwrap(); + let project = dir.path(); + { + let _host = HostVar::set("COMPOSER_VENDOR_DIR", "from-host"); + assert_eq!( + resolve_local_vendor_dir_sync(project), + Some(project.join("vendor")), + "host env hidden" + ); + } + + let _var = EnvGuard::set("COMPOSER_VENDOR_DIR", "held"); + assert_eq!( + resolve_local_vendor_dir_sync(project), + Some(project.join("held")), + "a value the test holds is honoured" + ); + } } diff --git a/crates/socket-patch-core/src/test_env.rs b/crates/socket-patch-core/src/test_env.rs index 6fe5f64db..811e13210 100644 --- a/crates/socket-patch-core/src/test_env.rs +++ b/crates/socket-patch-core/src/test_env.rs @@ -17,8 +17,14 @@ fn guarded() -> MutexGuard<'static, Vec> { /// host set. For lookups that must not see the developer's environment /// (`utils::go_env` in the unit tests). pub(crate) fn guarded_var(key: &str) -> Option { + guarded_var_os(key).and_then(|v| v.into_string().ok()) +} + +/// [`guarded_var`] without the UTF-8 requirement, for lookups that read +/// `std::env::var_os` in production (`crawlers::composer_crawler`). +pub(crate) fn guarded_var_os(key: &str) -> Option { let held = guarded().iter().any(|k| k == key); - held.then(|| std::env::var(key).ok()).flatten() + held.then(|| std::env::var_os(key)).flatten() } /// Sets or removes env vars and puts the previous values back on drop, a