diff --git a/crates/socket-patch-core/src/api/client.rs b/crates/socket-patch-core/src/api/client.rs index 0ed3470e3..05d3b3509 100644 --- a/crates/socket-patch-core/src/api/client.rs +++ b/crates/socket-patch-core/src/api/client.rs @@ -3293,9 +3293,9 @@ mod tests { #[serial_test::serial] async fn binary_url_uses_the_resolved_proxy_when_org_resolution_fails() { const HASH: &str = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"; - let _env = OrgResolutionEnvGuard::isolate(); - let saved_proxy = std::env::var("SOCKET_PROXY_URL").ok(); - std::env::set_var("SOCKET_PROXY_URL", "http://env-proxy.test:9999/"); + let _env = isolate_org_resolution_env(); + let _proxy = + crate::test_env::EnvGuard::set("SOCKET_PROXY_URL", "http://env-proxy.test:9999/"); let (client, use_public_proxy) = get_api_client_with_overrides(ApiClientEnvOverrides { api_url: Some("http://127.0.0.1:1".into()), @@ -3309,44 +3309,23 @@ mod tests { std::env::remove_var("SOCKET_PROXY_URL"); let (url, use_auth) = client.binary_url("blob", HASH); - match saved_proxy { - Some(v) => std::env::set_var("SOCKET_PROXY_URL", v), - None => std::env::remove_var("SOCKET_PROXY_URL"), - } - assert!(use_public_proxy); assert!(client.api_token().is_none(), "a proxy client has no bearer"); assert_eq!(url, format!("http://env-proxy.test:9999/patch/blob/{HASH}")); assert!(!use_auth, "the bearer must never target the proxy"); } - /// Guard that snapshots the env vars that can short-circuit org - /// auto-resolution, forces the "no ambient slug / offline flag / - /// socket-cli config" state, and restores the snapshot on drop (panic - /// included). Keeps the resolution tests below honest on a dev machine - /// with `socket login` state or ambient SOCKET_* vars. - struct OrgResolutionEnvGuard { - saved: Vec<(&'static str, Option)>, - } - impl OrgResolutionEnvGuard { - fn isolate() -> Self { - let names = ["SOCKET_ORG_SLUG", "SOCKET_OFFLINE", "SOCKET_NO_CONFIG"]; - let saved = names.iter().map(|&n| (n, std::env::var(n).ok())).collect(); - std::env::remove_var("SOCKET_ORG_SLUG"); - std::env::remove_var("SOCKET_OFFLINE"); - std::env::set_var("SOCKET_NO_CONFIG", "1"); - Self { saved } - } - } - impl Drop for OrgResolutionEnvGuard { - fn drop(&mut self) { - for (name, value) in self.saved.drain(..) { - match value { - Some(v) => std::env::set_var(name, v), - None => std::env::remove_var(name), - } - } - } + /// Forces the "no ambient slug / offline flag / socket-cli config" + /// state for the env vars that can short-circuit org auto-resolution; + /// the returned guard restores them on drop (panic included). Keeps + /// the resolution tests below honest on a dev machine with `socket + /// login` state or ambient SOCKET_* vars. + fn isolate_org_resolution_env() -> crate::test_env::EnvGuard { + crate::test_env::EnvGuard::many([ + ("SOCKET_ORG_SLUG", None), + ("SOCKET_OFFLINE", None), + ("SOCKET_NO_CONFIG", Some("1")), + ]) } /// The network half of `resolve_org_slug`: `GET /v0/organizations` @@ -3360,7 +3339,7 @@ mod tests { use wiremock::matchers::{method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; - let _env = OrgResolutionEnvGuard::isolate(); + let _env = isolate_org_resolution_env(); let server = MockServer::start().await; Mock::given(method("GET")) .and(path("/v0/organizations")) @@ -3403,7 +3382,7 @@ mod tests { use wiremock::matchers::{method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; - let _env = OrgResolutionEnvGuard::isolate(); + let _env = isolate_org_resolution_env(); let server = MockServer::start().await; Mock::given(method("GET")) .and(path("/v0/organizations")) @@ -3445,7 +3424,7 @@ mod tests { const HASH: &str = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"; const UUID: &str = "11111111-2222-4333-8444-555555555555"; - let _env = OrgResolutionEnvGuard::isolate(); + let _env = isolate_org_resolution_env(); let token = format!("sktsec_{}_api", "x".repeat(44)); let proxy = "http://proxy.example.test"; @@ -4207,7 +4186,7 @@ mod tests { #[tokio::test] #[serial_test::serial] async fn vendor_package_url_after_failed_org_resolution_targets_proxy_host() { - let _env = OrgResolutionEnvGuard::isolate(); + let _env = isolate_org_resolution_env(); let (client, _) = get_api_client_with_overrides(ApiClientEnvOverrides { api_url: Some("http://127.0.0.1:1".into()), api_token: Some("sktsec_x_api".into()), diff --git a/crates/socket-patch-core/src/crawlers/deno_crawler.rs b/crates/socket-patch-core/src/crawlers/deno_crawler.rs index f322324c9..e585e5281 100644 --- a/crates/socket-patch-core/src/crawlers/deno_crawler.rs +++ b/crates/socket-patch-core/src/crawlers/deno_crawler.rs @@ -692,7 +692,7 @@ mod tests { let deno_home = tempfile::tempdir().unwrap(); let jsr = deno_home.path().join("npm").join("jsr.io"); stage(&jsr, "@std", "path", "0.220.0").await; - let _g = EnvGuard::set("DENO_DIR", deno_home.path().to_str().unwrap()); + let _g = EnvGuard::set("DENO_DIR", deno_home.path()); let crawler = DenoCrawler; let opts = CrawlerOptions { @@ -719,7 +719,7 @@ mod tests { stage(&jsr, "@std", "path", "0.220.0").await; stage(&jsr, "@std", "fs", "1.0.0").await; stage(&jsr, "@other", "x", "2.0.0").await; - let _g = EnvGuard::set("DENO_DIR", deno_home.path().to_str().unwrap()); + let _g = EnvGuard::set("DENO_DIR", deno_home.path()); let opts = CrawlerOptions { cwd: project.path().to_path_buf(), @@ -810,7 +810,7 @@ mod tests { let jsr = deno_home.path().join("npm").join("jsr.io"); stage(&jsr, "@std", "path", "0.220.0").await; stage(&jsr, "@std", "fs", "1.0.0").await; - let _g = EnvGuard::set("DENO_DIR", deno_home.path().to_str().unwrap()); + let _g = EnvGuard::set("DENO_DIR", deno_home.path()); let opts = CrawlerOptions { cwd: project.path().to_path_buf(), global: false, @@ -943,31 +943,7 @@ mod tests { // ── deno_dir / cache-root resolution ─────────────────────────── - /// Save and restore an env var around a closure body. - struct EnvGuard { - key: &'static str, - prev: Option, - } - impl EnvGuard { - fn set(key: &'static str, value: &str) -> Self { - let prev = std::env::var(key).ok(); - std::env::set_var(key, value); - Self { key, prev } - } - fn unset(key: &'static str) -> Self { - let prev = std::env::var(key).ok(); - std::env::remove_var(key); - Self { key, prev } - } - } - impl Drop for EnvGuard { - fn drop(&mut self) { - match &self.prev { - Some(v) => std::env::set_var(self.key, v), - None => std::env::remove_var(self.key), - } - } - } + use crate::test_env::EnvGuard; /// #444: only Deno's `bin/./node_modules` whose config leaves /// `nodeModulesDir` unset (or `none`/`false`), with no `package.json`, diff --git a/crates/socket-patch-core/src/crawlers/go_crawler.rs b/crates/socket-patch-core/src/crawlers/go_crawler.rs index 908b039b4..24333f73b 100644 --- a/crates/socket-patch-core/src/crawlers/go_crawler.rs +++ b/crates/socket-patch-core/src/crawlers/go_crawler.rs @@ -1216,7 +1216,7 @@ mod tests { mkdir(&cache); write(&project.join("go.mod"), "module example.com/app\n"); let env = ( - EnvGuard::set("GOMODCACHE", cache.to_str().unwrap()), + EnvGuard::set("GOMODCACHE", &cache), EnvGuard::unset("GOWORK"), ); Fixture { @@ -1412,7 +1412,7 @@ mod tests { // GOWORK naming a file elsewhere (absolute on every platform: // `/x` has no drive on Windows, so go rejects it as relative). let elsewhere = f.project.parent().unwrap().join("elsewhere/go.work"); - let _gowork = EnvGuard::set("GOWORK", &elsewhere.to_string_lossy()); + let _gowork = EnvGuard::set("GOWORK", &elsewhere); assert_eq!(f.crawl().await, walked); } @@ -1498,31 +1498,7 @@ mod tests { // ---- get_gomodcache env tests ---- - /// Save and restore an env var around a test body. - struct EnvGuard { - key: &'static str, - prev: Option, - } - impl EnvGuard { - fn set(key: &'static str, value: &str) -> Self { - let prev = std::env::var(key).ok(); - std::env::set_var(key, value); - Self { key, prev } - } - fn unset(key: &'static str) -> Self { - let prev = std::env::var(key).ok(); - std::env::remove_var(key); - Self { key, prev } - } - } - impl Drop for EnvGuard { - fn drop(&mut self) { - match &self.prev { - Some(v) => std::env::set_var(self.key, v), - None => std::env::remove_var(self.key), - } - } - } + use crate::test_env::EnvGuard; #[test] #[serial_test::serial] @@ -1547,7 +1523,7 @@ mod tests { let _userprofile = EnvGuard::unset("USERPROFILE"); let first_a = std::env::join_paths([gopath_a.path().as_os_str(), home_b.path().as_os_str()]).unwrap(); - let _gopath = EnvGuard::set("GOPATH", first_a.to_str().unwrap()); + let _gopath = EnvGuard::set("GOPATH", &first_a); assert_eq!( GoCrawler::get_gomodcache(), Some(gopath_a.path().join("pkg").join("mod")), @@ -1568,7 +1544,7 @@ mod tests { // Case B: GOMODCACHE and GOPATH both set-but-empty fall all the way // through to the $HOME/go/pkg/mod default. let _gopath_empty = EnvGuard::set("GOPATH", ""); - let _home_b = EnvGuard::set("HOME", home_b.path().to_str().unwrap()); + let _home_b = EnvGuard::set("HOME", home_b.path()); assert_eq!( GoCrawler::get_gomodcache(), Some(home_b.path().join("go").join("pkg").join("mod")), @@ -1602,7 +1578,7 @@ mod tests { let gopath = dir.path().join("gopath"); let _gomodcache = EnvGuard::unset("GOMODCACHE"); let _gopath = EnvGuard::unset("GOPATH"); - let _goenv = EnvGuard::set("GOENV", file.to_str().unwrap()); + let _goenv = EnvGuard::set("GOENV", &file); std::fs::write(&file, format!("GOMODCACHE={}\n", cache.display())).unwrap(); assert_eq!(GoCrawler::get_gomodcache(), Some(cache.clone())); @@ -1614,7 +1590,7 @@ mod tests { ); let env_cache = dir.path().join("env-cache"); - let _env = EnvGuard::set("GOMODCACHE", env_cache.to_str().unwrap()); + let _env = EnvGuard::set("GOMODCACHE", &env_cache); assert_eq!(GoCrawler::get_gomodcache(), Some(env_cache)); } diff --git a/crates/socket-patch-core/src/crawlers/maven_crawler.rs b/crates/socket-patch-core/src/crawlers/maven_crawler.rs index 0822a50ab..20acf2921 100644 --- a/crates/socket-patch-core/src/crawlers/maven_crawler.rs +++ b/crates/socket-patch-core/src/crawlers/maven_crawler.rs @@ -2506,31 +2506,7 @@ mod tests { // ---- m2_repo_path env tests ---- - /// Save and restore an env var around a test body. - struct EnvGuard { - key: &'static str, - prev: Option, - } - impl EnvGuard { - fn set(key: &'static str, value: &str) -> Self { - let prev = std::env::var(key).ok(); - std::env::set_var(key, value); - Self { key, prev } - } - fn unset(key: &'static str) -> Self { - let prev = std::env::var(key).ok(); - std::env::remove_var(key); - Self { key, prev } - } - } - impl Drop for EnvGuard { - fn drop(&mut self) { - match &self.prev { - Some(v) => std::env::set_var(self.key, v), - None => std::env::remove_var(self.key), - } - } - } + use crate::test_env::EnvGuard; /// B66: with no usable home the default used to be the literal /// RELATIVE `~/.m2/repository`, which resolves against the working @@ -2584,7 +2560,7 @@ mod tests { let _args = EnvGuard::unset("MAVEN_ARGS"); let _opts = EnvGuard::set( "MAVEN_OPTS", - &format!("-Dmaven.repo.local={}", repo_local.path().display()), + format!("-Dmaven.repo.local={}", repo_local.path().display()), ); let repo = MavenCrawler::m2_repo_path(); @@ -2612,8 +2588,8 @@ mod tests { // Point the home directory at an empty dir so a developer's own // `~/.m2/settings.xml` can't change the result. let home = tempfile::tempdir().unwrap(); - let _home = EnvGuard::set("HOME", home.path().to_str().unwrap()); - let _profile = EnvGuard::set("USERPROFILE", home.path().to_str().unwrap()); + let _home = EnvGuard::set("HOME", home.path()); + let _profile = EnvGuard::set("USERPROFILE", home.path()); let repo = MavenCrawler::m2_repo_path(); assert_ne!( @@ -2871,7 +2847,7 @@ mod tests { .await .unwrap(); let missing = project.path().join("no-such-m2-repo"); - let _local = EnvGuard::set("MAVEN_REPO_LOCAL", missing.to_str().unwrap()); + let _local = EnvGuard::set("MAVEN_REPO_LOCAL", &missing); let crawler = MavenCrawler::new(); let options = CrawlerOptions { diff --git a/crates/socket-patch-core/src/crawlers/npm_crawler.rs b/crates/socket-patch-core/src/crawlers/npm_crawler.rs index 78b783151..0e937fd2f 100644 --- a/crates/socket-patch-core/src/crawlers/npm_crawler.rs +++ b/crates/socket-patch-core/src/crawlers/npm_crawler.rs @@ -5903,29 +5903,8 @@ mod tests { } } - /// Save and restore an env var around a test body (drop-safe). #[cfg(target_os = "macos")] - struct EnvGuard { - key: &'static str, - prev: Option, - } - #[cfg(target_os = "macos")] - impl EnvGuard { - fn set(key: &'static str, value: &std::ffi::OsStr) -> Self { - let prev = std::env::var(key).ok(); - std::env::set_var(key, value); - Self { key, prev } - } - } - #[cfg(target_os = "macos")] - impl Drop for EnvGuard { - fn drop(&mut self) { - match &self.prev { - Some(v) => std::env::set_var(self.key, v), - None => std::env::remove_var(self.key), - } - } - } + use crate::test_env::EnvGuard; /// Pins the literal volta (`.volta/tools/image/node/*/lib/ /// node_modules`) and fnm (`.fnm/node-versions/*/installation/lib/ diff --git a/crates/socket-patch-core/src/crawlers/python_crawler.rs b/crates/socket-patch-core/src/crawlers/python_crawler.rs index 778e1712e..a7780ee5b 100644 --- a/crates/socket-patch-core/src/crawlers/python_crawler.rs +++ b/crates/socket-patch-core/src/crawlers/python_crawler.rs @@ -7876,25 +7876,7 @@ G= async fn test_pyenv_root_env_var_site_packages_discovered() { // Save/restore guard so a panicking assertion cannot leak the // override into other #[serial] env-var tests. - struct EnvGuard { - key: &'static str, - prev: Option, - } - impl EnvGuard { - fn set(key: &'static str, value: &str) -> Self { - let prev = std::env::var(key).ok(); - std::env::set_var(key, value); - Self { key, prev } - } - } - impl Drop for EnvGuard { - fn drop(&mut self) { - match &self.prev { - Some(v) => std::env::set_var(self.key, v), - None => std::env::remove_var(self.key), - } - } - } + use crate::test_env::EnvGuard; let dir = tempfile::tempdir().unwrap(); let sp = dir @@ -7906,7 +7888,7 @@ G= .join("site-packages"); tokio::fs::create_dir_all(&sp).await.unwrap(); - let _pyenv_root = EnvGuard::set("PYENV_ROOT", dir.path().to_str().unwrap()); + let _pyenv_root = EnvGuard::set("PYENV_ROOT", dir.path()); let results = get_global_python_site_packages().await; assert!( results.contains(&sp), @@ -8060,25 +8042,7 @@ G= #[test] #[serial_test::serial] fn site_query_memo_follows_the_environment() { - struct EnvGuard { - key: &'static str, - prev: Option, - } - impl EnvGuard { - fn set(key: &'static str, value: &Path) -> Self { - let prev = std::env::var_os(key); - std::env::set_var(key, value); - Self { key, prev } - } - } - impl Drop for EnvGuard { - fn drop(&mut self) { - match &self.prev { - Some(v) => std::env::set_var(self.key, v), - None => std::env::remove_var(self.key), - } - } - } + use crate::test_env::EnvGuard; let memoized = || SITE_QUERY_MEMO.get_or_run(run_site_query); let base_a = tempfile::tempdir().unwrap(); diff --git a/crates/socket-patch-core/src/lib.rs b/crates/socket-patch-core/src/lib.rs index c15fe9e64..c0a9cacb0 100644 --- a/crates/socket-patch-core/src/lib.rs +++ b/crates/socket-patch-core/src/lib.rs @@ -19,4 +19,8 @@ pub mod vex; #[cfg(test)] mod golden; #[cfg(test)] +mod test_env; +#[cfg(test)] +mod test_env_tests; +#[cfg(test)] mod test_rng; diff --git a/crates/socket-patch-core/src/patch/jvm_jar.rs b/crates/socket-patch-core/src/patch/jvm_jar.rs index 1c9e24d93..1637ef6b4 100644 --- a/crates/socket-patch-core/src/patch/jvm_jar.rs +++ b/crates/socket-patch-core/src/patch/jvm_jar.rs @@ -832,27 +832,7 @@ mod tests { const PURL: &str = "pkg:maven/com.example/lib@1.0"; - /// Save/restore guard for env-var tests; every user is - /// `#[serial_test::serial]`. - struct EnvGuard { - key: &'static str, - prev: Option, - } - impl EnvGuard { - fn set(key: &'static str, value: &str) -> Self { - let prev = std::env::var(key).ok(); - std::env::set_var(key, value); - Self { key, prev } - } - } - impl Drop for EnvGuard { - fn drop(&mut self) { - match &self.prev { - Some(v) => std::env::set_var(self.key, v), - None => std::env::remove_var(self.key), - } - } - } + use crate::test_env::EnvGuard; fn record() -> HashMap { HashMap::from([( @@ -1146,7 +1126,7 @@ mod tests { .respond_with(ResponseTemplate::new(200).set_body_bytes(original.clone())) .mount(&server) .await; - let _registry = EnvGuard::set("SOCKET_MAVEN_REGISTRY", &server.uri()); + let _registry = EnvGuard::set("SOCKET_MAVEN_REGISTRY", server.uri()); restore.offline = false; let out = rollback_jar_swap(&restore, &[hash_dir.clone()]).await; assert!(out[0].success, "{:?}", out[0].error); diff --git a/crates/socket-patch-core/src/test_env.rs b/crates/socket-patch-core/src/test_env.rs new file mode 100644 index 000000000..fd34e92f5 --- /dev/null +++ b/crates/socket-patch-core/src/test_env.rs @@ -0,0 +1,65 @@ +//! The one restore-on-drop guard the tests use to change process env. The +//! integration tests that need it compile this same file in with +//! `#[path = "../src/test_env.rs"]`. Env is process-global, so every user +//! must also be `#[serial_test::serial]`. + +use std::ffi::{OsStr, OsString}; + +/// Sets or removes env vars and puts the previous values back on drop, a +/// panicking assertion included. Previous values are kept as `OsString`, +/// so a non-UTF-8 value survives the round trip. +#[must_use = "the variables are restored when the guard drops"] +pub(crate) struct EnvGuard { + saved: Vec<(OsString, Option)>, +} + +impl EnvGuard { + pub(crate) fn set(key: &str, value: impl AsRef) -> Self { + Self::many([(key, Some(value))]) + } + + pub(crate) fn unset(key: &str) -> Self { + Self::many([(key, None::<&str>)]) + } + + /// Applies each pair in order: `Some` sets the var, `None` removes it. + pub(crate) fn many(pairs: impl IntoIterator)>) -> Self + where + K: AsRef, + V: AsRef, + { + // Saved before each change, so a panic partway through (a key with + // `=` in it) still restores the vars already changed. + let mut guard = Self { saved: Vec::new() }; + for (key, value) in pairs { + let key = key.as_ref(); + guard + .saved + .push((key.to_os_string(), std::env::var_os(key))); + match value { + Some(v) => std::env::set_var(key, v), + None => std::env::remove_var(key), + } + } + guard + } +} + +impl Drop for EnvGuard { + fn drop(&mut self) { + // Newest first, so a key listed twice ends at its original value. + while let Some((key, prev)) = self.saved.pop() { + match prev { + Some(v) => std::env::set_var(&key, v), + None => std::env::remove_var(&key), + } + } + } +} + +/// Runs `f` with `pairs` applied (`None` removes the var), then restores +/// them, even if `f` panics. +pub(crate) fn with_env(pairs: &[(&str, Option<&str>)], f: impl FnOnce() -> T) -> T { + let _env = EnvGuard::many(pairs.iter().copied()); + f() +} diff --git a/crates/socket-patch-core/src/test_env_tests.rs b/crates/socket-patch-core/src/test_env_tests.rs new file mode 100644 index 000000000..4b4bcc80b --- /dev/null +++ b/crates/socket-patch-core/src/test_env_tests.rs @@ -0,0 +1,49 @@ +//! Self-tests for `test_env`. They live apart from it because the +//! integration tests compile `test_env.rs` in with `#[path]`, and these +//! should run once, in the lib's unit tests. + +use std::ffi::OsStr; + +use crate::test_env::{with_env, EnvGuard}; + +const KEY: &str = "SOCKET_PATCH_TEST_ENV_GUARD_PROBE"; + +#[test] +#[serial_test::serial] +fn restores_set_unset_and_repeated_keys_on_drop() { + std::env::set_var(KEY, "orig"); + { + let _a = EnvGuard::set(KEY, "a"); + assert_eq!(std::env::var(KEY).as_deref(), Ok("a")); + let _b = EnvGuard::many([(KEY, None::<&str>), (KEY, Some("c"))]); + assert_eq!(std::env::var(KEY).as_deref(), Ok("c")); + } + assert_eq!(std::env::var(KEY).as_deref(), Ok("orig")); + + std::env::remove_var(KEY); + drop(EnvGuard::set(KEY, "x")); + assert_eq!(std::env::var_os(KEY), None); +} + +#[test] +#[serial_test::serial] +fn with_env_restores_after_a_panic() { + std::env::remove_var(KEY); + let caught = std::panic::catch_unwind(|| { + with_env(&[(KEY, Some("leak"))], || panic!("intentional panic")) + }); + assert!(caught.is_err()); + assert_eq!(std::env::var_os(KEY), None); +} + +#[cfg(unix)] +#[test] +#[serial_test::serial] +fn keeps_a_non_utf8_previous_value() { + use std::os::unix::ffi::OsStrExt; + let raw = OsStr::from_bytes(b"\xff\xfe"); + std::env::set_var(KEY, raw); + drop(EnvGuard::unset(KEY)); + assert_eq!(std::env::var_os(KEY).as_deref(), Some(raw)); + std::env::remove_var(KEY); +} diff --git a/crates/socket-patch-core/src/utils/fs.rs b/crates/socket-patch-core/src/utils/fs.rs index af07667f2..2e61091dc 100644 --- a/crates/socket-patch-core/src/utils/fs.rs +++ b/crates/socket-patch-core/src/utils/fs.rs @@ -1487,24 +1487,7 @@ mod tests { /// Run `f` with `HOME`/`USERPROFILE` set to `home`/`profile` /// (`None` = unset), restoring both. fn with_home_env(home: Option<&str>, profile: Option<&str>, f: impl FnOnce() -> T) -> T { - let prev_home = std::env::var_os("HOME"); - let prev_profile = std::env::var_os("USERPROFILE"); - let set = |name: &str, v: Option<&str>| match v { - Some(v) => std::env::set_var(name, v), - None => std::env::remove_var(name), - }; - set("HOME", home); - set("USERPROFILE", profile); - let out = f(); - match prev_home { - Some(v) => std::env::set_var("HOME", v), - None => std::env::remove_var("HOME"), - } - match prev_profile { - Some(v) => std::env::set_var("USERPROFILE", v), - None => std::env::remove_var("USERPROFILE"), - } - out + crate::test_env::with_env(&[("HOME", home), ("USERPROFILE", profile)], f) } /// Regression: a set-but-empty `HOME` (stripped CI/container/sudo diff --git a/crates/socket-patch-core/src/utils/socket_cli_config.rs b/crates/socket-patch-core/src/utils/socket_cli_config.rs index 298b5b552..7175eb86c 100644 --- a/crates/socket-patch-core/src/utils/socket_cli_config.rs +++ b/crates/socket-patch-core/src/utils/socket_cli_config.rs @@ -308,25 +308,7 @@ mod tests { // Env-mutating tests below are serialized: XDG_DATA_HOME / HOME / // SOCKET_NO_CONFIG are process-global and shared with other suites. - fn with_env(pairs: &[(&str, Option<&str>)], f: impl FnOnce()) { - let saved: Vec<(&str, Option)> = pairs - .iter() - .map(|&(k, _)| (k, std::env::var(k).ok())) - .collect(); - for &(k, v) in pairs { - match v { - Some(v) => std::env::set_var(k, v), - None => std::env::remove_var(k), - } - } - f(); - for (k, v) in saved { - match v { - Some(v) => std::env::set_var(k, v), - None => std::env::remove_var(k), - } - } - } + use crate::test_env::with_env; #[test] #[serial_test::serial] diff --git a/crates/socket-patch-core/src/vendor/maven_repo.rs b/crates/socket-patch-core/src/vendor/maven_repo.rs index fd49914ec..7b959d1c6 100644 --- a/crates/socket-patch-core/src/vendor/maven_repo.rs +++ b/crates/socket-patch-core/src/vendor/maven_repo.rs @@ -2644,27 +2644,7 @@ mod tests { // ── coverage: refusal and edge arms ────────────────────────────────────── - /// Save/restore guard for env-var tests (same shape as the maven crawler's - /// tests); every user must also be `#[serial_test::serial]`. - struct EnvGuard { - key: &'static str, - prev: Option, - } - impl EnvGuard { - fn set(key: &'static str, value: &str) -> Self { - let prev = std::env::var(key).ok(); - std::env::set_var(key, value); - Self { key, prev } - } - } - impl Drop for EnvGuard { - fn drop(&mut self) { - match &self.prev { - Some(v) => std::env::set_var(self.key, v), - None => std::env::remove_var(self.key), - } - } - } + use crate::test_env::EnvGuard; fn service_cfg( api_url: Option<&str>, @@ -3298,7 +3278,7 @@ mod tests { use wiremock::matchers::{method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; let server = MockServer::start().await; - let _reg = EnvGuard::set("SOCKET_MAVEN_REGISTRY", &server.uri()); + let _reg = EnvGuard::set("SOCKET_MAVEN_REGISTRY", server.uri()); let cfg = service_cfg(None, crate::vendor::VendorSource::Service, false); let bytes = b"upstream"; let route = "/org/example/foo/1/foo-1.jar"; diff --git a/crates/socket-patch-core/tests/crawler_deno_e2e.rs b/crates/socket-patch-core/tests/crawler_deno_e2e.rs index 6c582558c..414adecaa 100644 --- a/crates/socket-patch-core/tests/crawler_deno_e2e.rs +++ b/crates/socket-patch-core/tests/crawler_deno_e2e.rs @@ -18,28 +18,11 @@ fn options_at(root: &Path) -> CrawlerOptions { } } -/// Save/restore an env var around a test body, restoring even if the -/// body panics mid-assert (important: these tests are `#[serial]`, so a -/// leaked `DENO_DIR` would poison sibling tests' default-resolution). -struct EnvGuard { - key: &'static str, - prev: Option, -} -impl EnvGuard { - fn set(key: &'static str, value: &Path) -> Self { - let prev = std::env::var(key).ok(); - std::env::set_var(key, value); - Self { key, prev } - } -} -impl Drop for EnvGuard { - fn drop(&mut self) { - match &self.prev { - Some(v) => std::env::set_var(self.key, v), - None => std::env::remove_var(self.key), - } - } -} +/// The core unit tests' env guard, compiled into this test binary too. +#[allow(dead_code)] +#[path = "../src/test_env.rs"] +mod test_env; +use test_env::EnvGuard; /// Stage a JSR package: `////mod.ts`. async fn stage_jsr_pkg(root: &Path, scope: &str, name: &str, version: &str) -> std::path::PathBuf { diff --git a/crates/socket-patch-core/tests/crawler_npm_e2e.rs b/crates/socket-patch-core/tests/crawler_npm_e2e.rs index 879c2f5e4..8afbbb1e1 100644 --- a/crates/socket-patch-core/tests/crawler_npm_e2e.rs +++ b/crates/socket-patch-core/tests/crawler_npm_e2e.rs @@ -370,34 +370,11 @@ fn get_yarn_global_prefix_with_mock_runner_success() { ); } -/// Sets environment variables for a test and puts the old values back on -/// drop, a panic included. -struct EnvGuard(Vec<(&'static str, Option)>); - -impl EnvGuard { - fn set(vars: &[(&'static str, &Path)]) -> Self { - let saved = vars - .iter() - .map(|(k, v)| { - let old = std::env::var_os(k); - std::env::set_var(k, v); - (*k, old) - }) - .collect(); - EnvGuard(saved) - } -} - -impl Drop for EnvGuard { - fn drop(&mut self) { - for (k, v) in self.0.drain(..) { - match v { - Some(v) => std::env::set_var(k, v), - None => std::env::remove_var(k), - } - } - } -} +/// The core unit tests' env guard, compiled into this test binary too. +#[allow(dead_code)] +#[path = "../src/test_env.rs"] +mod test_env; +use test_env::EnvGuard; /// #437: yarn 1.0.0 - 1.0.2 has no `yarn global dir` (it exits 1), so /// the folder yarn 1.0.x itself uses stands in for the answer it can't @@ -410,7 +387,10 @@ fn get_yarn_global_prefix_with_falls_back_for_yarn_1_0() { let home = tempfile::tempdir().unwrap(); let global = home.path().join(".config/yarn/global/node_modules"); std::fs::create_dir_all(global.join("left-pad")).unwrap(); - let _env = EnvGuard::set(&[("HOME", home.path()), ("FAKEROOTKEY", Path::new("1"))]); + let _env = EnvGuard::many([ + ("HOME", Some(home.path())), + ("FAKEROOTKEY", Some(Path::new("1"))), + ]); let runner = common::MockCommandRunner::new() .with_response("yarn", &["global", "dir"], None) .with_response("yarn", &["--version"], Some("1.0.2\n")); diff --git a/crates/socket-patch-core/tests/upstream_restore_golden.rs b/crates/socket-patch-core/tests/upstream_restore_golden.rs index 3493f9e4a..c07abed14 100644 --- a/crates/socket-patch-core/tests/upstream_restore_golden.rs +++ b/crates/socket-patch-core/tests/upstream_restore_golden.rs @@ -184,32 +184,11 @@ fn assert_round_trip( ); } -/// Sets env vars for the guard's lifetime (tests using it are `#[serial]`). -struct EnvGuard(Vec<(String, Option)>); - -impl EnvGuard { - fn set(vars: &[(&str, String)]) -> Self { - let saved = vars - .iter() - .map(|(k, _)| (k.to_string(), std::env::var(k).ok())) - .collect(); - for (k, v) in vars { - std::env::set_var(k, v); - } - EnvGuard(saved) - } -} - -impl Drop for EnvGuard { - fn drop(&mut self) { - for (k, v) in self.0.drain(..) { - match v { - Some(v) => std::env::set_var(&k, v), - None => std::env::remove_var(&k), - } - } - } -} +/// The core unit tests' env guard, compiled into this test binary too. +#[allow(dead_code)] +#[path = "../src/test_env.rs"] +mod test_env; +use test_env::EnvGuard; /// Serve an npm version document for every single-override npm case. async fn npm_mock(case: &Case) -> MockServer { @@ -259,7 +238,7 @@ async fn npm_flavor(flavor: &str, not_invertible: &[&str]) { continue; } let server = npm_mock(&case).await; - let _env = EnvGuard::set(&[("SOCKET_NPM_REGISTRY", server.uri())]); + let _env = EnvGuard::set("SOCKET_NPM_REGISTRY", server.uri()); let (after, statuses) = run_case(&case).await; assert_round_trip(&case, &after, &statuses); ran += 1; @@ -300,7 +279,7 @@ async fn package_lock_layouts_round_trip() { serde_json::Value::Array(basic.overrides.clone()), ); let server = npm_mock(&case).await; - let _env = EnvGuard::set(&[("SOCKET_NPM_REGISTRY", server.uri())]); + let _env = EnvGuard::set("SOCKET_NPM_REGISTRY", server.uri()); let (after, statuses) = run_case(&case).await; assert_round_trip(&case, &after, &statuses); } @@ -376,7 +355,7 @@ async fn cargo_goldens_round_trip() { .mount(&server) .await; } - let _env = EnvGuard::set(&[("SOCKET_CRATES_INDEX", server.uri())]); + let _env = EnvGuard::set("SOCKET_CRATES_INDEX", server.uri()); let (after, statuses) = run_case(&case).await; assert_round_trip(&case, &after, &statuses); ran += 1; @@ -413,7 +392,7 @@ async fn golang_goldens_round_trip() { .mount(&server) .await; } - let _env = EnvGuard::set(&[("SOCKET_GOSUMDB_URL", server.uri())]); + let _env = EnvGuard::set("SOCKET_GOSUMDB_URL", server.uri()); let (after, statuses) = run_case(&case).await; assert_round_trip(&case, &after, &statuses); ran += 1; @@ -550,7 +529,7 @@ async fn gem_mock(case: &Case) -> MockServer { async fn gem_run(case: &Case) -> (BTreeMap, Vec<(String, PinStatus)>) { let server = gem_mock(case).await; - let _env = EnvGuard::set(&[("SOCKET_RUBYGEMS_URL", server.uri())]); + let _env = EnvGuard::set("SOCKET_RUBYGEMS_URL", server.uri()); run_case(case).await } @@ -753,7 +732,7 @@ async fn gem_refusals_leave_everything_hosted() { // The registry does not know the gem. { let server = MockServer::start().await; - let _env = EnvGuard::set(&[("SOCKET_RUBYGEMS_URL", server.uri())]); + let _env = EnvGuard::set("SOCKET_RUBYGEMS_URL", server.uri()); let (after, statuses) = run_case(&case).await; assert_refused(&case, &after, &statuses, "Gemfile.lock", "404"); } @@ -870,7 +849,7 @@ async fn composer_run( tweak: impl Fn(&mut serde_json::Value), ) -> (BTreeMap, Vec<(String, PinStatus)>) { let server = composer_mock(case, tweak).await; - let _env = EnvGuard::set(&[("SOCKET_PACKAGIST_URL", server.uri())]); + let _env = EnvGuard::set("SOCKET_PACKAGIST_URL", server.uri()); run_case(case).await } @@ -1203,9 +1182,12 @@ async fn pypi_mock(releases: &[Release<'_>]) -> (MockServer, EnvGuard) { .await; } // The host's user- and system-level uv settings stay out of the tests. - let env = EnvGuard::set(&[ - ("SOCKET_PYPI_JSON_API", format!("{}/pypi", server.uri())), - ("UV_NO_CONFIG", "1".to_string()), + let env = EnvGuard::many([ + ( + "SOCKET_PYPI_JSON_API", + Some(format!("{}/pypi", server.uri())), + ), + ("UV_NO_CONFIG", Some("1".to_string())), ]); (server, env) } @@ -2398,14 +2380,14 @@ async fn pip_compile_pylock_build_filters_refuse_the_filtered_entry() { ), ("UV_CONFIG_FILE", "ci/uv.toml", "UV_CONFIG_FILE"), ] { - let _env = EnvGuard::set(&[(var, value.to_string())]); + let _env = EnvGuard::set(var, value); let input = tree(&[("pylock.toml", uv_pylock(false))]); let (why, _, _) = pypi_refusal(&input, &[urllib3_dep()], &RestoreOptions::default()).await; assert!(why.contains(expect), "{why}"); } // A `false` per-package cutoff on the command line exempts urllib3 // from `UV_EXCLUDE_NEWER`. - let _env = EnvGuard::set(&[("UV_EXCLUDE_NEWER", "2023-01-01T00:00:00Z".to_string())]); + let _env = EnvGuard::set("UV_EXCLUDE_NEWER", "2023-01-01T00:00:00Z"); let input = tree(&[( "pylock.toml", with_flags(" --exclude-newer-package urllib3=false"), @@ -2414,7 +2396,7 @@ async fn pip_compile_pylock_build_filters_refuse_the_filtered_entry() { drop(_env); // Empty is unset, and `false` turns the cutoff off. for value in ["", "false"] { - let _env = EnvGuard::set(&[("UV_EXCLUDE_NEWER", value.to_string())]); + let _env = EnvGuard::set("UV_EXCLUDE_NEWER", value); let input = tree(&[("pylock.toml", uv_pylock(false))]); assert_pypi_round_trip("UV_EXCLUDE_NEWER off", &input, &[urllib3_dep()], None).await; } @@ -2774,7 +2756,7 @@ async fn vlt_goldens_round_trip() { continue; } let server = vlt_mock(&case).await; - let _env = EnvGuard::set(&[("SOCKET_NPM_REGISTRY", server.uri())]); + let _env = EnvGuard::set("SOCKET_NPM_REGISTRY", server.uri()); let (after, statuses) = run_case(&case).await; assert_round_trip(&case, &after, &statuses); ran += 1; @@ -2915,7 +2897,7 @@ async fn nuget_goldens_round_trip() { } let mut case = case; let server = nuget_mock(&mut case).await; - let _env = EnvGuard::set(&[("SOCKET_NUGET_URL", server.uri())]); + let _env = EnvGuard::set("SOCKET_NUGET_URL", server.uri()); let (after, statuses) = run_case(&case).await; assert_round_trip(&case, &after, &statuses); ran += 1; @@ -2933,7 +2915,7 @@ async fn nuget_non_invertible_goldens_restore_or_refuse_as_documented() { .unwrap(); let mut case = case; let server = nuget_mock(&mut case).await; - let _env = EnvGuard::set(&[("SOCKET_NUGET_URL", server.uri())]); + let _env = EnvGuard::set("SOCKET_NUGET_URL", server.uri()); let (after, statuses) = run_case(&case).await; let [(_, status)] = &statuses[..] else { panic!("{name}: {statuses:?}");