1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
//! Process-wide test-environment barrier owned by shell dispatch.
//!
//! `shell_dispatcher.rs` is also compiled directly by integration harnesses,
//! outside the main binary crate. Keeping the barrier under that module makes
//! shell detection self-contained while `crate::test_support` re-exports the
//! same instance to its existing environment-mutating callers in the main crate.
use std::sync::{Mutex, MutexGuard, OnceLock, TryLockError};
use std::thread::ThreadId;
fn env_lock() -> &'static Mutex<()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
}
/// Who currently counts as "inside" the process-wide env lock.
///
/// The owner is the thread holding [`TestEnvLock`]. `adopted` holds helper
/// threads that owner explicitly enrolled with [`join_env_scope`] — see that
/// function for why a worker thread of the current test must not be treated as
/// a foreign reader.
#[derive(Default)]
struct EnvScope {
/// Bumped on every acquisition, so a ticket minted by an earlier test can
/// never enroll a thread into a later test's environment.
generation: u64,
owner: Option<ThreadId>,
adopted: Vec<ThreadId>,
}
fn env_scope() -> &'static Mutex<EnvScope> {
static SCOPE: OnceLock<Mutex<EnvScope>> = OnceLock::new();
SCOPE.get_or_init(|| Mutex::new(EnvScope::default()))
}
fn lock_env_scope() -> MutexGuard<'static, EnvScope> {
match env_scope().lock() {
Ok(scope) => scope,
Err(poisoned) => poisoned.into_inner(),
}
}
fn open_env_scope() {
let mut scope = lock_env_scope();
scope.generation = scope.generation.wrapping_add(1);
scope.owner = Some(std::thread::current().id());
scope.adopted.clear();
}
fn current_thread_owns_contended_env_lock() -> bool {
let scope = lock_env_scope();
let current = std::thread::current().id();
scope.owner == Some(current) || scope.adopted.contains(¤t)
}
/// Proof that the calling thread owns a live [`lock_test_env`] scope, handed to
/// a worker thread so it can join that scope with [`join_env_scope`].
///
/// Returns `None` when the caller is not the owner, so a ticket can never be
/// minted on behalf of a test that did not seal the environment.
#[derive(Clone, Copy, Debug)]
pub(crate) struct EnvScopeTicket {
generation: u64,
}
impl EnvScopeTicket {
/// Which sealed environment this ticket authorizes. Callers that gate real
/// disk writes on a live scope key their bookkeeping by this value, so a
/// straggler from generation N can never be mistaken for work belonging to
/// generation N+1.
pub(crate) fn generation(&self) -> u64 {
self.generation
}
}
/// The generation of the env scope the calling thread is currently inside, as
/// owner or as a [`join_env_scope`]-adopted worker; `None` when the thread is a
/// foreign reader with no sealed environment of its own.
///
/// This is the authorization primitive for anything that must only touch disk
/// on behalf of a test that actually sealed `HOME`. A process-global "writes
/// are enabled" flag cannot distinguish unrelated parallel tests.
pub(crate) fn current_env_scope_generation() -> Option<u64> {
let scope = lock_env_scope();
let current = std::thread::current().id();
if scope.owner == Some(current) || scope.adopted.contains(¤t) {
Some(scope.generation)
} else {
None
}
}
pub(crate) fn env_scope_ticket() -> Option<EnvScopeTicket> {
let scope = lock_env_scope();
(scope.owner == Some(std::thread::current().id())).then_some(EnvScopeTicket {
generation: scope.generation,
})
}
/// Enroll the calling thread in the ticket's env scope for as long as the
/// returned guard lives.
///
/// [`with_test_env_lock`] stops a foreign test from resolving another test's
/// temporary `HOME`. A helper thread doing work for the sealing test must see
/// that same environment without blocking on the mutex its owner holds.
pub(crate) fn join_env_scope(ticket: Option<EnvScopeTicket>) -> Option<EnvScopeMembership> {
let ticket = ticket?;
let mut scope = lock_env_scope();
if scope.owner.is_none() || scope.generation != ticket.generation {
return None;
}
let thread = std::thread::current().id();
if !scope.adopted.contains(&thread) {
scope.adopted.push(thread);
}
Some(EnvScopeMembership {
generation: ticket.generation,
thread,
})
}
pub(crate) struct EnvScopeMembership {
generation: u64,
thread: ThreadId,
}
impl Drop for EnvScopeMembership {
fn drop(&mut self) {
let mut scope = lock_env_scope();
if scope.generation == self.generation {
scope.adopted.retain(|thread| *thread != self.thread);
}
}
}
/// Owned process-wide test-environment lock.
///
/// Clearing the owner before the underlying mutex unlocks keeps re-entrant
/// reader detection exact. Closing the scope also evicts adopted workers, so
/// enrollment cannot outlive the test that granted it.
pub(crate) struct TestEnvLock {
_guard: MutexGuard<'static, ()>,
}
impl Drop for TestEnvLock {
fn drop(&mut self) {
let mut scope = lock_env_scope();
if scope.owner == Some(std::thread::current().id()) {
scope.owner = None;
scope.adopted.clear();
}
}
}
/// Acquire the process-wide env-var mutex.
///
/// If a prior test panicked while holding the lock, recover the guard instead
/// of cascading failures across unrelated tests.
pub(crate) fn lock_test_env() -> TestEnvLock {
let guard = match env_lock().lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
open_env_scope();
TestEnvLock { _guard: guard }
}
/// Read process-global test environment while respecting [`lock_test_env`].
///
/// The owner check makes the barrier re-entrant for a test that reads its own
/// guarded override.
pub(crate) fn with_test_env_lock<T>(read: impl FnOnce() -> T) -> T {
if current_thread_owns_contended_env_lock() {
return read();
}
let _guard = lock_test_env();
read()
}
pub(crate) fn current_thread_holds_test_env_lock() -> bool {
match env_lock().try_lock() {
Ok(guard) => {
drop(guard);
false
}
Err(TryLockError::Poisoned(poisoned)) => {
drop(poisoned.into_inner());
false
}
Err(TryLockError::WouldBlock) => current_thread_owns_contended_env_lock(),
}
}