use notify::*; use tempfile::TempDir; use std::fs; use std::io::Write; use std::path::PathBuf; use std::process; use std::sync::{ atomic::{AtomicBool, Ordering::SeqCst}, mpsc::{Receiver, TryRecvError}, Arc, }; use std::thread; use std::time::{Duration, Instant}; #[cfg(not(target_os = "windows"))] use std::os::unix::fs::PermissionsExt; #[cfg(not(target_os = "windows"))] const TIMEOUT_MS: u64 = 100; #[cfg(target_os = "windows")] const TIMEOUT_MS: u64 = 3000; // windows can take a while pub fn recv_events_with_timeout( rx: &Receiver, timeout: Duration, ) -> Vec<(PathBuf, Op, Option)> { let start = Instant::now(); let mut evs = Vec::new(); while start.elapsed() < timeout { match rx.try_recv() { Ok(RawEvent { path: Some(path), op: Ok(op), cookie, }) => { evs.push((path, op, cookie)); } Ok(RawEvent { path: None, .. }) => (), Ok(RawEvent { op: Err(e), .. }) => panic!("unexpected event err: {:?}", e), Err(TryRecvError::Empty) => (), Err(e) => panic!("unexpected channel err: {:?}", e), } thread::sleep(Duration::from_millis(1)); } evs } pub fn fail_after(test_name: &'static str, duration: Duration) -> impl Drop { struct SuccessOnDrop(Arc); impl Drop for SuccessOnDrop { fn drop(&mut self) { self.0.store(true, SeqCst) } } let finished = SuccessOnDrop(Arc::new(AtomicBool::new(false))); // timeout the test to catch deadlocks { let finished = finished.0.clone(); thread::spawn(move || { thread::sleep(duration); if finished.load(SeqCst) == false { println!("test `{}` timed out", test_name); process::abort(); } }); } finished } pub fn recv_events(rx: &Receiver) -> Vec<(PathBuf, Op, Option)> { recv_events_with_timeout(rx, Duration::from_millis(TIMEOUT_MS)) } // FSEvents tends to emit events multiple times and aggregate events, // so just check that all expected events arrive for each path, // and make sure the paths are in the correct order pub fn inflate_events(input: Vec<(PathBuf, Op, Option)>) -> Vec<(PathBuf, Op, Option)> { let mut output = Vec::new(); let mut path = None; let mut ops = Op::empty(); let mut cookie = None; for (e_p, e_o, e_c) in input { let p = match path { Some(p) => p, None => e_p.clone(), }; let c = match cookie { Some(c) => Some(c), None => e_c, }; if p == e_p && c == e_c { ops |= e_o; } else { output.push((p, ops, cookie)); ops = e_o; } path = Some(e_p); cookie = e_c; } if let Some(p) = path { output.push((p, ops, cookie)); } output } pub fn extract_cookies(events: &[(PathBuf, Op, Option)]) -> Vec { let mut cookies = Vec::new(); for &(_, _, e_c) in events { if let Some(cookie) = e_c { if !cookies.contains(&cookie) { cookies.push(cookie); } } } cookies } // Sleep for `duration` in milliseconds pub fn sleep(duration: u64) { thread::sleep(Duration::from_millis(duration)); } // Sleep for `duration` in milliseconds if running on macOS pub fn sleep_macos(duration: u64) { if cfg!(target_os = "macos") { thread::sleep(Duration::from_millis(duration)); } } // Sleep for `duration` in milliseconds if running on Windows pub fn sleep_windows(duration: u64) { if cfg!(target_os = "windows") { thread::sleep(Duration::from_millis(duration)); } } pub trait TestHelpers { /// Return path relative to the TempDir. Directory separator must be a forward slash, and will be converted to the platform's native separator. fn mkpath(&self, p: &str) -> PathBuf; /// Create file or directory. Directories must contain the phrase "dir" otherwise they will be interpreted as files. fn create(&self, p: &str); /// Create all files and directories in the `paths` list. Directories must contain the phrase "dir" otherwise they will be interpreted as files. fn create_all(&self, paths: Vec<&str>); /// Rename file or directory. fn rename(&self, a: &str, b: &str); /// Toggle "other" rights on linux and macOS and "readonly" on windows fn chmod(&self, p: &str); /// Write some data to a file fn write(&self, p: &str); /// Remove file or directory fn remove(&self, p: &str); } impl TestHelpers for TempDir { fn mkpath(&self, p: &str) -> PathBuf { let mut path = self .path() .canonicalize() .expect("failed to canonicalize path") .to_owned(); for part in p.split('/').collect::>() { if part != "." { path.push(part); } } path } fn create(&self, p: &str) { let path = self.mkpath(p); if path .components() .last() .unwrap() .as_os_str() .to_str() .unwrap() .contains("dir") { fs::create_dir_all(path).expect("failed to create directory"); } else { let parent = path .parent() .expect("failed to get parent directory") .to_owned(); if !parent.exists() { fs::create_dir_all(parent).expect("failed to create parent directory"); } fs::File::create(path).expect("failed to create file"); } } fn create_all(&self, paths: Vec<&str>) { for p in paths { self.create(p); } } fn rename(&self, a: &str, b: &str) { let path_a = self.mkpath(a); let path_b = self.mkpath(b); fs::rename(&path_a, &path_b).expect("failed to rename file or directory"); } #[cfg(not(target_os = "windows"))] fn chmod(&self, p: &str) { let path = self.mkpath(p); let mut permissions = fs::metadata(&path) .expect("failed to get metadata") .permissions(); let u = (permissions.mode() / 100) % 10; let g = (permissions.mode() / 10) % 10; let o = if permissions.mode() % 10 == 0 { g } else { 0 }; permissions.set_mode(u * 100 + g * 10 + o); fs::set_permissions(path, permissions).expect("failed to chmod file or directory"); } #[cfg(target_os = "windows")] fn chmod(&self, p: &str) { let path = self.mkpath(p); let mut permissions = fs::metadata(&path) .expect("failed to get metadata") .permissions(); let r = permissions.readonly(); permissions.set_readonly(!r); fs::set_permissions(path, permissions).expect("failed to chmod file or directory"); } fn write(&self, p: &str) { let path = self.mkpath(p); let mut file = fs::OpenOptions::new() .write(true) .open(path) .expect("failed to open file"); file.write(b"some data").expect("failed to write to file"); file.sync_all().expect("failed to sync file"); } fn remove(&self, p: &str) { let path = self.mkpath(p); if path.is_dir() { fs::remove_dir(path).expect("failed to remove directory"); } else { fs::remove_file(path).expect("failed to remove file"); } } } macro_rules! assert_eq_any { ($left:expr, $right1:expr, $right2:expr) => ({ match (&($left), &($right1), &($right2)) { (left_val, right1_val, right2_val) => { if *left_val != *right1_val && *left_val != *right2_val { panic!("assertion failed: `(left != right1 or right2)` (left: `{:?}`, right1: `{:?}`, right2: `{:?}`)", left_val, right1_val, right2_val) } } } }) }