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)]
113pub fn can_create_symlinks(target: &Path) -> bool {
114 #[cfg(unix)]
115 {
116 let _ = target;
117 true
118 }
119
120 #[cfg(windows)]
121 {
122 use std::fs;
123 use std::os::windows::fs::symlink_dir;
124 let test_src = target.join("__oxdock_test_symlink_src");
125 let test_dst = target.join("__oxdock_test_symlink_dst");
126 let _ = fs::create_dir_all(&test_src);
128 let ok = symlink_dir(&test_src, &test_dst).is_ok();
129 let _ = fs::remove_dir_all(&test_dst);
130 let _ = fs::remove_dir_all(&test_src);
131 ok
132 }
133
134 #[cfg(not(any(unix, windows)))]
135 {
136 let _ = target;
137 false
138 }
139}
140
141#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
146pub fn exit_status_from_code(code: i32) -> std::process::ExitStatus {
147 #[cfg(unix)]
148 {
149 use std::os::unix::process::ExitStatusExt;
150 ExitStatusExt::from_raw(code << 8)
151 }
152 #[cfg(windows)]
153 {
154 use std::os::windows::process::ExitStatusExt;
155 ExitStatusExt::from_raw(code as u32)
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::TestEnvGuard;
162 use std::env;
163
164 const SET_RESTORE_ABSENT: &str = "OXDOCK_SYS_TEST_UTILS_SET_RESTORE_ABSENT";
165 const SET_RESTORE_PREVIOUS: &str = "OXDOCK_SYS_TEST_UTILS_SET_RESTORE_PREVIOUS";
166 const REMOVE_RESTORE: &str = "OXDOCK_SYS_TEST_UTILS_REMOVE_RESTORE";
167 const KEY_A: &str = "OXDOCK_SYS_TEST_UTILS_KEY_A";
168 const KEY_B: &str = "OXDOCK_SYS_TEST_UTILS_KEY_B";
169 const NESTED: &str = "OXDOCK_SYS_TEST_UTILS_NESTED";
170
171 #[test]
172 fn set_guard_restores_absent_state_on_drop() {
173 drop(TestEnvGuard::remove(SET_RESTORE_ABSENT));
174
175 let guard = TestEnvGuard::set(SET_RESTORE_ABSENT, "value");
176 assert_eq!(env::var(SET_RESTORE_ABSENT).as_deref(), Ok("value"));
177 drop(guard);
178
179 assert!(env::var(SET_RESTORE_ABSENT).is_err());
180 }
181
182 #[test]
183 fn set_guard_restores_previous_value_on_drop() {
184 unsafe { env::set_var(SET_RESTORE_PREVIOUS, "original") };
187
188 let guard = TestEnvGuard::set(SET_RESTORE_PREVIOUS, "temporary");
189 assert_eq!(env::var(SET_RESTORE_PREVIOUS).as_deref(), Ok("temporary"));
190 drop(guard);
191
192 assert_eq!(env::var(SET_RESTORE_PREVIOUS).as_deref(), Ok("original"));
193 unsafe { env::remove_var(SET_RESTORE_PREVIOUS) };
194 }
195
196 #[test]
197 fn remove_guard_restores_previous_value_on_drop() {
198 unsafe { env::set_var(REMOVE_RESTORE, "keep-me") };
199
200 let guard = TestEnvGuard::remove(REMOVE_RESTORE);
201 assert!(env::var(REMOVE_RESTORE).is_err());
202 drop(guard);
203
204 assert_eq!(env::var(REMOVE_RESTORE).as_deref(), Ok("keep-me"));
205 unsafe { env::remove_var(REMOVE_RESTORE) };
206 }
207
208 #[test]
209 fn guards_for_different_keys_coexist() {
210 let a = TestEnvGuard::set(KEY_A, "1");
211 let b = TestEnvGuard::set(KEY_B, "2");
212
213 assert_eq!(env::var(KEY_A).as_deref(), Ok("1"));
214 assert_eq!(env::var(KEY_B).as_deref(), Ok("2"));
215
216 drop(b);
217 drop(a);
218 }
219
220 #[test]
221 #[should_panic(expected = "already guarded")]
222 fn same_key_nesting_panics_rather_than_deadlocking() {
223 let _outer = TestEnvGuard::set(NESTED, "outer");
224 let _inner = TestEnvGuard::set(NESTED, "inner");
225 }
226}