Skip to main content

nils_test_support/
lib.rs

1use std::env;
2use std::io;
3use std::path::{Path, PathBuf};
4use std::sync::{Mutex, MutexGuard};
5
6pub mod bin;
7pub mod cmd;
8pub mod fixtures;
9pub mod fs;
10pub mod git;
11pub mod http;
12pub mod stubs;
13
14static GLOBAL_STATE_LOCK: Mutex<()> = Mutex::new(());
15
16pub struct GlobalStateLock {
17    _guard: MutexGuard<'static, ()>,
18}
19
20impl GlobalStateLock {
21    pub fn new() -> Self {
22        let guard = match GLOBAL_STATE_LOCK.lock() {
23            Ok(guard) => guard,
24            Err(poisoned) => poisoned.into_inner(),
25        };
26        Self { _guard: guard }
27    }
28}
29
30impl Default for GlobalStateLock {
31    fn default() -> Self {
32        Self::new()
33    }
34}
35
36pub struct EnvGuard {
37    key: String,
38    original: Option<String>,
39}
40
41impl EnvGuard {
42    /// Requires holding `GlobalStateLock` to avoid concurrent global mutations.
43    pub fn set(lock: &GlobalStateLock, key: &str, value: &str) -> Self {
44        let _ = lock;
45        let original = env::var(key).ok();
46        // SAFETY: tests mutate process environment only while holding GlobalStateLock.
47        unsafe { env::set_var(key, value) };
48        Self {
49            key: key.to_string(),
50            original,
51        }
52    }
53
54    /// Requires holding `GlobalStateLock` to avoid concurrent global mutations.
55    pub fn remove(lock: &GlobalStateLock, key: &str) -> Self {
56        let _ = lock;
57        let original = env::var(key).ok();
58        // SAFETY: tests mutate process environment only while holding GlobalStateLock.
59        unsafe { env::remove_var(key) };
60        Self {
61            key: key.to_string(),
62            original,
63        }
64    }
65}
66
67impl Drop for EnvGuard {
68    fn drop(&mut self) {
69        match &self.original {
70            Some(value) => {
71                // SAFETY: tests mutate process environment only while holding GlobalStateLock.
72                unsafe { env::set_var(&self.key, value) };
73            }
74            None => {
75                // SAFETY: tests mutate process environment only while holding GlobalStateLock.
76                unsafe { env::remove_var(&self.key) };
77            }
78        }
79    }
80}
81
82pub struct CwdGuard {
83    original: PathBuf,
84}
85
86impl CwdGuard {
87    /// Requires holding `GlobalStateLock` to avoid concurrent global mutations.
88    pub fn set(lock: &GlobalStateLock, path: &Path) -> io::Result<Self> {
89        let _ = lock;
90        let original = env::current_dir()?;
91        env::set_current_dir(path)?;
92        Ok(Self { original })
93    }
94}
95
96impl Drop for CwdGuard {
97    fn drop(&mut self) {
98        let _ = env::set_current_dir(&self.original);
99    }
100}
101
102pub struct StubBinDir {
103    dir: tempfile::TempDir,
104}
105
106impl StubBinDir {
107    pub fn new() -> Self {
108        Self {
109            dir: tempfile::TempDir::new().expect("tempdir"),
110        }
111    }
112
113    pub fn path(&self) -> &Path {
114        self.dir.path()
115    }
116
117    pub fn path_str(&self) -> String {
118        self.dir.path().to_string_lossy().to_string()
119    }
120
121    pub fn write_exe(&self, name: &str, content: &str) {
122        write_exe(self.path(), name, content);
123    }
124}
125
126impl Default for StubBinDir {
127    fn default() -> Self {
128        Self::new()
129    }
130}
131
132pub fn write_exe(dir: &Path, name: &str, content: &str) {
133    let path = dir.join(name);
134    std::fs::write(&path, content).expect("write stub");
135    let mut perms = std::fs::metadata(&path).expect("meta").permissions();
136    #[cfg(unix)]
137    {
138        use std::os::unix::fs::PermissionsExt;
139        perms.set_mode(0o755);
140    }
141    std::fs::set_permissions(&path, perms).expect("chmod stub");
142}
143
144/// Requires holding `GlobalStateLock` to avoid concurrent global mutations.
145pub fn prepend_path(lock: &GlobalStateLock, dir: &Path) -> EnvGuard {
146    let _ = lock;
147    let mut paths: Vec<PathBuf> =
148        env::split_paths(&env::var_os("PATH").unwrap_or_default()).collect();
149    paths.insert(0, dir.to_path_buf());
150    let joined = env::join_paths(paths).expect("join paths");
151    let joined = joined.to_string_lossy().to_string();
152    EnvGuard::set(lock, "PATH", &joined)
153}
154
155#[cfg(test)]
156mod tests {
157    use super::{GLOBAL_STATE_LOCK, GlobalStateLock};
158
159    #[test]
160    fn global_state_lock_recovers_after_poison() {
161        let _ = std::panic::catch_unwind(|| {
162            let _guard = GLOBAL_STATE_LOCK.lock().expect("lock should be acquired");
163            panic!("intentional poison for recovery test");
164        });
165
166        let _lock = GlobalStateLock::new();
167    }
168}