-
-
Notifications
You must be signed in to change notification settings - Fork 921
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This adds a recursive watcher for inotify; per my suggestion in [1] it uses the "/..." syntax in the path to indicate recursive watches, similar to how Go tools work: w, _ := fsnotify.NewWatcher() w.Add("some/dir/...") This will watch "some/dir" as well as any subdirectories of "some/dir". The upshot of using a syntax like this is that we can: 1. With AddRecursive(path string) adding new Add* methods would become hard; for example AddMask("path", fsnotify.Create) to only get CREATE events; we'd then also have to add AddMaskRecursive(). Plus we'd have to add a RemoveRecursive() as well. 2. With Watcher.SetRecursive() like in #339 it's not possible to add some paths recursively and others non-recursively, which may be useful in some cases. Also, it makes it a bit easier to accept user input; in the CLI or config you can set "dir/..." and just pass that as-is to fsnotify, without applications having to write special code. For other watchers it will return ErrRecursionUnsupported for now; Windows support is already mostly finished in #339, and kqueue can be added in much the same way as inotify in a future PR. I also moved all test helpers to helper_test.go, and added a bunch of "shell-like" functions so you're not forever typing error checks and filepath.Join(). The new "eventCollector" is also useful in tests to conveniently collect a slice of events. TODO: - Also support recursion in Remove(), and deal with paths added with "...". I just updated the documentation but didn't actually implement anything. - A few test cases don't seem quite right; want to get #470 merged first as it really confuses things. - Maybe think of a few more test cases. [1]: #339 (comment)
- Loading branch information
Showing
7 changed files
with
595 additions
and
111 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,243 @@ | ||
//go:build !plan9 && !solaris | ||
// +build !plan9,!solaris | ||
|
||
package fsnotify | ||
|
||
import ( | ||
"fmt" | ||
"io/ioutil" | ||
"os" | ||
"os/exec" | ||
"path/filepath" | ||
"runtime" | ||
"strings" | ||
"sync" | ||
"sync/atomic" | ||
"testing" | ||
"time" | ||
) | ||
|
||
const ( | ||
eventSeparator = 50 * time.Millisecond | ||
waitForEvents = 500 * time.Millisecond | ||
) | ||
|
||
// newWatcher initializes an fsnotify Watcher instance. | ||
func newWatcher(t *testing.T) *Watcher { | ||
t.Helper() | ||
watcher, err := NewWatcher() | ||
if err != nil { | ||
t.Fatalf("NewWatcher() failed: %s", err) | ||
} | ||
return watcher | ||
} | ||
|
||
// addWatch adds a watch for a directory | ||
func addWatch(t *testing.T, watcher *Watcher, path ...string) { | ||
t.Helper() | ||
if len(path) < 1 { | ||
t.Fatalf("addWatch: path must have at least one element: %s", path) | ||
} | ||
err := watcher.Add(filepath.Join(path...)) | ||
if err != nil { | ||
t.Fatalf("addWatch(%q): %s", filepath.Join(path...), err) | ||
} | ||
} | ||
|
||
// 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(filepath.Join(path...), 0o0755) | ||
if err != nil { | ||
t.Fatalf("mkdirAll(%q): %s", filepath.Join(path...), err) | ||
} | ||
} | ||
|
||
// ln -s | ||
func symlink(t *testing.T, target string, link ...string) { | ||
t.Helper() | ||
if len(link) < 1 { | ||
t.Fatalf("symlink: link must have at least one element: %s", link) | ||
} | ||
err := os.Symlink(target, filepath.Join(link...)) | ||
if err != nil { | ||
t.Fatalf("symlink(%q, %q): %s", target, filepath.Join(link...), err) | ||
} | ||
} | ||
|
||
// cat | ||
func cat(t *testing.T, data string, path ...string) { | ||
t.Helper() | ||
if len(path) < 1 { | ||
t.Fatalf("cat: path must have at least one element: %s", path) | ||
} | ||
err := os.WriteFile(filepath.Join(path...), []byte(data), 0o644) | ||
if err != nil { | ||
t.Fatalf("cat(%q): %s", filepath.Join(path...), err) | ||
} | ||
} | ||
|
||
// touch | ||
func touch(t *testing.T, path ...string) { | ||
t.Helper() | ||
if len(path) < 1 { | ||
t.Fatalf("touch: path must have at least one element: %s", path) | ||
} | ||
fp, err := os.Create(filepath.Join(path...)) | ||
if err != nil { | ||
t.Fatalf("touch(%q): %s", filepath.Join(path...), err) | ||
} | ||
err = fp.Close() | ||
if err != nil { | ||
t.Fatalf("touch(%q): %s", filepath.Join(path...), err) | ||
} | ||
} | ||
|
||
// mv | ||
func mv(t *testing.T, src string, dst ...string) { | ||
t.Helper() | ||
if len(dst) < 1 { | ||
t.Fatalf("mv: dst must have at least one element: %s", dst) | ||
} | ||
|
||
var err error | ||
switch runtime.GOOS { | ||
case "windows", "plan9": | ||
err = os.Rename(src, filepath.Join(dst...)) | ||
default: | ||
err = exec.Command("mv", src, filepath.Join(dst...)).Run() | ||
} | ||
if err != nil { | ||
t.Fatalf("mv(%q, %q): %s", src, filepath.Join(dst...), err) | ||
} | ||
} | ||
|
||
// rm | ||
func rm(t *testing.T, path ...string) { | ||
t.Helper() | ||
if len(path) < 1 { | ||
t.Fatalf("rm: path must have at least one element: %s", path) | ||
} | ||
err := os.Remove(filepath.Join(path...)) | ||
if err != nil { | ||
t.Fatalf("rm(%q): %s", filepath.Join(path...), err) | ||
} | ||
} | ||
|
||
// rm -r | ||
func rmAll(t *testing.T, path ...string) { | ||
t.Helper() | ||
if len(path) < 1 { | ||
t.Fatalf("rmAll: path must have at least one element: %s", path) | ||
} | ||
err := os.RemoveAll(filepath.Join(path...)) | ||
if err != nil { | ||
t.Fatalf("rmAll(%q): %s", filepath.Join(path...), err) | ||
} | ||
} | ||
|
||
func errorContains(out error, want string) bool { | ||
if out == nil { | ||
return want == "" | ||
} | ||
if want == "" { | ||
return false | ||
} | ||
return strings.Contains(out.Error(), want) | ||
} | ||
|
||
// tempMkdir makes a temporary directory | ||
// | ||
// Deprecated: use t.TempDir() | ||
func tempMkdir(t *testing.T) string { | ||
dir, err := ioutil.TempDir("", "fsnotify") | ||
if err != nil { | ||
t.Fatalf("failed to create test directory: %s", err) | ||
} | ||
return dir | ||
} | ||
|
||
// tempMkFile makes a temporary file. | ||
// | ||
// Deprecated: use t.TempDir() and cat() | ||
func tempMkFile(t *testing.T, dir string) string { | ||
f, err := ioutil.TempFile(dir, "fsnotify") | ||
if err != nil { | ||
t.Fatalf("failed to create test file: %v", err) | ||
} | ||
defer f.Close() | ||
return f.Name() | ||
} | ||
|
||
// An atomic counter | ||
type counter struct{ val int32 } | ||
|
||
func (c *counter) increment() { atomic.AddInt32(&c.val, 1) } | ||
func (c *counter) value() int32 { return atomic.LoadInt32(&c.val) } | ||
func (c *counter) reset() { atomic.StoreInt32(&c.val, 0) } | ||
|
||
// Collect all events in an array. | ||
// | ||
// w := newCollector(t) | ||
// w.collect(r) | ||
// | ||
// .. do stuff .. | ||
// | ||
// events := w.stop(t) | ||
type eventCollector struct { | ||
w *Watcher | ||
events Events | ||
mu sync.Mutex | ||
} | ||
type Events []Event | ||
|
||
func (e Events) String() string { | ||
b := new(strings.Builder) | ||
for i, ee := range e { | ||
if i > 0 { | ||
b.WriteString("\n") | ||
} | ||
fmt.Fprintf(b, "%-20s %q", ee.Op.String(), ee.Name) | ||
} | ||
return b.String() | ||
} | ||
|
||
func newCollector(t *testing.T) *eventCollector { | ||
return &eventCollector{w: newWatcher(t)} | ||
} | ||
|
||
func (w *eventCollector) stop(t *testing.T) Events { | ||
time.Sleep(waitForEvents) | ||
err := w.w.Close() | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
w.mu.Lock() | ||
defer w.mu.Unlock() | ||
return w.events | ||
} | ||
|
||
func (w *eventCollector) collect(t *testing.T) { | ||
go func() { | ||
for { | ||
select { | ||
case e, ok := <-w.w.Errors: | ||
if !ok { | ||
return | ||
} | ||
t.Error(e) | ||
return | ||
case e, ok := <-w.w.Events: | ||
if !ok { | ||
return | ||
} | ||
w.mu.Lock() | ||
w.events = append(w.events, e) | ||
w.mu.Unlock() | ||
} | ||
} | ||
}() | ||
} |
Oops, something went wrong.