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