oxdock_sys_test_utils/
lib.rs1use std::collections::HashMap;
5use std::env;
6use std::sync::{Mutex, MutexGuard, OnceLock};
7use std::thread::{ThreadId, current};
8
9static KEY_LOCKS: OnceLock<Mutex<HashMap<&'static str, &'static Mutex<()>>>> = OnceLock::new();
14
15static KEY_OWNERS: OnceLock<Mutex<HashMap<&'static str, ThreadId>>> = OnceLock::new();
18
19fn key_owners() -> &'static Mutex<HashMap<&'static str, ThreadId>> {
20 KEY_OWNERS.get_or_init(|| Mutex::new(HashMap::new()))
21}
22
23#[allow(clippy::disallowed_methods)] fn key_lock(key: &'static str) -> &'static Mutex<()> {
26 let locks = KEY_LOCKS.get_or_init(|| Mutex::new(HashMap::new()));
27 let mut map = locks
28 .lock()
29 .unwrap_or_else(|poisoned| poisoned.into_inner());
30 map.entry(key)
31 .or_insert_with(|| Box::leak(Box::new(Mutex::new(()))))
32}
33
34fn acquire_key_lock(key: &'static str) -> MutexGuard<'static, ()> {
35 let thread = current();
36 {
37 let owners = key_owners()
38 .lock()
39 .unwrap_or_else(|poisoned| poisoned.into_inner());
40 if owners.get(key) == Some(&thread.id()) {
41 panic!("TestEnvGuard: environment variable {key} is already guarded on this thread");
42 }
43 }
44
45 let guard = key_lock(key)
46 .lock()
47 .unwrap_or_else(|poisoned| poisoned.into_inner());
48 key_owners()
49 .lock()
50 .unwrap_or_else(|poisoned| poisoned.into_inner())
51 .insert(key, thread.id());
52 guard
53}
54
55pub struct TestEnvGuard {
56 key: &'static str,
57 value: Option<String>,
58 _lock: MutexGuard<'static, ()>,
59}
60
61impl TestEnvGuard {
62 pub fn set(key: &'static str, value: &str) -> Self {
67 let lock = acquire_key_lock(key);
68 let prev = env::var(key).ok();
69 unsafe { env::set_var(key, value) };
70 Self {
71 key,
72 value: prev,
73 _lock: lock,
74 }
75 }
76
77 pub fn remove(key: &'static str) -> Self {
82 let lock = acquire_key_lock(key);
83 let prev = env::var(key).ok();
84 unsafe { env::remove_var(key) };
85 Self {
86 key,
87 value: prev,
88 _lock: lock,
89 }
90 }
91}
92
93impl Drop for TestEnvGuard {
94 fn drop(&mut self) {
95 match &self.value {
96 Some(value) => unsafe { env::set_var(self.key, value) },
97 None => unsafe { env::remove_var(self.key) },
98 }
99 key_owners()
100 .lock()
101 .unwrap_or_else(|poisoned| poisoned.into_inner())
102 .remove(self.key);
103 }
104}
105
106#[allow(clippy::disallowed_types)]
107use std::path::Path;
108
109#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
114pub fn can_create_symlinks(target: &Path) -> bool {
115 #[cfg(unix)]
116 {
117 let _ = target;
118 true
119 }
120
121 #[cfg(windows)]
122 {
123 use std::fs;
124 use std::os::windows::fs::symlink_dir;
125 let test_src = target.join("__oxdock_test_symlink_src");
126 let test_dst = target.join("__oxdock_test_symlink_dst");
127 let _ = fs::create_dir_all(&test_src);
129 let ok = symlink_dir(&test_src, &test_dst).is_ok();
130 let _ = fs::remove_dir_all(&test_dst);
131 let _ = fs::remove_dir_all(&test_src);
132 ok
133 }
134
135 #[cfg(not(any(unix, windows)))]
136 {
137 let _ = target;
138 false
139 }
140}
141
142#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
149pub fn exit_status_from_code(code: i32) -> std::process::ExitStatus {
150 #[cfg(unix)]
151 {
152 use std::os::unix::process::ExitStatusExt;
153 ExitStatusExt::from_raw(code << 8)
154 }
155 #[cfg(windows)]
156 {
157 use std::os::windows::process::ExitStatusExt;
158 ExitStatusExt::from_raw(code as u32)
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::TestEnvGuard;
165 use std::env;
166
167 const SET_RESTORE_ABSENT: &str = "OXDOCK_SYS_TEST_UTILS_SET_RESTORE_ABSENT";
168 const SET_RESTORE_PREVIOUS: &str = "OXDOCK_SYS_TEST_UTILS_SET_RESTORE_PREVIOUS";
169 const REMOVE_RESTORE: &str = "OXDOCK_SYS_TEST_UTILS_REMOVE_RESTORE";
170 const KEY_A: &str = "OXDOCK_SYS_TEST_UTILS_KEY_A";
171 const KEY_B: &str = "OXDOCK_SYS_TEST_UTILS_KEY_B";
172 const NESTED: &str = "OXDOCK_SYS_TEST_UTILS_NESTED";
173
174 #[test]
175 fn set_guard_restores_absent_state_on_drop() {
176 drop(TestEnvGuard::remove(SET_RESTORE_ABSENT));
177
178 let guard = TestEnvGuard::set(SET_RESTORE_ABSENT, "value");
179 assert_eq!(env::var(SET_RESTORE_ABSENT).as_deref(), Ok("value"));
180 drop(guard);
181
182 assert!(env::var(SET_RESTORE_ABSENT).is_err());
183 }
184
185 #[test]
186 fn set_guard_restores_previous_value_on_drop() {
187 unsafe { env::set_var(SET_RESTORE_PREVIOUS, "original") };
190
191 let guard = TestEnvGuard::set(SET_RESTORE_PREVIOUS, "temporary");
192 assert_eq!(env::var(SET_RESTORE_PREVIOUS).as_deref(), Ok("temporary"));
193 drop(guard);
194
195 assert_eq!(env::var(SET_RESTORE_PREVIOUS).as_deref(), Ok("original"));
196 unsafe { env::remove_var(SET_RESTORE_PREVIOUS) };
197 }
198
199 #[test]
200 fn remove_guard_restores_previous_value_on_drop() {
201 unsafe { env::set_var(REMOVE_RESTORE, "keep-me") };
202
203 let guard = TestEnvGuard::remove(REMOVE_RESTORE);
204 assert!(env::var(REMOVE_RESTORE).is_err());
205 drop(guard);
206
207 assert_eq!(env::var(REMOVE_RESTORE).as_deref(), Ok("keep-me"));
208 unsafe { env::remove_var(REMOVE_RESTORE) };
209 }
210
211 #[test]
212 fn guards_for_different_keys_coexist() {
213 let a = TestEnvGuard::set(KEY_A, "1");
214 let b = TestEnvGuard::set(KEY_B, "2");
215
216 assert_eq!(env::var(KEY_A).as_deref(), Ok("1"));
217 assert_eq!(env::var(KEY_B).as_deref(), Ok("2"));
218
219 drop(b);
220 drop(a);
221 }
222
223 #[test]
224 #[should_panic(expected = "already guarded")]
225 fn same_key_nesting_panics_rather_than_deadlocking() {
226 let _outer = TestEnvGuard::set(NESTED, "outer");
227 let _inner = TestEnvGuard::set(NESTED, "inner");
228 }
229}