From 736c884e348205ff9da32fbe6258bdd20a166a1e Mon Sep 17 00:00:00 2001 From: Martin Tournoij Date: Tue, 20 Dec 2022 13:50:49 +0100 Subject: [PATCH] Add recursive watcher for Windows backend (#540) Recursive watches can be added by using a "/..." parameter, similar to the Go command: w.Add("dir") // Behaves as before. w.Add("dir/...") // Watch dir and all paths underneath it. w.Remove("dir") // Remove the watch for dir and, if // recursive, all paths underneath it too w.Remove("dir/...") // Behaves like just "dir" if the path was // recursive, error otherwise (probably // want to add recursive remove too at some // point). The advantage of using "/..." vs. an option is that it can be easily specified in configuration files and the like; for example from a TOML file: [watches] dirs = ["/tmp/one", "/tmp/two/..."] Options for this were previously discussed at: https://github.com/fsnotify/fsnotify/pull/339#discussion_r788246013 This should be expanded to other backends too; I started with Windows because the implementation is the both the easiest and has the least amount of control (just setting a boolean parameter), and we can focus mostly on writing tests and documentation and the for it, and we can then match the inotify and kqueue behaviour to the Windows one. Fixes #21 Co-authored-by: Milas Bowman --- CHANGELOG.md | 5 +- backend_fen.go | 33 ++++++- backend_inotify.go | 33 ++++++- backend_inotify_test.go | 1 + backend_kqueue.go | 33 ++++++- backend_other.go | 33 ++++++- backend_windows.go | 73 ++++++++++---- fsnotify.go | 10 ++ fsnotify_test.go | 207 +++++++++++++++++++++++++++++++++++++++- helpers_test.go | 35 ++++--- mkdoc.zsh | 34 +++++-- 11 files changed, 436 insertions(+), 61 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31fa1a47..5e08ba52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ Unreleased - all: add `AddWith()`, which is identical to `Add()` but allows passing options. ([#521]) +- all: support recursively watching paths with `Add("path/...")`. ([#540]) + - windows: allow setting the buffer size with `fsnotify.WithBufferSize()`; the default of 64K is the highest value that works on all platforms and is enough for most purposes, but in some cases a highest buffer is needed. ([#521]) @@ -46,7 +48,7 @@ Unreleased - other: use the backend_other.go no-op if the `appengine` build tag is set; Google AppEngine forbids usage of the unsafe package so the inotify backend - won't work there. + won't compile there. [#371]: https://github.com/fsnotify/fsnotify/pull/371 @@ -58,6 +60,7 @@ Unreleased [#526]: https://github.com/fsnotify/fsnotify/pull/526 [#528]: https://github.com/fsnotify/fsnotify/pull/528 [#537]: https://github.com/fsnotify/fsnotify/pull/537 +[#540]: https://github.com/fsnotify/fsnotify/pull/540 1.6.0 - 2022-10-13 ------------------- diff --git a/backend_fen.go b/backend_fen.go index 255e5334..2c3e43dc 100644 --- a/backend_fen.go +++ b/backend_fen.go @@ -1,6 +1,9 @@ //go:build solaris // +build solaris +// Note: the documentation on the Watcher type and methods is generated from +// mkdoc.zsh + package fsnotify import ( @@ -63,6 +66,16 @@ import ( // The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to // control the maximum number of open files, as well as /etc/login.conf on BSD // systems. +// +// # Windows notes +// +// Paths can be added as "C:\path\to\dir", but forward slashes +// ("C:/path/to/dir") will also work. +// +// The default buffer size is 64K, which is the largest value that is guaranteed +// to work with SMB filesystems. If you have many events in quick succession +// this may not be enough, and you will have to use [WithBufferSize] to increase +// the value. type Watcher struct { // Events sends the filesystem change events. // @@ -92,6 +105,8 @@ type Watcher struct { // you may get hundreds of Write events, so you // probably want to wait until you've stopped receiving // them (see the dedup example in cmd/fsnotify). + // Some systems may send Write event for directories + // when the directory content changes. // // fsnotify.Chmod Attributes were changed. On Linux this is also sent // when a file is removed (or more accurately, when a @@ -184,7 +199,7 @@ func (w *Watcher) Close() error { // // A path can only be watched once; attempting to watch it more than once will // return an error. Paths that do not yet exist on the filesystem cannot be -// added. +// watched. // // A watch will be automatically removed if the watched path is deleted or // renamed. The exception is the Windows backend, which doesn't remove the @@ -200,8 +215,9 @@ func (w *Watcher) Close() error { // # Watching directories // // All files in a directory are monitored, including new files that are created -// after the watcher is started. Subdirectories are not watched (i.e. it's -// non-recursive). +// after the watcher is started. By default subdirectories are not watched (i.e. +// it's non-recursive), but if the path ends with "/..." all files and +// subdirectories are watched too. // // # Watching files // @@ -266,8 +282,15 @@ func (w *Watcher) AddWith(name string, opts ...addOpt) error { // Remove stops monitoring the path for changes. // -// Directories are always removed non-recursively. For example, if you added -// /tmp/dir and /tmp/dir/subdir then you will need to remove both. +// If the path was added as a recursive watch (e.g. as "/tmp/dir/...") then the +// entire recursive watch will be removed. You can use either "/tmp/dir" or +// "/tmp/dir/..." (they behave identically). +// +// You cannot remove individual files or subdirectories from recursive watches; +// e.g. Add("/tmp/path/...") and then Remove("/tmp/path/sub") will fail. +// +// For other watches directories are removed non-recursively. For example, if +// you added "/tmp/dir" and "/tmp/dir/subdir" then you will need to remove both. // // Removing a path that has not yet been added returns [ErrNonExistentWatch]. // diff --git a/backend_inotify.go b/backend_inotify.go index 2f47f4da..d9cb3a02 100644 --- a/backend_inotify.go +++ b/backend_inotify.go @@ -1,6 +1,9 @@ //go:build linux && !appengine // +build linux,!appengine +// Note: the documentation on the Watcher type and methods is generated from +// mkdoc.zsh + package fsnotify import ( @@ -66,6 +69,16 @@ import ( // The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to // control the maximum number of open files, as well as /etc/login.conf on BSD // systems. +// +// # Windows notes +// +// Paths can be added as "C:\path\to\dir", but forward slashes +// ("C:/path/to/dir") will also work. +// +// The default buffer size is 64K, which is the largest value that is guaranteed +// to work with SMB filesystems. If you have many events in quick succession +// this may not be enough, and you will have to use [WithBufferSize] to increase +// the value. type Watcher struct { // Events sends the filesystem change events. // @@ -95,6 +108,8 @@ type Watcher struct { // you may get hundreds of Write events, so you // probably want to wait until you've stopped receiving // them (see the dedup example in cmd/fsnotify). + // Some systems may send Write event for directories + // when the directory content changes. // // fsnotify.Chmod Attributes were changed. On Linux this is also sent // when a file is removed (or more accurately, when a @@ -206,7 +221,7 @@ func (w *Watcher) Close() error { // // A path can only be watched once; attempting to watch it more than once will // return an error. Paths that do not yet exist on the filesystem cannot be -// added. +// watched. // // A watch will be automatically removed if the watched path is deleted or // renamed. The exception is the Windows backend, which doesn't remove the @@ -222,8 +237,9 @@ func (w *Watcher) Close() error { // # Watching directories // // All files in a directory are monitored, including new files that are created -// after the watcher is started. Subdirectories are not watched (i.e. it's -// non-recursive). +// after the watcher is started. By default subdirectories are not watched (i.e. +// it's non-recursive), but if the path ends with "/..." all files and +// subdirectories are watched too. // // # Watching files // @@ -281,8 +297,15 @@ func (w *Watcher) AddWith(name string, opts ...addOpt) error { // Remove stops monitoring the path for changes. // -// Directories are always removed non-recursively. For example, if you added -// /tmp/dir and /tmp/dir/subdir then you will need to remove both. +// If the path was added as a recursive watch (e.g. as "/tmp/dir/...") then the +// entire recursive watch will be removed. You can use either "/tmp/dir" or +// "/tmp/dir/..." (they behave identically). +// +// You cannot remove individual files or subdirectories from recursive watches; +// e.g. Add("/tmp/path/...") and then Remove("/tmp/path/sub") will fail. +// +// For other watches directories are removed non-recursively. For example, if +// you added "/tmp/dir" and "/tmp/dir/subdir" then you will need to remove both. // // Removing a path that has not yet been added returns [ErrNonExistentWatch]. // diff --git a/backend_inotify_test.go b/backend_inotify_test.go index c857291a..6965ee19 100644 --- a/backend_inotify_test.go +++ b/backend_inotify_test.go @@ -93,6 +93,7 @@ func TestInotifyDeleteOpenFile(t *testing.T) { w.collect(t) rm(t, file) + eventSeparator() e := w.events(t) cmpEvents(t, tmp, e, newEvents(t, `chmod /file`)) diff --git a/backend_kqueue.go b/backend_kqueue.go index b83e7798..c02ad5a9 100644 --- a/backend_kqueue.go +++ b/backend_kqueue.go @@ -1,6 +1,9 @@ //go:build freebsd || openbsd || netbsd || dragonfly || darwin // +build freebsd openbsd netbsd dragonfly darwin +// Note: the documentation on the Watcher type and methods is generated from +// mkdoc.zsh + package fsnotify import ( @@ -63,6 +66,16 @@ import ( // The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to // control the maximum number of open files, as well as /etc/login.conf on BSD // systems. +// +// # Windows notes +// +// Paths can be added as "C:\path\to\dir", but forward slashes +// ("C:/path/to/dir") will also work. +// +// The default buffer size is 64K, which is the largest value that is guaranteed +// to work with SMB filesystems. If you have many events in quick succession +// this may not be enough, and you will have to use [WithBufferSize] to increase +// the value. type Watcher struct { // Events sends the filesystem change events. // @@ -92,6 +105,8 @@ type Watcher struct { // you may get hundreds of Write events, so you // probably want to wait until you've stopped receiving // them (see the dedup example in cmd/fsnotify). + // Some systems may send Write event for directories + // when the directory content changes. // // fsnotify.Chmod Attributes were changed. On Linux this is also sent // when a file is removed (or more accurately, when a @@ -237,7 +252,7 @@ func (w *Watcher) Close() error { // // A path can only be watched once; attempting to watch it more than once will // return an error. Paths that do not yet exist on the filesystem cannot be -// added. +// watched. // // A watch will be automatically removed if the watched path is deleted or // renamed. The exception is the Windows backend, which doesn't remove the @@ -253,8 +268,9 @@ func (w *Watcher) Close() error { // # Watching directories // // All files in a directory are monitored, including new files that are created -// after the watcher is started. Subdirectories are not watched (i.e. it's -// non-recursive). +// after the watcher is started. By default subdirectories are not watched (i.e. +// it's non-recursive), but if the path ends with "/..." all files and +// subdirectories are watched too. // // # Watching files // @@ -288,8 +304,15 @@ func (w *Watcher) AddWith(name string, opts ...addOpt) error { // Remove stops monitoring the path for changes. // -// Directories are always removed non-recursively. For example, if you added -// /tmp/dir and /tmp/dir/subdir then you will need to remove both. +// If the path was added as a recursive watch (e.g. as "/tmp/dir/...") then the +// entire recursive watch will be removed. You can use either "/tmp/dir" or +// "/tmp/dir/..." (they behave identically). +// +// You cannot remove individual files or subdirectories from recursive watches; +// e.g. Add("/tmp/path/...") and then Remove("/tmp/path/sub") will fail. +// +// For other watches directories are removed non-recursively. For example, if +// you added "/tmp/dir" and "/tmp/dir/subdir" then you will need to remove both. // // Removing a path that has not yet been added returns [ErrNonExistentWatch]. // diff --git a/backend_other.go b/backend_other.go index bbf85a2f..bc1cab11 100644 --- a/backend_other.go +++ b/backend_other.go @@ -1,6 +1,9 @@ //go:build appengine || (!darwin && !dragonfly && !freebsd && !openbsd && !linux && !netbsd && !solaris && !windows) // +build appengine !darwin,!dragonfly,!freebsd,!openbsd,!linux,!netbsd,!solaris,!windows +// Note: the documentation on the Watcher type and methods is generated from +// mkdoc.zsh + package fsnotify import "errors" @@ -55,6 +58,16 @@ import "errors" // The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to // control the maximum number of open files, as well as /etc/login.conf on BSD // systems. +// +// # Windows notes +// +// Paths can be added as "C:\path\to\dir", but forward slashes +// ("C:/path/to/dir") will also work. +// +// The default buffer size is 64K, which is the largest value that is guaranteed +// to work with SMB filesystems. If you have many events in quick succession +// this may not be enough, and you will have to use [WithBufferSize] to increase +// the value. type Watcher struct { // Events sends the filesystem change events. // @@ -84,6 +97,8 @@ type Watcher struct { // you may get hundreds of Write events, so you // probably want to wait until you've stopped receiving // them (see the dedup example in cmd/fsnotify). + // Some systems may send Write event for directories + // when the directory content changes. // // fsnotify.Chmod Attributes were changed. On Linux this is also sent // when a file is removed (or more accurately, when a @@ -119,7 +134,7 @@ func (w *Watcher) WatchList() []string { return nil } // // A path can only be watched once; attempting to watch it more than once will // return an error. Paths that do not yet exist on the filesystem cannot be -// added. +// watched. // // A watch will be automatically removed if the watched path is deleted or // renamed. The exception is the Windows backend, which doesn't remove the @@ -135,8 +150,9 @@ func (w *Watcher) WatchList() []string { return nil } // # Watching directories // // All files in a directory are monitored, including new files that are created -// after the watcher is started. Subdirectories are not watched (i.e. it's -// non-recursive). +// after the watcher is started. By default subdirectories are not watched (i.e. +// it's non-recursive), but if the path ends with "/..." all files and +// subdirectories are watched too. // // # Watching files // @@ -162,8 +178,15 @@ func (w *Watcher) AddWith(name string, opts ...addOpt) error { return nil } // Remove stops monitoring the path for changes. // -// Directories are always removed non-recursively. For example, if you added -// /tmp/dir and /tmp/dir/subdir then you will need to remove both. +// If the path was added as a recursive watch (e.g. as "/tmp/dir/...") then the +// entire recursive watch will be removed. You can use either "/tmp/dir" or +// "/tmp/dir/..." (they behave identically). +// +// You cannot remove individual files or subdirectories from recursive watches; +// e.g. Add("/tmp/path/...") and then Remove("/tmp/path/sub") will fail. +// +// For other watches directories are removed non-recursively. For example, if +// you added "/tmp/dir" and "/tmp/dir/subdir" then you will need to remove both. // // Removing a path that has not yet been added returns [ErrNonExistentWatch]. // diff --git a/backend_windows.go b/backend_windows.go index 2dd9aff6..77050aed 100644 --- a/backend_windows.go +++ b/backend_windows.go @@ -1,6 +1,13 @@ //go:build windows // +build windows +// Windows backend based on ReadDirectoryChangesW() +// +// https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw +// +// Note: the documentation on the Watcher type and methods is generated from +// mkdoc.zsh + package fsnotify import ( @@ -67,6 +74,16 @@ import ( // The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to // control the maximum number of open files, as well as /etc/login.conf on BSD // systems. +// +// # Windows notes +// +// Paths can be added as "C:\path\to\dir", but forward slashes +// ("C:/path/to/dir") will also work. +// +// The default buffer size is 64K, which is the largest value that is guaranteed +// to work with SMB filesystems. If you have many events in quick succession +// this may not be enough, and you will have to use [WithBufferSize] to increase +// the value. type Watcher struct { // Events sends the filesystem change events. // @@ -96,6 +113,8 @@ type Watcher struct { // you may get hundreds of Write events, so you // probably want to wait until you've stopped receiving // them (see the dedup example in cmd/fsnotify). + // Some systems may send Write event for directories + // when the directory content changes. // // fsnotify.Chmod Attributes were changed. On Linux this is also sent // when a file is removed (or more accurately, when a @@ -193,7 +212,7 @@ func (w *Watcher) Close() error { // // A path can only be watched once; attempting to watch it more than once will // return an error. Paths that do not yet exist on the filesystem cannot be -// added. +// watched. // // A watch will be automatically removed if the watched path is deleted or // renamed. The exception is the Windows backend, which doesn't remove the @@ -209,8 +228,9 @@ func (w *Watcher) Close() error { // # Watching directories // // All files in a directory are monitored, including new files that are created -// after the watcher is started. Subdirectories are not watched (i.e. it's -// non-recursive). +// after the watcher is started. By default subdirectories are not watched (i.e. +// it's non-recursive), but if the path ends with "/..." all files and +// subdirectories are watched too. // // # Watching files // @@ -258,8 +278,15 @@ func (w *Watcher) AddWith(name string, opts ...addOpt) error { // Remove stops monitoring the path for changes. // -// Directories are always removed non-recursively. For example, if you added -// /tmp/dir and /tmp/dir/subdir then you will need to remove both. +// If the path was added as a recursive watch (e.g. as "/tmp/dir/...") then the +// entire recursive watch will be removed. You can use either "/tmp/dir" or +// "/tmp/dir/..." (they behave identically). +// +// You cannot remove individual files or subdirectories from recursive watches; +// e.g. Add("/tmp/path/...") and then Remove("/tmp/path/sub") will fail. +// +// For other watches directories are removed non-recursively. For example, if +// you added "/tmp/dir" and "/tmp/dir/subdir" then you will need to remove both. // // Removing a path that has not yet been added returns [ErrNonExistentWatch]. // @@ -362,13 +389,14 @@ type inode struct { } type watch struct { - ov windows.Overlapped - ino *inode // i-number - path string // Directory path - mask uint64 // Directory itself is being watched with these notify flags - names map[string]uint64 // Map of names being watched and their notify flags - rename string // Remembers the old name while renaming a file - buf []byte // buffer, allocated later + ov windows.Overlapped + ino *inode // i-number + recurse bool // Recursive watch? + path string // Directory path + mask uint64 // Directory itself is being watched with these notify flags + names map[string]uint64 // Map of names being watched and their notify flags + rename string // Remembers the old name while renaming a file + buf []byte // buffer, allocated later } type ( @@ -442,6 +470,7 @@ func (m watchMap) set(ino *inode, watch *watch) { // Must run within the I/O thread. func (w *Watcher) addWatch(pathname string, flags uint64, bufsize int) error { + pathname, recurse := recursivePath(pathname) dir, err := w.getDir(pathname) if err != nil { return err @@ -461,10 +490,11 @@ func (w *Watcher) addWatch(pathname string, flags uint64, bufsize int) error { return os.NewSyscallError("CreateIoCompletionPort", err) } watchEntry = &watch{ - ino: ino, - path: dir, - names: make(map[string]uint64), - buf: make([]byte, bufsize), + ino: ino, + path: dir, + names: make(map[string]uint64), + recurse: recurse, + buf: make([]byte, bufsize), } w.mu.Lock() w.watches.set(ino, watchEntry) @@ -494,6 +524,8 @@ func (w *Watcher) addWatch(pathname string, flags uint64, bufsize int) error { // Must run within the I/O thread. func (w *Watcher) remWatch(pathname string) error { + pathname, recurse := recursivePath(pathname) + dir, err := w.getDir(pathname) if err != nil { return err @@ -507,6 +539,10 @@ func (w *Watcher) remWatch(pathname string) error { watch := w.watches.get(ino) w.mu.Unlock() + if recurse && !watch.recurse { + return fmt.Errorf("can't use \\... with non-recursive watch %q", pathname) + } + err = windows.CloseHandle(ino.handle) if err != nil { w.sendError(os.NewSyscallError("CloseHandle", err)) @@ -568,7 +604,7 @@ func (w *Watcher) startRead(watch *watch) error { hdr := (*reflect.SliceHeader)(unsafe.Pointer(&watch.buf)) rdErr := windows.ReadDirectoryChanges(watch.ino.handle, (*byte)(unsafe.Pointer(hdr.Data)), uint32(hdr.Len), - false, mask, nil, &watch.ov, 0) + watch.recurse, mask, nil, &watch.ov, 0) if rdErr != nil { err := os.NewSyscallError("ReadDirectoryChanges", rdErr) if rdErr == windows.ERROR_ACCESS_DENIED && watch.mask&provisional == 0 { @@ -595,9 +631,8 @@ func (w *Watcher) readEvents() { runtime.LockOSThread() for { + // This error is handled after the watch == nil check below. qErr := windows.GetQueuedCompletionStatus(w.port, &n, &key, &ov, windows.INFINITE) - // This error is handled after the watch == nil check below. NOTE: this - // seems odd, note sure if it's correct. watch := (*watch)(unsafe.Pointer(ov)) if watch == nil { diff --git a/fsnotify.go b/fsnotify.go index 142169da..c00ce762 100644 --- a/fsnotify.go +++ b/fsnotify.go @@ -12,6 +12,7 @@ package fsnotify import ( "errors" "fmt" + "path/filepath" "strings" ) @@ -131,3 +132,12 @@ func getOptions(opts ...addOpt) withOpts { func WithBufferSize(bytes int) addOpt { return func(opt *withOpts) { opt.bufsize = bytes } } + +// Check if this path is recursive (ends with "/..." or "\..."), and return the +// path with the /... stripped. +func recursivePath(path string) (string, bool) { + if filepath.Base(path) == "..." { + return filepath.Dir(path), true + } + return path, false +} diff --git a/fsnotify_test.go b/fsnotify_test.go index 12a284be..767654ea 100644 --- a/fsnotify_test.go +++ b/fsnotify_test.go @@ -656,7 +656,7 @@ func TestWatchAttrib(t *testing.T) { } } -func TestWatchRm(t *testing.T) { +func TestWatchRemove(t *testing.T) { tests := []testCase{ {"remove watched file", func(t *testing.T, w *Watcher, tmp string) { file := join(tmp, "file") @@ -759,6 +759,194 @@ func TestWatchRm(t *testing.T) { WRITE "/j" WRITE "/j" `}, + + {"remove recursive", func(t *testing.T, w *Watcher, tmp string) { + recurseOnly(t) + + mkdirAll(t, tmp, "dir1", "subdir") + mkdirAll(t, tmp, "dir2", "subdir") + touch(t, tmp, "dir1", "subdir", "file") + touch(t, tmp, "dir2", "subdir", "file") + + addWatch(t, w, tmp, "dir1", "...") + addWatch(t, w, tmp, "dir2", "...") + cat(t, "asd", tmp, "dir1", "subdir", "file") + cat(t, "asd", tmp, "dir2", "subdir", "file") + + if err := w.Remove(join(tmp, "dir1")); err != nil { + t.Fatal(err) + } + if err := w.Remove(join(tmp, "dir2", "...")); err != nil { + t.Fatal(err) + } + + if w := w.WatchList(); len(w) != 0 { + t.Errorf("WatchList not empty: %s", w) + } + + cat(t, "asd", tmp, "dir1", "subdir", "file") + cat(t, "asd", tmp, "dir2", "subdir", "file") + }, ` + write /dir1/subdir + write /dir1/subdir/file + write /dir2/subdir + write /dir2/subdir/file + `}, + } + + for _, tt := range tests { + tt := tt + tt.run(t) + } +} + +func TestWatchRecursive(t *testing.T) { + recurseOnly(t) + + tests := []testCase{ + // Make a nested directory tree, then write some files there. + {"basic", func(t *testing.T, w *Watcher, tmp string) { + mkdirAll(t, tmp, "/one/two/three/four") + addWatch(t, w, tmp, "/...") + + cat(t, "asd", tmp, "/file.txt") + cat(t, "asd", tmp, "/one/two/three/file.txt") + }, ` + create /file.txt # cat asd >file.txt + write /file.txt + + write /one/two/three # cat asd >one/two/three/file.txt + create /one/two/three/file.txt + write /one/two/three/file.txt + `}, + + // Create a new directory tree and then some files under that. + {"add directory", func(t *testing.T, w *Watcher, tmp string) { + mkdirAll(t, tmp, "/one/two/three/four") + addWatch(t, w, tmp, "/...") + + mkdirAll(t, tmp, "/one/two/new/dir") + touch(t, tmp, "/one/two/new/file") + touch(t, tmp, "/one/two/new/dir/file") + }, ` + write /one/two # mkdir -p one/two/new/dir + create /one/two/new + create /one/two/new/dir + + write /one/two/new # touch one/two/new/file + create /one/two/new/file + + create /one/two/new/dir/file # touch one/two/new/dir/file + `}, + + // Remove nested directory + {"remove directory", func(t *testing.T, w *Watcher, tmp string) { + mkdirAll(t, tmp, "one/two/three/four") + addWatch(t, w, tmp, "...") + + cat(t, "asd", tmp, "one/two/three/file.txt") + rmAll(t, tmp, "one/two") + }, ` + write /one/two/three # cat asd >one/two/three/file.txt + create /one/two/three/file.txt + write /one/two/three/file.txt + + write /one/two # rm -r one/two + write /one/two/three + remove /one/two/three/file.txt + remove /one/two/three/four + write /one/two/three + remove /one/two/three + write /one/two + remove /one/two + `}, + + // Rename nested directory + {"rename directory", func(t *testing.T, w *Watcher, tmp string) { + mkdirAll(t, tmp, "/one/two/three/four") + addWatch(t, w, tmp, "...") + + mv(t, join(tmp, "one"), tmp, "one-rename") + touch(t, tmp, "one-rename/file") + touch(t, tmp, "one-rename/two/three/file") + }, ` + rename "/one" # mv one one-rename + create "/one-rename" + + write "/one-rename" # touch one-rename/file + create "/one-rename/file" + + write "/one-rename/two/three" # touch one-rename/two/three/file + create "/one-rename/two/three/file" + `}, + + {"remove watched directory", func(t *testing.T, w *Watcher, tmp string) { + mk := func(r string) { + touch(t, r, "a") + touch(t, r, "b") + touch(t, r, "c") + touch(t, r, "d") + touch(t, r, "e") + touch(t, r, "f") + touch(t, r, "g") + + mkdir(t, r, "h") + mkdir(t, r, "h", "a") + mkdir(t, r, "i") + mkdir(t, r, "i", "a") + mkdir(t, r, "j") + mkdir(t, r, "j", "a") + } + mk(tmp) + mkdir(t, tmp, "sub") + mk(join(tmp, "sub")) + + addWatch(t, w, tmp, "...") + rmAll(t, tmp) + }, ` + remove "/a" + remove "/b" + remove "/c" + remove "/d" + remove "/e" + remove "/f" + remove "/g" + write "/h" + remove "/h/a" + write "/h" + remove "/h" + write "/i" + remove "/i/a" + write "/i" + remove "/i" + write "/j" + remove "/j/a" + write "/j" + remove "/j" + write "/sub" + remove "/sub/a" + remove "/sub/b" + remove "/sub/c" + remove "/sub/d" + remove "/sub/e" + remove "/sub/f" + remove "/sub/g" + write "/sub/h" + remove "/sub/h/a" + write "/sub/h" + remove "/sub/h" + write "/sub/i" + remove "/sub/i/a" + write "/sub/i" + remove "/sub/i" + write "/sub/j" + remove "/sub/j/a" + write "/sub/j" + remove "/sub/j" + write "/sub" + remove "/sub" + remove "/" + `}, } for _, tt := range tests { @@ -1063,6 +1251,23 @@ func TestRemove(t *testing.T) { w.Close() } }) + + t.Run("remove with ... when non-recursive", func(t *testing.T) { + recurseOnly(t) + t.Parallel() + + tmp := t.TempDir() + w := newWatcher(t) + addWatch(t, w, tmp) + + if err := w.Remove(join(tmp, "...")); err == nil { + t.Fatal("err was nil") + } + if err := w.Remove(tmp); err != nil { + t.Fatal(err) + } + }) + } func TestEventString(t *testing.T) { diff --git a/helpers_test.go b/helpers_test.go index 85c1fc2b..24f00299 100644 --- a/helpers_test.go +++ b/helpers_test.go @@ -138,19 +138,19 @@ func mkdir(t *testing.T, path ...string) { } // mkdir -p -// func mkdirAll(t *testing.T, path ...string) { -// t.Helper() -// if len(path) < 1 { -// t.Fatalf("mkdirAll: path must have at least one element: %s", path) -// } -// err := os.MkdirAll(join(path...), 0o0755) -// if err != nil { -// t.Fatalf("mkdirAll(%q): %s", join(path...), err) -// } -// if shouldWait(path...) { -// eventSeparator() -// } -// } +func mkdirAll(t *testing.T, path ...string) { + t.Helper() + if len(path) < 1 { + t.Fatalf("mkdirAll: path must have at least one element: %s", path) + } + err := os.MkdirAll(join(path...), 0o0755) + if err != nil { + t.Fatalf("mkdirAll(%q): %s", join(path...), err) + } + if shouldWait(path...) { + eventSeparator() + } +} // ln -s func symlink(t *testing.T, target string, link ...string) { @@ -576,3 +576,12 @@ func isSolaris() bool { } return false } + +func recurseOnly(t *testing.T) { + switch runtime.GOOS { + case "windows": + // Run test. + default: + t.Skip("recursion not yet supported on " + runtime.GOOS) + } +} diff --git a/mkdoc.zsh b/mkdoc.zsh index 228061e1..c7180fb2 100755 --- a/mkdoc.zsh +++ b/mkdoc.zsh @@ -2,8 +2,8 @@ [ "${ZSH_VERSION:-}" = "" ] && echo >&2 "Only works with zsh" && exit 1 setopt err_exit no_unset pipefail extended_glob -# Simple script to update the godoc comments on all watchers. Probably took me -# more time to write this than doing it manually, but ah well 🙃 +# Simple script to update the godoc comments on all watchers so you don't need +# to update the same comment 5 times. watcher=$(<