wsb 0.253.6349

Workspace tools for every project under $PRJ: git across submodules, template stamping, GitHub, and the workbench
Documentation
//! 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());
    }
}