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<()>
829 where
830 F: FnMut(PathBuf),
831 {
832 self.run_internal(|_, path| callback(path), Some(Duration::from_millis(ms))).await
833 }
834
835 fn should_watch<F>(&self, state: &WatcherState<F>, relative_path: &Path, is_dir: bool) -> bool {
836 let segments = path_to_segments(relative_path);
837
838 if state.exclude_patterns.iter().any(|p| p.check(&segments, false)) {
839 return false;
840 }
841
842 state.include_patterns.iter().any(|p| p.check(&segments, is_dir))
843 }
844
845 fn check_event<F>(&self, state: &WatcherState<F>, rel_path: &Path, is_dir: bool) -> bool {
846 if if is_dir { !self.match_dirs } else { !self.match_files } {
847 return false;
848 }
849 self.should_watch(state, rel_path, false)
850 }
851
852 fn emit_event<F>(
853 &self,
854 state: &mut WatcherState<F>,
855 event: WatchEvent,
856 rel_path: &Path,
857 ) where
858 F: FnMut(WatchEvent, PathBuf),
859 {
860 let path = if self.return_absolute {
861 if rel_path.as_os_str().is_empty() {
862 state.root.clone()
863 } else {
864 state.root.join(rel_path)
865 }
866 } else {
867 rel_path.to_path_buf()
868 };
869 (state.callback)(event, path);
870 }
871
872 fn add_watch_recursive<F>(
873 &self,
874 state: &mut WatcherState<F>,
875 initial_path: PathBuf,
876 emit_initial: bool,
877 ) where
878 F: FnMut(WatchEvent, PathBuf),
879 {
880 if state.paths.contains(&initial_path) {
881 return;
882 }
883
884 let mut stack = vec![initial_path];
885 while let Some(rel_path) = stack.pop() {
886 if !self.should_watch(state, &rel_path, true) {
887 continue;
888 }
889
890 let full_path = if rel_path.as_os_str().is_empty() {
891 state.root.clone()
892 } else {
893 state.root.join(&rel_path)
894 };
895
896 if !full_path.is_dir() {
897 continue;
898 }
899
900 let wd = match state.inotify.add_watch(&full_path, INOTIFY_MASK) {
901 Ok(wd) => wd,
902 Err(e) => {
903 eprintln!("Failed to add watch for {:?}: {}", full_path, e);
904 continue;
905 }
906 };
907
908 state.paths.insert(rel_path.clone());
909 state.watches.insert(wd, rel_path.clone());
910
911 if self.debug_watches_enabled {
912 (state.callback)(WatchEvent::DebugWatch, rel_path.clone());
913 }
914
915 if let Ok(entries) = std::fs::read_dir(&full_path) {
916 for entry in entries.flatten() {
917 if let Ok(ft) = entry.file_type() {
918 let child_rel_path = rel_path.join(entry.file_name());
919 let is_dir = ft.is_dir();
920
921 if emit_initial && self.check_event(state, &child_rel_path, is_dir) {
922 self.emit_event(state, WatchEvent::Initial, &child_rel_path);
923 }
924
925 if is_dir && !state.paths.contains(&child_rel_path) {
926 stack.push(child_rel_path);
927 }
928 }
929 }
930 }
931 }
932 }
933
934 async fn run_internal<F>(self, callback: F, debounce: Option<Duration>) -> Result<()>
935 where
936 F: FnMut(WatchEvent, PathBuf),
937 {
938 let includes = if self.includes.is_empty() {
940 vec!["**".to_string()]
941 } else {
942 self.includes.clone()
943 };
944
945 if includes.is_empty() {
947 loop {
948 tokio::time::sleep(Duration::from_secs(3600)).await;
949 }
950 }
951
952 let root = if self.base_dir.is_absolute() {
953 self.base_dir.clone()
954 } else {
955 std::env::current_dir()
956 .unwrap_or_else(|_| PathBuf::from("/"))
957 .join(&self.base_dir)
958 };
959
960 let mut state = WatcherState {
961 root,
962 inotify: Inotify::new()?,
963 watches: HashMap::new(),
964 paths: HashSet::new(),
965 include_patterns: includes.iter().map(|p| Pattern::parse(p)).collect(),
966 exclude_patterns: self.excludes.iter().map(|p| Pattern::parse(p)).collect(),
967 callback,
968 };
969
970 let emit_initial = self.watch_initial && debounce.is_none();
972 self.add_watch_recursive(&mut state, PathBuf::new(), emit_initial);
973
974 let mut debounce_deadline: Option<tokio::time::Instant> = None;
976 let mut debounce_first_path: Option<PathBuf> = None;
977
978 let mut buffer = [0u8; 8192];
980 loop {
981 let read_future = state.inotify.read_events(&mut buffer);
983
984 let read_result = if let Some(deadline) = debounce_deadline {
985 let now = tokio::time::Instant::now();
986 if deadline <= now {
987 debounce_deadline = None;
989 (state.callback)(WatchEvent::Update, debounce_first_path.take().unwrap_or_default());
990 continue;
991 }
992 match tokio::time::timeout(deadline - now, read_future).await {
994 Ok(result) => Some(result),
995 Err(_) => {
996 debounce_deadline = None;
998 (state.callback)(WatchEvent::Update, debounce_first_path.take().unwrap_or_default());
999 continue;
1000 }
1001 }
1002 } else {
1003 Some(read_future.await)
1004 };
1005
1006 let Some(result) = read_result else { continue };
1007
1008 match result {
1009 Ok(len) => {
1010 let events = parse_inotify_events(&buffer, len);
1011 let mut first_matching_path: Option<PathBuf> = None;
1012
1013 for (wd, mask, name_str) in events {
1014 if (mask & libc::IN_IGNORED as u32) != 0 {
1015 if let Some(path) = state.watches.remove(&wd) {
1016 state.paths.remove(&path);
1017 }
1018 continue;
1019 }
1020
1021 let rel_path = if let Some(dir_path) = state.watches.get(&wd) {
1022 dir_path.join(&name_str)
1023 } else {
1024 println!("Warning: received event for unknown watch descriptor {}", wd);
1025 continue;
1026 };
1027
1028 let is_dir = mask & libc::IN_ISDIR as u32 != 0;
1029 let is_create = (mask & libc::IN_CREATE as u32) != 0
1030 || (mask & libc::IN_MOVED_TO as u32) != 0;
1031 let is_delete = (mask & libc::IN_DELETE as u32) != 0
1032 || (mask & libc::IN_MOVED_FROM as u32) != 0;
1033 let is_update = (mask & libc::IN_MODIFY as u32) != 0
1034 || (mask & libc::IN_CLOSE_WRITE as u32) != 0;
1035
1036 if is_dir && is_create {
1037 self.add_watch_recursive(&mut state, rel_path.clone(), false);
1039 }
1040
1041 let event_type = if is_create && self.watch_create {
1042 WatchEvent::Create
1043 } else if is_delete && self.watch_delete {
1044 WatchEvent::Delete
1045 } else if is_update && self.watch_update {
1046 WatchEvent::Update
1047 } else {
1048 continue
1049 };
1050
1051 if !self.check_event(&state, &rel_path, is_dir) {
1052 continue;
1053 }
1054
1055 if first_matching_path.is_none() {
1056 first_matching_path = Some(rel_path.clone());
1057 }
1058
1059 if debounce.is_none() {
1061 self.emit_event(&mut state, event_type, &rel_path);
1062 }
1063 }
1064
1065 if let Some(d) = debounce {
1067 if let Some(path) = first_matching_path {
1068 if debounce_first_path.is_none() {
1069 debounce_first_path = Some(path);
1070 }
1071 debounce_deadline = Some(tokio::time::Instant::now() + d);
1072 }
1073 }
1074 }
1075 Err(e) => {
1076 eprintln!("Error reading inotify events: {}", e);
1077 tokio::time::sleep(Duration::from_millis(100)).await;
1078 }
1079 }
1080 }
1081 }
1082}
1083
1084#[cfg(test)]
1085mod tests {
1086 use super::*;
1087 use std::collections::HashSet;
1088 use std::sync::{Arc, Mutex};
1089 use tokio::task::JoinHandle;
1090
1091 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
1092 enum EventType {
1093 Create,
1094 Delete,
1095 Update,
1096 Initial,
1097 DebugWatch,
1098 }
1099
1100 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
1101 struct Event {
1102 path: PathBuf,
1103 event_type: EventType,
1104 }
1105
1106 type EventTracker = Arc<Mutex<Vec<Event>>>;
1107
1108 struct TestInstance {
1109 test_dir: PathBuf,
1110 tracker: EventTracker,
1111 watcher_handle: Option<JoinHandle<()>>,
1112 }
1113
1114 impl TestInstance {
1115 async fn new<F>(test_name: &str, configure: F) -> Self
1116 where
1117 F: FnOnce(Watcher) -> Watcher + Send + 'static,
1118 {
1119 let test_dir = std::env::current_dir()
1120 .unwrap()
1121 .join(format!(".file-watcher-test-{}", test_name));
1122
1123 if test_dir.exists() {
1124 std::fs::remove_dir_all(&test_dir).unwrap();
1125 }
1126 std::fs::create_dir(&test_dir).unwrap();
1127
1128 let tracker = Arc::new(Mutex::new(Vec::new()));
1129
1130 let tracker_clone = tracker.clone();
1131 let test_dir_clone = test_dir.clone();
1132
1133 let watcher_handle = tokio::spawn(async move {
1134 let watcher = Watcher::new()
1135 .set_base_dir(&test_dir_clone)
1136 .debug_watches(true);
1137
1138 let watcher = configure(watcher);
1139
1140 let _ = watcher
1141 .run(move |event_type, path| {
1142 tracker_clone.lock().unwrap().push(Event {
1143 path: path.clone(),
1144 event_type: match event_type {
1145 WatchEvent::Create => EventType::Create,
1146 WatchEvent::Delete => EventType::Delete,
1147 WatchEvent::Update => EventType::Update,
1148 WatchEvent::Initial => EventType::Initial,
1149 WatchEvent::DebugWatch => EventType::DebugWatch,
1150 },
1151 });
1152 })
1153 .await;
1154 });
1155
1156 tokio::time::sleep(Duration::from_millis(100)).await;
1157
1158 let instance = Self {
1159 test_dir,
1160 tracker,
1161 watcher_handle: Some(watcher_handle),
1162 };
1163
1164 instance.assert_events(&[], &[], &[], &[""]).await;
1165
1166 instance
1167 }
1168
1169 fn create_dir(&self, path: &str) {
1170 std::fs::create_dir_all(self.test_dir.join(path)).unwrap();
1171 }
1172
1173 fn write_file(&self, path: &str, content: &str) {
1174 let full_path = self.test_dir.join(path);
1175 if let Some(parent) = full_path.parent() {
1176 std::fs::create_dir_all(parent).unwrap();
1177 }
1178 std::fs::write(full_path, content).unwrap();
1179 }
1180
1181 fn remove_file(&self, path: &str) {
1182 std::fs::remove_file(self.test_dir.join(path)).unwrap();
1183 }
1184
1185 fn rename(&self, from: &str, to: &str) {
1186 std::fs::rename(self.test_dir.join(from), self.test_dir.join(to)).unwrap();
1187 }
1188
1189 async fn assert_events(
1190 &self,
1191 creates: &[&str],
1192 deletes: &[&str],
1193 updates: &[&str],
1194 watches: &[&str],
1195 ) {
1196 tokio::time::sleep(Duration::from_millis(200)).await;
1197
1198 let events = self.tracker.lock().unwrap().clone();
1199 let mut expected = HashSet::new();
1200
1201 for create in creates {
1202 expected.insert(Event {
1203 path: PathBuf::from(create),
1204 event_type: EventType::Create,
1205 });
1206 }
1207
1208 for delete in deletes {
1209 expected.insert(Event {
1210 path: PathBuf::from(delete),
1211 event_type: EventType::Delete,
1212 });
1213 }
1214
1215 for update in updates {
1216 expected.insert(Event {
1217 path: PathBuf::from(update),
1218 event_type: EventType::Update,
1219 });
1220 }
1221
1222 for watch in watches {
1223 expected.insert(Event {
1224 path: PathBuf::from(watch),
1225 event_type: EventType::DebugWatch,
1226 });
1227 }
1228
1229 let actual: HashSet<Event> = events.iter().cloned().collect();
1230
1231 for event in &actual {
1232 if !expected.contains(event) {
1233 panic!("Unexpected event: {:?}", event);
1234 }
1235 }
1236
1237 for event in &expected {
1238 if !actual.contains(event) {
1239 panic!(
1240 "Missing expected event: {:?}\nActual events: {:?}",
1241 event, actual
1242 );
1243 }
1244 }
1245
1246 self.tracker.lock().unwrap().clear();
1247 }
1248
1249 async fn assert_no_events(&self) {
1250 tokio::time::sleep(Duration::from_millis(500)).await;
1251 let events = self.tracker.lock().unwrap();
1252 assert_eq!(
1253 events.len(),
1254 0,
1255 "Expected no events, but got: {:?}",
1256 events
1257 );
1258 }
1259 }
1260
1261 impl Drop for TestInstance {
1262 fn drop(&mut self) {
1263 if let Some(handle) = self.watcher_handle.take() {
1264 handle.abort();
1265 }
1266 if self.test_dir.exists() {
1267 let _ = std::fs::remove_dir_all(&self.test_dir);
1268 }
1269 }
1270 }
1271
1272 #[tokio::test]
1273 async fn test_file_create_update_delete() {
1274 let test = TestInstance::new("create_update_delete", |b| b.add_include("**/*")).await;
1275
1276 test.write_file("test.txt", "");
1277 test.assert_events(&["test.txt"], &[], &["test.txt"], &[])
1278 .await;
1279
1280 test.write_file("test.txt", "hello");
1281 test.assert_events(&[], &[], &["test.txt"], &[]).await;
1282
1283 test.remove_file("test.txt");
1284 test.assert_events(&[], &["test.txt"], &[], &[]).await;
1285 }
1286
1287 #[tokio::test]
1288 async fn test_directory_operations() {
1289 let test = TestInstance::new("directory_operations", |b| b.add_include("**/*")).await;
1290
1291 test.create_dir("subdir");
1292 test.assert_events(&["subdir"], &[], &[], &["subdir"]).await;
1293
1294 test.write_file("subdir/file.txt", "");
1295 test.assert_events(&["subdir/file.txt"], &[], &["subdir/file.txt"], &[])
1296 .await;
1297 }
1298
1299 #[tokio::test]
1300 async fn test_move_operations() {
1301 let test = TestInstance::new("move_operations", |b| b.add_include("**/*")).await;
1302
1303 test.write_file("old.txt", "content");
1304 test.assert_events(&["old.txt"], &[], &["old.txt"], &[])
1305 .await;
1306
1307 test.rename("old.txt", "new.txt");
1308 test.assert_events(&["new.txt"], &["old.txt"], &[], &[])
1309 .await;
1310 }
1311
1312 #[tokio::test]
1313 async fn test_event_filtering() {
1314 let test = TestInstance::new("event_filtering", |b| {
1315 b.add_include("**/*")
1316 .watch_create(true)
1317 .watch_delete(false)
1318 .watch_update(false)
1319 })
1320 .await;
1321
1322 test.write_file("test.txt", "");
1323 test.assert_events(&["test.txt"], &[], &[], &[]).await;
1324
1325 test.write_file("test.txt", "hello");
1326 test.assert_no_events().await;
1327
1328 test.remove_file("test.txt");
1329 test.assert_no_events().await;
1330 }
1331
1332 #[tokio::test]
1333 async fn test_pattern_matching() {
1334 let test = TestInstance::new("pattern_matching", |b| b.add_include("**/*.txt")).await;
1335
1336 test.write_file("test.txt", "");
1337 test.assert_events(&["test.txt"], &[], &["test.txt"], &[])
1338 .await;
1339
1340 test.write_file("test.rs", "");
1341 test.assert_no_events().await;
1342 }
1343
1344 #[tokio::test]
1345 async fn test_matching_stops_at_depth() {
1346 let test = TestInstance::new("matching_stops_at_depth", |b| b.add_include("*/xyz/*.*")).await;
1347
1348 test.write_file("test.txt", "");
1349 test.assert_no_events().await;
1350
1351 test.create_dir("abc/xyz");
1352 test.assert_events(&[], &[], &[], &["abc", "abc/xyz"]).await;
1353
1354 test.create_dir("abc/hjk/a.b");
1355 test.assert_no_events().await;
1356
1357 test.create_dir("abc/xyz/a.b");
1358 test.assert_events(&["abc/xyz/a.b"], &[], &[], &[]).await; test.create_dir("abc/xyz/a.b/x.y");
1361 test.assert_events(&[], &[], &[], &[]).await;
1362 }
1363
1364 #[tokio::test]
1365 async fn test_exclude_prevents_watching() {
1366 let test = TestInstance::new("exclude_prevents_watch", |b| {
1367 b.add_include("**/*").add_exclude("node_modules/**")
1368 })
1369 .await;
1370
1371 test.create_dir("node_modules");
1372 tokio::time::sleep(Duration::from_millis(200)).await;
1373
1374 test.write_file("node_modules/package.json", "");
1375 test.assert_no_events().await;
1376
1377 test.write_file("test.txt", "");
1378 test.assert_events(&["test.txt"], &[], &["test.txt"], &[])
1379 .await;
1380 }
1381
1382 #[tokio::test]
1383 async fn test_pattern_file() {
1384 let test_dir = std::env::current_dir()
1386 .unwrap()
1387 .join(".file-watcher-test-pattern_file");
1388
1389 if test_dir.exists() {
1390 std::fs::remove_dir_all(&test_dir).unwrap();
1391 }
1392 std::fs::create_dir(&test_dir).unwrap();
1393
1394 std::fs::write(
1396 test_dir.join(".watchignore"),
1397 "# Comment line\nignored/**\n",
1398 )
1399 .unwrap();
1400
1401 let tracker = Arc::new(Mutex::new(Vec::<Event>::new()));
1403 let tracker_clone = tracker.clone();
1404 let test_dir_clone = test_dir.clone();
1405
1406 let watcher_handle = tokio::spawn(async move {
1407 let _ = Watcher::new()
1408 .set_base_dir(&test_dir_clone)
1409 .debug_watches(true)
1410 .add_include("**/*")
1411 .add_ignore_file(".watchignore")
1412 .run(move |event_type, path| {
1413 tracker_clone.lock().unwrap().push(Event {
1414 path: path.clone(),
1415 event_type: match event_type {
1416 WatchEvent::Create => EventType::Create,
1417 WatchEvent::Delete => EventType::Delete,
1418 WatchEvent::Update => EventType::Update,
1419 WatchEvent::Initial => EventType::Initial,
1420 WatchEvent::DebugWatch => EventType::DebugWatch,
1421 },
1422 });
1423 })
1424 .await;
1425 });
1426
1427 tokio::time::sleep(Duration::from_millis(100)).await;
1428 tracker.lock().unwrap().clear(); std::fs::create_dir(test_dir.join("ignored")).unwrap();
1432 tokio::time::sleep(Duration::from_millis(200)).await;
1433
1434 std::fs::write(test_dir.join("ignored/test.txt"), "").unwrap();
1436 tokio::time::sleep(Duration::from_millis(200)).await;
1437
1438 {
1440 let events = tracker.lock().unwrap();
1441 let has_ignored_events = events.iter().any(|e| {
1442 e.path.to_string_lossy().contains("ignored")
1443 && e.event_type != EventType::DebugWatch
1444 });
1445 assert!(
1446 !has_ignored_events,
1447 "Expected no events for ignored files, but got: {:?}",
1448 events
1449 );
1450 }
1451 tracker.lock().unwrap().clear();
1452
1453 std::fs::write(test_dir.join("normal.txt"), "").unwrap();
1455 tokio::time::sleep(Duration::from_millis(200)).await;
1456
1457 {
1458 let events = tracker.lock().unwrap();
1459 let has_normal = events
1460 .iter()
1461 .any(|e| e.path == PathBuf::from("normal.txt"));
1462 assert!(has_normal, "Expected event for normal.txt, got: {:?}", events);
1463 }
1464
1465 watcher_handle.abort();
1467 let _ = std::fs::remove_dir_all(&test_dir);
1468 }
1469
1470 #[tokio::test]
1471 async fn test_watch_initial() {
1472 let test_dir = std::env::current_dir()
1474 .unwrap()
1475 .join(".file-watcher-test-watch_initial");
1476
1477 if test_dir.exists() {
1478 std::fs::remove_dir_all(&test_dir).unwrap();
1479 }
1480 std::fs::create_dir(&test_dir).unwrap();
1481
1482 std::fs::write(test_dir.join("existing1.txt"), "content1").unwrap();
1484 std::fs::write(test_dir.join("existing2.txt"), "content2").unwrap();
1485 std::fs::create_dir(test_dir.join("subdir")).unwrap();
1486 std::fs::write(test_dir.join("subdir/nested.txt"), "nested").unwrap();
1487 std::fs::write(test_dir.join("ignored.rs"), "should be ignored").unwrap();
1488
1489 let tracker = Arc::new(Mutex::new(Vec::<Event>::new()));
1490 let tracker_clone = tracker.clone();
1491 let test_dir_clone = test_dir.clone();
1492
1493 let watcher_handle = tokio::spawn(async move {
1494 let _ = Watcher::new()
1495 .set_base_dir(&test_dir_clone)
1496 .add_include("**/*.txt")
1497 .watch_initial(true)
1498 .run(move |event_type, path| {
1499 tracker_clone.lock().unwrap().push(Event {
1500 path: path.clone(),
1501 event_type: match event_type {
1502 WatchEvent::Create => EventType::Create,
1503 WatchEvent::Delete => EventType::Delete,
1504 WatchEvent::Update => EventType::Update,
1505 WatchEvent::Initial => EventType::Initial,
1506 WatchEvent::DebugWatch => EventType::DebugWatch,
1507 },
1508 });
1509 })
1510 .await;
1511 });
1512
1513 tokio::time::sleep(Duration::from_millis(200)).await;
1514
1515 {
1517 let events = tracker.lock().unwrap();
1518 let initial_events: Vec<_> = events
1519 .iter()
1520 .filter(|e| e.event_type == EventType::Initial)
1521 .collect();
1522
1523 assert_eq!(
1524 initial_events.len(),
1525 3,
1526 "Expected 3 Initial events, got: {:?}",
1527 initial_events
1528 );
1529
1530 let paths: HashSet<_> = initial_events.iter().map(|e| e.path.clone()).collect();
1531 assert!(paths.contains(&PathBuf::from("existing1.txt")));
1532 assert!(paths.contains(&PathBuf::from("existing2.txt")));
1533 assert!(paths.contains(&PathBuf::from("subdir/nested.txt")));
1534
1535 assert!(!events.iter().any(|e| e.path.to_string_lossy().contains("ignored.rs")));
1537 }
1538
1539 tracker.lock().unwrap().clear();
1540
1541 std::fs::write(test_dir.join("new.txt"), "new content").unwrap();
1543 tokio::time::sleep(Duration::from_millis(200)).await;
1544
1545 {
1546 let events = tracker.lock().unwrap();
1547 let has_create = events
1548 .iter()
1549 .any(|e| e.path == PathBuf::from("new.txt") && e.event_type == EventType::Create);
1550 assert!(has_create, "Expected Create event for new.txt, got: {:?}", events);
1551 }
1552
1553 watcher_handle.abort();
1555 let _ = std::fs::remove_dir_all(&test_dir);
1556 }
1557
1558 #[tokio::test]
1559 async fn test_watch_initial_with_dirs() {
1560 let test_dir = std::env::current_dir()
1562 .unwrap()
1563 .join(".file-watcher-test-watch_initial_dirs");
1564
1565 if test_dir.exists() {
1566 std::fs::remove_dir_all(&test_dir).unwrap();
1567 }
1568 std::fs::create_dir(&test_dir).unwrap();
1569
1570 std::fs::write(test_dir.join("file.txt"), "content").unwrap();
1572 std::fs::create_dir(test_dir.join("mydir")).unwrap();
1573
1574 let tracker = Arc::new(Mutex::new(Vec::<Event>::new()));
1575 let tracker_clone = tracker.clone();
1576 let test_dir_clone = test_dir.clone();
1577
1578 let watcher_handle = tokio::spawn(async move {
1579 let _ = Watcher::new()
1580 .set_base_dir(&test_dir_clone)
1581 .add_include("**/*")
1582 .watch_initial(true)
1583 .match_files(true)
1584 .match_dirs(false) .run(move |event_type, path| {
1586 tracker_clone.lock().unwrap().push(Event {
1587 path: path.clone(),
1588 event_type: match event_type {
1589 WatchEvent::Create => EventType::Create,
1590 WatchEvent::Delete => EventType::Delete,
1591 WatchEvent::Update => EventType::Update,
1592 WatchEvent::Initial => EventType::Initial,
1593 WatchEvent::DebugWatch => EventType::DebugWatch,
1594 },
1595 });
1596 })
1597 .await;
1598 });
1599
1600 tokio::time::sleep(Duration::from_millis(200)).await;
1601
1602 {
1603 let events = tracker.lock().unwrap();
1604 let initial_events: Vec<_> = events
1605 .iter()
1606 .filter(|e| e.event_type == EventType::Initial)
1607 .collect();
1608
1609 assert_eq!(
1611 initial_events.len(),
1612 1,
1613 "Expected 1 Initial event (file only), got: {:?}",
1614 initial_events
1615 );
1616 assert_eq!(initial_events[0].path, PathBuf::from("file.txt"));
1617 }
1618
1619 watcher_handle.abort();
1621 let _ = std::fs::remove_dir_all(&test_dir);
1622 }
1623
1624 #[tokio::test]
1625 async fn test_watch_initial_disabled_by_default() {
1626 let test_dir = std::env::current_dir()
1628 .unwrap()
1629 .join(".file-watcher-test-watch_initial_disabled");
1630
1631 if test_dir.exists() {
1632 std::fs::remove_dir_all(&test_dir).unwrap();
1633 }
1634 std::fs::create_dir(&test_dir).unwrap();
1635
1636 std::fs::write(test_dir.join("existing.txt"), "content").unwrap();
1638
1639 let tracker = Arc::new(Mutex::new(Vec::<Event>::new()));
1640 let tracker_clone = tracker.clone();
1641 let test_dir_clone = test_dir.clone();
1642
1643 let watcher_handle = tokio::spawn(async move {
1644 let _ = Watcher::new()
1645 .set_base_dir(&test_dir_clone)
1646 .add_include("**/*.txt")
1647 .run(move |event_type, path| {
1649 tracker_clone.lock().unwrap().push(Event {
1650 path: path.clone(),
1651 event_type: match event_type {
1652 WatchEvent::Create => EventType::Create,
1653 WatchEvent::Delete => EventType::Delete,
1654 WatchEvent::Update => EventType::Update,
1655 WatchEvent::Initial => EventType::Initial,
1656 WatchEvent::DebugWatch => EventType::DebugWatch,
1657 },
1658 });
1659 })
1660 .await;
1661 });
1662
1663 tokio::time::sleep(Duration::from_millis(200)).await;
1664
1665 {
1666 let events = tracker.lock().unwrap();
1667 let initial_events: Vec<_> = events
1668 .iter()
1669 .filter(|e| e.event_type == EventType::Initial)
1670 .collect();
1671
1672 assert_eq!(
1674 initial_events.len(),
1675 0,
1676 "Expected no Initial events when watch_initial is disabled, got: {:?}",
1677 initial_events
1678 );
1679 }
1680
1681 watcher_handle.abort();
1683 let _ = std::fs::remove_dir_all(&test_dir);
1684 }
1685}