1#![deny(missing_docs)]
187
188pub use config::{Config, PathOp, RecursiveMode, WatchPathConfig, WindowsPathSeparatorStyle};
189pub use error::{Error, ErrorKind, Result, UpdatePathsError};
190pub use notify_types::event::{self, Event, EventKind, EventKindMask};
191use std::path::{Path, PathBuf};
192
193pub(crate) type StdResult<T, E> = std::result::Result<T, E>;
194pub(crate) type Receiver<T> = std::sync::mpsc::Receiver<T>;
195pub(crate) type Sender<T> = std::sync::mpsc::Sender<T>;
196#[cfg(any(
197 target_os = "linux",
198 target_os = "android",
199 all(target_os = "freebsd", notify_freebsd_inotify),
200 target_os = "windows",
201))]
202pub(crate) type BoundSender<T> = std::sync::mpsc::SyncSender<T>;
203
204#[inline]
205pub(crate) fn unbounded<T>() -> (Sender<T>, Receiver<T>) {
206 std::sync::mpsc::channel()
207}
208
209#[cfg(any(
210 target_os = "linux",
211 target_os = "android",
212 all(target_os = "freebsd", notify_freebsd_inotify),
213 target_os = "windows",
214))]
215#[inline]
216pub(crate) fn bounded<T>(cap: usize) -> (BoundSender<T>, Receiver<T>) {
217 std::sync::mpsc::sync_channel(cap)
218}
219
220#[cfg(all(target_os = "macos", not(feature = "macos_kqueue")))]
221pub use crate::fsevent::FsEventWatcher;
222#[cfg(any(
223 target_os = "linux",
224 target_os = "android",
225 all(target_os = "freebsd", notify_freebsd_inotify),
226))]
227pub use crate::inotify::INotifyWatcher;
228#[cfg(any(
229 target_os = "freebsd",
230 target_os = "openbsd",
231 target_os = "netbsd",
232 target_os = "dragonfly",
233 target_os = "ios",
234 all(target_os = "macos", feature = "macos_kqueue")
235))]
236pub use crate::kqueue::KqueueWatcher;
237pub use null::NullWatcher;
238pub use poll::PollWatcher;
239#[cfg(target_os = "windows")]
240pub use windows::ReadDirectoryChangesWatcher;
241
242#[cfg(all(target_os = "macos", not(feature = "macos_kqueue")))]
243pub mod fsevent;
244#[cfg(any(
245 target_os = "linux",
246 target_os = "android",
247 all(target_os = "freebsd", notify_freebsd_inotify),
248))]
249pub mod inotify;
250#[cfg(any(
251 target_os = "freebsd",
252 target_os = "openbsd",
253 target_os = "dragonfly",
254 target_os = "netbsd",
255 target_os = "ios",
256 all(target_os = "macos", feature = "macos_kqueue")
257))]
258pub mod kqueue;
259#[cfg(target_os = "windows")]
260pub mod windows;
261
262pub mod null;
263pub mod poll;
264
265mod config;
266mod error;
267mod paths;
268
269#[cfg(test)]
270pub(crate) mod test;
271
272pub trait EventHandler: Send + 'static {
291 fn handle_event(&mut self, event: Result<Event>);
293}
294
295impl<F> EventHandler for F
296where
297 F: FnMut(Result<Event>) + Send + 'static,
298{
299 fn handle_event(&mut self, event: Result<Event>) {
300 (self)(event);
301 }
302}
303
304#[cfg(feature = "crossbeam-channel")]
305impl EventHandler for crossbeam_channel::Sender<Result<Event>> {
306 fn handle_event(&mut self, event: Result<Event>) {
307 let _ = self.send(event);
308 }
309}
310
311#[cfg(feature = "flume")]
312impl EventHandler for flume::Sender<Result<Event>> {
313 fn handle_event(&mut self, event: Result<Event>) {
314 let _ = self.send(event);
315 }
316}
317
318#[cfg(feature = "futures")]
319impl EventHandler for futures::channel::mpsc::UnboundedSender<Result<Event>> {
320 fn handle_event(&mut self, event: Result<Event>) {
321 let _ = self.unbounded_send(event);
322 }
323}
324
325#[cfg(feature = "tokio")]
326impl EventHandler for tokio::sync::mpsc::UnboundedSender<Result<Event>> {
327 fn handle_event(&mut self, event: Result<Event>) {
328 let _ = self.send(event);
329 }
330}
331
332impl EventHandler for std::sync::mpsc::Sender<Result<Event>> {
333 fn handle_event(&mut self, event: Result<Event>) {
334 let _ = self.send(event);
335 }
336}
337
338#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
340#[non_exhaustive]
341pub enum WatcherKind {
342 Inotify,
344 Fsevent,
346 Kqueue,
348 PollWatcher,
350 ReadDirectoryChangesWatcher,
352 NullWatcher,
354}
355
356pub trait Watcher {
380 fn new<F: EventHandler>(event_handler: F, config: config::Config) -> Result<Self>
382 where
383 Self: Sized;
384 fn watch(&mut self, path: &Path, recursive_mode: RecursiveMode) -> Result<()>;
413
414 fn watch_with(&mut self, path: &Path, config: WatchPathConfig) -> Result<()> {
424 self.watch(path, config.recursive_mode())
425 }
426
427 fn unwatch(&mut self, path: &Path) -> Result<()>;
434
435 fn update_paths(&mut self, ops: Vec<PathOp>) -> StdResult<(), UpdatePathsError> {
473 update_paths(ops, |op| match op {
474 PathOp::Watch(path, config) => self
475 .watch_with(&path, config.clone())
476 .map_err(|e| (PathOp::Watch(path, config), e)),
477 PathOp::Unwatch(path) => self.unwatch(&path).map_err(|e| (PathOp::Unwatch(path), e)),
478 })
479 }
480
481 fn configure(&mut self, _option: Config) -> Result<bool> {
491 Ok(false)
492 }
493
494 fn watched_paths(&self) -> Result<Vec<(PathBuf, RecursiveMode)>> {
503 Err(Error::generic(
504 "listing watched paths is not supported by this watcher",
505 ))
506 }
507
508 fn kind() -> WatcherKind
510 where
511 Self: Sized;
512}
513
514#[cfg(any(
516 target_os = "linux",
517 target_os = "android",
518 all(target_os = "freebsd", notify_freebsd_inotify),
519))]
520pub type RecommendedWatcher = INotifyWatcher;
521#[cfg(all(target_os = "macos", not(feature = "macos_kqueue")))]
523pub type RecommendedWatcher = FsEventWatcher;
524#[cfg(target_os = "windows")]
526pub type RecommendedWatcher = ReadDirectoryChangesWatcher;
527#[cfg(any(
529 all(target_os = "freebsd", not(notify_freebsd_inotify)),
530 target_os = "openbsd",
531 target_os = "netbsd",
532 target_os = "dragonfly",
533 target_os = "ios",
534 all(target_os = "macos", feature = "macos_kqueue")
535))]
536pub type RecommendedWatcher = KqueueWatcher;
537#[cfg(not(any(
539 target_os = "linux",
540 target_os = "android",
541 target_os = "macos",
542 target_os = "windows",
543 target_os = "freebsd",
544 target_os = "openbsd",
545 target_os = "netbsd",
546 target_os = "dragonfly",
547 target_os = "ios"
548)))]
549pub type RecommendedWatcher = PollWatcher;
550
551pub fn recommended_watcher<F>(event_handler: F) -> Result<RecommendedWatcher>
567where
568 F: EventHandler,
569{
570 RecommendedWatcher::new(event_handler, Config::default())
572}
573
574pub(crate) fn update_paths<F>(ops: Vec<PathOp>, mut apply: F) -> StdResult<(), UpdatePathsError>
575where
576 F: FnMut(PathOp) -> StdResult<(), (PathOp, Error)>,
577{
578 let mut iter = ops.into_iter();
579 while let Some(op) = iter.next() {
580 if let Err((error_op, source)) = apply(op) {
581 return Err(UpdatePathsError {
582 source,
583 origin: Some(error_op),
584 remaining: iter.collect(),
585 });
586 }
587 }
588 Ok(())
589}
590
591#[cfg(test)]
592mod tests {
593 use std::{
594 collections::HashSet,
595 fs, iter,
596 path::{Path, PathBuf},
597 sync::mpsc,
598 time::{Duration, Instant},
599 };
600
601 use tempfile::tempdir;
602
603 use super::{
604 Config, Error, ErrorKind, Event, NullWatcher, PathOp, PollWatcher, RecommendedWatcher,
605 RecursiveMode, Result, StdResult, WatchPathConfig, Watcher, WatcherKind,
606 };
607 use crate::test::*;
608
609 #[test]
610 fn test_object_safe() {
611 let _watcher: &dyn Watcher = &NullWatcher;
612 }
613
614 #[test]
615 fn test_debug_impl() {
616 macro_rules! assert_debug_impl {
617 ($t:ty) => {{
618 #[allow(dead_code)]
619 trait NeedsDebug: std::fmt::Debug {}
620 impl NeedsDebug for $t {}
621 }};
622 }
623
624 assert_debug_impl!(Config);
625 assert_debug_impl!(Error);
626 assert_debug_impl!(ErrorKind);
627 assert_debug_impl!(NullWatcher);
628 assert_debug_impl!(PollWatcher);
629 assert_debug_impl!(RecommendedWatcher);
630 assert_debug_impl!(RecursiveMode);
631 assert_debug_impl!(WatchPathConfig);
632 assert_debug_impl!(WatcherKind);
633 }
634
635 include!("freebsd_version.rs");
636
637 fn freebsd_inotify_supported(output: &str) -> bool {
638 parse_freebsd_release(output).is_some_and(|release| release >= FREEBSD_INOTIFY_MIN)
639 }
640
641 #[test]
642 fn freebsd_inotify_release_threshold() {
643 for (version, expected) in [
644 ("13.5-RELEASE", false),
645 ("14.4-RELEASE", false),
646 ("14.4-RELEASE-p3", false),
647 ("14.4-STABLE", false),
648 ("14.5-BETA1", true),
649 ("14.5-RELEASE", true),
650 ("14.5-RELEASE-p1", true),
651 ("14.5-STABLE", true),
652 ("14.6-RELEASE", true),
653 ("15.0-RELEASE", true),
654 ("15.1-RELEASE", true),
655 ("16.0-CURRENT", true),
656 ] {
657 assert_eq!(freebsd_inotify_supported(version), expected, "{version}");
658 }
659 assert_eq!(parse_freebsd_release(""), None);
660 assert_eq!(parse_freebsd_release("14"), None);
661 assert_eq!(parse_freebsd_release("14."), None);
662 }
663
664 #[cfg(target_os = "freebsd")]
665 #[test]
666 fn recommended_watcher_matches_freebsd_version() {
667 let output = std::process::Command::new("/bin/freebsd-version")
668 .arg("-u")
669 .output()
670 .expect("run freebsd-version");
671 assert!(output.status.success(), "freebsd-version failed");
672
673 let version = std::str::from_utf8(&output.stdout).expect("parse freebsd-version output");
674 let expected = if cfg!(feature = "freebsd_inotify") || freebsd_inotify_supported(version) {
675 WatcherKind::Inotify
676 } else {
677 WatcherKind::Kqueue
678 };
679
680 assert_eq!(RecommendedWatcher::kind(), expected);
681 }
682
683 fn iter_with_timeout(rx: &mpsc::Receiver<Result<Event>>) -> impl Iterator<Item = Event> + '_ {
684 let deadline = Instant::now() + Duration::from_secs(10);
686 iter::from_fn(move || {
687 if Instant::now() >= deadline {
688 return None;
689 }
690 Some(
691 rx.recv_timeout(deadline - Instant::now())
692 .expect("did not receive expected event")
693 .expect("received an error"),
694 )
695 })
696 }
697
698 fn canonical_or_path(path: &Path) -> PathBuf {
699 path.canonicalize()
700 .expect("test paths should always be canonicalizable")
701 }
702
703 fn canonical_watch_set(
704 paths: Vec<(PathBuf, RecursiveMode)>,
705 ) -> HashSet<(PathBuf, RecursiveMode)> {
706 paths
707 .into_iter()
708 .map(|(path, recursive_mode)| (canonical_or_path(&path), recursive_mode))
709 .collect()
710 }
711
712 fn matches_path(path: &Path, expected: &Path, canonical_expected: Option<&PathBuf>) -> bool {
713 path == expected || canonical_expected.is_some_and(|canonical| path == canonical)
714 }
715
716 fn watch_with_retry(
717 watcher: &mut RecommendedWatcher,
718 path: impl AsRef<Path>,
719 recursive_mode: RecursiveMode,
720 ) -> Result<()> {
721 const FSEVENT_WATCH_RETRIES: usize = 5;
722 const FSEVENT_WATCH_RETRY_BASE_DELAY: Duration = Duration::from_millis(50);
723
724 let path = path.as_ref();
725 for attempt in 0..=FSEVENT_WATCH_RETRIES {
726 match watcher.watch(path, recursive_mode) {
727 Ok(()) => return Ok(()),
728 Err(err)
729 if RecommendedWatcher::kind() == WatcherKind::Fsevent
730 && matches!(
731 &err.kind,
732 ErrorKind::Generic(message)
733 if message == "unable to start FSEvent stream"
734 )
735 && attempt < FSEVENT_WATCH_RETRIES =>
736 {
737 let _ = watcher.unwatch(path);
738 let delay_factor = 1u32 << attempt;
739 std::thread::sleep(FSEVENT_WATCH_RETRY_BASE_DELAY * delay_factor);
740 }
741 Err(err) => return Err(err),
742 }
743 }
744
745 unreachable!("watch() retries must return or error")
746 }
747
748 fn update_paths_unwatch_with_retry(
749 watcher: &mut RecommendedWatcher,
750 path: &Path,
751 ) -> Result<()> {
752 const FSEVENT_UNWATCH_RETRIES: usize = 5;
753 const FSEVENT_UNWATCH_RETRY_BASE_DELAY: Duration = Duration::from_millis(50);
754
755 for attempt in 0..=FSEVENT_UNWATCH_RETRIES {
756 match watcher.update_paths(vec![PathOp::unwatch(path)]) {
757 Ok(()) => return Ok(()),
758 Err(err)
759 if RecommendedWatcher::kind() == WatcherKind::Fsevent
760 && matches!(
761 &err.source.kind,
762 ErrorKind::Io(io_err) if io_err.raw_os_error() == Some(9)
763 )
764 && attempt < FSEVENT_UNWATCH_RETRIES =>
765 {
766 let delay_factor = 1u32 << attempt;
767 std::thread::sleep(FSEVENT_UNWATCH_RETRY_BASE_DELAY * delay_factor);
768 }
769 Err(err) => return Err(err.into()),
770 }
771 }
772
773 unreachable!("fsevent unwatch retries must return or error")
774 }
775
776 #[test]
777 fn integration() -> std::result::Result<(), Box<dyn std::error::Error>> {
778 let dir = tempdir()?;
779
780 let (tx, rx) = std::sync::mpsc::channel();
782 let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
783 watch_with_retry(&mut watcher, dir.path(), RecursiveMode::Recursive)?;
784
785 let file_path = dir.path().join("file.txt");
787 fs::write(&file_path, b"Lorem ipsum")?;
788
789 println!("waiting for event at {}", file_path.display());
790
791 for event in iter_with_timeout(&rx) {
793 if event.paths == vec![file_path.clone()]
794 || event.paths == vec![file_path.canonicalize()?]
795 {
796 return Ok(());
797 }
798
799 println!("unexpected event: {event:?}");
800 }
801
802 panic!("did not receive expected event");
803 }
804
805 #[test]
806 fn event_paths_preserve_relative_watch_root()
807 -> std::result::Result<(), Box<dyn std::error::Error>> {
808 let cwd = std::env::current_dir()?;
809 let dir = tempfile::Builder::new()
810 .prefix("notify-relative-")
811 .tempdir_in(&cwd)?;
812 let relative_dir = dir.path().strip_prefix(&cwd)?.to_path_buf();
813 let relative_file = relative_dir.join("file.txt");
814 let absolute_file = dir.path().join("file.txt");
815
816 let (tx, rx) = std::sync::mpsc::channel();
817 let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
818 watch_with_retry(&mut watcher, &relative_dir, RecursiveMode::Recursive)?;
819
820 assert!(
821 watcher
822 .watched_paths()?
823 .iter()
824 .any(|(path, mode)| path == &relative_dir && *mode == RecursiveMode::Recursive),
825 "watched_paths() did not preserve relative watch path"
826 );
827
828 fs::write(&absolute_file, b"Lorem ipsum")?;
829
830 for event in iter_with_timeout(&rx) {
831 if event.paths.iter().any(|path| path == &relative_file) {
832 assert!(
833 event.paths.iter().all(|path| path.is_relative()),
834 "relative watch emitted absolute path: {event:?}"
835 );
836 return Ok(());
837 }
838
839 println!("unexpected event: {event:?}");
840 }
841
842 panic!("did not receive expected relative event path");
843 }
844
845 #[test]
846 #[cfg(target_os = "windows")]
847 fn test_windows_trash_dir() -> std::result::Result<(), Box<dyn std::error::Error>> {
848 use crate::recommended_watcher;
849
850 let dir = tempdir()?;
851 let child_dir = dir.path().join("child");
852 fs::create_dir(&child_dir)?;
853
854 let mut watcher = recommended_watcher(|_| {
855 })?;
857 watcher.watch(&child_dir, RecursiveMode::NonRecursive)?;
858
859 trash::delete(&child_dir)?;
860
861 watcher.watch(dir.path(), RecursiveMode::NonRecursive)?;
862
863 Ok(())
864 }
865
866 #[test]
867 fn test_update_paths() -> std::result::Result<(), Box<dyn std::error::Error>> {
868 let dir = tempdir()?;
869
870 let dir_a = dir.path().join("a");
871 let dir_b = dir.path().join("b");
872
873 fs::create_dir(&dir_a)?;
874 fs::create_dir(&dir_b)?;
875
876 let (tx, rx) = std::sync::mpsc::channel();
877 let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
878
879 const FSEVENT_UPDATE_PATHS_RETRIES: usize = 5;
881 const FSEVENT_UPDATE_PATHS_RETRY_BASE_DELAY: Duration = Duration::from_millis(50);
882 for attempt in 0..=FSEVENT_UPDATE_PATHS_RETRIES {
883 match watcher.update_paths(vec![
884 PathOp::Watch(
885 dir_a.clone(),
886 WatchPathConfig::new(RecursiveMode::Recursive),
887 ),
888 PathOp::Watch(
889 dir_b.clone(),
890 WatchPathConfig::new(RecursiveMode::Recursive),
891 ),
892 ]) {
893 Ok(()) => break,
894 Err(err)
895 if RecommendedWatcher::kind() == WatcherKind::Fsevent
896 && matches!(
897 &err.source.kind,
898 ErrorKind::Generic(message)
899 if message == "unable to start FSEvent stream"
900 )
901 && attempt < FSEVENT_UPDATE_PATHS_RETRIES =>
902 {
903 let delay_factor = 1u32 << attempt;
904 std::thread::sleep(FSEVENT_UPDATE_PATHS_RETRY_BASE_DELAY * delay_factor);
905 }
906 Err(err) => return Err(err.into()),
907 }
908 }
909
910 let a_file1 = dir_a.join("file1");
912 let b_file1 = dir_b.join("file1");
913 fs::write(&a_file1, b"Lorem ipsum")?;
914 fs::write(&b_file1, b"Lorem ipsum")?;
915 let a_file1_canonical = a_file1.canonicalize().ok();
916 let b_file1_canonical = b_file1.canonicalize().ok();
917
918 let mut a_file1_encountered: bool = false;
920 let mut b_file1_encountered: bool = false;
921 for event in iter_with_timeout(&rx) {
922 for path in event.paths {
923 a_file1_encountered = a_file1_encountered
924 || matches_path(&path, &a_file1, a_file1_canonical.as_ref());
925 b_file1_encountered = b_file1_encountered
926 || matches_path(&path, &b_file1, b_file1_canonical.as_ref());
927 }
928 if a_file1_encountered && b_file1_encountered {
929 break;
930 }
931 }
932 assert!(a_file1_encountered, "Did not receive event of {a_file1:?}");
933 assert!(b_file1_encountered, "Did not receive event of {b_file1:?}");
934
935 update_paths_unwatch_with_retry(&mut watcher, &dir_a)?;
937
938 let a_file2 = dir_a.join("file2");
940 let b_file2 = dir_b.join("file2");
941 fs::write(&a_file2, b"Lorem ipsum")?;
942 fs::write(&b_file2, b"Lorem ipsum")?;
943 let a_file2_canonical = a_file2.canonicalize().ok();
944 let b_file2_canonical = b_file2.canonicalize().ok();
945
946 for event in iter_with_timeout(&rx) {
948 for path in event.paths {
949 assert!(
950 !matches_path(&path, &a_file2, a_file2_canonical.as_ref()),
951 "Event of {a_file2:?} should not be received"
952 );
953 if matches_path(&path, &b_file2, b_file2_canonical.as_ref()) {
954 return Ok(());
955 }
956 }
957 }
958 panic!("Did not receive the event of {b_file2:?}");
959 }
960
961 #[test]
962 fn watched_paths_reflect_watch_and_unwatch()
963 -> std::result::Result<(), Box<dyn std::error::Error>> {
964 let dir = tempdir()?;
965 let dir_a = dir.path().join("a");
966 let dir_b = dir.path().join("b");
967 fs::create_dir(&dir_a)?;
968 fs::create_dir(&dir_b)?;
969
970 let (tx, _rx) = std::sync::mpsc::channel();
971 let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
972
973 watch_with_retry(&mut watcher, &dir_a, RecursiveMode::Recursive)?;
974 watch_with_retry(&mut watcher, &dir_b, RecursiveMode::NonRecursive)?;
975
976 let watched = canonical_watch_set(watcher.watched_paths()?);
977 assert!(watched.contains(&(canonical_or_path(&dir_a), RecursiveMode::Recursive)));
978 assert!(watched.contains(&(canonical_or_path(&dir_b), RecursiveMode::NonRecursive)));
979
980 watcher.unwatch(&dir_a)?;
981
982 let watched = canonical_watch_set(watcher.watched_paths()?);
983 assert!(!watched.contains(&(canonical_or_path(&dir_a), RecursiveMode::Recursive)));
984 assert!(watched.contains(&(canonical_or_path(&dir_b), RecursiveMode::NonRecursive)));
985
986 Ok(())
987 }
988
989 #[test]
990 fn rewatching_same_path_replaces_recursive_mode()
991 -> std::result::Result<(), Box<dyn std::error::Error>> {
992 let dir = tempdir()?;
993 let root = canonical_or_path(dir.path());
994
995 let (tx, _rx) = std::sync::mpsc::channel();
996 let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
997
998 watch_with_retry(&mut watcher, dir.path(), RecursiveMode::Recursive)?;
999 watch_with_retry(&mut watcher, dir.path(), RecursiveMode::NonRecursive)?;
1000
1001 let watched = canonical_watch_set(watcher.watched_paths()?);
1002 assert!(watched.contains(&(root.clone(), RecursiveMode::NonRecursive)));
1003 assert!(!watched.contains(&(root.clone(), RecursiveMode::Recursive)));
1004 assert_eq!(
1005 watched.iter().filter(|(path, _mode)| path == &root).count(),
1006 1
1007 );
1008
1009 watcher.unwatch(dir.path())?;
1010
1011 let watched = canonical_watch_set(watcher.watched_paths()?);
1012 assert!(!watched.iter().any(|(path, _mode)| path == &root));
1013
1014 Ok(())
1015 }
1016
1017 #[test]
1018 fn overlapping_recursive_watch_preserves_explicit_child()
1019 -> std::result::Result<(), Box<dyn std::error::Error>> {
1020 let dir = tempdir()?;
1021 let child = dir.path().join("child");
1022 fs::create_dir(&child)?;
1023
1024 let (tx, _rx) = std::sync::mpsc::channel();
1025 let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
1026
1027 watch_with_retry(&mut watcher, &child, RecursiveMode::NonRecursive)?;
1028 watch_with_retry(&mut watcher, dir.path(), RecursiveMode::Recursive)?;
1029
1030 let watched = canonical_watch_set(watcher.watched_paths()?);
1031 assert!(watched.contains(&(canonical_or_path(dir.path()), RecursiveMode::Recursive)));
1032 assert!(watched.contains(&(canonical_or_path(&child), RecursiveMode::NonRecursive)));
1033
1034 watcher.unwatch(dir.path())?;
1035
1036 let watched = canonical_watch_set(watcher.watched_paths()?);
1037 assert!(!watched.contains(&(canonical_or_path(dir.path()), RecursiveMode::Recursive)));
1038 assert!(watched.contains(&(canonical_or_path(&child), RecursiveMode::NonRecursive)));
1039
1040 Ok(())
1041 }
1042
1043 #[test]
1044 fn overlapping_recursive_child_rewrites_descendant_event_paths()
1045 -> std::result::Result<(), Box<dyn std::error::Error>> {
1046 let cwd = std::env::current_dir()?;
1047 let dir = tempfile::Builder::new()
1048 .prefix("notify-overlap-")
1049 .tempdir_in(&cwd)?;
1050 let relative_dir = dir.path().strip_prefix(&cwd)?.to_path_buf();
1051 let child = dir.path().join("child");
1052 let grandchild = child.join("grandchild");
1053 fs::create_dir_all(&grandchild)?;
1054
1055 let (tx, rx) = std::sync::mpsc::channel();
1056 let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
1057
1058 watch_with_retry(&mut watcher, &relative_dir, RecursiveMode::Recursive)?;
1059 watch_with_retry(&mut watcher, &child, RecursiveMode::Recursive)?;
1060 watcher.unwatch(&relative_dir)?;
1061
1062 let watched = watcher.watched_paths()?;
1063 assert!(
1064 watched
1065 .iter()
1066 .any(|(path, mode)| path == &child && *mode == RecursiveMode::Recursive),
1067 "watched_paths() did not preserve explicit child path: {watched:?}"
1068 );
1069
1070 let file = grandchild.join("file.txt");
1071 let stale_file = relative_dir
1072 .join("child")
1073 .join("grandchild")
1074 .join("file.txt");
1075 fs::write(&file, b"Lorem ipsum")?;
1076
1077 for event in iter_with_timeout(&rx) {
1078 if event.paths.iter().any(|path| path == &file) {
1079 assert!(
1080 event.paths.iter().all(|path| path.is_absolute()),
1081 "absolute child watch emitted non-absolute path: {event:?}"
1082 );
1083 return Ok(());
1084 }
1085
1086 assert!(
1087 !event.paths.iter().any(|path| path == &stale_file),
1088 "stale parent-relative file path after parent unwatch: {event:?}"
1089 );
1090 }
1091
1092 panic!("did not receive expected child event path");
1093 }
1094
1095 #[test]
1096 fn update_paths_in_a_loop_with_errors() -> StdResult<(), Box<dyn std::error::Error>> {
1097 let dir = tempdir()?;
1098 let existing_dir_1 = dir.path().join("existing_dir_1");
1099 let not_existent_file = dir.path().join("not_existent_file");
1100 let existing_dir_2 = dir.path().join("existing_dir_2");
1101
1102 fs::create_dir(&existing_dir_1)?;
1103 fs::create_dir(&existing_dir_2)?;
1104
1105 let mut paths_to_add = vec![
1106 PathOp::watch_recursive(existing_dir_1.clone()),
1107 PathOp::watch_recursive(not_existent_file.clone()),
1108 PathOp::watch_recursive(existing_dir_2.clone()),
1109 ];
1110
1111 let (tx, rx) = std::sync::mpsc::channel();
1112 let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
1113
1114 while !paths_to_add.is_empty() {
1115 if let Err(e) = watcher.update_paths(std::mem::take(&mut paths_to_add)) {
1116 paths_to_add = e.remaining;
1117 }
1118 }
1119
1120 fs::write(existing_dir_1.join("1"), "")?;
1121 fs::write(¬_existent_file, "")?;
1122 let waiting_path = existing_dir_2.join("1");
1123 fs::write(&waiting_path, "")?;
1124 let waiting_path_canonical = waiting_path.canonicalize().ok();
1125
1126 for event in iter_with_timeout(&rx) {
1127 let path = event
1128 .paths
1129 .first()
1130 .unwrap_or_else(|| panic!("event must have a path: {event:?}"));
1131 assert!(
1132 path != ¬_existent_file,
1133 "unexpected {not_existent_file:?} event"
1134 );
1135 if matches_path(path, &waiting_path, waiting_path_canonical.as_ref()) {
1136 return Ok(());
1137 }
1138 }
1139
1140 panic!("Did not receive the event of {waiting_path:?}");
1141 }
1142
1143 #[test]
1144 fn create_file() {
1145 let tmpdir = testdir();
1146 let (mut watcher, mut rx) = recommended_channel();
1147 watcher.watch_recursively(&tmpdir);
1148
1149 let path = tmpdir.path().join("entry");
1150 std::fs::File::create_new(&path).expect("create");
1151
1152 rx.wait_unordered([expected(path).create()]);
1153 }
1154
1155 #[test]
1156 fn create_dir() {
1157 let tmpdir = testdir();
1158 let (mut watcher, mut rx) = recommended_channel();
1159 watcher.watch_recursively(&tmpdir);
1160
1161 let path = tmpdir.path().join("entry");
1162 std::fs::create_dir(&path).expect("create");
1163
1164 rx.wait_unordered([expected(path).create()]);
1165 }
1166
1167 #[test]
1168 fn modify_file() {
1169 let tmpdir = testdir();
1170 let (mut watcher, mut rx) = recommended_channel();
1171
1172 let path = tmpdir.path().join("entry");
1173 std::fs::File::create_new(&path).expect("create");
1174
1175 watcher.watch_recursively(&tmpdir);
1176 std::fs::write(&path, b"123").expect("write");
1177
1178 rx.wait_unordered([expected(path).modify()]);
1179 }
1180
1181 #[test]
1182 fn remove_file() {
1183 let tmpdir = testdir();
1184 let (mut watcher, mut rx) = recommended_channel();
1185
1186 let path = tmpdir.path().join("entry");
1187 std::fs::File::create_new(&path).expect("create");
1188
1189 watcher.watch_recursively(&tmpdir);
1190 std::fs::remove_file(&path).expect("remove");
1191
1192 rx.wait_unordered([expected(path).remove()]);
1193 }
1194
1195 #[cfg(feature = "futures")]
1196 #[tokio::test]
1197 async fn futures_unbounded_sender_as_handler() {
1198 use crate::recommended_watcher;
1199 use futures::StreamExt;
1200
1201 let tmpdir = testdir();
1202 let (tx, mut rx) = futures::channel::mpsc::unbounded();
1203 let mut watcher = recommended_watcher(tx).unwrap();
1204 watcher
1205 .watch(tmpdir.path(), RecursiveMode::NonRecursive)
1206 .unwrap();
1207
1208 std::fs::create_dir(tmpdir.path().join("1")).unwrap();
1209
1210 tokio::time::timeout(Duration::from_secs(5), rx.next())
1211 .await
1212 .expect("timeout")
1213 .expect("No event")
1214 .expect("Error");
1215 }
1216
1217 #[cfg(feature = "tokio")]
1218 #[tokio::test]
1219 async fn tokio_unbounded_sender_as_handler() {
1220 use crate::recommended_watcher;
1221
1222 let tmpdir = testdir();
1223 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
1224 let mut watcher = recommended_watcher(tx).unwrap();
1225 watcher
1226 .watch(tmpdir.path(), RecursiveMode::NonRecursive)
1227 .unwrap();
1228
1229 std::fs::create_dir(tmpdir.path().join("1")).unwrap();
1230
1231 tokio::time::timeout(Duration::from_secs(5), rx.recv())
1232 .await
1233 .expect("timeout")
1234 .expect("No event")
1235 .expect("Error");
1236 }
1237
1238 #[test]
1239 fn update_paths_error_contains_errored_path() {
1240 let err = super::update_paths(
1241 [
1242 PathOp::unwatch("1"),
1243 PathOp::unwatch("2"),
1244 PathOp::unwatch("3"),
1245 ]
1246 .into(),
1247 |op| {
1248 if op.as_path() == Path::new("2") {
1249 Err((op, super::Error::path_not_found()))
1250 } else {
1251 Ok(())
1252 }
1253 },
1254 )
1255 .unwrap_err();
1256 assert_eq!(
1257 &err.into_iter().map(PathOp::into_path).collect::<Vec<_>>(),
1258 &[PathBuf::from("2"), PathBuf::from("3"),]
1259 )
1260 }
1261}