1use std::collections::{HashMap, HashSet};
141use std::ffi::CString;
142use std::fs;
143use std::io::{BufRead, BufReader, Result};
144use std::os::unix::ffi::OsStrExt;
145use std::os::unix::io::AsRawFd;
146use std::path::{Path, PathBuf};
147use std::time::Duration;
148use tokio::io::unix::AsyncFd;
149
150#[derive(Debug, Clone)]
155struct GlobPattern {
156 pattern: String,
157}
158
159impl PartialEq for GlobPattern {
160 fn eq(&self, other: &Self) -> bool {
161 self.pattern == other.pattern
162 }
163}
164
165impl GlobPattern {
166 fn new(pattern: &str) -> Self {
167 Self {
168 pattern: pattern.to_string(),
169 }
170 }
171
172 fn matches(&self, text: &str) -> bool {
173 Self::match_recursive(self.pattern.as_bytes(), text.as_bytes())
174 }
175
176 fn match_recursive(pattern: &[u8], text: &[u8]) -> bool {
177 let mut p = 0;
178 let mut t = 0;
179
180 let mut star_p = None;
182 let mut star_t = None;
183
184 while t < text.len() {
185 if p < pattern.len() {
186 match pattern[p] {
187 b'*' => {
188 star_p = Some(p);
190 star_t = Some(t);
191 p += 1;
192 continue;
193 }
194 b'?' => {
195 p += 1;
197 t += 1;
198 continue;
199 }
200 b'[' => {
201 if let Some((matched, end_pos)) =
203 Self::match_char_class(&pattern[p..], text[t])
204 {
205 if matched {
206 p += end_pos;
207 t += 1;
208 continue;
209 }
210 }
211 }
213 c => {
214 if c == text[t] {
216 p += 1;
217 t += 1;
218 continue;
219 }
220 }
222 }
223 }
224
225 if let (Some(sp), Some(st)) = (star_p, star_t) {
227 p = sp + 1;
229 star_t = Some(st + 1);
230 t = st + 1;
231 } else {
232 return false;
233 }
234 }
235
236 while p < pattern.len() && pattern[p] == b'*' {
238 p += 1;
239 }
240
241 p == pattern.len()
242 }
243
244 fn match_char_class(pattern: &[u8], ch: u8) -> Option<(bool, usize)> {
247 if pattern.is_empty() || pattern[0] != b'[' {
248 return None;
249 }
250
251 let mut i = 1;
252 let mut matched = false;
253 let negated = i < pattern.len() && (pattern[i] == b'!' || pattern[i] == b'^');
254 if negated {
255 i += 1;
256 }
257
258 while i < pattern.len() {
259 if pattern[i] == b']' && i > 1 + (negated as usize) {
260 return Some((matched != negated, i + 1));
262 }
263
264 if i + 2 < pattern.len() && pattern[i + 1] == b'-' && pattern[i + 2] != b']' {
266 let start = pattern[i];
267 let end = pattern[i + 2];
268 if ch >= start && ch <= end {
269 matched = true;
270 }
271 i += 3;
272 } else {
273 if pattern[i] == ch {
275 matched = true;
276 }
277 i += 1;
278 }
279 }
280
281 None
283 }
284}
285
286#[derive(Debug, Clone, PartialEq)]
287enum Segment {
288 Exact(String),
289 Wildcard(GlobPattern),
290 DoubleWildcard, }
292
293#[derive(Debug, Clone)]
294struct Pattern {
295 segments: Vec<Segment>,
296}
297
298impl Pattern {
299 fn parse(pattern: &str) -> Self {
300 let mut segments = Vec::new();
301
302 let effective_pattern = if !pattern.contains('/') {
304 format!("**/{}", pattern)
305 } else {
306 pattern.trim_start_matches('/').to_string()
307 };
308
309 let normalized = effective_pattern.replace("//", "/");
310
311 for part in normalized.split('/') {
312 if part.is_empty() || part == "." {
313 continue;
314 }
315
316 if part == "**" {
317 segments.push(Segment::DoubleWildcard);
318 } else if part.contains('*') || part.contains('?') || part.contains('[') {
319 segments.push(Segment::Wildcard(GlobPattern::new(part)));
320 } else {
321 segments.push(Segment::Exact(part.to_string()));
322 }
323 }
324
325 Pattern { segments }
326 }
327
328 fn check(&self, path_segments: &[String], allow_prefix: bool) -> bool {
329 let pattern_segments = &self.segments;
330 let mut path_index = 0;
331
332 for pattern_index in 0..pattern_segments.len() {
333 let pattern_segment = &pattern_segments[pattern_index];
334
335 if path_index >= path_segments.len() {
336 if pattern_segment == &Segment::DoubleWildcard && pattern_index == pattern_segments.len() - 1
338 {
339 return true;
341 }
342 return allow_prefix;
344 }
345
346 match &pattern_segment {
347 Segment::Exact(s) => {
348 if s != &path_segments[path_index] {
349 return false;
350 }
351 path_index += 1;
352 }
353 Segment::Wildcard(p) => {
354 if !p.matches(&path_segments[path_index]) {
355 return false;
356 }
357 path_index += 1;
358 }
359 Segment::DoubleWildcard => {
360 if allow_prefix {
361 return true;
364 }
365
366 let patterns_left = pattern_segments.len() - (pattern_index + 1);
367 let next_path_index = path_segments.len() - patterns_left;
368 if next_path_index < path_index {
369 return false;
370 }
371 path_index = next_path_index;
372 }
373 }
374 }
375
376 if path_index < path_segments.len() {
378 return false;
379 }
380
381 return !allow_prefix;
384 }
385}
386
387struct Inotify {
390 fd: AsyncFd<i32>,
391}
392
393impl Inotify {
394 fn new() -> Result<Self> {
395 let fd = unsafe { libc::inotify_init1(libc::IN_NONBLOCK | libc::IN_CLOEXEC) };
396 if fd < 0 {
397 return Err(std::io::Error::last_os_error());
398 }
399 Ok(Self {
400 fd: AsyncFd::new(fd)?,
401 })
402 }
403
404 fn add_watch(&self, path: &Path, mask: u32) -> Result<i32> {
405 let c_path = CString::new(path.as_os_str().as_bytes())
406 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
407 let wd = unsafe { libc::inotify_add_watch(self.fd.as_raw_fd(), c_path.as_ptr(), mask) };
408 if wd < 0 {
409 return Err(std::io::Error::last_os_error());
410 }
411 Ok(wd)
412 }
413
414 async fn read_events(&self, buffer: &mut [u8]) -> Result<usize> {
415 loop {
416 let mut guard = self.fd.readable().await?;
417 match guard.try_io(|inner| {
418 let res = unsafe {
419 libc::read(
420 inner.as_raw_fd(),
421 buffer.as_mut_ptr() as *mut _,
422 buffer.len(),
423 )
424 };
425 if res < 0 {
426 Err(std::io::Error::last_os_error())
427 } else {
428 Ok(res as usize)
429 }
430 }) {
431 Ok(Ok(len)) => return Ok(len),
432 Ok(Err(e)) => {
433 if e.kind() == std::io::ErrorKind::WouldBlock {
434 continue;
435 }
436 return Err(e);
437 }
438 Err(_) => continue,
439 }
440 }
441 }
442}
443
444impl Drop for Inotify {
445 fn drop(&mut self) {
446 unsafe { libc::close(self.fd.as_raw_fd()) };
447 }
448}
449
450fn path_to_segments(path: &Path) -> Vec<String> {
453 let path_str = path.to_string_lossy();
454 let path_str = path_str.replace("//", "/");
455 path_str
456 .split('/')
457 .filter(|s| !s.is_empty())
458 .map(|s| s.to_string())
459 .collect()
460}
461
462const INOTIFY_MASK: u32 = libc::IN_MODIFY
463 | libc::IN_CLOSE_WRITE
464 | libc::IN_CREATE
465 | libc::IN_DELETE
466 | libc::IN_MOVED_FROM
467 | libc::IN_MOVED_TO
468 | libc::IN_DONT_FOLLOW;
469
470
471fn parse_inotify_events(buffer: &[u8], len: usize) -> Vec<(i32, u32, String)> {
472 let mut events = Vec::new();
473 let mut ptr = buffer.as_ptr();
474 let end = unsafe { ptr.add(len) };
475
476 while ptr < end {
477 let event = unsafe { &*(ptr as *const libc::inotify_event) };
478 let name_len = event.len as usize;
479
480 if name_len > 0 {
481 let name_ptr = unsafe { ptr.add(std::mem::size_of::<libc::inotify_event>()) };
482 let name_slice =
483 unsafe { std::slice::from_raw_parts(name_ptr as *const u8, name_len) };
484 let name_str = String::from_utf8_lossy(name_slice)
485 .trim_matches(char::from(0))
486 .to_string();
487 events.push((event.wd, event.mask, name_str));
488 }
489
490 ptr = unsafe { ptr.add(std::mem::size_of::<libc::inotify_event>() + name_len) };
491 }
492
493 events
494}
495
496#[derive(Debug, Clone, Copy, PartialEq, Eq)]
501pub enum WatchEvent {
502 Create,
506 Delete,
510 Update,
516 Initial,
522 DebugWatch,
527}
528
529pub struct Watcher {
552 includes: Vec<String>,
553 excludes: Vec<String>,
554 base_dir: PathBuf,
555 watch_create: bool,
556 watch_delete: bool,
557 watch_update: bool,
558 watch_initial: bool,
559 match_files: bool,
560 match_dirs: bool,
561 return_absolute: bool,
562 debug_watches_enabled: bool,
563}
564
565#[deprecated(since = "0.1.2", note = "Renamed to Watcher")]
567pub type WatchBuilder = Watcher;
568
569impl Default for Watcher {
570 fn default() -> Self {
571 Self::new()
572 }
573}
574
575struct WatcherState<F> {
577 root: PathBuf,
578 inotify: Inotify,
579 watches: HashMap<i32, PathBuf>,
580 paths: HashSet<PathBuf>,
581 include_patterns: Vec<Pattern>,
582 exclude_patterns: Vec<Pattern>,
583 callback: F,
584}
585
586impl Watcher {
587 pub fn new() -> Self {
597 Watcher {
598 includes: Vec::new(),
599 excludes: Vec::new(),
600 base_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/")),
601 watch_create: true,
602 watch_delete: true,
603 watch_update: true,
604 watch_initial: false,
605 match_files: true,
606 match_dirs: true,
607 return_absolute: false,
608 debug_watches_enabled: false,
609 }
610 }
611
612 pub fn debug_watches(mut self, enabled: bool) -> Self {
617 self.debug_watches_enabled = enabled;
618 self
619 }
620
621 pub fn add_include(mut self, pattern: impl Into<String>) -> Self {
632 self.includes.push(pattern.into());
633 self
634 }
635
636 pub fn add_includes(mut self, patterns: impl IntoIterator<Item = impl Into<String>>) -> Self {
638 self.includes.extend(patterns.into_iter().map(|p| p.into()));
639 self
640 }
641
642 pub fn add_exclude(mut self, pattern: impl Into<String>) -> Self {
646 self.excludes.push(pattern.into());
647 self
648 }
649
650 pub fn add_excludes(mut self, patterns: impl IntoIterator<Item = impl Into<String>>) -> Self {
652 self.excludes.extend(patterns.into_iter().map(|p| p.into()));
653 self
654 }
655
656 pub fn add_ignore_file(mut self, path: impl AsRef<Path>) -> Self {
683 let path = path.as_ref();
684
685 let full_path = if path.is_absolute() {
687 path.to_path_buf()
688 } else {
689 self.base_dir.join(path)
690 };
691
692 if let Ok(file) = fs::File::open(&full_path) {
693 let reader = BufReader::new(file);
694 let mut has_negation = false;
695 for line in reader.lines().map_while(Result::ok) {
696 let trimmed = line.trim();
697
698 if trimmed.is_empty() || trimmed.starts_with('#') {
700 continue;
701 }
702
703 if trimmed.starts_with('!') {
705 has_negation = true;
706 } else {
707 self.excludes.push(trimmed.to_string());
709 }
710 }
711 if has_negation {
712 println!("Warning: negation patterns (!) in {} are ignored; excludes always take precedence over includes in this library", full_path.display());
713 }
714 }
715
716 self
717 }
718
719 pub fn set_base_dir(mut self, base_dir: impl Into<PathBuf>) -> Self {
724 self.base_dir = base_dir.into();
725 self
726 }
727
728 pub fn watch_create(mut self, enabled: bool) -> Self {
732 self.watch_create = enabled;
733 self
734 }
735
736 pub fn watch_delete(mut self, enabled: bool) -> Self {
740 self.watch_delete = enabled;
741 self
742 }
743
744 pub fn watch_update(mut self, enabled: bool) -> Self {
748 self.watch_update = enabled;
749 self
750 }
751
752 pub fn watch_initial(mut self, enabled: bool) -> Self {
778 self.watch_initial = enabled;
779 self
780 }
781
782 pub fn match_files(mut self, enabled: bool) -> Self {
786 self.match_files = enabled;
787 self
788 }
789
790 pub fn match_dirs(mut self, enabled: bool) -> Self {
794 self.match_dirs = enabled;
795 self
796 }
797
798 pub fn return_absolute(mut self, enabled: bool) -> Self {
803 self.return_absolute = enabled;
804 self
805 }
806
807 pub async fn run<F>(self, callback: F) -> Result<()>
815 where
816 F: FnMut(WatchEvent, PathBuf),
817 {
818 self.run_internal(callback, None).await
819 }
820
821 pub async fn run_debounced<F>(self, ms: u64, mut callback: F) -> Result<()>
830 where
831 F: FnMut(),
832 {
833 self.run_internal(|_, _| callback(), Some(Duration::from_millis(ms))).await
834 }
835
836 fn should_watch<F>(&self, state: &WatcherState<F>, relative_path: &Path, is_dir: bool) -> bool {
837 let segments = path_to_segments(relative_path);
838
839 if state.exclude_patterns.iter().any(|p| p.check(&segments, false)) {
840 return false;
841 }
842
843 state.include_patterns.iter().any(|p| p.check(&segments, is_dir))
844 }
845
846 fn make_callback_path<F>(&self, state: &WatcherState<F>, rel_path: &Path) -> PathBuf {
847 if self.return_absolute {
848 if rel_path.as_os_str().is_empty() {
849 state.root.clone()
850 } else {
851 state.root.join(rel_path)
852 }
853 } else {
854 rel_path.to_path_buf()
855 }
856 }
857
858 fn emit_event<F>(
859 &self,
860 state: &mut WatcherState<F>,
861 event: WatchEvent,
862 rel_path: &Path,
863 is_dir: bool,
864 ) where
865 F: FnMut(WatchEvent, PathBuf),
866 {
867 if if is_dir { !self.match_dirs } else { !self.match_files } {
868 return;
869 }
870
871 if !self.should_watch(state, rel_path, false) {
872 return;
873 }
874
875 let path = self.make_callback_path(state, rel_path);
876 (state.callback)(event, path);
877 }
878
879 fn add_watch_recursive<F>(
880 &self,
881 state: &mut WatcherState<F>,
882 initial_path: PathBuf,
883 emit_initial: bool,
884 ) where
885 F: FnMut(WatchEvent, PathBuf),
886 {
887 if state.paths.contains(&initial_path) {
888 return;
889 }
890
891 let mut stack = vec![initial_path];
892 while let Some(rel_path) = stack.pop() {
893 if !self.should_watch(state, &rel_path, true) {
894 continue;
895 }
896
897 let full_path = if rel_path.as_os_str().is_empty() {
898 state.root.clone()
899 } else {
900 state.root.join(&rel_path)
901 };
902
903 if !full_path.is_dir() {
904 continue;
905 }
906
907 let wd = match state.inotify.add_watch(&full_path, INOTIFY_MASK) {
908 Ok(wd) => wd,
909 Err(e) => {
910 eprintln!("Failed to add watch for {:?}: {}", full_path, e);
911 continue;
912 }
913 };
914
915 state.paths.insert(rel_path.clone());
916 state.watches.insert(wd, rel_path.clone());
917
918 if self.debug_watches_enabled {
919 let path = self.make_callback_path(state, &rel_path);
920 (state.callback)(WatchEvent::DebugWatch, path);
921 }
922
923 if let Ok(entries) = std::fs::read_dir(&full_path) {
924 for entry in entries.flatten() {
925 if let Ok(ft) = entry.file_type() {
926 let child_rel_path = rel_path.join(entry.file_name());
927 let is_dir = ft.is_dir();
928
929 if emit_initial {
930 self.emit_event(state, WatchEvent::Initial, &child_rel_path, is_dir);
931 }
932
933 if is_dir && !state.paths.contains(&child_rel_path) {
934 stack.push(child_rel_path);
935 }
936 }
937 }
938 }
939 }
940 }
941
942 async fn run_internal<F>(self, callback: F, debounce: Option<Duration>) -> Result<()>
943 where
944 F: FnMut(WatchEvent, PathBuf),
945 {
946 let includes = if self.includes.is_empty() {
948 vec!["**".to_string()]
949 } else {
950 self.includes.clone()
951 };
952
953 if includes.is_empty() {
955 loop {
956 tokio::time::sleep(Duration::from_secs(3600)).await;
957 }
958 }
959
960 let root = if self.base_dir.is_absolute() {
961 self.base_dir.clone()
962 } else {
963 std::env::current_dir()
964 .unwrap_or_else(|_| PathBuf::from("/"))
965 .join(&self.base_dir)
966 };
967
968 let mut state = WatcherState {
969 root,
970 inotify: Inotify::new()?,
971 watches: HashMap::new(),
972 paths: HashSet::new(),
973 include_patterns: includes.iter().map(|p| Pattern::parse(p)).collect(),
974 exclude_patterns: self.excludes.iter().map(|p| Pattern::parse(p)).collect(),
975 callback,
976 };
977
978 let emit_initial = self.watch_initial && debounce.is_none();
980 self.add_watch_recursive(&mut state, PathBuf::new(), emit_initial);
981
982 let mut debounce_deadline: Option<tokio::time::Instant> = None;
984
985 let mut buffer = [0u8; 8192];
987 loop {
988 let read_future = state.inotify.read_events(&mut buffer);
990
991 let read_result = if let Some(deadline) = debounce_deadline {
992 let now = tokio::time::Instant::now();
993 if deadline <= now {
994 debounce_deadline = None;
996 (state.callback)(WatchEvent::Update, PathBuf::new());
997 continue;
998 }
999 match tokio::time::timeout(deadline - now, read_future).await {
1001 Ok(result) => Some(result),
1002 Err(_) => {
1003 debounce_deadline = None;
1005 (state.callback)(WatchEvent::Update, PathBuf::new());
1006 continue;
1007 }
1008 }
1009 } else {
1010 Some(read_future.await)
1011 };
1012
1013 let Some(result) = read_result else { continue };
1014
1015 match result {
1016 Ok(len) => {
1017 let events = parse_inotify_events(&buffer, len);
1018 let mut had_matching_event = false;
1019
1020 for (wd, mask, name_str) in events {
1021 if (mask & libc::IN_IGNORED as u32) != 0 {
1022 if let Some(path) = state.watches.remove(&wd) {
1023 state.paths.remove(&path);
1024 }
1025 continue;
1026 }
1027
1028 let rel_path = if let Some(dir_path) = state.watches.get(&wd) {
1029 dir_path.join(&name_str)
1030 } else {
1031 println!("Warning: received event for unknown watch descriptor {}", wd);
1032 continue;
1033 };
1034
1035 let is_dir = mask & libc::IN_ISDIR as u32 != 0;
1036 let is_create = (mask & libc::IN_CREATE as u32) != 0
1037 || (mask & libc::IN_MOVED_TO as u32) != 0;
1038 let is_delete = (mask & libc::IN_DELETE as u32) != 0
1039 || (mask & libc::IN_MOVED_FROM as u32) != 0;
1040 let is_update = (mask & libc::IN_MODIFY as u32) != 0
1041 || (mask & libc::IN_CLOSE_WRITE as u32) != 0;
1042
1043 if is_dir && is_create {
1044 self.add_watch_recursive(&mut state, rel_path.clone(), false);
1046 }
1047
1048 let event_type = if is_create && self.watch_create {
1049 WatchEvent::Create
1050 } else if is_delete && self.watch_delete {
1051 WatchEvent::Delete
1052 } else if is_update && self.watch_update {
1053 WatchEvent::Update
1054 } else {
1055 continue
1056 };
1057
1058 if if is_dir { !self.match_dirs } else { !self.match_files } {
1059 continue;
1060 }
1061
1062 if !self.should_watch(&state, &rel_path, false) {
1063 continue;
1064 }
1065
1066 had_matching_event = true;
1067
1068 if debounce.is_none() {
1070 let path = self.make_callback_path(&state, &rel_path);
1071 (state.callback)(event_type, path);
1072 }
1073 }
1074
1075 if let Some(d) = debounce {
1077 if had_matching_event {
1078 debounce_deadline = Some(tokio::time::Instant::now() + d);
1079 }
1080 }
1081 }
1082 Err(e) => {
1083 eprintln!("Error reading inotify events: {}", e);
1084 tokio::time::sleep(Duration::from_millis(100)).await;
1085 }
1086 }
1087 }
1088 }
1089}
1090
1091#[cfg(test)]
1092mod tests {
1093 use super::*;
1094 use std::collections::HashSet;
1095 use std::sync::{Arc, Mutex};
1096 use tokio::task::JoinHandle;
1097
1098 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
1099 enum EventType {
1100 Create,
1101 Delete,
1102 Update,
1103 Initial,
1104 DebugWatch,
1105 }
1106
1107 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
1108 struct Event {
1109 path: PathBuf,
1110 event_type: EventType,
1111 }
1112
1113 type EventTracker = Arc<Mutex<Vec<Event>>>;
1114
1115 struct TestInstance {
1116 test_dir: PathBuf,
1117 tracker: EventTracker,
1118 watcher_handle: Option<JoinHandle<()>>,
1119 }
1120
1121 impl TestInstance {
1122 async fn new<F>(test_name: &str, configure: F) -> Self
1123 where
1124 F: FnOnce(Watcher) -> Watcher + Send + 'static,
1125 {
1126 let test_dir = std::env::current_dir()
1127 .unwrap()
1128 .join(format!(".file-watcher-test-{}", test_name));
1129
1130 if test_dir.exists() {
1131 std::fs::remove_dir_all(&test_dir).unwrap();
1132 }
1133 std::fs::create_dir(&test_dir).unwrap();
1134
1135 let tracker = Arc::new(Mutex::new(Vec::new()));
1136
1137 let tracker_clone = tracker.clone();
1138 let test_dir_clone = test_dir.clone();
1139
1140 let watcher_handle = tokio::spawn(async move {
1141 let watcher = Watcher::new()
1142 .set_base_dir(&test_dir_clone)
1143 .debug_watches(true);
1144
1145 let watcher = configure(watcher);
1146
1147 let _ = watcher
1148 .run(move |event_type, path| {
1149 tracker_clone.lock().unwrap().push(Event {
1150 path: path.clone(),
1151 event_type: match event_type {
1152 WatchEvent::Create => EventType::Create,
1153 WatchEvent::Delete => EventType::Delete,
1154 WatchEvent::Update => EventType::Update,
1155 WatchEvent::Initial => EventType::Initial,
1156 WatchEvent::DebugWatch => EventType::DebugWatch,
1157 },
1158 });
1159 })
1160 .await;
1161 });
1162
1163 tokio::time::sleep(Duration::from_millis(100)).await;
1164
1165 let instance = Self {
1166 test_dir,
1167 tracker,
1168 watcher_handle: Some(watcher_handle),
1169 };
1170
1171 instance.assert_events(&[], &[], &[], &[""]).await;
1172
1173 instance
1174 }
1175
1176 fn create_dir(&self, path: &str) {
1177 std::fs::create_dir_all(self.test_dir.join(path)).unwrap();
1178 }
1179
1180 fn write_file(&self, path: &str, content: &str) {
1181 let full_path = self.test_dir.join(path);
1182 if let Some(parent) = full_path.parent() {
1183 std::fs::create_dir_all(parent).unwrap();
1184 }
1185 std::fs::write(full_path, content).unwrap();
1186 }
1187
1188 fn remove_file(&self, path: &str) {
1189 std::fs::remove_file(self.test_dir.join(path)).unwrap();
1190 }
1191
1192 fn rename(&self, from: &str, to: &str) {
1193 std::fs::rename(self.test_dir.join(from), self.test_dir.join(to)).unwrap();
1194 }
1195
1196 async fn assert_events(
1197 &self,
1198 creates: &[&str],
1199 deletes: &[&str],
1200 updates: &[&str],
1201 watches: &[&str],
1202 ) {
1203 tokio::time::sleep(Duration::from_millis(200)).await;
1204
1205 let events = self.tracker.lock().unwrap().clone();
1206 let mut expected = HashSet::new();
1207
1208 for create in creates {
1209 expected.insert(Event {
1210 path: PathBuf::from(create),
1211 event_type: EventType::Create,
1212 });
1213 }
1214
1215 for delete in deletes {
1216 expected.insert(Event {
1217 path: PathBuf::from(delete),
1218 event_type: EventType::Delete,
1219 });
1220 }
1221
1222 for update in updates {
1223 expected.insert(Event {
1224 path: PathBuf::from(update),
1225 event_type: EventType::Update,
1226 });
1227 }
1228
1229 for watch in watches {
1230 expected.insert(Event {
1231 path: PathBuf::from(watch),
1232 event_type: EventType::DebugWatch,
1233 });
1234 }
1235
1236 let actual: HashSet<Event> = events.iter().cloned().collect();
1237
1238 for event in &actual {
1239 if !expected.contains(event) {
1240 panic!("Unexpected event: {:?}", event);
1241 }
1242 }
1243
1244 for event in &expected {
1245 if !actual.contains(event) {
1246 panic!(
1247 "Missing expected event: {:?}\nActual events: {:?}",
1248 event, actual
1249 );
1250 }
1251 }
1252
1253 self.tracker.lock().unwrap().clear();
1254 }
1255
1256 async fn assert_no_events(&self) {
1257 tokio::time::sleep(Duration::from_millis(500)).await;
1258 let events = self.tracker.lock().unwrap();
1259 assert_eq!(
1260 events.len(),
1261 0,
1262 "Expected no events, but got: {:?}",
1263 events
1264 );
1265 }
1266 }
1267
1268 impl Drop for TestInstance {
1269 fn drop(&mut self) {
1270 if let Some(handle) = self.watcher_handle.take() {
1271 handle.abort();
1272 }
1273 if self.test_dir.exists() {
1274 let _ = std::fs::remove_dir_all(&self.test_dir);
1275 }
1276 }
1277 }
1278
1279 #[tokio::test]
1280 async fn test_file_create_update_delete() {
1281 let test = TestInstance::new("create_update_delete", |b| b.add_include("**/*")).await;
1282
1283 test.write_file("test.txt", "");
1284 test.assert_events(&["test.txt"], &[], &["test.txt"], &[])
1285 .await;
1286
1287 test.write_file("test.txt", "hello");
1288 test.assert_events(&[], &[], &["test.txt"], &[]).await;
1289
1290 test.remove_file("test.txt");
1291 test.assert_events(&[], &["test.txt"], &[], &[]).await;
1292 }
1293
1294 #[tokio::test]
1295 async fn test_directory_operations() {
1296 let test = TestInstance::new("directory_operations", |b| b.add_include("**/*")).await;
1297
1298 test.create_dir("subdir");
1299 test.assert_events(&["subdir"], &[], &[], &["subdir"]).await;
1300
1301 test.write_file("subdir/file.txt", "");
1302 test.assert_events(&["subdir/file.txt"], &[], &["subdir/file.txt"], &[])
1303 .await;
1304 }
1305
1306 #[tokio::test]
1307 async fn test_move_operations() {
1308 let test = TestInstance::new("move_operations", |b| b.add_include("**/*")).await;
1309
1310 test.write_file("old.txt", "content");
1311 test.assert_events(&["old.txt"], &[], &["old.txt"], &[])
1312 .await;
1313
1314 test.rename("old.txt", "new.txt");
1315 test.assert_events(&["new.txt"], &["old.txt"], &[], &[])
1316 .await;
1317 }
1318
1319 #[tokio::test]
1320 async fn test_event_filtering() {
1321 let test = TestInstance::new("event_filtering", |b| {
1322 b.add_include("**/*")
1323 .watch_create(true)
1324 .watch_delete(false)
1325 .watch_update(false)
1326 })
1327 .await;
1328
1329 test.write_file("test.txt", "");
1330 test.assert_events(&["test.txt"], &[], &[], &[]).await;
1331
1332 test.write_file("test.txt", "hello");
1333 test.assert_no_events().await;
1334
1335 test.remove_file("test.txt");
1336 test.assert_no_events().await;
1337 }
1338
1339 #[tokio::test]
1340 async fn test_pattern_matching() {
1341 let test = TestInstance::new("pattern_matching", |b| b.add_include("**/*.txt")).await;
1342
1343 test.write_file("test.txt", "");
1344 test.assert_events(&["test.txt"], &[], &["test.txt"], &[])
1345 .await;
1346
1347 test.write_file("test.rs", "");
1348 test.assert_no_events().await;
1349 }
1350
1351 #[tokio::test]
1352 async fn test_matching_stops_at_depth() {
1353 let test = TestInstance::new("matching_stops_at_depth", |b| b.add_include("*/xyz/*.*")).await;
1354
1355 test.write_file("test.txt", "");
1356 test.assert_no_events().await;
1357
1358 test.create_dir("abc/xyz");
1359 test.assert_events(&[], &[], &[], &["abc", "abc/xyz"]).await;
1360
1361 test.create_dir("abc/hjk/a.b");
1362 test.assert_no_events().await;
1363
1364 test.create_dir("abc/xyz/a.b");
1365 test.assert_events(&["abc/xyz/a.b"], &[], &[], &[]).await; test.create_dir("abc/xyz/a.b/x.y");
1368 test.assert_events(&[], &[], &[], &[]).await;
1369 }
1370
1371 #[tokio::test]
1372 async fn test_exclude_prevents_watching() {
1373 let test = TestInstance::new("exclude_prevents_watch", |b| {
1374 b.add_include("**/*").add_exclude("node_modules/**")
1375 })
1376 .await;
1377
1378 test.create_dir("node_modules");
1379 tokio::time::sleep(Duration::from_millis(200)).await;
1380
1381 test.write_file("node_modules/package.json", "");
1382 test.assert_no_events().await;
1383
1384 test.write_file("test.txt", "");
1385 test.assert_events(&["test.txt"], &[], &["test.txt"], &[])
1386 .await;
1387 }
1388
1389 #[tokio::test]
1390 async fn test_pattern_file() {
1391 let test_dir = std::env::current_dir()
1393 .unwrap()
1394 .join(".file-watcher-test-pattern_file");
1395
1396 if test_dir.exists() {
1397 std::fs::remove_dir_all(&test_dir).unwrap();
1398 }
1399 std::fs::create_dir(&test_dir).unwrap();
1400
1401 std::fs::write(
1403 test_dir.join(".watchignore"),
1404 "# Comment line\nignored/**\n",
1405 )
1406 .unwrap();
1407
1408 let tracker = Arc::new(Mutex::new(Vec::<Event>::new()));
1410 let tracker_clone = tracker.clone();
1411 let test_dir_clone = test_dir.clone();
1412
1413 let watcher_handle = tokio::spawn(async move {
1414 let _ = Watcher::new()
1415 .set_base_dir(&test_dir_clone)
1416 .debug_watches(true)
1417 .add_include("**/*")
1418 .add_ignore_file(".watchignore")
1419 .run(move |event_type, path| {
1420 tracker_clone.lock().unwrap().push(Event {
1421 path: path.clone(),
1422 event_type: match event_type {
1423 WatchEvent::Create => EventType::Create,
1424 WatchEvent::Delete => EventType::Delete,
1425 WatchEvent::Update => EventType::Update,
1426 WatchEvent::Initial => EventType::Initial,
1427 WatchEvent::DebugWatch => EventType::DebugWatch,
1428 },
1429 });
1430 })
1431 .await;
1432 });
1433
1434 tokio::time::sleep(Duration::from_millis(100)).await;
1435 tracker.lock().unwrap().clear(); std::fs::create_dir(test_dir.join("ignored")).unwrap();
1439 tokio::time::sleep(Duration::from_millis(200)).await;
1440
1441 std::fs::write(test_dir.join("ignored/test.txt"), "").unwrap();
1443 tokio::time::sleep(Duration::from_millis(200)).await;
1444
1445 {
1447 let events = tracker.lock().unwrap();
1448 let has_ignored_events = events.iter().any(|e| {
1449 e.path.to_string_lossy().contains("ignored")
1450 && e.event_type != EventType::DebugWatch
1451 });
1452 assert!(
1453 !has_ignored_events,
1454 "Expected no events for ignored files, but got: {:?}",
1455 events
1456 );
1457 }
1458 tracker.lock().unwrap().clear();
1459
1460 std::fs::write(test_dir.join("normal.txt"), "").unwrap();
1462 tokio::time::sleep(Duration::from_millis(200)).await;
1463
1464 {
1465 let events = tracker.lock().unwrap();
1466 let has_normal = events
1467 .iter()
1468 .any(|e| e.path == PathBuf::from("normal.txt"));
1469 assert!(has_normal, "Expected event for normal.txt, got: {:?}", events);
1470 }
1471
1472 watcher_handle.abort();
1474 let _ = std::fs::remove_dir_all(&test_dir);
1475 }
1476
1477 #[tokio::test]
1478 async fn test_watch_initial() {
1479 let test_dir = std::env::current_dir()
1481 .unwrap()
1482 .join(".file-watcher-test-watch_initial");
1483
1484 if test_dir.exists() {
1485 std::fs::remove_dir_all(&test_dir).unwrap();
1486 }
1487 std::fs::create_dir(&test_dir).unwrap();
1488
1489 std::fs::write(test_dir.join("existing1.txt"), "content1").unwrap();
1491 std::fs::write(test_dir.join("existing2.txt"), "content2").unwrap();
1492 std::fs::create_dir(test_dir.join("subdir")).unwrap();
1493 std::fs::write(test_dir.join("subdir/nested.txt"), "nested").unwrap();
1494 std::fs::write(test_dir.join("ignored.rs"), "should be ignored").unwrap();
1495
1496 let tracker = Arc::new(Mutex::new(Vec::<Event>::new()));
1497 let tracker_clone = tracker.clone();
1498 let test_dir_clone = test_dir.clone();
1499
1500 let watcher_handle = tokio::spawn(async move {
1501 let _ = Watcher::new()
1502 .set_base_dir(&test_dir_clone)
1503 .add_include("**/*.txt")
1504 .watch_initial(true)
1505 .run(move |event_type, path| {
1506 tracker_clone.lock().unwrap().push(Event {
1507 path: path.clone(),
1508 event_type: match event_type {
1509 WatchEvent::Create => EventType::Create,
1510 WatchEvent::Delete => EventType::Delete,
1511 WatchEvent::Update => EventType::Update,
1512 WatchEvent::Initial => EventType::Initial,
1513 WatchEvent::DebugWatch => EventType::DebugWatch,
1514 },
1515 });
1516 })
1517 .await;
1518 });
1519
1520 tokio::time::sleep(Duration::from_millis(200)).await;
1521
1522 {
1524 let events = tracker.lock().unwrap();
1525 let initial_events: Vec<_> = events
1526 .iter()
1527 .filter(|e| e.event_type == EventType::Initial)
1528 .collect();
1529
1530 assert_eq!(
1531 initial_events.len(),
1532 3,
1533 "Expected 3 Initial events, got: {:?}",
1534 initial_events
1535 );
1536
1537 let paths: HashSet<_> = initial_events.iter().map(|e| e.path.clone()).collect();
1538 assert!(paths.contains(&PathBuf::from("existing1.txt")));
1539 assert!(paths.contains(&PathBuf::from("existing2.txt")));
1540 assert!(paths.contains(&PathBuf::from("subdir/nested.txt")));
1541
1542 assert!(!events.iter().any(|e| e.path.to_string_lossy().contains("ignored.rs")));
1544 }
1545
1546 tracker.lock().unwrap().clear();
1547
1548 std::fs::write(test_dir.join("new.txt"), "new content").unwrap();
1550 tokio::time::sleep(Duration::from_millis(200)).await;
1551
1552 {
1553 let events = tracker.lock().unwrap();
1554 let has_create = events
1555 .iter()
1556 .any(|e| e.path == PathBuf::from("new.txt") && e.event_type == EventType::Create);
1557 assert!(has_create, "Expected Create event for new.txt, got: {:?}", events);
1558 }
1559
1560 watcher_handle.abort();
1562 let _ = std::fs::remove_dir_all(&test_dir);
1563 }
1564
1565 #[tokio::test]
1566 async fn test_watch_initial_with_dirs() {
1567 let test_dir = std::env::current_dir()
1569 .unwrap()
1570 .join(".file-watcher-test-watch_initial_dirs");
1571
1572 if test_dir.exists() {
1573 std::fs::remove_dir_all(&test_dir).unwrap();
1574 }
1575 std::fs::create_dir(&test_dir).unwrap();
1576
1577 std::fs::write(test_dir.join("file.txt"), "content").unwrap();
1579 std::fs::create_dir(test_dir.join("mydir")).unwrap();
1580
1581 let tracker = Arc::new(Mutex::new(Vec::<Event>::new()));
1582 let tracker_clone = tracker.clone();
1583 let test_dir_clone = test_dir.clone();
1584
1585 let watcher_handle = tokio::spawn(async move {
1586 let _ = Watcher::new()
1587 .set_base_dir(&test_dir_clone)
1588 .add_include("**/*")
1589 .watch_initial(true)
1590 .match_files(true)
1591 .match_dirs(false) .run(move |event_type, path| {
1593 tracker_clone.lock().unwrap().push(Event {
1594 path: path.clone(),
1595 event_type: match event_type {
1596 WatchEvent::Create => EventType::Create,
1597 WatchEvent::Delete => EventType::Delete,
1598 WatchEvent::Update => EventType::Update,
1599 WatchEvent::Initial => EventType::Initial,
1600 WatchEvent::DebugWatch => EventType::DebugWatch,
1601 },
1602 });
1603 })
1604 .await;
1605 });
1606
1607 tokio::time::sleep(Duration::from_millis(200)).await;
1608
1609 {
1610 let events = tracker.lock().unwrap();
1611 let initial_events: Vec<_> = events
1612 .iter()
1613 .filter(|e| e.event_type == EventType::Initial)
1614 .collect();
1615
1616 assert_eq!(
1618 initial_events.len(),
1619 1,
1620 "Expected 1 Initial event (file only), got: {:?}",
1621 initial_events
1622 );
1623 assert_eq!(initial_events[0].path, PathBuf::from("file.txt"));
1624 }
1625
1626 watcher_handle.abort();
1628 let _ = std::fs::remove_dir_all(&test_dir);
1629 }
1630
1631 #[tokio::test]
1632 async fn test_watch_initial_disabled_by_default() {
1633 let test_dir = std::env::current_dir()
1635 .unwrap()
1636 .join(".file-watcher-test-watch_initial_disabled");
1637
1638 if test_dir.exists() {
1639 std::fs::remove_dir_all(&test_dir).unwrap();
1640 }
1641 std::fs::create_dir(&test_dir).unwrap();
1642
1643 std::fs::write(test_dir.join("existing.txt"), "content").unwrap();
1645
1646 let tracker = Arc::new(Mutex::new(Vec::<Event>::new()));
1647 let tracker_clone = tracker.clone();
1648 let test_dir_clone = test_dir.clone();
1649
1650 let watcher_handle = tokio::spawn(async move {
1651 let _ = Watcher::new()
1652 .set_base_dir(&test_dir_clone)
1653 .add_include("**/*.txt")
1654 .run(move |event_type, path| {
1656 tracker_clone.lock().unwrap().push(Event {
1657 path: path.clone(),
1658 event_type: match event_type {
1659 WatchEvent::Create => EventType::Create,
1660 WatchEvent::Delete => EventType::Delete,
1661 WatchEvent::Update => EventType::Update,
1662 WatchEvent::Initial => EventType::Initial,
1663 WatchEvent::DebugWatch => EventType::DebugWatch,
1664 },
1665 });
1666 })
1667 .await;
1668 });
1669
1670 tokio::time::sleep(Duration::from_millis(200)).await;
1671
1672 {
1673 let events = tracker.lock().unwrap();
1674 let initial_events: Vec<_> = events
1675 .iter()
1676 .filter(|e| e.event_type == EventType::Initial)
1677 .collect();
1678
1679 assert_eq!(
1681 initial_events.len(),
1682 0,
1683 "Expected no Initial events when watch_initial is disabled, got: {:?}",
1684 initial_events
1685 );
1686 }
1687
1688 watcher_handle.abort();
1690 let _ = std::fs::remove_dir_all(&test_dir);
1691 }
1692}