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 resolve_base_dir(base_dir: PathBuf) -> PathBuf {
453 if base_dir.is_absolute() {
454 base_dir
455 } else {
456 std::env::current_dir()
457 .unwrap_or_else(|_| PathBuf::from("/"))
458 .join(base_dir)
459 }
460}
461
462fn path_to_segments(path: &Path) -> Vec<String> {
463 let path_str = path.to_string_lossy();
464 let path_str = path_str.replace("//", "/");
465 path_str
466 .split('/')
467 .filter(|s| !s.is_empty())
468 .map(|s| s.to_string())
469 .collect()
470}
471
472fn should_watch(
473 relative_path: &Path,
474 include_patterns: &[Pattern],
475 exclude_patterns: &[Pattern],
476 is_dir: bool,
477) -> bool {
478 let segments = path_to_segments(relative_path);
479
480 if exclude_patterns.iter().any(|p| p.check(&segments, false)) {
481 return false;
482 }
483
484 include_patterns.iter().any(|p| p.check(&segments, is_dir))
485}
486
487const INOTIFY_MASK: u32 = libc::IN_MODIFY
488 | libc::IN_CLOSE_WRITE
489 | libc::IN_CREATE
490 | libc::IN_DELETE
491 | libc::IN_MOVED_FROM
492 | libc::IN_MOVED_TO
493 | libc::IN_DONT_FOLLOW;
494
495
496fn add_watch_recursive<F>(
497 initial_path: PathBuf,
498 root: &Path,
499 inotify: &Inotify,
500 watches: &mut HashMap<i32, PathBuf>,
501 paths: &mut HashSet<PathBuf>,
502 include_patterns: &[Pattern],
503 exclude_patterns: &[Pattern],
504 debug_watches_enabled: bool,
505 return_absolute: bool,
506 callback: &mut F,
507) where
508 F: FnMut(WatchEvent, PathBuf),
509{
510 if paths.contains(&initial_path) {
511 return;
512 }
513
514 let mut stack = vec![initial_path];
515 while let Some(rel_path) = stack.pop() {
516 if !should_watch(&rel_path, include_patterns, exclude_patterns, true) {
517 continue;
518 }
519
520 let full_path = if rel_path.as_os_str().is_empty() {
521 root.to_path_buf()
522 } else {
523 root.join(&rel_path)
524 };
525
526 if !full_path.is_dir() {
527 continue;
528 }
529
530 let wd = match inotify.add_watch(&full_path, INOTIFY_MASK) {
531 Ok(wd) => wd,
532 Err(e) => {
533 eprintln!("Failed to add watch for {:?}: {}", full_path, e);
534 continue;
535 }
536 };
537
538 paths.insert(rel_path.clone());
539 watches.insert(wd, rel_path.clone());
540
541 if debug_watches_enabled {
542 let callback_path = if return_absolute {
543 full_path.clone()
544 } else {
545 rel_path.clone()
546 };
547 callback(WatchEvent::DebugWatch, callback_path);
548 }
549
550 if let Ok(entries) = std::fs::read_dir(&full_path) {
551 for entry in entries.flatten() {
552 if let Ok(ft) = entry.file_type() {
553 if ft.is_dir() {
554 let child_rel_path = rel_path.join(entry.file_name());
555 if !paths.contains(&child_rel_path) {
556 stack.push(child_rel_path);
557 }
558 }
559 }
560 }
561 }
562 }
563}
564
565fn parse_inotify_events(buffer: &[u8], len: usize) -> Vec<(i32, u32, String)> {
566 let mut events = Vec::new();
567 let mut ptr = buffer.as_ptr();
568 let end = unsafe { ptr.add(len) };
569
570 while ptr < end {
571 let event = unsafe { &*(ptr as *const libc::inotify_event) };
572 let name_len = event.len as usize;
573
574 if name_len > 0 {
575 let name_ptr = unsafe { ptr.add(std::mem::size_of::<libc::inotify_event>()) };
576 let name_slice =
577 unsafe { std::slice::from_raw_parts(name_ptr as *const u8, name_len) };
578 let name_str = String::from_utf8_lossy(name_slice)
579 .trim_matches(char::from(0))
580 .to_string();
581 events.push((event.wd, event.mask, name_str));
582 }
583
584 ptr = unsafe { ptr.add(std::mem::size_of::<libc::inotify_event>() + name_len) };
585 }
586
587 events
588}
589
590#[derive(Debug, Clone, Copy, PartialEq, Eq)]
595pub enum WatchEvent {
596 Create,
600 Delete,
604 Update,
610 DebugWatch,
615}
616
617pub struct WatchBuilder {
640 includes: Option<Vec<String>>,
641 excludes: Vec<String>,
642 base_dir: PathBuf,
643 watch_create: bool,
644 watch_delete: bool,
645 watch_update: bool,
646 match_files: bool,
647 match_dirs: bool,
648 return_absolute: bool,
649 debug_watches_enabled: bool,
650}
651
652impl Default for WatchBuilder {
653 fn default() -> Self {
654 Self::new()
655 }
656}
657
658impl WatchBuilder {
659 pub fn new() -> Self {
669 WatchBuilder {
670 includes: Some(Vec::new()),
671 excludes: Vec::new(),
672 base_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/")),
673 watch_create: true,
674 watch_delete: true,
675 watch_update: true,
676 match_files: true,
677 match_dirs: true,
678 return_absolute: false,
679 debug_watches_enabled: false,
680 }
681 }
682
683 pub fn debug_watches(mut self, enabled: bool) -> Self {
688 self.debug_watches_enabled = enabled;
689 self
690 }
691
692 pub fn add_include(mut self, pattern: impl Into<String>) -> Self {
703 if self.includes.is_none() {
704 self.includes = Some(Vec::new());
705 }
706 self.includes.as_mut().unwrap().push(pattern.into());
707 self
708 }
709
710 pub fn add_includes(mut self, patterns: impl IntoIterator<Item = impl Into<String>>) -> Self {
712 if self.includes.is_none() {
713 self.includes = Some(Vec::new());
714 }
715 self.includes
716 .as_mut()
717 .unwrap()
718 .extend(patterns.into_iter().map(|p| p.into()));
719 self
720 }
721
722 pub fn add_exclude(mut self, pattern: impl Into<String>) -> Self {
726 self.excludes.push(pattern.into());
727 self
728 }
729
730 pub fn add_excludes(mut self, patterns: impl IntoIterator<Item = impl Into<String>>) -> Self {
732 self.excludes
733 .extend(patterns.into_iter().map(|p| p.into()));
734 self
735 }
736
737 pub fn add_ignore_file(mut self, path: impl AsRef<Path>) -> Self {
764 let path = path.as_ref();
765
766 let full_path = if path.is_absolute() {
768 path.to_path_buf()
769 } else {
770 self.base_dir.join(path)
771 };
772
773 if let Ok(file) = fs::File::open(&full_path) {
774 let reader = BufReader::new(file);
775 let mut has_negation = false;
776 for line in reader.lines().map_while(Result::ok) {
777 let trimmed = line.trim();
778
779 if trimmed.is_empty() || trimmed.starts_with('#') {
781 continue;
782 }
783
784 if trimmed.starts_with('!') {
786 has_negation = true;
787 } else {
788 self.excludes.push(trimmed.to_string());
790 }
791 }
792 if has_negation {
793 println!("Warning: negation patterns (!) in {} are ignored; excludes always take precedence over includes in this library", full_path.display());
794 }
795 }
796
797 self
798 }
799
800 pub fn set_base_dir(mut self, base_dir: impl Into<PathBuf>) -> Self {
805 self.base_dir = base_dir.into();
806 self
807 }
808
809 pub fn watch_create(mut self, enabled: bool) -> Self {
813 self.watch_create = enabled;
814 self
815 }
816
817 pub fn watch_delete(mut self, enabled: bool) -> Self {
821 self.watch_delete = enabled;
822 self
823 }
824
825 pub fn watch_update(mut self, enabled: bool) -> Self {
829 self.watch_update = enabled;
830 self
831 }
832
833 pub fn match_files(mut self, enabled: bool) -> Self {
837 self.match_files = enabled;
838 self
839 }
840
841 pub fn match_dirs(mut self, enabled: bool) -> Self {
845 self.match_dirs = enabled;
846 self
847 }
848
849 pub fn return_absolute(mut self, enabled: bool) -> Self {
854 self.return_absolute = enabled;
855 self
856 }
857
858 pub async fn run<F>(self, callback: F) -> Result<()>
866 where
867 F: FnMut(WatchEvent, PathBuf),
868 {
869 self.run_internal(callback, None).await
870 }
871
872 pub async fn run_debounced<F>(self, ms: u64, mut callback: F) -> Result<()>
881 where
882 F: FnMut(),
883 {
884 self.run_internal(|_, _| callback(), Some(Duration::from_millis(ms))).await
885 }
886
887 async fn run_internal<F>(self, mut callback: F, debounce: Option<Duration>) -> Result<()>
888 where
889 F: FnMut(WatchEvent, PathBuf),
890 {
891 let includes = if let Some(includes) = self.includes {
892 includes
893 } else {
894 vec!["**".to_string()]
895 };
896
897 if includes.is_empty() {
899 loop {
900 tokio::time::sleep(Duration::from_secs(3600)).await;
901 }
902 }
903
904 let excludes = self.excludes;
905 let root = self.base_dir.clone();
906 let watch_create = self.watch_create;
907 let watch_delete = self.watch_delete;
908 let watch_update = self.watch_update;
909 let match_files = self.match_files;
910 let match_dirs = self.match_dirs;
911 let return_absolute = self.return_absolute;
912 let debug_watches_enabled = self.debug_watches_enabled;
913
914 let root = resolve_base_dir(root);
915
916 let include_patterns: Vec<Pattern> = includes.iter().map(|p| Pattern::parse(p)).collect();
917 let exclude_patterns: Vec<Pattern> = excludes.iter().map(|p| Pattern::parse(p)).collect();
918
919 let inotify = Inotify::new()?;
920 let mut watches = HashMap::<i32, PathBuf>::new();
921 let mut paths = HashSet::<PathBuf>::new();
922
923 add_watch_recursive(
925 PathBuf::new(),
926 &root,
927 &inotify,
928 &mut watches,
929 &mut paths,
930 &include_patterns,
931 &exclude_patterns,
932 debug_watches_enabled,
933 return_absolute,
934 &mut callback,
935 );
936
937 let mut debounce_deadline: Option<tokio::time::Instant> = None;
939
940 let mut buffer = [0u8; 8192];
942 loop {
943 let read_future = inotify.read_events(&mut buffer);
945
946 let read_result = if let Some(deadline) = debounce_deadline {
947 let now = tokio::time::Instant::now();
948 if deadline <= now {
949 debounce_deadline = None;
951 callback(WatchEvent::Update, PathBuf::new());
952 continue;
953 }
954 match tokio::time::timeout(deadline - now, read_future).await {
956 Ok(result) => Some(result),
957 Err(_) => {
958 debounce_deadline = None;
960 callback(WatchEvent::Update, PathBuf::new());
961 continue;
962 }
963 }
964 } else {
965 Some(read_future.await)
966 };
967
968 let Some(result) = read_result else { continue };
969
970 match result {
971 Ok(len) => {
972 let events = parse_inotify_events(&buffer, len);
973 let mut had_matching_event = false;
974
975 for (wd, mask, name_str) in events {
976 if (mask & libc::IN_IGNORED as u32) != 0 {
977 if let Some(path) = watches.remove(&wd) {
978 paths.remove(&path);
979 }
980 continue;
981 }
982
983 let rel_path = if let Some(dir_path) = watches.get(&wd) {
984 dir_path.join(&name_str)
985 } else {
986 println!("Warning: received event for unknown watch descriptor {}", wd);
987 continue;
988 };
989
990 let is_dir = mask & libc::IN_ISDIR as u32 != 0;
991 let is_create = (mask & libc::IN_CREATE as u32) != 0
992 || (mask & libc::IN_MOVED_TO as u32) != 0;
993 let is_delete = (mask & libc::IN_DELETE as u32) != 0
994 || (mask & libc::IN_MOVED_FROM as u32) != 0;
995 let is_update = (mask & libc::IN_MODIFY as u32) != 0
996 || (mask & libc::IN_CLOSE_WRITE as u32) != 0;
997
998
999 if is_dir && is_create {
1000 add_watch_recursive(
1002 rel_path.clone(),
1003 &root,
1004 &inotify,
1005 &mut watches,
1006 &mut paths,
1007 &include_patterns,
1008 &exclude_patterns,
1009 debug_watches_enabled,
1010 return_absolute,
1011 &mut callback,
1012 );
1013 }
1014
1015 let event_type = if is_create && watch_create {
1016 WatchEvent::Create
1017 } else if is_delete && watch_delete {
1018 WatchEvent::Delete
1019 } else if is_update && watch_update {
1020 WatchEvent::Update
1021 } else {
1022 continue
1023 };
1024
1025 if if is_dir { !match_dirs } else { !match_files } {
1026 continue;
1027 }
1028
1029 if !should_watch(&rel_path, &include_patterns, &exclude_patterns, false) {
1030 continue;
1031 }
1032
1033 had_matching_event = true;
1034
1035 if debounce.is_none() {
1037 let callback_path = if return_absolute {
1038 if rel_path.as_os_str().is_empty() {
1039 root.clone()
1040 } else {
1041 root.join(&rel_path)
1042 }
1043 } else {
1044 rel_path
1045 };
1046 callback(event_type, callback_path);
1047 }
1048 }
1049
1050 if let Some(d) = debounce {
1052 if had_matching_event {
1053 debounce_deadline = Some(tokio::time::Instant::now() + d);
1054 }
1055 }
1056 }
1057 Err(e) => {
1058 eprintln!("Error reading inotify events: {}", e);
1059 tokio::time::sleep(Duration::from_millis(100)).await;
1060 }
1061 }
1062 }
1063 }
1064}
1065
1066#[cfg(test)]
1067mod tests {
1068 use super::*;
1069 use std::collections::HashSet;
1070 use std::sync::{Arc, Mutex};
1071 use tokio::task::JoinHandle;
1072
1073 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
1074 enum EventType {
1075 Create,
1076 Delete,
1077 Update,
1078 DebugWatch,
1079 }
1080
1081 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
1082 struct Event {
1083 path: PathBuf,
1084 event_type: EventType,
1085 }
1086
1087 type EventTracker = Arc<Mutex<Vec<Event>>>;
1088
1089 struct TestInstance {
1090 test_dir: PathBuf,
1091 tracker: EventTracker,
1092 watcher_handle: Option<JoinHandle<()>>,
1093 }
1094
1095 impl TestInstance {
1096 async fn new<F>(test_name: &str, configure: F) -> Self
1097 where
1098 F: FnOnce(WatchBuilder) -> WatchBuilder + Send + 'static,
1099 {
1100 let test_dir = std::env::current_dir()
1101 .unwrap()
1102 .join(format!(".file-watcher-test-{}", test_name));
1103
1104 if test_dir.exists() {
1105 std::fs::remove_dir_all(&test_dir).unwrap();
1106 }
1107 std::fs::create_dir(&test_dir).unwrap();
1108
1109 let tracker = Arc::new(Mutex::new(Vec::new()));
1110
1111 let tracker_clone = tracker.clone();
1112 let test_dir_clone = test_dir.clone();
1113
1114 let watcher_handle = tokio::spawn(async move {
1115 let builder = WatchBuilder::new()
1116 .set_base_dir(&test_dir_clone)
1117 .debug_watches(true);
1118
1119 let builder = configure(builder);
1120
1121 let _ = builder
1122 .run(move |event_type, path| {
1123 tracker_clone.lock().unwrap().push(Event {
1124 path: path.clone(),
1125 event_type: match event_type {
1126 WatchEvent::Create => EventType::Create,
1127 WatchEvent::Delete => EventType::Delete,
1128 WatchEvent::Update => EventType::Update,
1129 WatchEvent::DebugWatch => EventType::DebugWatch,
1130 },
1131 });
1132 })
1133 .await;
1134 });
1135
1136 tokio::time::sleep(Duration::from_millis(100)).await;
1137
1138 let instance = Self {
1139 test_dir,
1140 tracker,
1141 watcher_handle: Some(watcher_handle),
1142 };
1143
1144 instance.assert_events(&[], &[], &[], &[""]).await;
1145
1146 instance
1147 }
1148
1149 fn create_dir(&self, path: &str) {
1150 std::fs::create_dir_all(self.test_dir.join(path)).unwrap();
1151 }
1152
1153 fn write_file(&self, path: &str, content: &str) {
1154 let full_path = self.test_dir.join(path);
1155 if let Some(parent) = full_path.parent() {
1156 std::fs::create_dir_all(parent).unwrap();
1157 }
1158 std::fs::write(full_path, content).unwrap();
1159 }
1160
1161 fn remove_file(&self, path: &str) {
1162 std::fs::remove_file(self.test_dir.join(path)).unwrap();
1163 }
1164
1165 fn rename(&self, from: &str, to: &str) {
1166 std::fs::rename(self.test_dir.join(from), self.test_dir.join(to)).unwrap();
1167 }
1168
1169 async fn assert_events(
1170 &self,
1171 creates: &[&str],
1172 deletes: &[&str],
1173 updates: &[&str],
1174 watches: &[&str],
1175 ) {
1176 tokio::time::sleep(Duration::from_millis(200)).await;
1177
1178 let events = self.tracker.lock().unwrap().clone();
1179 let mut expected = HashSet::new();
1180
1181 for create in creates {
1182 expected.insert(Event {
1183 path: PathBuf::from(create),
1184 event_type: EventType::Create,
1185 });
1186 }
1187
1188 for delete in deletes {
1189 expected.insert(Event {
1190 path: PathBuf::from(delete),
1191 event_type: EventType::Delete,
1192 });
1193 }
1194
1195 for update in updates {
1196 expected.insert(Event {
1197 path: PathBuf::from(update),
1198 event_type: EventType::Update,
1199 });
1200 }
1201
1202 for watch in watches {
1203 expected.insert(Event {
1204 path: PathBuf::from(watch),
1205 event_type: EventType::DebugWatch,
1206 });
1207 }
1208
1209 let actual: HashSet<Event> = events.iter().cloned().collect();
1210
1211 for event in &actual {
1212 if !expected.contains(event) {
1213 panic!("Unexpected event: {:?}", event);
1214 }
1215 }
1216
1217 for event in &expected {
1218 if !actual.contains(event) {
1219 panic!(
1220 "Missing expected event: {:?}\nActual events: {:?}",
1221 event, actual
1222 );
1223 }
1224 }
1225
1226 self.tracker.lock().unwrap().clear();
1227 }
1228
1229 async fn assert_no_events(&self) {
1230 tokio::time::sleep(Duration::from_millis(500)).await;
1231 let events = self.tracker.lock().unwrap();
1232 assert_eq!(
1233 events.len(),
1234 0,
1235 "Expected no events, but got: {:?}",
1236 events
1237 );
1238 }
1239 }
1240
1241 impl Drop for TestInstance {
1242 fn drop(&mut self) {
1243 if let Some(handle) = self.watcher_handle.take() {
1244 handle.abort();
1245 }
1246 if self.test_dir.exists() {
1247 let _ = std::fs::remove_dir_all(&self.test_dir);
1248 }
1249 }
1250 }
1251
1252 #[tokio::test]
1253 async fn test_file_create_update_delete() {
1254 let test = TestInstance::new("create_update_delete", |b| b.add_include("**/*")).await;
1255
1256 test.write_file("test.txt", "");
1257 test.assert_events(&["test.txt"], &[], &["test.txt"], &[])
1258 .await;
1259
1260 test.write_file("test.txt", "hello");
1261 test.assert_events(&[], &[], &["test.txt"], &[]).await;
1262
1263 test.remove_file("test.txt");
1264 test.assert_events(&[], &["test.txt"], &[], &[]).await;
1265 }
1266
1267 #[tokio::test]
1268 async fn test_directory_operations() {
1269 let test = TestInstance::new("directory_operations", |b| b.add_include("**/*")).await;
1270
1271 test.create_dir("subdir");
1272 test.assert_events(&["subdir"], &[], &[], &["subdir"]).await;
1273
1274 test.write_file("subdir/file.txt", "");
1275 test.assert_events(&["subdir/file.txt"], &[], &["subdir/file.txt"], &[])
1276 .await;
1277 }
1278
1279 #[tokio::test]
1280 async fn test_move_operations() {
1281 let test = TestInstance::new("move_operations", |b| b.add_include("**/*")).await;
1282
1283 test.write_file("old.txt", "content");
1284 test.assert_events(&["old.txt"], &[], &["old.txt"], &[])
1285 .await;
1286
1287 test.rename("old.txt", "new.txt");
1288 test.assert_events(&["new.txt"], &["old.txt"], &[], &[])
1289 .await;
1290 }
1291
1292 #[tokio::test]
1293 async fn test_event_filtering() {
1294 let test = TestInstance::new("event_filtering", |b| {
1295 b.add_include("**/*")
1296 .watch_create(true)
1297 .watch_delete(false)
1298 .watch_update(false)
1299 })
1300 .await;
1301
1302 test.write_file("test.txt", "");
1303 test.assert_events(&["test.txt"], &[], &[], &[]).await;
1304
1305 test.write_file("test.txt", "hello");
1306 test.assert_no_events().await;
1307
1308 test.remove_file("test.txt");
1309 test.assert_no_events().await;
1310 }
1311
1312 #[tokio::test]
1313 async fn test_pattern_matching() {
1314 let test = TestInstance::new("pattern_matching", |b| b.add_include("**/*.txt")).await;
1315
1316 test.write_file("test.txt", "");
1317 test.assert_events(&["test.txt"], &[], &["test.txt"], &[])
1318 .await;
1319
1320 test.write_file("test.rs", "");
1321 test.assert_no_events().await;
1322 }
1323
1324 #[tokio::test]
1325 async fn test_matching_stops_at_depth() {
1326 let test = TestInstance::new("matching_stops_at_depth", |b| b.add_include("*/xyz/*.*")).await;
1327
1328 test.write_file("test.txt", "");
1329 test.assert_no_events().await;
1330
1331 test.create_dir("abc/xyz");
1332 test.assert_events(&[], &[], &[], &["abc", "abc/xyz"]).await;
1333
1334 test.create_dir("abc/hjk/a.b");
1335 test.assert_no_events().await;
1336
1337 test.create_dir("abc/xyz/a.b");
1338 test.assert_events(&["abc/xyz/a.b"], &[], &[], &[]).await; test.create_dir("abc/xyz/a.b/x.y");
1341 test.assert_events(&[], &[], &[], &[]).await;
1342 }
1343
1344 #[tokio::test]
1345 async fn test_exclude_prevents_watching() {
1346 let test = TestInstance::new("exclude_prevents_watch", |b| {
1347 b.add_include("**/*").add_exclude("node_modules/**")
1348 })
1349 .await;
1350
1351 test.create_dir("node_modules");
1352 tokio::time::sleep(Duration::from_millis(200)).await;
1353
1354 test.write_file("node_modules/package.json", "");
1355 test.assert_no_events().await;
1356
1357 test.write_file("test.txt", "");
1358 test.assert_events(&["test.txt"], &[], &["test.txt"], &[])
1359 .await;
1360 }
1361
1362 #[tokio::test]
1363 async fn test_pattern_file() {
1364 let test_dir = std::env::current_dir()
1366 .unwrap()
1367 .join(".file-watcher-test-pattern_file");
1368
1369 if test_dir.exists() {
1370 std::fs::remove_dir_all(&test_dir).unwrap();
1371 }
1372 std::fs::create_dir(&test_dir).unwrap();
1373
1374 std::fs::write(
1376 test_dir.join(".watchignore"),
1377 "# Comment line\nignored/**\n",
1378 )
1379 .unwrap();
1380
1381 let tracker = Arc::new(Mutex::new(Vec::<Event>::new()));
1383 let tracker_clone = tracker.clone();
1384 let test_dir_clone = test_dir.clone();
1385
1386 let watcher_handle = tokio::spawn(async move {
1387 let _ = WatchBuilder::new()
1388 .set_base_dir(&test_dir_clone)
1389 .debug_watches(true)
1390 .add_include("**/*")
1391 .add_ignore_file(".watchignore")
1392 .run(move |event_type, path| {
1393 tracker_clone.lock().unwrap().push(Event {
1394 path: path.clone(),
1395 event_type: match event_type {
1396 WatchEvent::Create => EventType::Create,
1397 WatchEvent::Delete => EventType::Delete,
1398 WatchEvent::Update => EventType::Update,
1399 WatchEvent::DebugWatch => EventType::DebugWatch,
1400 },
1401 });
1402 })
1403 .await;
1404 });
1405
1406 tokio::time::sleep(Duration::from_millis(100)).await;
1407 tracker.lock().unwrap().clear(); std::fs::create_dir(test_dir.join("ignored")).unwrap();
1411 tokio::time::sleep(Duration::from_millis(200)).await;
1412
1413 std::fs::write(test_dir.join("ignored/test.txt"), "").unwrap();
1415 tokio::time::sleep(Duration::from_millis(200)).await;
1416
1417 {
1419 let events = tracker.lock().unwrap();
1420 let has_ignored_events = events.iter().any(|e| {
1421 e.path.to_string_lossy().contains("ignored")
1422 && e.event_type != EventType::DebugWatch
1423 });
1424 assert!(
1425 !has_ignored_events,
1426 "Expected no events for ignored files, but got: {:?}",
1427 events
1428 );
1429 }
1430 tracker.lock().unwrap().clear();
1431
1432 std::fs::write(test_dir.join("normal.txt"), "").unwrap();
1434 tokio::time::sleep(Duration::from_millis(200)).await;
1435
1436 {
1437 let events = tracker.lock().unwrap();
1438 let has_normal = events
1439 .iter()
1440 .any(|e| e.path == PathBuf::from("normal.txt"));
1441 assert!(has_normal, "Expected event for normal.txt, got: {:?}", events);
1442 }
1443
1444 watcher_handle.abort();
1446 let _ = std::fs::remove_dir_all(&test_dir);
1447 }
1448}