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 pub fn set(lock: &GlobalStateLock, key: &str, value: &str) -> Self {
44 let _ = lock;
45 let original = env::var(key).ok();
46 unsafe { env::set_var(key, value) };
48 Self {
49 key: key.to_string(),
50 original,
51 }
52 }
53
54 pub fn remove(lock: &GlobalStateLock, key: &str) -> Self {
56 let _ = lock;
57 let original = env::var(key).ok();
58 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 unsafe { env::set_var(&self.key, value) };
73 }
74 None => {
75 unsafe { env::remove_var(&self.key) };
77 }
78 }
79 }
80}
81
82pub struct CwdGuard {
83 original: PathBuf,
84}
85
86impl CwdGuard {
87 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
144pub 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}