package app import ( "log/slog" "path/filepath" "testing" "time" "github.com/fsnotify/fsnotify" ) func TestFileWatcherFiltersProcessableEvents(t *testing.T) { t.Parallel() root := t.TempDir() watcher := &FileWatcher{rootDir: root, logger: slog.Default()} tests := []struct { name string path string op fsnotify.Op want bool }{ {name: "markdown write", path: "guide.md", op: fsnotify.Write, want: true}, {name: "markdown create", path: "nested/guide.md", op: fsnotify.Create, want: true}, {name: "case insensitive markdown", path: "guide.MD", op: fsnotify.Write, want: true}, {name: "markdown delete", path: "guide.md", op: fsnotify.Remove, want: true}, {name: "non markdown write", path: "logo.webp", op: fsnotify.Write, want: false}, {name: "non markdown delete", path: "logo.webp", op: fsnotify.Remove, want: false}, {name: "hidden markdown file", path: ".guide.md", op: fsnotify.Write, want: false}, {name: "hidden directory", path: ".cache/guide.md", op: fsnotify.Write, want: false}, {name: "backup file", path: "guide.md~", op: fsnotify.Write, want: false}, {name: "vim swap", path: "guide.md.swp", op: fsnotify.Write, want: false}, {name: "emacs lock file", path: "#guide.md#", op: fsnotify.Write, want: false}, {name: "chmod only", path: "guide.md", op: fsnotify.Chmod, want: false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { event := fsnotify.Event{Name: filepath.Join(root, tt.path), Op: tt.op} if got := watcher.isProcessableEvent(event); got != tt.want { t.Fatalf("isProcessableEvent() = %v, want %v", got, tt.want) } }) } } func TestFileWatcherDebouncesPendingSyncs(t *testing.T) { t.Parallel() root := t.TempDir() watcher := &FileWatcher{ rootDir: root, logger: slog.Default(), debounce: 10 * time.Millisecond, } var changed []string watcher.SetOnChange(func(path string) { changed = append(changed, path) }) pending := make(map[string]struct{}) var timer *time.Timer var timerC <-chan time.Time defer watcher.stopTimer(timer) watcher.scheduleSync(filepath.Join(root, "one.md"), pending, &timer, &timerC) watcher.scheduleSync(filepath.Join(root, "two.md"), pending, &timer, &timerC) select { case <-timerC: watcher.flushPending(pending) case <-time.After(250 * time.Millisecond): t.Fatal("debounce timer did not fire") } if len(changed) != 1 { t.Fatalf("onChange calls = %d, want 1", len(changed)) } if len(pending) != 0 { t.Fatalf("pending changes = %d, want 0", len(pending)) } }