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
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
//! One process has one environment, so tests that set a variable take turns.
//!
//! `std::env::set_var` writes to the whole process. A test binary runs its
//! tests on many threads at once, so a test that sets `MFR_CARTRIDGE_CHANNEL`
//! and asserts on what was resolved is asserting about a value another test
//! may have overwritten between the two lines — and the failure lands on
//! whichever test lost the race, which is not the one that has the bug. That
//! is how `test8816` came to fail once every few full runs and pass every time
//! it was run alone.
//!
//! Every save-and-restore guard in the tree got this half right: each put its
//! own variables back. None of them stopped a SECOND test from running in
//! between. This is the one that takes the turn.
//!
//! Public rather than `#[cfg(test)]`, because integration tests are separate
//! crates and would otherwise each grow their own copy — which is the thing
//! that went wrong.
use std::sync::{Mutex, MutexGuard, OnceLock};
fn turn() -> &'static Mutex<()> {
static TURN: OnceLock<Mutex<()>> = OnceLock::new();
TURN.get_or_init(|| Mutex::new(()))
}
thread_local! {
/// How many guards THIS test holds.
///
/// Counted rather than refused, because a test legitimately reaches for
/// two: one helper points `WSB_STATE_DIR` at a scratch store and another
/// sets the variable under test, and neither knows about the other. Taken
/// as a second lock that would be waiting on a guard only this thread can
/// release — and a test binary that hangs says nothing at all about what
/// is wrong with it; the run simply stops.
///
/// The turn is taken by the outermost guard and released when it drops, so
/// no other thread gets in between the two halves of one test.
static DEPTH: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
/// The environment, held for as long as this value lives.
///
/// While it is alive no other holder can change a variable; when it drops,
/// every variable it touched goes back to what it was — including ones set
/// through [`Environment::set`] after it was taken, which is what a test that
/// resolves twice under two registries needs.
pub struct Environment {
restore: Vec<(String, Option<String>)>,
// Declared last: dropped after the variables are restored, so the next
// waiting test sees the environment already put back rather than the
// leftovers of this one. `None` in a guard nested inside another on the
// same thread — the turn is already this test's.
_turn: Option<MutexGuard<'static, ()>>,
}
impl Environment {
/// Take the environment and set these variables. An empty value REMOVES
/// the variable: "unset" is a state tests assert about, and setting a
/// variable to the empty string is a different one that several callers
/// meant to write.
pub fn set(pairs: &[(&str, &str)]) -> Self {
let already = DEPTH.with(|depth| {
let held = depth.get();
depth.set(held + 1);
held
});
let taken = if already == 0 {
// A test that panics while holding the turn poisons the mutex. The
// data behind it is `()`, so there is nothing to be suspicious of,
// and refusing here would turn one failing test into every test
// failing.
Some(turn().lock().unwrap_or_else(|held| held.into_inner()))
} else {
None
};
let mut environment = Environment { restore: Vec::new(), _turn: taken };
environment.change(pairs);
environment
}
/// Take the environment without changing anything yet.
///
/// For a test that reads what is there, or removes a variable, or sets one
/// whose name it computes.
pub fn held() -> Self {
Self::set(&[])
}
/// Change more variables under the turn already held.
pub fn change(&mut self, pairs: &[(&str, &str)]) {
for (name, value) in pairs {
self.remember(name);
if value.is_empty() {
std::env::remove_var(name);
} else {
std::env::set_var(name, value);
}
}
}
/// Set one variable to a value that may legitimately be empty.
pub fn set_exactly(&mut self, name: &str, value: &str) {
self.remember(name);
std::env::set_var(name, value);
}
/// Unset one variable for the rest of the turn.
pub fn unset(&mut self, name: &str) {
self.remember(name);
std::env::remove_var(name);
}
/// The first value a variable had when this turn started, remembered once.
///
/// Once, deliberately: a test that sets the same variable three times must
/// restore the value from BEFORE the first of them.
fn remember(&mut self, name: &str) {
if !self.restore.iter().any(|(held, _)| held == name) {
self.restore.push((name.to_string(), std::env::var(name).ok()));
}
}
}
impl Drop for Environment {
fn drop(&mut self) {
for (name, previous) in &self.restore {
match previous {
Some(value) => std::env::set_var(name, value),
None => std::env::remove_var(name),
}
}
DEPTH.with(|depth| depth.set(depth.get().saturating_sub(1)));
}
}
#[cfg(test)]
mod tests {
use super::*;
/// TEST11516: two tests that set the same variable do not overlap.
///
/// The property the whole file exists for, and the one every hand-written
/// save-and-restore guard in the tree lacked. Two threads each set a
/// variable to their own value and then read it back after giving the
/// other one every chance to run: without the turn, one of them reads the
/// other's value and the test that fails is the innocent one.
///
/// A negative control is not hypothetical here — removing `_turn` from the
/// struct makes this fail within a few runs.
#[test]
fn test11516_setting_a_variable_takes_a_turn() {
const NAME: &str = "WSB_TEST_TURN_TAKING";
let readers: Vec<_> = ["first", "second", "third"]
.into_iter()
.map(|mine| {
std::thread::spawn(move || {
for _ in 0..40 {
let _held = Environment::set(&[(NAME, mine)]);
std::thread::yield_now();
std::thread::sleep(std::time::Duration::from_micros(200));
assert_eq!(
std::env::var(NAME).as_deref(),
Ok(mine),
"another test changed {NAME} while this one held it"
);
}
})
})
.collect();
for reader in readers {
reader.join().expect("no thread saw another's value");
}
assert!(
std::env::var(NAME).is_err(),
"the variable outlived every turn that set it"
);
}
/// TEST11517: what a variable was set to three times is restored to what
/// it was before the first of them.
///
/// A guard that remembers on every change restores the SECOND value, which
/// is one this test never had and the next test inherits.
#[test]
fn test11517_the_value_restored_is_the_one_from_before_the_turn() {
const NAME: &str = "WSB_TEST_RESTORED_ONCE";
// The value that was there before any turn was taken.
std::env::set_var(NAME, "original");
{
let mut held = Environment::set(&[(NAME, "one")]);
held.change(&[(NAME, "two")]);
held.change(&[(NAME, "three")]);
assert_eq!(std::env::var(NAME).as_deref(), Ok("three"));
}
assert_eq!(
std::env::var(NAME).as_deref(),
Ok("original"),
"the guard restored a value the test set rather than the one it found"
);
std::env::remove_var(NAME);
// And a variable that was not there at all comes back not there.
drop(Environment::set(&[(NAME, "transient")]));
assert!(std::env::var(NAME).is_err(), "an unset variable came back set");
}
/// TEST11519: one test may take two guards, and the turn spans both.
///
/// A test that points `WSB_STATE_DIR` at a scratch store with one helper
/// and sets the variable under test with another holds two. Waiting for a
/// lock this same thread holds would hang the binary with no output naming
/// a test — which the first draft of TEST11517 did.
///
/// The turn must still be held from the first guard to the last: releasing
/// it when the INNER one drops would let another thread in halfway through
/// a test, which is the whole defect this module exists for.
#[test]
fn test11519_a_nested_guard_shares_the_one_turn() {
const NAME: &str = "WSB_TEST_NESTED";
let waiting = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let outer = Environment::set(&[(NAME, "outer")]);
{
let _inner = Environment::set(&[(NAME, "inner")]);
assert_eq!(std::env::var(NAME).as_deref(), Ok("inner"));
}
assert_eq!(
std::env::var(NAME).as_deref(),
Ok("outer"),
"the inner guard restored past its own change"
);
// Another thread must not get in while the outer guard is alive.
let got_in = waiting.clone();
let other = std::thread::spawn(move || {
let _theirs = Environment::set(&[(NAME, "theirs")]);
got_in.store(true, std::sync::atomic::Ordering::SeqCst);
});
std::thread::sleep(std::time::Duration::from_millis(50));
assert!(
!waiting.load(std::sync::atomic::Ordering::SeqCst),
"another test took the environment while this one still held it"
);
drop(outer);
other.join().expect("the waiting thread gets its turn");
assert!(std::env::var(NAME).is_err());
}
/// TEST11518: an empty value means the variable is not there.
///
/// Several callers wrote `("MFR_CARTRIDGE_REGISTRY_URL", "")` meaning "no
/// registry", and code that asks `var().is_ok()` reads an empty string as
/// a registry that is set.
#[test]
fn test11518_an_empty_value_removes_the_variable() {
const NAME: &str = "WSB_TEST_EMPTY_IS_UNSET";
std::env::set_var(NAME, "something");
{
let _held = Environment::set(&[(NAME, "")]);
assert!(std::env::var(NAME).is_err(), "empty was set as a value");
}
assert_eq!(std::env::var(NAME).as_deref(), Ok("something"));
std::env::remove_var(NAME);
// And a value that is meant to be the empty string still can be.
let mut held = Environment::held();
held.set_exactly(NAME, "");
assert_eq!(std::env::var(NAME).as_deref(), Ok(""));
drop(held);
assert!(std::env::var(NAME).is_err());
}
}