Skip to main content

lean_ctx_fs/
env_lock.rs

1// SPDX-License-Identifier: Apache-2.0
2//! The process-wide test environment lock.
3//!
4//! `std::env::set_var` / `std::env::remove_var` became `unsafe` in Rust 2024
5//! because they are not thread-safe: a concurrent environment read from another
6//! thread is undefined behaviour. Every test that mutates the environment
7//! serializes through [`test_env_lock`], here and in the engine (which
8//! re-exports it as `core::data_dir::test_env_lock`), so there is exactly one
9//! lock per process. The mutation helpers `set_var` / `remove_var` exist
10//! only for tests (`cfg(test)` or the `test-support` feature).
11
12static TEST_ENV_MUTEX: std::sync::OnceLock<std::sync::Mutex<()>> = std::sync::OnceLock::new();
13
14thread_local! {
15    /// Re-entrancy depth for the current thread. >0 means this thread already
16    /// holds the lock, so a nested acquire is a no-op bump instead of a
17    /// self-deadlock.
18    static TEST_ENV_DEPTH: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
19    /// The real guard, held only while depth transitions 0→…→0.
20    static TEST_ENV_HELD: std::cell::RefCell<Option<std::sync::MutexGuard<'static, ()>>> =
21        const { std::cell::RefCell::new(None) };
22}
23
24/// Reentrant test env lock. Serializes env-mutating tests across threads (the
25/// point — `std::env::set_var` is not thread-safe) while letting the SAME
26/// thread re-acquire without deadlocking. A test and a `setup()` helper (or
27/// `isolated_data_dir`) can both take it; only the outermost acquisition holds
28/// the underlying mutex, nested ones just bump a per-thread depth. Other
29/// threads still block, so cross-test serialization is unchanged.
30pub fn test_env_lock() -> TestEnvGuard {
31    let mutex = TEST_ENV_MUTEX.get_or_init(|| std::sync::Mutex::new(()));
32    TEST_ENV_DEPTH.with(|depth| {
33        if depth.get() == 0 {
34            let guard = mutex
35                .lock()
36                .unwrap_or_else(std::sync::PoisonError::into_inner);
37            TEST_ENV_HELD.with(|held| *held.borrow_mut() = Some(guard));
38        }
39        depth.set(depth.get() + 1);
40    });
41    TestEnvGuard {
42        _not_send_sync: std::marker::PhantomData,
43    }
44}
45
46/// Whether the current thread holds [`test_env_lock`]. The lock belongs to
47/// the thread that took it: a guard held by another thread does not count.
48#[cfg(any(test, feature = "test-support"))]
49pub fn holds_test_env_lock() -> bool {
50    TEST_ENV_DEPTH.with(|depth| depth.get() > 0)
51}
52
53/// RAII guard for [`test_env_lock`]. Dropping the outermost one releases the
54/// underlying mutex; nested guards just decrement the depth.
55///
56/// The lock and its depth live in the acquiring thread's own storage, so the
57/// guard must be dropped on that thread: it is neither `Send` nor `Sync`.
58/// Moved to another thread it would decrement that thread's depth and leave
59/// the lock held here.
60pub struct TestEnvGuard {
61    _not_send_sync: std::marker::PhantomData<*const ()>,
62}
63
64impl Drop for TestEnvGuard {
65    fn drop(&mut self) {
66        TEST_ENV_DEPTH.with(|depth| {
67            let next = depth.get().saturating_sub(1);
68            depth.set(next);
69            if next == 0 {
70                TEST_ENV_HELD.with(|held| *held.borrow_mut() = None);
71            }
72        });
73    }
74}
75
76/// Sets `key` to `value` in the process environment (test-only).
77#[cfg(any(test, feature = "test-support"))]
78pub fn set_var<K: AsRef<std::ffi::OsStr>, V: AsRef<std::ffi::OsStr>>(key: K, value: V) {
79    assert_lock_held();
80    // SAFETY: the assertion above holds, so this thread owns test_env_lock()
81    // and no other thread reads or writes the environment concurrently.
82    unsafe { std::env::set_var(key, value) };
83}
84
85/// Removes `key` from the process environment (test-only).
86#[cfg(any(test, feature = "test-support"))]
87pub fn remove_var<K: AsRef<std::ffi::OsStr>>(key: K) {
88    assert_lock_held();
89    // SAFETY: the assertion above holds, so this thread owns test_env_lock()
90    // and no other thread reads or writes the environment concurrently.
91    unsafe { std::env::remove_var(key) };
92}
93
94/// The precondition of the two functions above, checked in every build of the
95/// tests and before anything is changed. It does not take the lock itself:
96/// that would hide a caller whose lock does not span its whole mutation.
97#[cfg(any(test, feature = "test-support"))]
98fn assert_lock_held() {
99    assert!(
100        holds_test_env_lock(),
101        "test environment mutation requires the current thread to hold test_env_lock()"
102    );
103}