diff --git a/notify-debouncer-full/CHANGELOG.md b/notify-debouncer-full/CHANGELOG.md index 9fe51148..904ccda0 100644 --- a/notify-debouncer-full/CHANGELOG.md +++ b/notify-debouncer-full/CHANGELOG.md @@ -2,8 +2,10 @@ ## debouncer-full 0.7.1 (unreleased) - FEATURE: impl `EventHandler` for `futures::channel::mpsc::UnboundedSender` and `tokio::sync::mpsc::UnboundedSender` behind the `futures` and `tokio` feature flags [#767] +- FEATURE: add support of a watcher's method `update_paths` [#705] [#767]: https://github.com/notify-rs/notify/pull/767 +[#705]: https://github.com/notify-rs/notify/pull/705 ## debouncer-full 0.7.0 (2026-01-23) diff --git a/notify-debouncer-full/src/lib.rs b/notify-debouncer-full/src/lib.rs index 416a2827..47a561f5 100644 --- a/notify-debouncer-full/src/lib.rs +++ b/notify-debouncer-full/src/lib.rs @@ -95,7 +95,8 @@ pub use notify_types::debouncer_full::DebouncedEvent; use file_id::FileId; use notify::{ event::{ModifyKind, RemoveKind, RenameMode}, - Error, ErrorKind, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher, WatcherKind, + Error, ErrorKind, Event, EventKind, PathOp, RecommendedWatcher, RecursiveMode, + UpdatePathsError, Watcher, WatcherKind, }; /// The set of requirements for watcher debounce event handling functions. @@ -627,6 +628,76 @@ impl Debouncer { Ok(()) } + /// Add/remove paths to watch in batch. + /// + /// For some [`Watcher`] implementations this method provides better performance than multiple + /// calls to [`Watcher::watch`] and [`Watcher::unwatch`] if you want to add/remove many paths at once. + /// + /// # Errors + /// + /// Returns [`UpdatePathsError`] if any operation fails. Operations are applied sequentially. + /// When an error occurs, processing stops: operations before `origin` have been applied, + /// `origin` is the operation that failed (if known), and `remaining` are the operations that + /// were not attempted. `remaining` does not include `origin`. + /// + /// # Examples + /// + /// ``` + /// # use notify::{Watcher, RecursiveMode, PathOp}; + /// # use notify_debouncer_full::{RecommendedCache, new_debouncer_opt}; + /// # use std::path::{Path, PathBuf}; + /// # fn main() -> Result<(), Box> { + /// # let mut debouncer = new_debouncer_opt::<_, notify::NullWatcher, _>( + /// # std::time::Duration::from_secs(1), + /// # None, + /// # |e| {}, + /// # RecommendedCache::new(), + /// # Default::default() + /// # )?; + /// debouncer.update_paths([ + /// PathOp::watch_recursive("path/to/file"), + /// PathOp::unwatch("path/to/file2"), + /// ])?; + /// # Ok(()) + /// # } + /// ``` + pub fn update_paths>( + &mut self, + ops: impl IntoIterator, + ) -> std::result::Result<(), UpdatePathsError> { + let mut paths = Vec::new(); + let ops: Vec<_> = ops + .into_iter() + .map(Into::into) + .inspect(|op| { + paths.push(( + op.as_path().to_path_buf(), + match op { + PathOp::Watch(_, config) => Some(config.recursive_mode()), + PathOp::Unwatch(_) => None, + }, + )); + }) + .collect(); + + let res = self.watcher.update_paths(ops); + let updated_len = match res.as_ref() { + Ok(()) => paths.len(), + Err(e) => { + let failed = usize::from(e.origin.is_some()); + paths.len().saturating_sub(e.remaining.len() + failed) + } + }; + let updated_paths = &paths[..updated_len]; + for (path, watch_mode) in updated_paths { + match watch_mode { + Some(recursive_mode) => self.add_root(path, *recursive_mode), + None => self.remove_root(path), + } + } + res + } + pub fn configure(&mut self, option: notify::Config) -> notify::Result { self.watcher.configure(option) } @@ -789,7 +860,10 @@ fn sort_events(events: Vec) -> Vec { #[cfg(test)] mod tests { - use std::{fs, path::Path}; + use std::{ + fs, + path::{Path, PathBuf}, + }; use super::*; @@ -799,6 +873,42 @@ mod tests { use testing::TestCase; use time::MockTime; + #[derive(Debug)] + struct FailingWatcher { + fail_path: PathBuf, + } + + impl Watcher for FailingWatcher { + fn new( + _event_handler: F, + _config: notify::Config, + ) -> notify::Result { + Ok(Self { + fail_path: PathBuf::from("bad"), + }) + } + + fn watch(&mut self, path: &Path, _recursive_mode: RecursiveMode) -> notify::Result<()> { + if path == self.fail_path { + Err(Error::path_not_found()) + } else { + Ok(()) + } + } + + fn unwatch(&mut self, path: &Path) -> notify::Result<()> { + if path == self.fail_path { + Err(Error::path_not_found()) + } else { + Ok(()) + } + } + + fn kind() -> WatcherKind { + WatcherKind::NullWatcher + } + } + #[rstest] fn state( #[values( @@ -1014,4 +1124,89 @@ mod tests { .expect("No event") .expect("error"); } + + #[test] + fn update_paths() -> Result<(), Box> { + let dir1 = tempdir()?; + let dir2 = tempdir()?; + + // set up the watcher + let (tx, rx) = std::sync::mpsc::channel(); + let mut debouncer = new_debouncer(Duration::from_millis(10), None, tx)?; + debouncer.update_paths([ + PathOp::watch_recursive(dir1.path()), + PathOp::watch_recursive(dir2.path()), + ])?; + + // create a new file + let file_path1 = dir1.path().join("file.txt"); + let file_path2 = dir2.path().join("file.txt"); + fs::write(&file_path1, b"Lorem ipsum1")?; + fs::write(&file_path2, b"Lorem ipsum1")?; + + println!( + "waiting for events at {:?} and {:?}", + file_path1, file_path2 + ); + + // wait for up to 10 seconds for the create event, ignore all other events + let deadline = Instant::now() + Duration::from_secs(10); + let mut received = (false, false); + while deadline > Instant::now() { + let events = rx + .recv_timeout(deadline - Instant::now()) + .expect("did not receive expected event") + .expect("received an error"); + + for event in events { + println!("event {event:?}"); + if event.event.paths == vec![file_path1.clone()] + || event.event.paths == vec![file_path1.canonicalize()?] + { + received.0 = true; + } + + if event.event.paths == vec![file_path2.clone()] + || event.event.paths == vec![file_path2.canonicalize()?] + { + received.1 = true; + } + + if received == (true, true) { + return Ok(()); + } + } + } + + panic!("did not receive expected event"); + } + + #[test] + fn update_paths_error_does_not_add_failed_root() -> Result<(), Box> { + let mut debouncer = new_debouncer_opt::<_, FailingWatcher, NoCache>( + Duration::from_millis(20), + Some(Duration::from_millis(5)), + |_| {}, + NoCache::new(), + notify::Config::default(), + )?; + + let err = debouncer + .update_paths([ + PathOp::watch_recursive("ok1"), + PathOp::watch_recursive("bad"), + PathOp::watch_recursive("ok2"), + ]) + .unwrap_err(); + assert!(err.origin.is_some()); + assert_eq!(err.remaining.len(), 1); + + let roots = debouncer.data.lock().unwrap().roots.clone(); + assert_eq!( + roots, + vec![(PathBuf::from("ok1"), RecursiveMode::Recursive)] + ); + + Ok(()) + } } diff --git a/notify/CHANGELOG.md b/notify/CHANGELOG.md index 8b2b4630..3b9a80ae 100644 --- a/notify/CHANGELOG.md +++ b/notify/CHANGELOG.md @@ -2,6 +2,12 @@ ## notify 9.0.0 (unreleased) +- FEATURE: remove `Watcher::paths_mut` and introduce `update_paths` [#705] +- FEATURE: impl `EventHandler` for `futures::channel::mpsc::UnboundedSender` and `tokio::sync::mpsc::UnboundedSender` behind the `futures` and `tokio` feature flags [#767] + +[#705]: https://github.com/notify-rs/notify/pull/705 +[#767]: https://github.com/notify-rs/notify/pull/767 + ## notify 9.0.0-rc.1 (2026-01-25) > [!IMPORTANT] @@ -15,7 +21,6 @@ - FIX: Fix the bug that `INotifyWatcher` keeps watching deleted paths [#720] - FIX: Fixed ordering where `FsEventWatcher` emitted `Remove` events non-terminally [#747] - FIX: [macOS] throw `FsEventWatcher` stream start error properly [#733] -- FEATURE: impl `EventHandler` for `futures::channel::mpsc::UnboundedSender` and `tokio::sync::mpsc::UnboundedSender` behind the `futures` and `tokio` feature flags [#767] [#718]: https://github.com/notify-rs/notify/pull/718 [#720]: https://github.com/notify-rs/notify/pull/720 @@ -23,7 +28,6 @@ [#733]: https://github.com/notify-rs/notify/pull/733 [#736]: https://github.com/notify-rs/notify/pull/736 [#747]: https://github.com/notify-rs/notify/pull/747 -[#767]: https://github.com/notify-rs/notify/pull/767 ## notify 8.2.0 (2025-08-03) - FEATURE: notify user if inotify's `max_user_watches` has been reached [#698] diff --git a/notify/src/config.rs b/notify/src/config.rs index fe0888d0..7bffab53 100644 --- a/notify/src/config.rs +++ b/notify/src/config.rs @@ -1,7 +1,10 @@ //! Configuration types use notify_types::event::EventKindMask; -use std::time::Duration; +use std::{ + path::{Path, PathBuf}, + time::Duration, +}; /// Indicates whether only the provided directory or its sub-directories as well should be watched #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)] @@ -165,6 +168,81 @@ impl Default for Config { } } +/// Single watch backend configuration +/// +/// This contains some settings that may relate to only one specific backend, +/// such as to correctly configure each backend regardless of what is selected during runtime. +#[derive(Debug)] +pub struct WatchPathConfig { + recursive_mode: RecursiveMode, +} + +impl WatchPathConfig { + /// Creates new instance with provided [`RecursiveMode`] + pub fn new(recursive_mode: RecursiveMode) -> Self { + Self { recursive_mode } + } + + /// Set [`RecursiveMode`] for the watch + pub fn with_recursive_mode(mut self, recursive_mode: RecursiveMode) -> Self { + self.recursive_mode = recursive_mode; + self + } + + /// Returns current setting + pub fn recursive_mode(&self) -> RecursiveMode { + self.recursive_mode + } +} + +/// An operation to apply to a watcher +/// +/// See [`Watcher::update_paths`] for more information +#[derive(Debug)] +pub enum PathOp { + /// Path should be watched + Watch(PathBuf, WatchPathConfig), + + /// Path should be unwatched + Unwatch(PathBuf), +} + +impl PathOp { + /// Watch the path with [`RecursiveMode::Recursive`] + pub fn watch_recursive>(path: P) -> Self { + Self::Watch(path.into(), WatchPathConfig::new(RecursiveMode::Recursive)) + } + + /// Watch the path with [`RecursiveMode::NonRecursive`] + pub fn watch_non_recursive>(path: P) -> Self { + Self::Watch( + path.into(), + WatchPathConfig::new(RecursiveMode::NonRecursive), + ) + } + + /// Unwatch the path + pub fn unwatch>(path: P) -> Self { + Self::Unwatch(path.into()) + } + + /// Returns the path associated with this operation. + pub fn as_path(&self) -> &Path { + match self { + PathOp::Watch(p, _) => p, + PathOp::Unwatch(p) => p, + } + } + + /// Returns the path associated with this operation. + pub fn into_path(self) -> PathBuf { + match self { + PathOp::Watch(p, _) => p, + PathOp::Unwatch(p) => p, + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/notify/src/error.rs b/notify/src/error.rs index 3d88bd7f..3d9f60ad 100644 --- a/notify/src/error.rs +++ b/notify/src/error.rs @@ -1,7 +1,8 @@ //! Error types -use crate::Config; +use crate::{Config, PathOp}; use std::error::Error as StdError; +use std::fmt::Debug; use std::path::PathBuf; use std::result::Result as StdResult; use std::{self, fmt, io}; @@ -158,14 +159,86 @@ impl From> for Error { } } -#[test] -fn display_formatted_errors() { - let expected = "Some error"; +/// The error provided by [`crate::Watcher::update_paths`] method. +/// +/// Operations are applied in order. If an error occurs, processing stops and the +/// error carries the failed operation (if known) and any remaining operations that +/// were not attempted. +#[derive(Debug)] +pub struct UpdatePathsError { + /// The original error + pub source: Error, + + /// The operation that caused the error. + /// + /// If set, all operations before it were applied successfully. + /// `None` if the error was not caused by a specific operation + /// (e.g. failure to start the watcher after successfully updating paths). + pub origin: Option, + + /// The remaining operations that haven't been applied. + /// + /// This list does not include `origin`. To retry in order, handle `origin` + /// first (if present), then `remaining`. + pub remaining: Vec, +} + +impl fmt::Display for UpdatePathsError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "unable to apply the batch operation: {}", self.source) + } +} + +impl StdError for UpdatePathsError { + fn source(&self) -> Option<&(dyn StdError + 'static)> { + Some(&self.source) + } +} + +impl From for Error { + fn from(value: UpdatePathsError) -> Self { + value.source + } +} + +impl IntoIterator for UpdatePathsError { + type Item = PathOp; + + type IntoIter = std::iter::Chain, std::vec::IntoIter>; + + fn into_iter(self) -> Self::IntoIter { + self.origin.into_iter().chain(self.remaining) + } +} - assert_eq!(expected, format!("{}", Error::generic(expected))); +#[cfg(test)] +mod tests { + use super::*; - assert_eq!( - expected, - format!("{}", Error::io(io::Error::other(expected))) - ); + #[test] + fn display_formatted_errors() { + let expected = "Some error"; + + assert_eq!(expected, format!("{}", Error::generic(expected))); + + assert_eq!( + expected, + format!("{}", Error::io(io::Error::other(expected))) + ); + } + + #[test] + fn display_update_paths() { + let actual = UpdatePathsError { + source: Error::generic("Some error"), + origin: None, + remaining: Default::default(), + } + .to_string(); + + assert_eq!( + format!("unable to apply the batch operation: Some error"), + actual + ); + } } diff --git a/notify/src/fsevent.rs b/notify/src/fsevent.rs index 4b9100ad..69d28e4a 100644 --- a/notify/src/fsevent.rs +++ b/notify/src/fsevent.rs @@ -14,10 +14,9 @@ #![allow(non_upper_case_globals, dead_code)] -use crate::event::*; +use crate::{event::*, PathOp}; use crate::{ - unbounded, Config, Error, EventHandler, EventKindMask, PathsMut, RecursiveMode, Result, Sender, - Watcher, + unbounded, Config, Error, EventHandler, EventKindMask, RecursiveMode, Result, Sender, Watcher, }; use objc2_core_foundation as cf; use objc2_core_services as fs; @@ -264,27 +263,6 @@ unsafe extern "C-unwind" fn release_context(info: *const libc::c_void) { } } -struct FsEventPathsMut<'a>(&'a mut FsEventWatcher); -impl<'a> FsEventPathsMut<'a> { - fn new(watcher: &'a mut FsEventWatcher) -> Self { - watcher.stop(); - Self(watcher) - } -} -impl PathsMut for FsEventPathsMut<'_> { - fn add(&mut self, path: &Path, recursive_mode: RecursiveMode) -> Result<()> { - self.0.append_path(path, recursive_mode) - } - - fn remove(&mut self, path: &Path) -> Result<()> { - self.0.remove_path(path) - } - - fn commit(self: Box) -> Result<()> { - self.0.run() - } -} - impl FsEventWatcher { fn from_event_handler( event_handler: Arc>, @@ -318,6 +296,39 @@ impl FsEventWatcher { result } + fn update_paths_inner( + &mut self, + ops: Vec, + ) -> crate::StdResult<(), crate::UpdatePathsError> { + self.stop(); + + let result = crate::update_paths(ops, |op| match op { + crate::PathOp::Watch(path, config) => self + .append_path(&path, config.recursive_mode()) + .map_err(|e| (PathOp::Watch(path, config), e)), + crate::PathOp::Unwatch(path) => self + .remove_path(&path) + .map_err(|e| (PathOp::Unwatch(path), e)), + }); + + match self.run() { + Err(run_error) => match result { + Ok(()) => Err(crate::UpdatePathsError { + source: run_error, + origin: None, + remaining: Default::default(), + }), + Err(path_op_error) => { + log::error!( + "Unable to run fsevents watcher after updating paths error: {run_error:?}" + ); + Err(path_op_error) + } + }, + Ok(()) => result, + } + } + #[inline] fn is_running(&self) -> bool { self.runloop.is_some() @@ -601,14 +612,14 @@ impl Watcher for FsEventWatcher { self.watch_inner(path, recursive_mode) } - fn paths_mut<'me>(&'me mut self) -> Box { - Box::new(FsEventPathsMut::new(self)) - } - fn unwatch(&mut self, path: &Path) -> Result<()> { self.unwatch_inner(path) } + fn update_paths(&mut self, ops: Vec) -> crate::StdResult<(), crate::UpdatePathsError> { + self.update_paths_inner(ops) + } + fn configure(&mut self, config: Config) -> Result { let (tx, rx) = unbounded(); self.configure_raw_mode(config, tx); @@ -667,7 +678,7 @@ unsafe fn path_to_cfstring_ref( mod tests { use std::time::Duration; - use crate::ErrorKind; + use crate::{ErrorKind, WatchPathConfig}; use super::*; use crate::test::*; @@ -1143,15 +1154,18 @@ mod tests { let tmpdir = testdir(); let (mut watcher, _rx) = watcher(); - // use path_mut, otherwise it's too slow - let mut paths = watcher.watcher.paths_mut(); + let mut paths = Vec::new(); + for i in 0..=4096 { let path = tmpdir.path().join(format!("dir_{i}/subdir")); std::fs::create_dir_all(&path).expect("create_dir"); - paths.add(&path, RecursiveMode::NonRecursive).expect("add"); + paths.push(PathOp::Watch( + path, + WatchPathConfig::new(RecursiveMode::NonRecursive), + )); } - let result = paths.commit(); - assert!(result.is_err()); + + assert!(watcher.watcher.update_paths(paths).is_err()); } #[test] diff --git a/notify/src/lib.rs b/notify/src/lib.rs index 0d432894..181416b9 100644 --- a/notify/src/lib.rs +++ b/notify/src/lib.rs @@ -162,11 +162,12 @@ #![deny(missing_docs)] -pub use config::{Config, RecursiveMode}; -pub use error::{Error, ErrorKind, Result}; +pub use config::{Config, PathOp, RecursiveMode, WatchPathConfig}; +pub use error::{Error, ErrorKind, Result, UpdatePathsError}; pub use notify_types::event::{self, Event, EventKind, EventKindMask}; use std::path::Path; +pub(crate) type StdResult = std::result::Result; pub(crate) type Receiver = std::sync::mpsc::Receiver; pub(crate) type Sender = std::sync::mpsc::Sender; #[cfg(any(target_os = "linux", target_os = "android", target_os = "windows"))] @@ -310,23 +311,6 @@ pub enum WatcherKind { NullWatcher, } -/// Providing methods for adding and removing paths to watch. -/// -/// `Box` is created by [`Watcher::paths_mut`]. See its documentation for more. -pub trait PathsMut { - /// Add a new path to watch. See [`Watcher::watch`] for more. - fn add(&mut self, path: &Path, recursive_mode: RecursiveMode) -> Result<()>; - - /// Remove a path from watching. See [`Watcher::unwatch`] for more. - fn remove(&mut self, path: &Path) -> Result<()>; - - /// Ensure added/removed paths are applied. - /// - /// The behaviour of dropping a [`PathsMut`] without calling [`commit`] is unspecified. - /// The implementation is free to ignore the changes or not, and may leave the watcher in a started or stopped state. - fn commit(self: Box) -> Result<()>; -} - /// Type that can deliver file activity notifications /// /// `Watcher` is implemented per platform using the best implementation available on that platform. @@ -362,40 +346,49 @@ pub trait Watcher { /// fails. fn unwatch(&mut self, path: &Path) -> Result<()>; - /// Add/remove paths to watch. + /// Add/remove paths to watch in batch. /// - /// For some watcher implementations this method provides better performance than multiple calls to [`Watcher::watch`] and [`Watcher::unwatch`] if you want to add/remove many paths at once. + /// For some [`Watcher`] implementations this method provides better performance than multiple + /// calls to [`Watcher::watch`] and [`Watcher::unwatch`] if you want to add/remove many paths at once. + /// + /// # Errors + /// + /// Returns [`UpdatePathsError`] if any operation fails. Operations are applied sequentially. + /// When an error occurs, processing stops: operations before `origin` have been applied, + /// `origin` is the operation that failed (if known), and `remaining` are the operations that + /// were not attempted. `remaining` does not include `origin`. /// /// # Examples /// /// ``` - /// # use notify::{Watcher, RecursiveMode, Result}; - /// # use std::path::Path; - /// # fn main() -> Result<()> { - /// # let many_paths_to_add = vec![]; - /// let mut watcher = notify::recommended_watcher(|_event| { /* event handler */ })?; - /// let mut watcher_paths = watcher.paths_mut(); + /// # use notify::{Watcher, RecursiveMode, PathOp}; + /// # use std::path::{Path, PathBuf}; + /// # fn main() -> Result<(), Box> { + /// # let many_paths_to_add: Vec = vec![]; + /// # let many_paths_to_remove: Vec = vec![]; + /// let mut watcher = notify::NullWatcher; + /// let mut batch = Vec::new(); + /// /// for path in many_paths_to_add { - /// watcher_paths.add(path, RecursiveMode::Recursive)?; + /// batch.push(PathOp::watch_recursive(path)); + /// } + /// + /// for path in many_paths_to_remove { + /// batch.push(PathOp::unwatch(path)); /// } - /// watcher_paths.commit()?; + /// + /// // real work is done there + /// watcher.update_paths(batch)?; /// # Ok(()) /// # } /// ``` - fn paths_mut<'me>(&'me mut self) -> Box { - struct DefaultPathsMut<'a, T: ?Sized>(&'a mut T); - impl PathsMut for DefaultPathsMut<'_, T> { - fn add(&mut self, path: &Path, recursive_mode: RecursiveMode) -> Result<()> { - self.0.watch(path, recursive_mode) - } - fn remove(&mut self, path: &Path) -> Result<()> { - self.0.unwatch(path) - } - fn commit(self: Box) -> Result<()> { - Ok(()) - } - } - Box::new(DefaultPathsMut(self)) + fn update_paths(&mut self, ops: Vec) -> StdResult<(), UpdatePathsError> { + update_paths(ops, |op| match op { + PathOp::Watch(path, config) => self + .watch(&path, config.recursive_mode()) + .map_err(|e| (PathOp::Watch(path, config), e)), + PathOp::Unwatch(path) => self.unwatch(&path).map_err(|e| (PathOp::Unwatch(path), e)), + }) } /// Configure the watcher at runtime. @@ -459,10 +452,28 @@ where RecommendedWatcher::new(event_handler, Config::default()) } +pub(crate) fn update_paths(ops: Vec, mut apply: F) -> StdResult<(), UpdatePathsError> +where + F: FnMut(PathOp) -> StdResult<(), (PathOp, Error)>, +{ + let mut iter = ops.into_iter(); + while let Some(op) = iter.next() { + if let Err((error_op, source)) = apply(op) { + return Err(UpdatePathsError { + source, + origin: Some(error_op), + remaining: iter.collect(), + }); + } + } + Ok(()) +} + #[cfg(test)] mod tests { use std::{ fs, iter, + path::{Path, PathBuf}, sync::mpsc, time::{Duration, Instant}, }; @@ -470,8 +481,8 @@ mod tests { use tempfile::tempdir; use super::{ - Config, Error, ErrorKind, Event, NullWatcher, PollWatcher, RecommendedWatcher, - RecursiveMode, Result, Watcher, WatcherKind, + Config, Error, ErrorKind, Event, NullWatcher, PathOp, PollWatcher, RecommendedWatcher, + RecursiveMode, Result, StdResult, WatchPathConfig, Watcher, WatcherKind, }; use crate::test::*; @@ -545,7 +556,28 @@ mod tests { } #[test] - fn test_paths_mut() -> std::result::Result<(), Box> { + #[cfg(target_os = "windows")] + fn test_windows_trash_dir() -> std::result::Result<(), Box> { + use crate::recommended_watcher; + + let dir = tempdir()?; + let child_dir = dir.path().join("child"); + fs::create_dir(&child_dir)?; + + let mut watcher = recommended_watcher(|_| { + // Do something with the event + })?; + watcher.watch(&child_dir, RecursiveMode::NonRecursive)?; + + trash::delete(&child_dir)?; + + watcher.watch(dir.path(), RecursiveMode::NonRecursive)?; + + Ok(()) + } + + #[test] + fn test_update_paths() -> std::result::Result<(), Box> { let dir = tempdir()?; let dir_a = dir.path().join("a"); @@ -558,12 +590,16 @@ mod tests { let mut watcher = RecommendedWatcher::new(tx, Config::default())?; // start watching a and b - { - let mut watcher_paths = watcher.paths_mut(); - watcher_paths.add(&dir_a, RecursiveMode::Recursive)?; - watcher_paths.add(&dir_b, RecursiveMode::Recursive)?; - watcher_paths.commit()?; - } + watcher.update_paths(vec![ + PathOp::Watch( + dir_a.clone(), + WatchPathConfig::new(RecursiveMode::Recursive), + ), + PathOp::Watch( + dir_b.clone(), + WatchPathConfig::new(RecursiveMode::Recursive), + ), + ])?; // create file1 in both a and b let a_file1 = dir_a.join("file1"); @@ -589,11 +625,7 @@ mod tests { assert!(b_file1_encountered, "Did not receive event of {b_file1:?}"); // stop watching a - { - let mut watcher_paths = watcher.paths_mut(); - watcher_paths.remove(&dir_a)?; - watcher_paths.commit()?; - } + watcher.update_paths(vec![PathOp::unwatch(&dir_a)])?; // create file2 in both a and b let a_file2 = dir_a.join("file2"); @@ -616,6 +648,54 @@ mod tests { panic!("Did not receive the event of {b_file2:?}"); } + #[test] + fn update_paths_in_a_loop_with_errors() -> StdResult<(), Box> { + let dir = tempdir()?; + let existing_dir_1 = dir.path().join("existing_dir_1"); + let not_existent_file = dir.path().join("not_existent_file"); + let existing_dir_2 = dir.path().join("existing_dir_2"); + + fs::create_dir(&existing_dir_1)?; + fs::create_dir(&existing_dir_2)?; + + let mut paths_to_add = vec![ + PathOp::watch_recursive(existing_dir_1.clone()), + PathOp::watch_recursive(not_existent_file.clone()), + PathOp::watch_recursive(existing_dir_2.clone()), + ]; + + let (tx, rx) = std::sync::mpsc::channel(); + let mut watcher = RecommendedWatcher::new(tx, Config::default())?; + + while !paths_to_add.is_empty() { + if let Err(e) = watcher.update_paths(std::mem::take(&mut paths_to_add)) { + paths_to_add = e.remaining; + } + } + + fs::write(existing_dir_1.join("1"), "")?; + fs::write(¬_existent_file, "")?; + let waiting_path = existing_dir_2.join("1"); + fs::write(&waiting_path, "")?; + + for event in iter_with_timeout(&rx) { + let path = event + .paths + .first() + .unwrap_or_else(|| panic!("event must have a path: {event:?}")); + assert!( + path != ¬_existent_file, + "unexpected {:?} event", + not_existent_file + ); + if path == &waiting_path || path == &waiting_path.canonicalize()? { + return Ok(()); + } + } + + panic!("Did not receive the event of {waiting_path:?}"); + } + #[test] fn create_file() { let tmpdir = testdir(); @@ -710,4 +790,28 @@ mod tests { .expect("No event") .expect("Error"); } + + #[test] + fn update_paths_error_contains_errored_path() { + let err = super::update_paths( + [ + PathOp::unwatch("1"), + PathOp::unwatch("2"), + PathOp::unwatch("3"), + ] + .into(), + |op| { + if op.as_path() == Path::new("2") { + Err((op, super::Error::path_not_found())) + } else { + Ok(()) + } + }, + ) + .unwrap_err(); + assert_eq!( + &err.into_iter().map(PathOp::into_path).collect::>(), + &[PathBuf::from("2"), PathBuf::from("3"),] + ) + } }