diff options
Diffstat (limited to 'src/watcher.rs')
| -rw-r--r-- | src/watcher.rs | 251 |
1 files changed, 250 insertions, 1 deletions
diff --git a/src/watcher.rs b/src/watcher.rs index ac35b5b5..e5b9458a 100644 --- a/src/watcher.rs +++ b/src/watcher.rs @@ -2,7 +2,7 @@ use std::path::PathBuf; use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; +use std::sync::{mpsc, Arc}; use std::thread; use std::time::Duration; @@ -20,6 +20,14 @@ impl Drop for Watcher { impl Watcher { pub fn new(path: PathBuf, changed: SyncSender<()>) -> Self { + Self::with_start_notification(path, changed, None) + } + + pub fn with_start_notification( + path: PathBuf, + changed: SyncSender<()>, + started: Option<mpsc::SyncSender<()>>, + ) -> Self { let should_stop = Arc::new(AtomicBool::new(false)); { @@ -40,6 +48,10 @@ impl Watcher { .and_then(|canon| Ok((canon.metadata()?.modified()?, canon))) .ok(); + if let Some(started) = started { + let _ = started.send(()); + } + loop { thread::sleep(Duration::from_millis(500)); @@ -72,3 +84,240 @@ impl Watcher { Self { should_stop } } } + +#[cfg(test)] +mod tests { + use std::error::Error; + use std::fs::File; + use std::io::Write; + use std::sync::atomic::AtomicU8; + use std::time::SystemTime; + + use calloop::channel::sync_channel; + use calloop::EventLoop; + use xshell::{cmd, Shell}; + + use super::*; + + fn check( + setup: impl FnOnce(&Shell) -> Result<(), Box<dyn Error>>, + change: impl FnOnce(&Shell) -> Result<(), Box<dyn Error>>, + ) { + let sh = Shell::new().unwrap(); + let temp_dir = sh.create_temp_dir().unwrap(); + sh.change_dir(temp_dir.path()); + // let dir = sh.create_dir("xshell").unwrap(); + // sh.change_dir(dir); + + let mut config_path = sh.current_dir(); + config_path.push("niri"); + config_path.push("config.kdl"); + + setup(&sh).unwrap(); + + let changed = AtomicU8::new(0); + + let mut event_loop = EventLoop::try_new().unwrap(); + let loop_handle = event_loop.handle(); + + let (tx, rx) = sync_channel(1); + let (started_tx, started_rx) = mpsc::sync_channel(1); + let _watcher = Watcher::with_start_notification(config_path.clone(), tx, Some(started_tx)); + loop_handle + .insert_source(rx, |_, _, _| { + changed.fetch_add(1, Ordering::SeqCst); + }) + .unwrap(); + started_rx.recv().unwrap(); + + // HACK: if we don't sleep, files might have the same mtime. + thread::sleep(Duration::from_millis(100)); + + change(&sh).unwrap(); + + event_loop + .dispatch(Duration::from_millis(750), &mut ()) + .unwrap(); + + assert_eq!(changed.load(Ordering::SeqCst), 1); + + // Verify that the watcher didn't break. + sh.write_file(&config_path, "c").unwrap(); + + event_loop + .dispatch(Duration::from_millis(750), &mut ()) + .unwrap(); + + assert_eq!(changed.load(Ordering::SeqCst), 2); + } + + #[test] + fn change_file() { + check( + |sh| { + sh.write_file("niri/config.kdl", "a")?; + Ok(()) + }, + |sh| { + sh.write_file("niri/config.kdl", "b")?; + Ok(()) + }, + ); + } + + #[test] + fn create_file() { + check( + |sh| { + sh.create_dir("niri")?; + Ok(()) + }, + |sh| { + sh.write_file("niri/config.kdl", "a")?; + Ok(()) + }, + ); + } + + #[test] + fn create_dir_and_file() { + check( + |_sh| Ok(()), + |sh| { + sh.write_file("niri/config.kdl", "a")?; + Ok(()) + }, + ); + } + + #[test] + fn change_linked_file() { + check( + |sh| { + sh.write_file("niri/config2.kdl", "a")?; + cmd!(sh, "ln -s config2.kdl niri/config.kdl").run()?; + Ok(()) + }, + |sh| { + sh.write_file("niri/config2.kdl", "b")?; + Ok(()) + }, + ); + } + + #[test] + fn change_file_in_linked_dir() { + check( + |sh| { + sh.write_file("niri2/config.kdl", "a")?; + cmd!(sh, "ln -s niri2 niri").run()?; + Ok(()) + }, + |sh| { + sh.write_file("niri2/config.kdl", "b")?; + Ok(()) + }, + ); + } + + #[test] + fn recreate_file() { + check( + |sh| { + sh.write_file("niri/config.kdl", "a")?; + Ok(()) + }, + |sh| { + sh.remove_path("niri/config.kdl")?; + sh.write_file("niri/config.kdl", "b")?; + Ok(()) + }, + ); + } + + #[test] + fn recreate_dir() { + check( + |sh| { + sh.write_file("niri/config.kdl", "a")?; + Ok(()) + }, + |sh| { + sh.remove_path("niri")?; + sh.write_file("niri/config.kdl", "b")?; + Ok(()) + }, + ); + } + + #[test] + fn swap_dir() { + check( + |sh| { + sh.write_file("niri/config.kdl", "a")?; + Ok(()) + }, + |sh| { + sh.write_file("niri2/config.kdl", "b")?; + sh.remove_path("niri")?; + cmd!(sh, "mv niri2 niri").run()?; + Ok(()) + }, + ); + } + + #[test] + fn swap_just_link() { + // NixOS setup: link path changes, mtime stays constant. + check( + |sh| { + let mut dir = sh.current_dir(); + dir.push("niri"); + sh.create_dir(&dir)?; + + let mut d2 = dir.clone(); + d2.push("config2.kdl"); + let mut c2 = File::create(d2).unwrap(); + write!(c2, "a")?; + c2.flush()?; + c2.set_modified(SystemTime::UNIX_EPOCH)?; + c2.sync_all()?; + drop(c2); + + let mut d3 = dir.clone(); + d3.push("config3.kdl"); + let mut c3 = File::create(d3).unwrap(); + write!(c3, "b")?; + c3.flush()?; + c3.set_modified(SystemTime::UNIX_EPOCH)?; + c3.sync_all()?; + drop(c3); + + cmd!(sh, "ln -s config2.kdl niri/config.kdl").run()?; + Ok(()) + }, + |sh| { + cmd!(sh, "unlink niri/config.kdl").run()?; + cmd!(sh, "ln -s config3.kdl niri/config.kdl").run()?; + Ok(()) + }, + ); + } + + #[test] + fn swap_dir_link() { + check( + |sh| { + sh.write_file("niri2/config.kdl", "a")?; + cmd!(sh, "ln -s niri2 niri").run()?; + Ok(()) + }, + |sh| { + sh.write_file("niri3/config.kdl", "b")?; + cmd!(sh, "unlink niri").run()?; + cmd!(sh, "ln -s niri3 niri").run()?; + Ok(()) + }, + ); + } +} |
