videre_core/
io_timeout.rs1use std::path::Path;
2use std::sync::mpsc;
3use std::thread;
4use std::time::{Duration, Instant};
5
6pub const DEFAULT_IO_TIMEOUT: Duration = Duration::from_secs(20);
12
13pub struct TimedOut;
18
19pub fn run_with_timeout<T, F>(timeout: Duration, f: F) -> Result<T, TimedOut>
24where
25 F: FnOnce() -> T + Send + 'static,
26 T: Send + 'static,
27{
28 let (tx, rx) = mpsc::channel();
29 thread::spawn(move || {
30 let _ = tx.send(f());
31 });
32 rx.recv_timeout(timeout).map_err(|_| TimedOut)
33}
34
35#[derive(Debug, PartialEq, Eq)]
37pub enum WaitOutcome {
38 Success,
39 Failed,
40 TimedOut,
41}
42
43pub fn wait_with_timeout(child: &mut std::process::Child, timeout: Duration) -> WaitOutcome {
48 let start = Instant::now();
49 loop {
50 match child.try_wait() {
51 Ok(Some(status)) => {
52 return if status.success() {
53 WaitOutcome::Success
54 } else {
55 WaitOutcome::Failed
56 };
57 }
58 Ok(None) => {
59 if start.elapsed() >= timeout {
60 let _ = child.kill();
61 let _ = child.wait();
62 return WaitOutcome::TimedOut;
63 }
64 thread::sleep(Duration::from_millis(50));
65 }
66 Err(_) => return WaitOutcome::Failed,
67 }
68 }
69}
70
71pub fn absence_is_trustworthy(path: &Path) -> bool {
94 let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) else {
95 return false;
96 };
97 let parent = parent.to_path_buf();
98 run_with_timeout(DEFAULT_IO_TIMEOUT, move || parent.is_dir()).unwrap_or(false)
99}
100
101#[cfg(test)]
102mod absence_tests {
103 use super::*;
104
105 fn tmp(name: &str) -> std::path::PathBuf {
106 let d = std::env::temp_dir().join(format!("videre-absence-{}-{name}", std::process::id()));
107 std::fs::create_dir_all(&d).unwrap();
108 d
109 }
110
111 #[test]
112 fn a_missing_file_in_an_existing_directory_is_trustworthy() {
113 let dir = tmp("present");
114 assert!(absence_is_trustworthy(&dir.join("gone.jpg")));
115 let _ = std::fs::remove_dir_all(&dir);
116 }
117
118 #[test]
119 fn a_missing_file_in_a_missing_directory_is_not() {
120 let dir = tmp("absent");
122 let nested = dir.join("subdir");
123 assert!(!absence_is_trustworthy(&nested.join("gone.jpg")));
124 let _ = std::fs::remove_dir_all(&dir);
125 }
126
127 #[test]
128 fn a_real_present_file_is_trustworthy_too() {
129 let dir = tmp("realfile");
132 let f = dir.join("here.jpg");
133 std::fs::write(&f, b"x").unwrap();
134 assert!(absence_is_trustworthy(&f));
135 let _ = std::fs::remove_dir_all(&dir);
136 }
137
138 #[test]
139 fn a_path_without_a_usable_parent_is_not_trustworthy() {
140 assert!(!absence_is_trustworthy(Path::new("/")));
143 assert!(!absence_is_trustworthy(Path::new("bare-name.jpg")));
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn returns_ok_when_operation_finishes_before_timeout() {
153 let result = run_with_timeout(Duration::from_secs(1), || 42);
154 assert!(result.is_ok());
155 assert_eq!(result.ok(), Some(42));
156 }
157
158 #[test]
159 fn returns_timed_out_when_operation_exceeds_timeout() {
160 let result = run_with_timeout(Duration::from_millis(50), || {
161 thread::sleep(Duration::from_secs(5));
162 42
163 });
164 assert!(result.is_err());
165 }
166
167 #[test]
168 fn wait_with_timeout_returns_success_for_fast_process() {
169 let mut child = std::process::Command::new("true").spawn().unwrap();
170 assert_eq!(
171 wait_with_timeout(&mut child, Duration::from_secs(5)),
172 WaitOutcome::Success
173 );
174 }
175
176 #[test]
177 fn wait_with_timeout_kills_and_returns_timed_out_for_slow_process() {
178 let mut child = std::process::Command::new("sleep")
179 .arg("5")
180 .spawn()
181 .unwrap();
182 let start = Instant::now();
183 assert_eq!(
184 wait_with_timeout(&mut child, Duration::from_millis(200)),
185 WaitOutcome::TimedOut
186 );
187 assert!(start.elapsed() < Duration::from_secs(2));
188 }
189}