From f929fb95d208f029ffc311731b05888870ee2be6 Mon Sep 17 00:00:00 2001 From: Leynos Date: Tue, 12 Aug 2025 02:07:02 +0100 Subject: [PATCH 1/5] Guard NINJA_ENV overrides with mockable Env --- tests/ninja_env_tests.rs | 24 ++++++++++++++++++ tests/runner_tests.rs | 22 +++++++---------- tests/support/mod.rs | 2 ++ tests/support/ninja_env.rs | 50 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 13 deletions(-) create mode 100644 tests/ninja_env_tests.rs create mode 100644 tests/support/ninja_env.rs diff --git a/tests/ninja_env_tests.rs b/tests/ninja_env_tests.rs new file mode 100644 index 00000000..1fa1f36b --- /dev/null +++ b/tests/ninja_env_tests.rs @@ -0,0 +1,24 @@ +use mockable::MockEnv; +use netsuke::runner::NINJA_ENV; +use support::env_lock::EnvLock; +use support::ninja_env::override_ninja_env; + +mod support; + +#[test] +fn override_ninja_env_restores_original() { + let _lock = EnvLock::acquire(); + let mut env = MockEnv::new(); + env.expect_raw() + .withf(|k| k == NINJA_ENV) + .returning(|_| Ok("orig".to_string())); + + { + let _guard = override_ninja_env(&env, "new"); + assert_eq!(std::env::var(NINJA_ENV).as_deref(), Ok("new")); + } + assert_eq!(std::env::var(NINJA_ENV).as_deref(), Ok("orig")); + // Clean up to avoid leaking environment state. `remove_var` is `unsafe` + // on Rust 2024; the lock above serialises this mutation. + unsafe { std::env::remove_var(NINJA_ENV) }; +} diff --git a/tests/runner_tests.rs b/tests/runner_tests.rs index abff32e9..b291a4fb 100644 --- a/tests/runner_tests.rs +++ b/tests/runner_tests.rs @@ -1,5 +1,6 @@ +use mockable::DefaultEnv; use netsuke::cli::{BuildArgs, Cli, Commands}; -use netsuke::runner::{BuildTargets, NINJA_ENV, run, run_ninja}; +use netsuke::runner::{BuildTargets, run, run_ninja}; use rstest::{fixture, rstest}; use serial_test::serial; use std::path::{Path, PathBuf}; @@ -8,6 +9,7 @@ use std::path::{Path, PathBuf}; mod check_ninja; mod support; use support::env_lock::EnvLock; +use support::ninja_env::override_ninja_env; use support::path_guard::PathGuard; /// Fixture: Put a fake `ninja` (that checks for a build file) on PATH. @@ -220,10 +222,12 @@ fn run_manifest_subcommand_writes_file() { #[serial] fn run_respects_env_override_for_ninja() { let (temp_dir, ninja_path) = support::fake_ninja(0); - let original = std::env::var_os(NINJA_ENV); - unsafe { - std::env::set_var(NINJA_ENV, &ninja_path); - } + let program = ninja_path.to_string_lossy().into_owned(); + let env = DefaultEnv::new(); + let _lock = EnvLock::acquire(); + // `set_var` is `unsafe` on Rust 2024; the lock serialises the mutation and + // `override_ninja_env` restores the original value via its guard. + let _guard = override_ninja_env(&env, &program); let temp = tempfile::tempdir().expect("temp dir"); let manifest_path = temp.path().join("Netsukefile"); @@ -241,14 +245,6 @@ fn run_respects_env_override_for_ninja() { let result = run(&cli); assert!(result.is_ok()); - - unsafe { - if let Some(val) = original { - std::env::set_var(NINJA_ENV, val); - } else { - std::env::remove_var(NINJA_ENV); - } - } drop(ninja_path); drop(temp_dir); } diff --git a/tests/support/mod.rs b/tests/support/mod.rs index 0dae0142..a3a5a6a6 100644 --- a/tests/support/mod.rs +++ b/tests/support/mod.rs @@ -4,6 +4,7 @@ //! logging utilities used in behavioural tests. pub mod env_lock; +pub mod ninja_env; pub mod path_guard; #[expect(unused_imports, reason = "re-export for selective test crates")] @@ -17,6 +18,7 @@ use tempfile::TempDir; /// Create a fake Ninja executable that exits with `exit_code`. /// /// Returns the temporary directory and the path to the executable. +#[allow(dead_code, reason = "only some test crates spawn fake ninja binaries")] pub fn fake_ninja(exit_code: i32) -> (TempDir, PathBuf) { let dir = TempDir::new().expect("temp dir"); let path = dir.path().join("ninja"); diff --git a/tests/support/ninja_env.rs b/tests/support/ninja_env.rs new file mode 100644 index 00000000..7c6c13e0 --- /dev/null +++ b/tests/support/ninja_env.rs @@ -0,0 +1,50 @@ +//! Override and restore `NINJA_ENV` for tests. +//! +//! Provides a helper that sets the `NETSUKE_NINJA` environment variable while +//! ensuring it is restored afterwards. This uses [`EnvLock`] to serialise +//! mutations to the global environment and captures the previous value through a +//! `mockable::Env` implementation so tests can inject their own state. + +use mockable::Env; +use netsuke::runner::NINJA_ENV; + +/// Guard that resets `NINJA_ENV` on drop. +/// +/// Holding the guard keeps the environment override in place. Dropping it +/// restores the prior value, cleaning up global state even if a test panics. +#[derive(Debug)] +pub struct NinjaEnvGuard { + original: Option, +} + +/// Set `NINJA_ENV` to `value`, returning a guard that restores the previous +/// value when dropped. +/// +/// # Examples +/// ```ignore +/// use mockable::DefaultEnv; +/// use tests::support::ninja_env::override_ninja_env; +/// let env = DefaultEnv::new(); +/// let _guard = override_ninja_env(&env, "/usr/bin/ninja"); +/// ``` +#[allow(dead_code, reason = "only some tests override NINJA_ENV")] +pub fn override_ninja_env(env: &impl Env, value: &str) -> NinjaEnvGuard { + let original = env.raw(NINJA_ENV).ok(); + // Callers must hold [`EnvLock`](super::env_lock::EnvLock) to serialise this + // mutation. `set_var` is `unsafe` on Rust 2024 and the guard restores the + // prior value on drop. + unsafe { std::env::set_var(NINJA_ENV, value) }; + NinjaEnvGuard { original } +} + +impl Drop for NinjaEnvGuard { + fn drop(&mut self) { + unsafe { + if let Some(ref val) = self.original { + std::env::set_var(NINJA_ENV, val); + } else { + std::env::remove_var(NINJA_ENV); + } + } + } +} From f432ca21f1684f101ed82281f169e1d266d4e4ef Mon Sep 17 00:00:00 2001 From: Leynos Date: Tue, 12 Aug 2025 02:19:06 +0100 Subject: [PATCH 2/5] Test NINJA_ENV guard edge cases --- tests/ninja_env_tests.rs | 36 ++++++++++++++++++++++++++++++++++++ tests/support/ninja_env.rs | 20 ++++++++++++++------ 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/tests/ninja_env_tests.rs b/tests/ninja_env_tests.rs index 1fa1f36b..4afb8425 100644 --- a/tests/ninja_env_tests.rs +++ b/tests/ninja_env_tests.rs @@ -1,5 +1,6 @@ use mockable::MockEnv; use netsuke::runner::NINJA_ENV; +use std::env::VarError; use support::env_lock::EnvLock; use support::ninja_env::override_ninja_env; @@ -11,6 +12,7 @@ fn override_ninja_env_restores_original() { let mut env = MockEnv::new(); env.expect_raw() .withf(|k| k == NINJA_ENV) + .times(1) .returning(|_| Ok("orig".to_string())); { @@ -22,3 +24,37 @@ fn override_ninja_env_restores_original() { // on Rust 2024; the lock above serialises this mutation. unsafe { std::env::remove_var(NINJA_ENV) }; } + +#[test] +fn override_ninja_env_removes_when_unset() { + let _lock = EnvLock::acquire(); + let mut env = MockEnv::new(); + env.expect_raw() + .withf(|k| k == NINJA_ENV) + .times(1) + .returning(|_| Err(VarError::NotPresent)); + + { + let _guard = override_ninja_env(&env, "new"); + assert_eq!(std::env::var(NINJA_ENV).as_deref(), Ok("new")); + } + assert!(std::env::var(NINJA_ENV).is_err()); + unsafe { std::env::remove_var(NINJA_ENV) }; +} + +#[test] +fn override_ninja_env_restores_empty() { + let _lock = EnvLock::acquire(); + let mut env = MockEnv::new(); + env.expect_raw() + .withf(|k| k == NINJA_ENV) + .times(1) + .returning(|_| Ok(String::new())); + + { + let _guard = override_ninja_env(&env, "new"); + assert_eq!(std::env::var(NINJA_ENV).as_deref(), Ok("new")); + } + assert_eq!(std::env::var(NINJA_ENV).as_deref(), Ok("")); + unsafe { std::env::remove_var(NINJA_ENV) }; +} diff --git a/tests/support/ninja_env.rs b/tests/support/ninja_env.rs index 7c6c13e0..7ddcf5a3 100644 --- a/tests/support/ninja_env.rs +++ b/tests/support/ninja_env.rs @@ -1,9 +1,9 @@ -//! Override and restore `NINJA_ENV` for tests. +//! Override and restore [`NINJA_ENV`] for tests. //! -//! Provides a helper that sets the `NETSUKE_NINJA` environment variable while -//! ensuring it is restored afterwards. This uses [`EnvLock`] to serialise -//! mutations to the global environment and captures the previous value through a -//! `mockable::Env` implementation so tests can inject their own state. +//! Provides a helper that sets [`NINJA_ENV`] while ensuring it is restored +//! afterwards. This uses [`EnvLock`] to serialise mutations to the global +//! environment and captures the previous value through a `mockable::Env` +//! implementation so tests can inject their own state. use mockable::Env; use netsuke::runner::NINJA_ENV; @@ -17,9 +17,14 @@ pub struct NinjaEnvGuard { original: Option, } -/// Set `NINJA_ENV` to `value`, returning a guard that restores the previous +/// Set [`NINJA_ENV`] to `value`, returning a guard that restores the previous /// value when dropped. /// +/// # Thread Safety +/// +/// This function is **not thread-safe**. Callers must hold +/// [`EnvLock`](super::env_lock::EnvLock) to serialise this mutation. +/// /// # Examples /// ```ignore /// use mockable::DefaultEnv; @@ -39,6 +44,9 @@ pub fn override_ninja_env(env: &impl Env, value: &str) -> NinjaEnvGuard { impl Drop for NinjaEnvGuard { fn drop(&mut self) { + // Safety: callers hold [`EnvLock`] for the guard's lifetime, so these + // `set_var`/`remove_var` calls are serialised. Both functions are + // `unsafe` on Rust 2024. unsafe { if let Some(ref val) = self.original { std::env::set_var(NINJA_ENV, val); From 248afec27f4a9b97fbcd75ebed67d2b3114dde2b Mon Sep 17 00:00:00 2001 From: Leynos Date: Tue, 12 Aug 2025 09:45:31 +0100 Subject: [PATCH 3/5] Expect unused code in test helpers --- tests/assert_cmd_tests.rs | 1 + tests/cucumber.rs | 1 + tests/ninja_env_tests.rs | 4 ++++ tests/support/mod.rs | 8 +++++++- tests/support/ninja_env.rs | 5 ++++- 5 files changed, 17 insertions(+), 2 deletions(-) diff --git a/tests/assert_cmd_tests.rs b/tests/assert_cmd_tests.rs index 22ba6591..97767222 100644 --- a/tests/assert_cmd_tests.rs +++ b/tests/assert_cmd_tests.rs @@ -8,6 +8,7 @@ use assert_cmd::Command; use std::fs; use tempfile::tempdir; +#[expect(unused, reason = "support module exports helpers unused in this test")] mod support; #[test] diff --git a/tests/cucumber.rs b/tests/cucumber.rs index b4b7c5c3..d512ec20 100644 --- a/tests/cucumber.rs +++ b/tests/cucumber.rs @@ -28,6 +28,7 @@ mod check_ninja; #[path = "support/env.rs"] mod env; mod steps; +#[expect(unused, reason = "support module exports helpers unused in this test")] mod support; #[tokio::main] diff --git a/tests/ninja_env_tests.rs b/tests/ninja_env_tests.rs index 4afb8425..78777c37 100644 --- a/tests/ninja_env_tests.rs +++ b/tests/ninja_env_tests.rs @@ -4,6 +4,10 @@ use std::env::VarError; use support::env_lock::EnvLock; use support::ninja_env::override_ninja_env; +#[expect( + unused, + reason = "support module exports helpers unused in these tests" +)] mod support; #[test] diff --git a/tests/support/mod.rs b/tests/support/mod.rs index a3a5a6a6..d5e51161 100644 --- a/tests/support/mod.rs +++ b/tests/support/mod.rs @@ -18,7 +18,13 @@ use tempfile::TempDir; /// Create a fake Ninja executable that exits with `exit_code`. /// /// Returns the temporary directory and the path to the executable. -#[allow(dead_code, reason = "only some test crates spawn fake ninja binaries")] +#[cfg_attr( + not(test), + expect( + unused_code, + reason = "only some test crates spawn fake ninja binaries" + ) +)] pub fn fake_ninja(exit_code: i32) -> (TempDir, PathBuf) { let dir = TempDir::new().expect("temp dir"); let path = dir.path().join("ninja"); diff --git a/tests/support/ninja_env.rs b/tests/support/ninja_env.rs index 7ddcf5a3..ae2fb74c 100644 --- a/tests/support/ninja_env.rs +++ b/tests/support/ninja_env.rs @@ -32,7 +32,10 @@ pub struct NinjaEnvGuard { /// let env = DefaultEnv::new(); /// let _guard = override_ninja_env(&env, "/usr/bin/ninja"); /// ``` -#[allow(dead_code, reason = "only some tests override NINJA_ENV")] +#[cfg_attr( + not(test), + expect(unused_code, reason = "only some tests override NINJA_ENV") +)] pub fn override_ninja_env(env: &impl Env, value: &str) -> NinjaEnvGuard { let original = env.raw(NINJA_ENV).ok(); // Callers must hold [`EnvLock`](super::env_lock::EnvLock) to serialise this From d8ed0bf22eb69551f2e4382d67c2a6067d2de0bd Mon Sep 17 00:00:00 2001 From: Leynos Date: Tue, 12 Aug 2025 10:31:18 +0100 Subject: [PATCH 4/5] Clarify override usage paths --- tests/ninja_env_tests.rs | 14 +++++++------- tests/runner_tests.rs | 4 ++-- tests/support/env_lock.rs | 1 + tests/support/ninja_env.rs | 33 ++++++++++++++++++++++----------- 4 files changed, 32 insertions(+), 20 deletions(-) diff --git a/tests/ninja_env_tests.rs b/tests/ninja_env_tests.rs index 78777c37..faf8ae38 100644 --- a/tests/ninja_env_tests.rs +++ b/tests/ninja_env_tests.rs @@ -12,7 +12,6 @@ mod support; #[test] fn override_ninja_env_restores_original() { - let _lock = EnvLock::acquire(); let mut env = MockEnv::new(); env.expect_raw() .withf(|k| k == NINJA_ENV) @@ -20,18 +19,18 @@ fn override_ninja_env_restores_original() { .returning(|_| Ok("orig".to_string())); { - let _guard = override_ninja_env(&env, "new"); + let _guard = override_ninja_env(EnvLock::acquire(), &env, "new"); assert_eq!(std::env::var(NINJA_ENV).as_deref(), Ok("new")); } assert_eq!(std::env::var(NINJA_ENV).as_deref(), Ok("orig")); // Clean up to avoid leaking environment state. `remove_var` is `unsafe` - // on Rust 2024; the lock above serialises this mutation. + // on Rust 2024; a fresh lock serialises this mutation. + let _cleanup = EnvLock::acquire(); unsafe { std::env::remove_var(NINJA_ENV) }; } #[test] fn override_ninja_env_removes_when_unset() { - let _lock = EnvLock::acquire(); let mut env = MockEnv::new(); env.expect_raw() .withf(|k| k == NINJA_ENV) @@ -39,16 +38,16 @@ fn override_ninja_env_removes_when_unset() { .returning(|_| Err(VarError::NotPresent)); { - let _guard = override_ninja_env(&env, "new"); + let _guard = override_ninja_env(EnvLock::acquire(), &env, "new"); assert_eq!(std::env::var(NINJA_ENV).as_deref(), Ok("new")); } assert!(std::env::var(NINJA_ENV).is_err()); + let _cleanup = EnvLock::acquire(); unsafe { std::env::remove_var(NINJA_ENV) }; } #[test] fn override_ninja_env_restores_empty() { - let _lock = EnvLock::acquire(); let mut env = MockEnv::new(); env.expect_raw() .withf(|k| k == NINJA_ENV) @@ -56,9 +55,10 @@ fn override_ninja_env_restores_empty() { .returning(|_| Ok(String::new())); { - let _guard = override_ninja_env(&env, "new"); + let _guard = override_ninja_env(EnvLock::acquire(), &env, "new"); assert_eq!(std::env::var(NINJA_ENV).as_deref(), Ok("new")); } assert_eq!(std::env::var(NINJA_ENV).as_deref(), Ok("")); + let _cleanup = EnvLock::acquire(); unsafe { std::env::remove_var(NINJA_ENV) }; } diff --git a/tests/runner_tests.rs b/tests/runner_tests.rs index b291a4fb..c221a490 100644 --- a/tests/runner_tests.rs +++ b/tests/runner_tests.rs @@ -222,12 +222,12 @@ fn run_manifest_subcommand_writes_file() { #[serial] fn run_respects_env_override_for_ninja() { let (temp_dir, ninja_path) = support::fake_ninja(0); + // `NINJA_ENV` expects UTF-8; lossy conversion is acceptable in this test. let program = ninja_path.to_string_lossy().into_owned(); let env = DefaultEnv::new(); - let _lock = EnvLock::acquire(); // `set_var` is `unsafe` on Rust 2024; the lock serialises the mutation and // `override_ninja_env` restores the original value via its guard. - let _guard = override_ninja_env(&env, &program); + let _guard = override_ninja_env(EnvLock::acquire(), &env, &program); let temp = tempfile::tempdir().expect("temp dir"); let manifest_path = temp.path().join("Netsukefile"); diff --git a/tests/support/env_lock.rs b/tests/support/env_lock.rs index cf22b3c2..333c7f0c 100644 --- a/tests/support/env_lock.rs +++ b/tests/support/env_lock.rs @@ -10,6 +10,7 @@ use std::sync::{Mutex, MutexGuard}; static ENV_LOCK: Mutex<()> = Mutex::new(()); #[allow(dead_code, reason = "only some tests mutate PATH")] +#[derive(Debug)] /// RAII guard that holds the global environment lock. pub struct EnvLock(MutexGuard<'static, ()>); diff --git a/tests/support/ninja_env.rs b/tests/support/ninja_env.rs index ae2fb74c..8fcf00dc 100644 --- a/tests/support/ninja_env.rs +++ b/tests/support/ninja_env.rs @@ -5,15 +5,19 @@ //! environment and captures the previous value through a `mockable::Env` //! implementation so tests can inject their own state. +use super::env_lock::EnvLock; use mockable::Env; use netsuke::runner::NINJA_ENV; /// Guard that resets `NINJA_ENV` on drop. /// /// Holding the guard keeps the environment override in place. Dropping it -/// restores the prior value, cleaning up global state even if a test panics. +/// restores the prior value while releasing the environment lock, cleaning up +/// global state even if a test panics. +#[must_use] #[derive(Debug)] pub struct NinjaEnvGuard { + _lock: EnvLock, original: Option, } @@ -22,32 +26,39 @@ pub struct NinjaEnvGuard { /// /// # Thread Safety /// -/// This function is **not thread-safe**. Callers must hold -/// [`EnvLock`](super::env_lock::EnvLock) to serialise this mutation. +/// This function is **not thread-safe**. Callers must supply an +/// [`EnvLock`](super::env_lock::EnvLock), which is stored in the returned guard +/// to serialise the mutation and ensure restoration occurs before the lock is +/// released. +/// +/// Drop order is enforced: dropping the guard restores [`NINJA_ENV`] and only +/// then releases the lock. /// /// # Examples /// ```ignore /// use mockable::DefaultEnv; -/// use tests::support::ninja_env::override_ninja_env; +/// use crate::support::{env_lock::EnvLock, ninja_env::override_ninja_env}; /// let env = DefaultEnv::new(); -/// let _guard = override_ninja_env(&env, "/usr/bin/ninja"); +/// let _guard = override_ninja_env(EnvLock::acquire(), &env, "/usr/bin/ninja"); /// ``` #[cfg_attr( not(test), expect(unused_code, reason = "only some tests override NINJA_ENV") )] -pub fn override_ninja_env(env: &impl Env, value: &str) -> NinjaEnvGuard { +pub fn override_ninja_env(lock: EnvLock, env: &impl Env, value: &str) -> NinjaEnvGuard { let original = env.raw(NINJA_ENV).ok(); - // Callers must hold [`EnvLock`](super::env_lock::EnvLock) to serialise this - // mutation. `set_var` is `unsafe` on Rust 2024 and the guard restores the - // prior value on drop. + // Safety: `EnvLock` serialises this mutation. `set_var` is `unsafe` on Rust + // 2024 and the guard restores the prior value on drop. unsafe { std::env::set_var(NINJA_ENV, value) }; - NinjaEnvGuard { original } + NinjaEnvGuard { + _lock: lock, + original, + } } impl Drop for NinjaEnvGuard { fn drop(&mut self) { - // Safety: callers hold [`EnvLock`] for the guard's lifetime, so these + // Safety: the guard holds [`EnvLock`] for its lifetime, so these // `set_var`/`remove_var` calls are serialised. Both functions are // `unsafe` on Rust 2024. unsafe { From ebf596ea9693118ecc2ddf692c8f66306fb77be9 Mon Sep 17 00:00:00 2001 From: Leynos Date: Tue, 12 Aug 2025 21:05:35 +0100 Subject: [PATCH 5/5] Parameterise NINJA_ENV guard tests --- tests/ninja_env_tests.rs | 65 ++++++++++++++++------------------------ 1 file changed, 25 insertions(+), 40 deletions(-) diff --git a/tests/ninja_env_tests.rs b/tests/ninja_env_tests.rs index faf8ae38..661f9f3f 100644 --- a/tests/ninja_env_tests.rs +++ b/tests/ninja_env_tests.rs @@ -1,5 +1,6 @@ use mockable::MockEnv; use netsuke::runner::NINJA_ENV; +use rstest::rstest; use std::env::VarError; use support::env_lock::EnvLock; use support::ninja_env::override_ninja_env; @@ -10,55 +11,39 @@ use support::ninja_env::override_ninja_env; )] mod support; -#[test] -fn override_ninja_env_restores_original() { +#[rstest] +#[case(Some("orig"))] +#[case(None)] +#[case(Some(""))] +fn override_ninja_env_restores(#[case] original: Option<&'static str>) { let mut env = MockEnv::new(); - env.expect_raw() - .withf(|k| k == NINJA_ENV) - .times(1) - .returning(|_| Ok("orig".to_string())); - - { - let _guard = override_ninja_env(EnvLock::acquire(), &env, "new"); - assert_eq!(std::env::var(NINJA_ENV).as_deref(), Ok("new")); + match original { + Some(val) => { + let returned = val.to_string(); + env.expect_raw() + .withf(|k| k == NINJA_ENV) + .times(1) + .return_once(move |_| Ok(returned)); + } + None => { + env.expect_raw() + .withf(|k| k == NINJA_ENV) + .times(1) + .return_once(|_| Err(VarError::NotPresent)); + } } - assert_eq!(std::env::var(NINJA_ENV).as_deref(), Ok("orig")); - // Clean up to avoid leaking environment state. `remove_var` is `unsafe` - // on Rust 2024; a fresh lock serialises this mutation. - let _cleanup = EnvLock::acquire(); - unsafe { std::env::remove_var(NINJA_ENV) }; -} - -#[test] -fn override_ninja_env_removes_when_unset() { - let mut env = MockEnv::new(); - env.expect_raw() - .withf(|k| k == NINJA_ENV) - .times(1) - .returning(|_| Err(VarError::NotPresent)); { let _guard = override_ninja_env(EnvLock::acquire(), &env, "new"); assert_eq!(std::env::var(NINJA_ENV).as_deref(), Ok("new")); } - assert!(std::env::var(NINJA_ENV).is_err()); - let _cleanup = EnvLock::acquire(); - unsafe { std::env::remove_var(NINJA_ENV) }; -} - -#[test] -fn override_ninja_env_restores_empty() { - let mut env = MockEnv::new(); - env.expect_raw() - .withf(|k| k == NINJA_ENV) - .times(1) - .returning(|_| Ok(String::new())); - { - let _guard = override_ninja_env(EnvLock::acquire(), &env, "new"); - assert_eq!(std::env::var(NINJA_ENV).as_deref(), Ok("new")); + match original { + Some(val) => assert_eq!(std::env::var(NINJA_ENV).as_deref(), Ok(val)), + None => assert!(std::env::var(NINJA_ENV).is_err()), } - assert_eq!(std::env::var(NINJA_ENV).as_deref(), Ok("")); + let _cleanup = EnvLock::acquire(); + // SAFETY: `EnvLock` serialises this mutation; see above for details. unsafe { std::env::remove_var(NINJA_ENV) }; }