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
337 && pattern_index == pattern_segments.len() - 1
338 {
339 return true;
340 }
341 return allow_prefix;
342 }
343
344 match &pattern_segment {
345 Segment::Exact(s) => {
346 if s != &path_segments[path_index] {
347 return false;
348 }
349 path_index += 1;
350 }
351 Segment::Wildcard(p) => {
352 if !p.matches(&path_segments[path_index]) {
353 return false;
354 }
355 path_index += 1;
356 }
357 Segment::DoubleWildcard => {
358 if allow_prefix {
359 return true;
360 }
361
362 let patterns_left = pattern_segments.len() - (pattern_index + 1);
363 let next_path_index = path_segments.len() - patterns_left;
364 if next_path_index < path_index {
365 return false;
366 }
367 path_index = next_path_index;
368 }
369 }
370 }
371
372 allow_prefix || path_index == path_segments.len()
373 }
374}
375
376struct Inotify {
379 fd: AsyncFd<i32>,
380}
381
382impl Inotify {
383 fn new() -> Result<Self> {
384 let fd = unsafe { libc::inotify_init1(libc::IN_NONBLOCK | libc::IN_CLOEXEC) };
385 if fd < 0 {
386 return Err(std::io::Error::last_os_error());
387 }
388 Ok(Self {
389 fd: AsyncFd::new(fd)?,
390 })
391 }
392
393 fn add_watch(&self, path: &Path, mask: u32) -> Result<i32> {
394 let c_path = CString::new(path.as_os_str().as_bytes())
395 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
396 let wd = unsafe { libc::inotify_add_watch(self.fd.as_raw_fd(), c_path.as_ptr(), mask) };
397 if wd < 0 {
398 return Err(std::io::Error::last_os_error());
399 }
400 Ok(wd)
401 }
402
403 async fn read_events(&self, buffer: &mut [u8]) -> Result<usize> {
404 loop {
405 let mut guard = self.fd.readable().await?;
406 match guard.try_io(|inner| {
407 let res = unsafe {
408 libc::read(
409 inner.as_raw_fd(),
410 buffer.as_mut_ptr() as *mut _,
411 buffer.len(),
412 )
413 };
414 if res < 0 {
415 Err(std::io::Error::last_os_error())
416 } else {
417 Ok(res as usize)
418 }
419 }) {
420 Ok(Ok(len)) => return Ok(len),
421 Ok(Err(e)) => {
422 if e.kind() == std::io::ErrorKind::WouldBlock {
423 continue;
424 }
425 return Err(e);
426 }
427 Err(_) => continue,
428 }
429 }
430 }
431}
432
433impl Drop for Inotify {
434 fn drop(&mut self) {
435 unsafe { libc::close(self.fd.as_raw_fd()) };
436 }
437}
438
439fn resolve_base_dir(base_dir: PathBuf) -> PathBuf {
442 if base_dir.is_absolute() {
443 base_dir
444 } else {
445 std::env::current_dir()
446 .unwrap_or_else(|_| PathBuf::from("/"))
447 .join(base_dir)
448 }
449}
450
451fn path_to_segments(path: &Path) -> Vec<String> {
452 let path_str = path.to_string_lossy();
453 let path_str = path_str.replace("//", "/");
454 path_str
455 .split('/')
456 .filter(|s| !s.is_empty())
457 .map(|s| s.to_string())
458 .collect()
459}
460
461fn should_watch(
462 relative_path: &Path,
463 include_patterns: &[Pattern],
464 exclude_patterns: &[Pattern],
465 is_dir: bool,
466) -> bool {
467 let segments = path_to_segments(relative_path);
468
469 if exclude_patterns.iter().any(|p| p.check(&segments, false)) {
470 return false;
471 }
472
473 include_patterns.iter().any(|p| p.check(&segments, is_dir))
474}
475
476fn add_watch_recursive<F>(
477 start_rel_path: PathBuf,
478 root: &Path,
479 inotify: &Inotify,
480 watches: &mut HashMap<i32, PathBuf>,
481 paths: &mut HashSet<PathBuf>,
482 include_patterns: &[Pattern],
483 exclude_patterns: &[Pattern],
484 debug_watches_enabled: bool,
485 return_absolute: bool,
486 callback: &mut F,
487) where
488 F: FnMut(WatchEvent, PathBuf),
489{
490 let mut stack = vec![start_rel_path];
491 while let Some(rel_path) = stack.pop() {
492 if !should_watch(&rel_path, include_patterns, exclude_patterns, true) {
493 continue;
494 }
495
496 if paths.contains(&rel_path) {
497 continue;
498 }
499
500 let full_path = if rel_path.as_os_str().is_empty() {
501 root.to_path_buf()
502 } else {
503 root.join(&rel_path)
504 };
505
506 if !full_path.is_dir() {
507 continue;
508 }
509
510 let mask = libc::IN_MODIFY
511 | libc::IN_CLOSE_WRITE
512 | libc::IN_CREATE
513 | libc::IN_DELETE
514 | libc::IN_MOVED_FROM
515 | libc::IN_MOVED_TO
516 | libc::IN_DONT_FOLLOW;
517 match inotify.add_watch(&full_path, mask as u32) {
518 Ok(wd) => {
519 paths.insert(rel_path.clone());
520 watches.insert(wd, rel_path.clone());
521
522 if debug_watches_enabled {
523 let callback_path = if return_absolute {
524 full_path.clone()
525 } else {
526 rel_path.clone()
527 };
528 callback(WatchEvent::DebugWatch, callback_path);
529 }
530
531 if let Ok(entries) = std::fs::read_dir(&full_path) {
532 for entry in entries.flatten() {
533 if let Ok(ft) = entry.file_type() {
534 if ft.is_dir() {
535 let child_rel_path = if rel_path.as_os_str().is_empty() {
536 PathBuf::from(entry.file_name())
537 } else {
538 rel_path.join(entry.file_name())
539 };
540 stack.push(child_rel_path);
541 }
542 }
543 }
544 }
545 }
546 Err(e) => {
547 eprintln!("{}", e);
548 }
549 }
550 }
551}
552
553fn find_watch_start_dir(pattern: &Pattern, root: &Path) -> PathBuf {
554 let mut current_path = PathBuf::new();
555 let mut found_wildcard = false;
556
557 for segment in &pattern.segments {
558 match segment {
559 Segment::Exact(s) => {
560 if !found_wildcard {
561 current_path.push(s);
562 }
563 }
564 _ => {
565 found_wildcard = true;
566 break;
567 }
568 }
569 }
570
571 if found_wildcard && !current_path.as_os_str().is_empty() {
572 current_path.pop();
573 }
574
575 if !found_wildcard && !current_path.as_os_str().is_empty() {
576 current_path.pop();
577 }
578
579 loop {
580 let full_path = if current_path.as_os_str().is_empty() {
581 root.to_path_buf()
582 } else {
583 root.join(¤t_path)
584 };
585
586 if full_path.exists() && full_path.is_dir() {
587 break;
588 }
589
590 if current_path.as_os_str().is_empty() {
591 break;
592 }
593
594 current_path.pop();
595 }
596
597 current_path
598}
599
600fn parse_inotify_events(buffer: &[u8], len: usize) -> Vec<(i32, u32, String)> {
601 let mut events = Vec::new();
602 let mut ptr = buffer.as_ptr();
603 let end = unsafe { ptr.add(len) };
604
605 while ptr < end {
606 let event = unsafe { &*(ptr as *const libc::inotify_event) };
607 let name_len = event.len as usize;
608
609 if name_len > 0 {
610 let name_ptr = unsafe { ptr.add(std::mem::size_of::<libc::inotify_event>()) };
611 let name_slice =
612 unsafe { std::slice::from_raw_parts(name_ptr as *const u8, name_len) };
613 let name_str = String::from_utf8_lossy(name_slice)
614 .trim_matches(char::from(0))
615 .to_string();
616 events.push((event.wd, event.mask, name_str));
617 }
618
619 ptr = unsafe { ptr.add(std::mem::size_of::<libc::inotify_event>() + name_len) };
620 }
621
622 events
623}
624
625#[derive(Debug, Clone, Copy, PartialEq, Eq)]
630pub enum WatchEvent {
631 Create,
635 Delete,
639 Update,
645 DebugWatch,
650}
651
652pub struct WatchBuilder {
675 includes: Option<Vec<String>>,
676 excludes: Vec<String>,
677 base_dir: PathBuf,
678 watch_create: bool,
679 watch_delete: bool,
680 watch_update: bool,
681 match_files: bool,
682 match_dirs: bool,
683 return_absolute: bool,
684 debug_watches_enabled: bool,
685}
686
687impl Default for WatchBuilder {
688 fn default() -> Self {
689 Self::new()
690 }
691}
692
693impl WatchBuilder {
694 pub fn new() -> Self {
704 WatchBuilder {
705 includes: Some(Vec::new()),
706 excludes: Vec::new(),
707 base_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/")),
708 watch_create: true,
709 watch_delete: true,
710 watch_update: true,
711 match_files: true,
712 match_dirs: true,
713 return_absolute: false,
714 debug_watches_enabled: false,
715 }
716 }
717
718 pub fn debug_watches(mut self, enabled: bool) -> Self {
723 self.debug_watches_enabled = enabled;
724 self
725 }
726
727 pub fn add_include(mut self, pattern: impl Into<String>) -> Self {
738 if self.includes.is_none() {
739 self.includes = Some(Vec::new());
740 }
741 self.includes.as_mut().unwrap().push(pattern.into());
742 self
743 }
744
745 pub fn add_includes(mut self, patterns: impl IntoIterator<Item = impl Into<String>>) -> Self {
747 if self.includes.is_none() {
748 self.includes = Some(Vec::new());
749 }
750 self.includes
751 .as_mut()
752 .unwrap()
753 .extend(patterns.into_iter().map(|p| p.into()));
754 self
755 }
756
757 pub fn add_exclude(mut self, pattern: impl Into<String>) -> Self {
761 self.excludes.push(pattern.into());
762 self
763 }
764
765 pub fn add_excludes(mut self, patterns: impl IntoIterator<Item = impl Into<String>>) -> Self {
767 self.excludes
768 .extend(patterns.into_iter().map(|p| p.into()));
769 self
770 }
771
772 pub fn add_ignore_file(mut self, path: impl AsRef<Path>) -> Self {
799 let path = path.as_ref();
800
801 let full_path = if path.is_absolute() {
803 path.to_path_buf()
804 } else {
805 self.base_dir.join(path)
806 };
807
808 if let Ok(file) = fs::File::open(&full_path) {
809 let reader = BufReader::new(file);
810 let mut has_negation = false;
811 for line in reader.lines().map_while(Result::ok) {
812 let trimmed = line.trim();
813
814 if trimmed.is_empty() || trimmed.starts_with('#') {
816 continue;
817 }
818
819 if trimmed.starts_with('!') {
821 has_negation = true;
822 } else {
823 self.excludes.push(trimmed.to_string());
825 }
826 }
827 if has_negation {
828 println!("Warning: negation patterns (!) in {} are ignored; excludes always take precedence over includes in this library", full_path.display());
829 }
830 }
831
832 self
833 }
834
835 pub fn set_base_dir(mut self, base_dir: impl Into<PathBuf>) -> Self {
840 self.base_dir = base_dir.into();
841 self
842 }
843
844 pub fn watch_create(mut self, enabled: bool) -> Self {
848 self.watch_create = enabled;
849 self
850 }
851
852 pub fn watch_delete(mut self, enabled: bool) -> Self {
856 self.watch_delete = enabled;
857 self
858 }
859
860 pub fn watch_update(mut self, enabled: bool) -> Self {
864 self.watch_update = enabled;
865 self
866 }
867
868 pub fn match_files(mut self, enabled: bool) -> Self {
872 self.match_files = enabled;
873 self
874 }
875
876 pub fn match_dirs(mut self, enabled: bool) -> Self {
880 self.match_dirs = enabled;
881 self
882 }
883
884 pub fn return_absolute(mut self, enabled: bool) -> Self {
889 self.return_absolute = enabled;
890 self
891 }
892
893 pub async fn run<F>(self, callback: F) -> Result<()>
901 where
902 F: FnMut(WatchEvent, PathBuf),
903 {
904 self.run_internal(callback, None).await
905 }
906
907 pub async fn run_debounced<F>(self, ms: u64, mut callback: F) -> Result<()>
916 where
917 F: FnMut(),
918 {
919 self.run_internal(|_, _| callback(), Some(Duration::from_millis(ms))).await
920 }
921
922 async fn run_internal<F>(self, mut callback: F, debounce: Option<Duration>) -> Result<()>
923 where
924 F: FnMut(WatchEvent, PathBuf),
925 {
926 let includes = if let Some(includes) = self.includes {
927 includes
928 } else {
929 vec!["**".to_string()]
930 };
931
932 if includes.is_empty() {
934 loop {
935 tokio::time::sleep(Duration::from_secs(3600)).await;
936 }
937 }
938
939 let excludes = self.excludes;
940 let root = self.base_dir.clone();
941 let watch_create = self.watch_create;
942 let watch_delete = self.watch_delete;
943 let watch_update = self.watch_update;
944 let match_files = self.match_files;
945 let match_dirs = self.match_dirs;
946 let return_absolute = self.return_absolute;
947 let debug_watches_enabled = self.debug_watches_enabled;
948
949 let root = resolve_base_dir(root);
950
951 let include_patterns: Vec<Pattern> = includes.iter().map(|p| Pattern::parse(p)).collect();
952 let exclude_patterns: Vec<Pattern> = excludes.iter().map(|p| Pattern::parse(p)).collect();
953
954 let inotify = Inotify::new()?;
955 let mut watches = HashMap::<i32, PathBuf>::new();
956 let mut paths = HashSet::<PathBuf>::new();
957
958 for pattern in &include_patterns {
960 let watch_dir = find_watch_start_dir(pattern, &root);
961 add_watch_recursive(
962 watch_dir,
963 &root,
964 &inotify,
965 &mut watches,
966 &mut paths,
967 &include_patterns,
968 &exclude_patterns,
969 debug_watches_enabled,
970 return_absolute,
971 &mut callback,
972 );
973 }
974
975 let mut debounce_deadline: Option<tokio::time::Instant> = None;
977
978 let mut buffer = [0u8; 8192];
980 loop {
981 let read_future = 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 callback(WatchEvent::Update, PathBuf::new());
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 callback(WatchEvent::Update, PathBuf::new());
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 had_matching_event = false;
1012
1013 for (wd, mask, name_str) in events {
1014 let rel_path = {
1015 if (mask & libc::IN_IGNORED as u32) != 0 {
1016 if let Some(path) = watches.remove(&wd) {
1017 paths.remove(&path);
1018 }
1019 continue;
1020 }
1021 if let Some(dir_path) = watches.get(&wd) {
1022 Some(dir_path.join(&name_str))
1023 } else {
1024 None
1025 }
1026 };
1027
1028 if let Some(rel_path) = rel_path {
1029 if (mask & libc::IN_ISDIR as u32) != 0 {
1030 if (mask & libc::IN_CREATE as u32) != 0
1031 || (mask & libc::IN_MOVED_TO as u32) != 0
1032 {
1033 add_watch_recursive(
1034 rel_path.clone(),
1035 &root,
1036 &inotify,
1037 &mut watches,
1038 &mut paths,
1039 &include_patterns,
1040 &exclude_patterns,
1041 debug_watches_enabled,
1042 return_absolute,
1043 &mut callback,
1044 );
1045 }
1046 }
1047
1048 if should_watch(&rel_path, &include_patterns, &exclude_patterns, false)
1049 {
1050 let is_create = (mask & libc::IN_CREATE as u32) != 0
1051 || (mask & libc::IN_MOVED_TO as u32) != 0;
1052 let is_delete = (mask & libc::IN_DELETE as u32) != 0
1053 || (mask & libc::IN_MOVED_FROM as u32) != 0;
1054 let is_update = (mask & libc::IN_MODIFY as u32) != 0
1055 || (mask & libc::IN_CLOSE_WRITE as u32) != 0;
1056
1057 let event_type = if is_create && watch_create {
1058 Some(WatchEvent::Create)
1059 } else if is_delete && watch_delete {
1060 Some(WatchEvent::Delete)
1061 } else if is_update && watch_update {
1062 Some(WatchEvent::Update)
1063 } else {
1064 None
1065 };
1066
1067 if let Some(event_type) = event_type {
1068 let is_dir = (mask & libc::IN_ISDIR as u32) != 0;
1069 let should_match_type = if is_dir { match_dirs } else { match_files };
1070
1071 if should_match_type {
1072 had_matching_event = true;
1073
1074 if debounce.is_none() {
1076 let callback_path = if return_absolute {
1077 if rel_path.as_os_str().is_empty() {
1078 root.clone()
1079 } else {
1080 root.join(&rel_path)
1081 }
1082 } else {
1083 rel_path
1084 };
1085 callback(event_type, callback_path);
1086 }
1087 }
1088 }
1089 }
1090 }
1091 }
1092
1093 if let Some(d) = debounce {
1095 if had_matching_event {
1096 debounce_deadline = Some(tokio::time::Instant::now() + d);
1097 }
1098 }
1099 }
1100 Err(e) => {
1101 eprintln!("Error reading inotify events: {}", e);
1102 tokio::time::sleep(Duration::from_millis(100)).await;
1103 }
1104 }
1105 }
1106 }
1107}
1108
1109#[cfg(test)]
1110mod tests {
1111 use super::*;
1112 use std::collections::HashSet;
1113 use std::sync::{Arc, Mutex};
1114 use tokio::task::JoinHandle;
1115
1116 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
1117 enum EventType {
1118 Create,
1119 Delete,
1120 Update,
1121 DebugWatch,
1122 }
1123
1124 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
1125 struct Event {
1126 path: PathBuf,
1127 event_type: EventType,
1128 }
1129
1130 type EventTracker = Arc<Mutex<Vec<Event>>>;
1131
1132 struct TestInstance {
1133 test_dir: PathBuf,
1134 tracker: EventTracker,
1135 watcher_handle: Option<JoinHandle<()>>,
1136 }
1137
1138 impl TestInstance {
1139 async fn new<F>(test_name: &str, configure: F) -> Self
1140 where
1141 F: FnOnce(WatchBuilder) -> WatchBuilder + Send + 'static,
1142 {
1143 let test_dir = std::env::current_dir()
1144 .unwrap()
1145 .join(format!(".file-watcher-test-{}", test_name));
1146
1147 if test_dir.exists() {
1148 std::fs::remove_dir_all(&test_dir).unwrap();
1149 }
1150 std::fs::create_dir(&test_dir).unwrap();
1151
1152 let tracker = Arc::new(Mutex::new(Vec::new()));
1153
1154 let tracker_clone = tracker.clone();
1155 let test_dir_clone = test_dir.clone();
1156
1157 let watcher_handle = tokio::spawn(async move {
1158 let builder = WatchBuilder::new()
1159 .set_base_dir(&test_dir_clone)
1160 .debug_watches(true);
1161
1162 let builder = configure(builder);
1163
1164 let _ = builder
1165 .run(move |event_type, path| {
1166 tracker_clone.lock().unwrap().push(Event {
1167 path: path.clone(),
1168 event_type: match event_type {
1169 WatchEvent::Create => EventType::Create,
1170 WatchEvent::Delete => EventType::Delete,
1171 WatchEvent::Update => EventType::Update,
1172 WatchEvent::DebugWatch => EventType::DebugWatch,
1173 },
1174 });
1175 })
1176 .await;
1177 });
1178
1179 tokio::time::sleep(Duration::from_millis(100)).await;
1180
1181 let instance = Self {
1182 test_dir,
1183 tracker,
1184 watcher_handle: Some(watcher_handle),
1185 };
1186
1187 instance.assert_events(&[], &[], &[], &[""]).await;
1188
1189 instance
1190 }
1191
1192 fn create_dir(&self, path: &str) {
1193 std::fs::create_dir(self.test_dir.join(path)).unwrap();
1194 }
1195
1196 fn write_file(&self, path: &str, content: &str) {
1197 let full_path = self.test_dir.join(path);
1198 if let Some(parent) = full_path.parent() {
1199 std::fs::create_dir_all(parent).unwrap();
1200 }
1201 std::fs::write(full_path, content).unwrap();
1202 }
1203
1204 fn remove_file(&self, path: &str) {
1205 std::fs::remove_file(self.test_dir.join(path)).unwrap();
1206 }
1207
1208 fn rename(&self, from: &str, to: &str) {
1209 std::fs::rename(self.test_dir.join(from), self.test_dir.join(to)).unwrap();
1210 }
1211
1212 async fn assert_events(
1213 &self,
1214 creates: &[&str],
1215 deletes: &[&str],
1216 updates: &[&str],
1217 watches: &[&str],
1218 ) {
1219 tokio::time::sleep(Duration::from_millis(200)).await;
1220
1221 let events = self.tracker.lock().unwrap().clone();
1222 let mut expected = HashSet::new();
1223
1224 for create in creates {
1225 expected.insert(Event {
1226 path: PathBuf::from(create),
1227 event_type: EventType::Create,
1228 });
1229 }
1230
1231 for delete in deletes {
1232 expected.insert(Event {
1233 path: PathBuf::from(delete),
1234 event_type: EventType::Delete,
1235 });
1236 }
1237
1238 for update in updates {
1239 expected.insert(Event {
1240 path: PathBuf::from(update),
1241 event_type: EventType::Update,
1242 });
1243 }
1244
1245 for watch in watches {
1246 expected.insert(Event {
1247 path: PathBuf::from(watch),
1248 event_type: EventType::DebugWatch,
1249 });
1250 }
1251
1252 let actual: HashSet<Event> = events.iter().cloned().collect();
1253
1254 for event in &actual {
1255 if !expected.contains(event) {
1256 panic!("Unexpected event: {:?}", event);
1257 }
1258 }
1259
1260 for event in &expected {
1261 if !actual.contains(event) {
1262 panic!(
1263 "Missing expected event: {:?}\nActual events: {:?}",
1264 event, actual
1265 );
1266 }
1267 }
1268
1269 self.tracker.lock().unwrap().clear();
1270 }
1271
1272 async fn assert_no_events(&self) {
1273 tokio::time::sleep(Duration::from_millis(500)).await;
1274 let events = self.tracker.lock().unwrap();
1275 assert_eq!(
1276 events.len(),
1277 0,
1278 "Expected no events, but got: {:?}",
1279 events
1280 );
1281 }
1282 }
1283
1284 impl Drop for TestInstance {
1285 fn drop(&mut self) {
1286 if let Some(handle) = self.watcher_handle.take() {
1287 handle.abort();
1288 }
1289 if self.test_dir.exists() {
1290 let _ = std::fs::remove_dir_all(&self.test_dir);
1291 }
1292 }
1293 }
1294
1295 #[tokio::test]
1296 async fn test_file_create_update_delete() {
1297 let test = TestInstance::new("create_update_delete", |b| b.add_include("**/*")).await;
1298
1299 test.write_file("test.txt", "");
1300 test.assert_events(&["test.txt"], &[], &["test.txt"], &[])
1301 .await;
1302
1303 test.write_file("test.txt", "hello");
1304 test.assert_events(&[], &[], &["test.txt"], &[]).await;
1305
1306 test.remove_file("test.txt");
1307 test.assert_events(&[], &["test.txt"], &[], &[]).await;
1308 }
1309
1310 #[tokio::test]
1311 async fn test_directory_operations() {
1312 let test = TestInstance::new("directory_operations", |b| b.add_include("**/*")).await;
1313
1314 test.create_dir("subdir");
1315 test.assert_events(&["subdir"], &[], &[], &["subdir"]).await;
1316
1317 test.write_file("subdir/file.txt", "");
1318 test.assert_events(&["subdir/file.txt"], &[], &["subdir/file.txt"], &[])
1319 .await;
1320 }
1321
1322 #[tokio::test]
1323 async fn test_move_operations() {
1324 let test = TestInstance::new("move_operations", |b| b.add_include("**/*")).await;
1325
1326 test.write_file("old.txt", "content");
1327 test.assert_events(&["old.txt"], &[], &["old.txt"], &[])
1328 .await;
1329
1330 test.rename("old.txt", "new.txt");
1331 test.assert_events(&["new.txt"], &["old.txt"], &[], &[])
1332 .await;
1333 }
1334
1335 #[tokio::test]
1336 async fn test_event_filtering() {
1337 let test = TestInstance::new("event_filtering", |b| {
1338 b.add_include("**/*")
1339 .watch_create(true)
1340 .watch_delete(false)
1341 .watch_update(false)
1342 })
1343 .await;
1344
1345 test.write_file("test.txt", "");
1346 test.assert_events(&["test.txt"], &[], &[], &[]).await;
1347
1348 test.write_file("test.txt", "hello");
1349 test.assert_no_events().await;
1350
1351 test.remove_file("test.txt");
1352 test.assert_no_events().await;
1353 }
1354
1355 #[tokio::test]
1356 async fn test_pattern_matching() {
1357 let test = TestInstance::new("pattern_matching", |b| b.add_include("**/*.txt")).await;
1358
1359 test.write_file("test.txt", "");
1360 test.assert_events(&["test.txt"], &[], &["test.txt"], &[])
1361 .await;
1362
1363 test.write_file("test.rs", "");
1364 test.assert_no_events().await;
1365 }
1366
1367 #[tokio::test]
1368 async fn test_exclude_prevents_watching() {
1369 let test = TestInstance::new("exclude_prevents_watch", |b| {
1370 b.add_include("**/*").add_exclude("node_modules/**")
1371 })
1372 .await;
1373
1374 test.create_dir("node_modules");
1375 tokio::time::sleep(Duration::from_millis(200)).await;
1376
1377 test.write_file("node_modules/package.json", "");
1378 test.assert_no_events().await;
1379
1380 test.write_file("test.txt", "");
1381 test.assert_events(&["test.txt"], &[], &["test.txt"], &[])
1382 .await;
1383 }
1384
1385 #[tokio::test]
1386 async fn test_pattern_file() {
1387 let test_dir = std::env::current_dir()
1389 .unwrap()
1390 .join(".file-watcher-test-pattern_file");
1391
1392 if test_dir.exists() {
1393 std::fs::remove_dir_all(&test_dir).unwrap();
1394 }
1395 std::fs::create_dir(&test_dir).unwrap();
1396
1397 std::fs::write(
1399 test_dir.join(".watchignore"),
1400 "# Comment line\nignored/**\n",
1401 )
1402 .unwrap();
1403
1404 let tracker = Arc::new(Mutex::new(Vec::<Event>::new()));
1406 let tracker_clone = tracker.clone();
1407 let test_dir_clone = test_dir.clone();
1408
1409 let watcher_handle = tokio::spawn(async move {
1410 let _ = WatchBuilder::new()
1411 .set_base_dir(&test_dir_clone)
1412 .debug_watches(true)
1413 .add_include("**/*")
1414 .add_ignore_file(".watchignore")
1415 .run(move |event_type, path| {
1416 tracker_clone.lock().unwrap().push(Event {
1417 path: path.clone(),
1418 event_type: match event_type {
1419 WatchEvent::Create => EventType::Create,
1420 WatchEvent::Delete => EventType::Delete,
1421 WatchEvent::Update => EventType::Update,
1422 WatchEvent::DebugWatch => EventType::DebugWatch,
1423 },
1424 });
1425 })
1426 .await;
1427 });
1428
1429 tokio::time::sleep(Duration::from_millis(100)).await;
1430 tracker.lock().unwrap().clear(); std::fs::create_dir(test_dir.join("ignored")).unwrap();
1434 tokio::time::sleep(Duration::from_millis(200)).await;
1435
1436 std::fs::write(test_dir.join("ignored/test.txt"), "").unwrap();
1438 tokio::time::sleep(Duration::from_millis(200)).await;
1439
1440 {
1442 let events = tracker.lock().unwrap();
1443 let has_ignored_events = events.iter().any(|e| {
1444 e.path.to_string_lossy().contains("ignored")
1445 && e.event_type != EventType::DebugWatch
1446 });
1447 assert!(
1448 !has_ignored_events,
1449 "Expected no events for ignored files, but got: {:?}",
1450 events
1451 );
1452 }
1453 tracker.lock().unwrap().clear();
1454
1455 std::fs::write(test_dir.join("normal.txt"), "").unwrap();
1457 tokio::time::sleep(Duration::from_millis(200)).await;
1458
1459 {
1460 let events = tracker.lock().unwrap();
1461 let has_normal = events
1462 .iter()
1463 .any(|e| e.path == PathBuf::from("normal.txt"));
1464 assert!(has_normal, "Expected event for normal.txt, got: {:?}", events);
1465 }
1466
1467 watcher_handle.abort();
1469 let _ = std::fs::remove_dir_all(&test_dir);
1470 }
1471}