diff --git a/fsnotify.go b/fsnotify.go index 1610d663..4ff4e3db 100644 --- a/fsnotify.go +++ b/fsnotify.go @@ -43,6 +43,12 @@ const ( Chmod ) +// Common errors that can be reported by a watcher +var ( + ErrNonExistentWatch = errors.New("can't remove non-existent watcher") + ErrEventOverflow = errors.New("fsnotify queue overflow") +) + func (op Op) String() string { var b strings.Builder if op.Has(Create) { @@ -77,9 +83,3 @@ func (e Event) Has(op Op) bool { return e.Op.Has(op) } func (e Event) String() string { return fmt.Sprintf("%q: %s", e.Name, e.Op.String()) } - -// Common errors that can be reported by a watcher -var ( - ErrNonExistentWatch = errors.New("can't remove non-existent watcher") - ErrEventOverflow = errors.New("fsnotify queue overflow") -) diff --git a/fsnotify_test.go b/fsnotify_test.go index 51aa49c5..5f6c8827 100644 --- a/fsnotify_test.go +++ b/fsnotify_test.go @@ -1,71 +1,36 @@ -// Copyright 2016 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - //go:build !plan9 // +build !plan9 package fsnotify import ( - "os" "testing" - "time" ) -func TestEventStringWithValue(t *testing.T) { - for opMask, expectedString := range map[Op]string{ - Chmod | Create: `"/usr/someFile": CREATE|CHMOD`, - Rename: `"/usr/someFile": RENAME`, - Remove: `"/usr/someFile": REMOVE`, - Write | Chmod: `"/usr/someFile": WRITE|CHMOD`, - } { - event := Event{Name: "/usr/someFile", Op: opMask} - if event.String() != expectedString { - t.Fatalf("Expected %s, got: %v", expectedString, event.String()) - } - - } -} - -func TestEventOpStringWithValue(t *testing.T) { - expectedOpString := "WRITE|CHMOD" - event := Event{Name: "someFile", Op: Write | Chmod} - if event.Op.String() != expectedOpString { - t.Fatalf("Expected %s, got: %v", expectedOpString, event.Op.String()) - } -} - -func TestEventOpStringWithNoValue(t *testing.T) { - expectedOpString := "" - event := Event{Name: "testFile", Op: 0} - if event.Op.String() != expectedOpString { - t.Fatalf("Expected %s, got: %v", expectedOpString, event.Op.String()) - } -} - -// TestWatcherClose tests that the goroutine started by creating the watcher can be -// signalled to return at any time, even if there is no goroutine listening on the events -// or errors channels. -func TestWatcherClose(t *testing.T) { - t.Parallel() - - name := tempMkFile(t, "") - w := newWatcher(t) - err := w.Add(name) - if err != nil { - t.Fatal(err) - } - - err = os.Remove(name) - if err != nil { - t.Fatal(err) +func TestEventString(t *testing.T) { + tests := []struct { + in Event + want string + }{ + {Event{}, `"": `}, + {Event{"/file", 0}, `"/file": `}, + + {Event{"/file", Chmod | Create}, + `"/file": CREATE|CHMOD`}, + {Event{"/file", Rename}, + `"/file": RENAME`}, + {Event{"/file", Remove}, + `"/file": REMOVE`}, + {Event{"/file", Write | Chmod}, + `"/file": WRITE|CHMOD`}, } - // Allow the watcher to receive the event. - time.Sleep(time.Millisecond * 100) - err = w.Close() - if err != nil { - t.Fatal(err) + for _, tt := range tests { + t.Run("", func(t *testing.T) { + have := tt.in.String() + if have != tt.want { + t.Errorf("\nhave: %q\nwant: %q", have, tt.want) + } + }) } } diff --git a/helpers_test.go b/helpers_test.go new file mode 100644 index 00000000..cb2f1212 --- /dev/null +++ b/helpers_test.go @@ -0,0 +1,437 @@ +package fsnotify + +import ( + "fmt" + "io/fs" + "os" + "os/exec" + "path/filepath" + "runtime" + "sort" + "strings" + "sync" + "testing" + "time" +) + +type testCase struct { + name string + ops func(*testing.T, *Watcher, string) + want string +} + +func (tt testCase) run(t *testing.T) { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + tmp := t.TempDir() + + w := newCollector(t) + w.collect(t) + + tt.ops(t, w.w, tmp) + + cmpEvents(t, tmp, w.stop(t), newEvents(t, tt.want)) + }) +} + +// We wait a little bit after most commands; gives the system some time to sync +// things and makes things more consistent across platforms. +func eventSeparator() { time.Sleep(50 * time.Millisecond) } +func waitForEvents() { time.Sleep(500 * time.Millisecond) } + +// newWatcher initializes an fsnotify Watcher instance. +func newWatcher(t *testing.T, add ...string) *Watcher { + t.Helper() + w, err := NewWatcher() + if err != nil { + t.Fatalf("newWatcher: %s", err) + } + for _, a := range add { + err := w.Add(a) + if err != nil { + t.Fatalf("newWatcher: add %q: %s", a, err) + } + } + return w +} + +// 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) + } +} + +const noWait = "" + +func shouldWait(path ...string) bool { + // Take advantage of the fact that filepath.Join skips empty parameters. + for _, p := range path { + if p == "" { + return false + } + } + return true +} + +// mkdir +func mkdir(t *testing.T, path ...string) { + t.Helper() + if len(path) < 1 { + t.Fatalf("mkdir: path must have at least one element: %s", path) + } + err := os.Mkdir(filepath.Join(path...), 0o0755) + if err != nil { + t.Fatalf("mkdir(%q): %s", filepath.Join(path...), err) + } + if shouldWait(path...) { + eventSeparator() + } +} + +// 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) +// } +// if shouldWait(path...) { +// eventSeparator() +// } +// } + +// 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) + } + if shouldWait(link...) { + eventSeparator() + } +} + +// 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 := func() error { + fp, err := os.OpenFile(filepath.Join(path...), os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) + if err != nil { + return err + } + if err := fp.Sync(); err != nil { + return err + } + if shouldWait(path...) { + eventSeparator() + } + if _, err := fp.WriteString(data); err != nil { + return err + } + if err := fp.Sync(); err != nil { + return err + } + if shouldWait(path...) { + eventSeparator() + } + return fp.Close() + }() + 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) + } + if shouldWait(path...) { + eventSeparator() + } +} + +// 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) + } + if shouldWait(dst...) { + eventSeparator() + } +} + +// 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) + } + if shouldWait(path...) { + eventSeparator() + } +} + +// 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) + } + if shouldWait(path...) { + eventSeparator() + } +} + +// chmod +func chmod(t *testing.T, mode fs.FileMode, path ...string) { + t.Helper() + if len(path) < 1 { + t.Fatalf("chmod: path must have at least one element: %s", path) + } + err := os.Chmod(filepath.Join(path...), mode) + if err != nil { + t.Fatalf("chmod(%q): %s", filepath.Join(path...), err) + } + if shouldWait(path...) { + eventSeparator() + } +} + +// 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 + done chan struct{} +} + +func newCollector(t *testing.T) *eventCollector { + return &eventCollector{w: newWatcher(t), done: make(chan struct{})} +} + +func (w *eventCollector) stop(t *testing.T) Events { + waitForEvents() + + go func() { + err := w.w.Close() + if err != nil { + t.Error(err) + } + }() + + select { + case <-time.After(1 * time.Second): + t.Fatal("event stream was not closed after 1 second") + case <-w.done: + } + + 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 { + w.done <- struct{}{} + return + } + t.Error(e) + return + case e, ok := <-w.w.Events: + if !ok { + w.done <- struct{}{} + return + } + w.mu.Lock() + w.events = append(w.events, e) + w.mu.Unlock() + } + } + }() +} + +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(), filepath.ToSlash(ee.Name)) + } + return b.String() +} + +func (e Events) TrimPrefix(prefix string) Events { + for i := range e { + if e[i].Name == prefix { + e[i].Name = "/" + } else { + e[i].Name = strings.TrimPrefix(e[i].Name, prefix) + } + } + return e +} + +func (e Events) copy() Events { + cp := make(Events, len(e)) + copy(cp, e) + return cp +} + +// Create a new Events list from a string; for example: +// +// CREATE path +// CREATE|WRITE path +// +// Every event is one line, and any whitespace between the event and path are +// ignored. The path can optionally be surrounded in ". Anything after a "#" is +// ignored. +// +// Platform-specific tests can be added after GOOS: +// +// # Tested if nothing else matches +// CREATE path +// +// # Windows-specific test. +// windows: +// WRITE path +func newEvents(t *testing.T, s string) Events { + t.Helper() + + var ( + lines = strings.Split(s, "\n") + group string + events = make(map[string]Events) + ) + for no, line := range lines { + if i := strings.IndexByte(line, '#'); i > -1 { + line = line[:i] + } + line = strings.TrimSpace(line) + if line == "" { + continue + } + if strings.HasSuffix(line, ":") { + group = strings.TrimRight(line, ":") + continue + } + + fields := strings.Fields(line) + if len(fields) < 2 { + t.Fatalf("newEvents: line %d has less than 2 fields: %s", no, line) + } + + path := strings.Trim(fields[len(fields)-1], `"`) + + var op Op + for _, e := range fields[:len(fields)-1] { + if e == "|" { + continue + } + for _, ee := range strings.Split(e, "|") { + switch strings.ToUpper(ee) { + case "CREATE": + op |= Create + case "WRITE": + op |= Write + case "REMOVE": + op |= Remove + case "RENAME": + op |= Rename + case "CHMOD": + op |= Chmod + default: + t.Fatalf("newEvents: line %d has unknown event %q: %s", no, ee, line) + } + } + } + events[group] = append(events[group], Event{Name: path, Op: op}) + } + + if e, ok := events[runtime.GOOS]; ok { + return e + } + return events[""] +} + +func cmpEvents(t *testing.T, tmp string, have, want Events) { + t.Helper() + + have = have.TrimPrefix(tmp) + + haveSort, wantSort := have.copy(), want.copy() + sort.Slice(haveSort, func(i, j int) bool { + return haveSort[i].String() > haveSort[j].String() + }) + sort.Slice(wantSort, func(i, j int) bool { + return wantSort[i].String() > wantSort[j].String() + }) + + if haveSort.String() != wantSort.String() { + t.Errorf("\nhave:\n%s\nwant:\n%s", indent(have), indent(want)) + } +} + +func indent(s fmt.Stringer) string { + return "\t" + strings.ReplaceAll(s.String(), "\n", "\n\t") +} diff --git a/inotify.go b/inotify.go index 71254996..454e81e2 100644 --- a/inotify.go +++ b/inotify.go @@ -22,7 +22,9 @@ import ( // Watcher watches a set of files, delivering events to a channel. type Watcher struct { - fd int // https://github.com/golang/go/issues/26439 can't call .Fd() on os.FIle or Read will no longer return on Close() + // Store fd here as os.File.Read() will no longer return on close after + // calling Fd(). See: https://github.com/golang/go/issues/26439 + fd int Events chan Event Errors chan error mu sync.Mutex // Map access diff --git a/inotify_test.go b/inotify_test.go index 9aed1bec..785888bc 100644 --- a/inotify_test.go +++ b/inotify_test.go @@ -18,163 +18,119 @@ import ( "time" ) -func TestInotifyCloseRightAway(t *testing.T) { - w, err := NewWatcher() - if err != nil { - t.Fatalf("Failed to create watcher") +// TODO: I'm not sure if these tests are still needed; I think they've become +// redundant after epoll was removed. Keep them for now to be sure. +func TestInotifyClose(t *testing.T) { + isWatcherReallyClosed := func(t *testing.T, w *Watcher) { + select { + case err, ok := <-w.Errors: + if ok { + t.Fatalf("w.Errors is not closed; readEvents is still alive after closing (error: %v)", err) + } + default: + t.Fatalf("w.Errors would have blocked; readEvents is still alive!") + } + select { + case _, ok := <-w.Events: + if ok { + t.Fatalf("w.Events is not closed; readEvents is still alive after closing") + } + default: + t.Fatalf("w.Events would have blocked; readEvents is still alive!") + } } - // Close immediately; it won't even reach the first unix.Read. - w.Close() + t.Run("close immediately", func(t *testing.T) { + t.Parallel() + w := newWatcher(t) - // Wait for the close to complete. - <-time.After(50 * time.Millisecond) - isWatcherReallyClosed(t, w) -} + w.Close() // Close immediately; it won't even reach the first unix.Read. + <-time.After(50 * time.Millisecond) // Wait for the close to complete. + isWatcherReallyClosed(t, w) + }) -func TestInotifyCloseSlightlyLater(t *testing.T) { - w, err := NewWatcher() - if err != nil { - t.Fatalf("Failed to create watcher") - } + t.Run("close slightly later", func(t *testing.T) { + t.Parallel() + w := newWatcher(t) - // Wait until readEvents has reached unix.Read, and Close. - <-time.After(50 * time.Millisecond) - w.Close() + <-time.After(50 * time.Millisecond) // Wait until readEvents has reached unix.Read, and Close. + w.Close() + <-time.After(50 * time.Millisecond) // Wait for the close to complete. + isWatcherReallyClosed(t, w) + }) - // Wait for the close to complete. - <-time.After(50 * time.Millisecond) - isWatcherReallyClosed(t, w) -} + t.Run("close slightly later with watch", func(t *testing.T) { + t.Parallel() + w := newWatcher(t) + addWatch(t, w, t.TempDir()) -func TestInotifyCloseSlightlyLaterWithWatch(t *testing.T) { - testDir := tempMkdir(t) - defer os.RemoveAll(testDir) + <-time.After(50 * time.Millisecond) // Wait until readEvents has reached unix.Read, and Close. + w.Close() + <-time.After(50 * time.Millisecond) // Wait for the close to complete. + isWatcherReallyClosed(t, w) + }) - w, err := NewWatcher() - if err != nil { - t.Fatalf("Failed to create watcher") - } - w.Add(testDir) + t.Run("close after read", func(t *testing.T) { + t.Parallel() + tmp := t.TempDir() + w := newWatcher(t) + addWatch(t, w, tmp) - // Wait until readEvents has reached unix.Read, and Close. - <-time.After(50 * time.Millisecond) - w.Close() + touch(t, tmp, "somethingSOMETHINGsomethingSOMETHING") // Generate an event. - // Wait for the close to complete. - <-time.After(50 * time.Millisecond) - isWatcherReallyClosed(t, w) -} + <-time.After(50 * time.Millisecond) // Wait for readEvents to read the event, then close the watcher. + w.Close() + <-time.After(50 * time.Millisecond) // Wait for the close to complete. + isWatcherReallyClosed(t, w) + }) -func TestInotifyCloseAfterRead(t *testing.T) { - testDir := tempMkdir(t) - defer os.RemoveAll(testDir) + t.Run("replace after close", func(t *testing.T) { + t.Parallel() - w, err := NewWatcher() - if err != nil { - t.Fatalf("Failed to create watcher") - } + tmp := t.TempDir() + w := newWatcher(t) + defer w.Close() - err = w.Add(testDir) - if err != nil { - t.Fatalf("Failed to add .") - } - - // Generate an event. - os.Create(filepath.Join(testDir, "somethingSOMETHINGsomethingSOMETHING")) - - // Wait for readEvents to read the event, then close the watcher. - <-time.After(50 * time.Millisecond) - w.Close() - - // Wait for the close to complete. - <-time.After(50 * time.Millisecond) - isWatcherReallyClosed(t, w) -} - -func isWatcherReallyClosed(t *testing.T, w *Watcher) { - select { - case err, ok := <-w.Errors: - if ok { - t.Fatalf("w.Errors is not closed; readEvents is still alive after closing (error: %v)", err) + addWatch(t, w, tmp) + touch(t, tmp, "testfile") + select { + case <-w.Events: + case err := <-w.Errors: + t.Fatalf("Error from watcher: %v", err) + case <-time.After(50 * time.Millisecond): + t.Fatalf("Took too long to wait for event") } - default: - t.Fatalf("w.Errors would have blocked; readEvents is still alive!") - } - select { - case _, ok := <-w.Events: - if ok { - t.Fatalf("w.Events is not closed; readEvents is still alive after closing") + // At this point, we've received one event, so the goroutine is ready and it + // blocking on unix.Read. Now try to swap the file descriptor under its + // nose. + w.Close() + w, err := NewWatcher() + defer func() { _ = w.Close() }() + if err != nil { + t.Fatalf("Failed to create second watcher: %v", err) } - default: - t.Fatalf("w.Events would have blocked; readEvents is still alive!") - } -} - -func TestInotifyCloseCreate(t *testing.T) { - testDir := tempMkdir(t) - defer os.RemoveAll(testDir) - - w, err := NewWatcher() - if err != nil { - t.Fatalf("Failed to create watcher: %v", err) - } - defer w.Close() - - err = w.Add(testDir) - if err != nil { - t.Fatalf("Failed to add testDir: %v", err) - } - h, err := os.Create(filepath.Join(testDir, "testfile")) - if err != nil { - t.Fatalf("Failed to create file in testdir: %v", err) - } - h.Close() - select { - case <-w.Events: - case err := <-w.Errors: - t.Fatalf("Error from watcher: %v", err) - case <-time.After(50 * time.Millisecond): - t.Fatalf("Took too long to wait for event") - } - - // At this point, we've received one event, so the goroutine is ready. - // It's also blocking on unix.Read. - // Now we try to swap the file descriptor under its nose. - w.Close() - w, err = NewWatcher() - defer func() { _ = w.Close() }() - if err != nil { - t.Fatalf("Failed to create second watcher: %v", err) - } - <-time.After(50 * time.Millisecond) - err = w.Add(testDir) - if err != nil { - t.Fatalf("Error adding testDir again: %v", err) - } + <-time.After(50 * time.Millisecond) + err = w.Add(tmp) + if err != nil { + t.Fatalf("Error adding tmp dir again: %v", err) + } + }) } -// This test verifies the watcher can keep up with file creations/deletions -// when under load. +// Verify the watcher can keep up with file creations/deletions when under load. +// +// TODO: should probably be in integrations_test. func TestInotifyStress(t *testing.T) { maxNumToCreate := 1000 - testDir := tempMkdir(t) - defer os.RemoveAll(testDir) - testFilePrefix := filepath.Join(testDir, "testfile") + tmp := t.TempDir() + testFilePrefix := filepath.Join(tmp, "testfile") - w, err := NewWatcher() - if err != nil { - t.Fatalf("Failed to create watcher: %v", err) - } + w := newWatcher(t) defer w.Close() - - err = w.Add(testDir) - if err != nil { - t.Fatalf("Failed to add testDir: %v", err) - } + addWatch(t, w, tmp) doneChan := make(chan struct{}) // The buffer ensures that the file generation goroutine is never blocked. @@ -203,7 +159,7 @@ func TestInotifyStress(t *testing.T) { for i := 0; i < maxNumToCreate; i++ { testFile := fmt.Sprintf("%s%d", testFilePrefix, i) - err = os.Remove(testFile) + err := os.Remove(testFile) if err != nil { errChan <- fmt.Errorf("Remove failed: %v", err) } @@ -274,70 +230,17 @@ func TestInotifyStress(t *testing.T) { } } -func TestInotifyRemoveTwice(t *testing.T) { - testDir := tempMkdir(t) - defer os.RemoveAll(testDir) - testFile := filepath.Join(testDir, "testfile") - - handle, err := os.Create(testFile) - if err != nil { - t.Fatalf("Create failed: %v", err) - } - handle.Close() - - w, err := NewWatcher() - if err != nil { - t.Fatalf("Failed to create watcher: %v", err) - } - defer w.Close() - - err = w.Add(testFile) - if err != nil { - t.Fatalf("Failed to add testFile: %v", err) - } - - err = w.Remove(testFile) - if err != nil { - t.Fatalf("wanted successful remove but got: %v", err) - } - - err = w.Remove(testFile) - if err == nil { - t.Fatalf("no error on removing invalid file") - } else if !errors.Is(err, ErrNonExistentWatch) { - t.Fatalf("unexpected error %v on removing invalid file", err) - } - - w.mu.Lock() - defer w.mu.Unlock() - if len(w.watches) != 0 { - t.Fatalf("Expected watches len is 0, but got: %d, %v", len(w.watches), w.watches) - } - if len(w.paths) != 0 { - t.Fatalf("Expected paths len is 0, but got: %d, %v", len(w.paths), w.paths) - } -} - func TestInotifyInnerMapLength(t *testing.T) { - testDir := tempMkdir(t) - defer os.RemoveAll(testDir) - testFile := filepath.Join(testDir, "testfile") + t.Parallel() - handle, err := os.Create(testFile) - if err != nil { - t.Fatalf("Create failed: %v", err) - } - handle.Close() + tmp := t.TempDir() + file := filepath.Join(tmp, "testfile") - w, err := NewWatcher() - if err != nil { - t.Fatalf("Failed to create watcher: %v", err) - } + touch(t, file) + + w := newWatcher(t) + addWatch(t, w, file) - err = w.Add(testFile) - if err != nil { - t.Fatalf("Failed to add testFile: %v", err) - } var wg sync.WaitGroup wg.Add(1) go func() { @@ -347,10 +250,7 @@ func TestInotifyInnerMapLength(t *testing.T) { } }() - err = os.Remove(testFile) - if err != nil { - t.Fatalf("Failed to remove testFile: %v", err) - } + rm(t, file) <-w.Events // consume Remove event <-time.After(50 * time.Millisecond) // wait IN_IGNORE propagated @@ -370,47 +270,36 @@ func TestInotifyInnerMapLength(t *testing.T) { } func TestInotifyOverflow(t *testing.T) { + t.Parallel() + // We need to generate many more events than the - // fs.inotify.max_queued_events sysctl setting. - // We use multiple goroutines (one per directory) - // to speed up file creation. + // fs.inotify.max_queued_events sysctl setting. We use multiple goroutines + // (one per directory) to speed up file creation. numDirs := 128 numFiles := 1024 - testDir := tempMkdir(t) - defer os.RemoveAll(testDir) - - w, err := NewWatcher() - if err != nil { - t.Fatalf("Failed to create watcher: %v", err) - } + tmp := t.TempDir() + w := newWatcher(t) defer w.Close() for dn := 0; dn < numDirs; dn++ { - testSubdir := fmt.Sprintf("%s/%d", testDir, dn) - - err := os.Mkdir(testSubdir, 0o777) - if err != nil { - t.Fatalf("Cannot create subdir: %v", err) - } - - err = w.Add(testSubdir) - if err != nil { - t.Fatalf("Failed to add subdir: %v", err) - } + dir := fmt.Sprintf("%s/%d", tmp, dn) + mkdir(t, dir, noWait) + addWatch(t, w, dir) } errChan := make(chan error, numDirs*numFiles) - // All events need to be in the inotify queue before pulling events off it to trigger this error. + // All events need to be in the inotify queue before pulling events off it + // to trigger this error. wg := sync.WaitGroup{} for dn := 0; dn < numDirs; dn++ { - testSubdir := fmt.Sprintf("%s/%d", testDir, dn) + dir := fmt.Sprintf("%s/%d", tmp, dn) wg.Add(1) go func() { for fn := 0; fn < numFiles; fn++ { - testFile := fmt.Sprintf("%s/%d", testSubdir, fn) + testFile := fmt.Sprintf("%s/%d", dir, fn) handle, err := os.Create(testFile) if err != nil { @@ -446,7 +335,7 @@ func TestInotifyOverflow(t *testing.T) { t.Fatalf("Got an error from watcher: %v", err) } case evt := <-w.Events: - if !strings.HasPrefix(evt.Name, testDir) { + if !strings.HasPrefix(evt.Name, tmp) { t.Fatalf("Got an event for an unknown file: %s", evt.Name) } if evt.Op == Create { @@ -465,31 +354,103 @@ func TestInotifyOverflow(t *testing.T) { } } -func TestInotifyWatchList(t *testing.T) { - testDir := tempMkdir(t) - defer os.RemoveAll(testDir) - testFile := filepath.Join(testDir, "testfile") +func TestInotifyNoBlockingSyscalls(t *testing.T) { + test := func() error { + getThreads := func() (int, error) { + // return pprof.Lookup("threadcreate").Count() + d := fmt.Sprintf("/proc/%d/task", os.Getpid()) + ls, err := os.ReadDir(d) + if err != nil { + return 0, fmt.Errorf("reading %q: %s", d, err) + } + return len(ls), nil + } - handle, err := os.Create(testFile) - if err != nil { - t.Fatalf("Create failed: %v", err) + w := newWatcher(t) + start, err := getThreads() + if err != nil { + return err + } + + // Call readEvents a bunch of times; if this function has a blocking raw + // syscall, it'll create many new kthreads + for i := 0; i <= 60; i++ { + go w.readEvents() + } + + time.Sleep(2 * time.Second) + + end, err := getThreads() + if err != nil { + return err + } + if diff := end - start; diff > 0 { + return fmt.Errorf("Got a nonzero diff %v. starting: %v. ending: %v", diff, start, end) + } + return nil } - handle.Close() - w, err := NewWatcher() + // This test can be a bit flaky, so run it twice and consider it "failed" + // only if both fail. + err := test() if err != nil { - t.Fatalf("Failed to create watcher: %v", err) + time.Sleep(2 * time.Second) + err := test() + if err != nil { + t.Fatal(err) + } } +} + +// TODO: the below should probably be in integration_test, as they're not really +// inotify-specific as far as I can see. + +func TestInotifyRemoveTwice(t *testing.T) { + t.Parallel() + + tmp := t.TempDir() + testFile := filepath.Join(tmp, "testfile") + + touch(t, testFile) + + w := newWatcher(t) defer w.Close() + addWatch(t, w, testFile) - err = w.Add(testFile) + err := w.Remove(testFile) if err != nil { - t.Fatalf("Failed to add testFile: %v", err) + t.Fatal(err) } - err = w.Add(testDir) - if err != nil { - t.Fatalf("Failed to add testDir: %v", err) + + err = w.Remove(testFile) + if err == nil { + t.Fatalf("no error on removing invalid file") + } else if !errors.Is(err, ErrNonExistentWatch) { + t.Fatalf("remove %q: %s", testFile, err) + } + + w.mu.Lock() + defer w.mu.Unlock() + if len(w.watches) != 0 { + t.Fatalf("Expected watches len is 0, but got: %d, %v", len(w.watches), w.watches) + } + if len(w.paths) != 0 { + t.Fatalf("Expected paths len is 0, but got: %d, %v", len(w.paths), w.paths) } +} + +func TestInotifyWatchList(t *testing.T) { + t.Parallel() + + tmp := t.TempDir() + testFile := filepath.Join(tmp, "testfile") + + touch(t, testFile) + + w := newWatcher(t) + defer w.Close() + addWatch(t, w, testFile) + addWatch(t, w, tmp) value := w.WatchList() @@ -503,28 +464,20 @@ func TestInotifyWatchList(t *testing.T) { } func TestInotifyDeleteOpenedFile(t *testing.T) { - testDir := tempMkdir(t) - defer os.RemoveAll(testDir) + t.Parallel() - testFile := filepath.Join(testDir, "testfile") + tmp := t.TempDir() + testFile := filepath.Join(tmp, "testfile") - // create and open a file fd, err := os.Create(testFile) if err != nil { t.Fatalf("Create failed: %v", err) } defer fd.Close() - w, err := NewWatcher() - if err != nil { - t.Fatalf("Failed to create watcher: %v", err) - } + w := newWatcher(t) defer w.Close() - - err = w.Add(testFile) - if err != nil { - t.Fatalf("Failed to add watch for %s: %v", testFile, err) - } + addWatch(t, w, testFile) checkEvent := func(exp Op) { select { @@ -538,47 +491,12 @@ func TestInotifyDeleteOpenedFile(t *testing.T) { } } - // Remove the (opened) file, check Chmod event (notifying - // about file link count change) is received - err = os.Remove(testFile) - if err != nil { - t.Fatalf("Failed to remove file: %s", err) - } + // Remove the (opened) file, check Chmod event (notifying about file link + // count change) is received + rm(t, testFile) checkEvent(Chmod) // Close the file, check Remove event is received fd.Close() checkEvent(Remove) } - -func TestINotifyNoBlockingSyscalls(t *testing.T) { - getThreads := func() int { - d := fmt.Sprintf("/proc/%d/task", os.Getpid()) - ls, err := os.ReadDir(d) - if err != nil { - t.Fatalf("reading %q: %s", d, err) - } - return len(ls) - } - - w, err := NewWatcher() - if err != nil { - t.Fatalf("Failed to create watcher: %v", err) - } - - startingThreads := getThreads() - // Call readEvents a bunch of times; if this function has a blocking raw syscall, it'll create many new kthreads - for i := 0; i <= 60; i++ { - go w.readEvents() - } - - // Bad synchronization mechanism - time.Sleep(time.Second * 2) - - endingThreads := getThreads() - - // Did we spawn any new threads? - if diff := endingThreads - startingThreads; diff > 0 { - t.Fatalf("Got a nonzero diff %v. starting: %v. ending: %v", diff, startingThreads, endingThreads) - } -} diff --git a/integration_darwin_test.go b/integration_darwin_test.go index c43d00c6..06fd665c 100644 --- a/integration_darwin_test.go +++ b/integration_darwin_test.go @@ -10,7 +10,6 @@ import ( "strconv" "strings" "testing" - "time" "golang.org/x/sys/unix" ) @@ -25,12 +24,12 @@ func darwinVersion() (int, error) { return strconv.Atoi(s) } -// testExchangedataForWatcher tests the watcher with the exchangedata operation on macOS. +// testExchangedataForWatcher tests the watcher with the exchangedata operation +// on macOS. This is widely used for atomic saves on macOS, e.g. TextMate and in +// Apple's NSDocument. // -// This is widely used for atomic saves on macOS, e.g. TextMate and in Apple's NSDocument. -// -// See https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man2/exchangedata.2.html -// Also see: https://github.com/textmate/textmate/blob/cd016be29489eba5f3c09b7b70b06da134dda550/Frameworks/io/src/swap_file_data.cc#L20 +// https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man2/exchangedata.2.html +// https://github.com/textmate/textmate/blob/cd016be2/Frameworks/io/src/swap_file_data.cc#L20 func testExchangedataForWatcher(t *testing.T, watchDir bool) { osVersion, err := darwinVersion() if err != nil { @@ -40,14 +39,8 @@ func testExchangedataForWatcher(t *testing.T, watchDir bool) { t.Skip("Exchangedata is deprecated in macOS 10.13") } - // Create directory to watch - testDir1 := tempMkdir(t) - - // For the intermediate file - testDir2 := tempMkdir(t) - - defer os.RemoveAll(testDir1) - defer os.RemoveAll(testDir2) + testDir1 := t.TempDir() // Create directory to watch + testDir2 := t.TempDir() // For the intermediate file resolvedFilename := "TestFsnotifyEvents.file" @@ -63,105 +56,69 @@ func testExchangedataForWatcher(t *testing.T, watchDir bool) { // Make sure we create the file before we start watching createAndSyncFile(t, resolved) - watcher := newWatcher(t) + w := newCollector(t) + w.collect(t) // Test both variants in isolation if watchDir { - addWatch(t, watcher, testDir1) + addWatch(t, w.w, testDir1) } else { - addWatch(t, watcher, resolved) + addWatch(t, w.w, resolved) } - // Receive errors on the error channel on a separate goroutine - go func() { - for err := range watcher.Errors { - t.Errorf("error received: %s", err) - } - }() - - // Receive events on the event channel on a separate goroutine - eventstream := watcher.Events - var removeReceived counter - var createReceived counter - - done := make(chan bool) - - go func() { - for event := range eventstream { - // Only count relevant events - if event.Name == filepath.Clean(resolved) { - if event.Op&Remove == Remove { - removeReceived.increment() - } - if event.Op&Create == Create { - createReceived.increment() - } - } - t.Logf("event received: %s", event) - } - done <- true - }() - - // Repeat to make sure the watched file/directory "survives" the REMOVE/CREATE loop. + // Repeat to make sure the watched file/directory "survives" the + // REMOVE/CREATE loop. for i := 1; i <= 3; i++ { - // The intermediate file is created in a folder outside the watcher - createAndSyncFile(t, intermediate) + createAndSyncFile(t, intermediate) // intermediate file is created outside the watcher - // 1. Swap - if err := unix.Exchangedata(intermediate, resolved, 0); err != nil { + if err := unix.Exchangedata(intermediate, resolved, 0); err != nil { // 1. Swap t.Fatalf("[%d] exchangedata failed: %s", i, err) } - - time.Sleep(50 * time.Millisecond) - - // 2. Delete the intermediate file - err := os.Remove(intermediate) - + eventSeparator() + err := os.Remove(intermediate) // delete the intermediate file if err != nil { t.Fatalf("[%d] remove %s failed: %s", i, intermediate, err) } - time.Sleep(50 * time.Millisecond) - + eventSeparator() } - // We expect this event to be received almost immediately, but let's wait 500 ms to be sure - time.Sleep(500 * time.Millisecond) - // The events will be (CHMOD + REMOVE + CREATE) X 2. Let's focus on the last two: - if removeReceived.value() < 3 { - t.Fatal("fsnotify remove events have not been received after 500 ms") + events := w.stop(t) + var rm, create Events + for _, e := range events { + if e.Has(Create) { + create = append(create, e) + } + if e.Has(Remove) { + rm = append(rm, e) + } } - - if createReceived.value() < 3 { - t.Fatal("fsnotify create events have not been received after 500 ms") + if len(rm) < 3 { + t.Fatalf("less than 3 REMOVE events:\n%s", events) } + if len(create) < 3 { + t.Fatalf("less than 3 CREATE events:\n%s", events) + } +} - watcher.Close() - t.Log("waiting for the event channel to become closed...") - select { - case <-done: - t.Log("event channel closed") - case <-time.After(2 * time.Second): - t.Fatal("event stream was not closed after 2 seconds") +func createAndSyncFile(t *testing.T, filepath string) { + f1, err := os.OpenFile(filepath, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + t.Fatalf("creating %s failed: %s", filepath, err) } + f1.Sync() + f1.Close() } // TestExchangedataInWatchedDir test exchangedata operation on file in watched dir. func TestExchangedataInWatchedDir(t *testing.T) { + t.Parallel() testExchangedataForWatcher(t, true) } // TestExchangedataInWatchedDir test exchangedata operation on watched file. func TestExchangedataInWatchedFile(t *testing.T) { + t.Parallel() testExchangedataForWatcher(t, false) } - -func createAndSyncFile(t *testing.T, filepath string) { - f1, err := os.OpenFile(filepath, os.O_WRONLY|os.O_CREATE, 0666) - if err != nil { - t.Fatalf("creating %s failed: %s", filepath, err) - } - f1.Sync() - f1.Close() -} diff --git a/integration_test.go b/integration_test.go index c4e1d3e6..6ae1ae07 100644 --- a/integration_test.go +++ b/integration_test.go @@ -1,1279 +1,475 @@ -// Copyright 2010 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - //go:build !plan9 && !solaris // +build !plan9,!solaris package fsnotify import ( - "io/ioutil" - "os" - "os/exec" - "path" + "fmt" "path/filepath" "runtime" "strings" - "sync" "sync/atomic" "testing" "time" ) -const ( - eventSeparator = 50 * time.Millisecond - waitForEvents = 500 * time.Millisecond -) - -// 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) -} - -// tempMkdir makes a temporary directory -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. -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() -} - -// newWatcher initializes an fsnotify Watcher instance. -func newWatcher(t *testing.T) *Watcher { - watcher, err := NewWatcher() - if err != nil { - t.Fatalf("NewWatcher() failed: %s", err) +func TestWatch(t *testing.T) { + tests := []testCase{ + {"multiple creates", func(t *testing.T, w *Watcher, tmp string) { + file := filepath.Join(tmp, "file") + addWatch(t, w, tmp) + + cat(t, "data", file) + rm(t, file) + + touch(t, file) // Recreate the file + cat(t, "data", file) // Modify + cat(t, "data", file) // Modify + }, ` + create /file + write /file + remove /file + create /file + write /file + write /file + `}, + + {"dir only", func(t *testing.T, w *Watcher, tmp string) { + beforeWatch := filepath.Join(tmp, "beforewatch") + file := filepath.Join(tmp, "file") + + touch(t, beforeWatch) + addWatch(t, w, tmp) + + cat(t, "data", file) + rm(t, file) + rm(t, beforeWatch) + }, ` + create /file + write /file + remove /file + remove /beforewatch + `}, + + {"subdir", func(t *testing.T, w *Watcher, tmp string) { + addWatch(t, w, tmp) + + file := filepath.Join(tmp, "file") + dir := filepath.Join(tmp, "sub") + dirfile := filepath.Join(tmp, "sub/file2") + + mkdir(t, dir) // Create sub-directory + touch(t, file) // Create a file + touch(t, dirfile) // Create a file (Should not see this! we are not watching subdir) + time.Sleep(200 * time.Millisecond) + rmAll(t, dir) // Make sure receive deletes for both file and sub-directory + rm(t, file) + }, ` + create /sub + create /file + remove /sub + remove /file + + # Windows includes a write for the /sub dir too, two of them even(?) + windows: + create /sub + create /file + write /sub + write /sub + remove /sub + remove /file + `}, + } + + for _, tt := range tests { + tt := tt + tt.run(t) } - return watcher } -// addWatch adds a watch for a directory -func addWatch(t *testing.T, watcher *Watcher, dir string) { - if err := watcher.Add(dir); err != nil { - t.Fatalf("watcher.Add(%q) failed: %s", dir, err) - } -} - -func TestFsnotifyMultipleOperations(t *testing.T) { - if runtime.GOOS == "netbsd" { - t.Skip("NetBSD behaviour is not fully correct") // TODO: investigate and fix. - } - - watcher := newWatcher(t) - - // Receive errors on the error channel on a separate goroutine - var wg sync.WaitGroup - wg.Add(1) - go func() { - defer wg.Done() - for err := range watcher.Errors { - t.Errorf("error received: %s", err) - } - }() - - // Create directory to watch - testDir := tempMkdir(t) - defer os.RemoveAll(testDir) - - // Create directory that's not watched - testDirToMoveFiles := tempMkdir(t) - defer os.RemoveAll(testDirToMoveFiles) - - testFile := filepath.Join(testDir, "TestFsnotifySeq.testfile") - testFileRenamed := filepath.Join(testDirToMoveFiles, "TestFsnotifySeqRename.testfile") - - addWatch(t, watcher, testDir) - - // Receive events on the event channel on a separate goroutine - eventstream := watcher.Events - var createReceived, modifyReceived, deleteReceived, renameReceived counter - done := make(chan bool) - go func() { - for event := range eventstream { - // Only count relevant events - if event.Name == filepath.Clean(testDir) || event.Name == filepath.Clean(testFile) { - t.Logf("event received: %s", event) - if event.Op&Remove == Remove { - deleteReceived.increment() - } - if event.Op&Write == Write { - modifyReceived.increment() - } - if event.Op&Create == Create { - createReceived.increment() - } - if event.Op&Rename == Rename { - renameReceived.increment() - } - } else { - t.Logf("unexpected event received: %s", event) +func TestWatchRename(t *testing.T) { + tests := []testCase{ + {"rename file", func(t *testing.T, w *Watcher, tmp string) { + file := filepath.Join(tmp, "file") + + addWatch(t, w, tmp) + cat(t, "asd", file) + mv(t, file, tmp, "renamed") + }, ` + create /file + write /file + rename /file + create /renamed + `}, + + {"rename from unwatched directory", func(t *testing.T, w *Watcher, tmp string) { + unwatched := t.TempDir() + + addWatch(t, w, tmp) + touch(t, unwatched, "file") + mv(t, filepath.Join(unwatched, "file"), tmp, "file") + }, ` + create /file + `}, + + {"rename to unwatched directory", func(t *testing.T, w *Watcher, tmp string) { + if runtime.GOOS == "netbsd" { + t.Skip("NetBSD behaviour is not fully correct") // TODO: investigate and fix. } - } - done <- true - }() - - // Create a file - // This should add at least one event to the fsnotify event queue - var f *os.File - f, err := os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) - if err != nil { - t.Fatalf("creating test file failed: %s", err) - } - f.Sync() - - time.Sleep(eventSeparator) - f.WriteString("data") - f.Sync() - f.Close() - - time.Sleep(eventSeparator) // give system time to sync write change before delete - - if err := testRename(testFile, testFileRenamed); err != nil { - t.Fatalf("rename failed: %s", err) - } - // Modify the file outside of the watched dir - f, err = os.Open(testFileRenamed) - if err != nil { - t.Fatalf("open test renamed file failed: %s", err) - } - f.WriteString("data") - f.Sync() - f.Close() - - time.Sleep(eventSeparator) // give system time to sync write change before delete - - // Recreate the file that was moved - f, err = os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) - if err != nil { - t.Fatalf("creating test file failed: %s", err) - } - f.Close() - time.Sleep(eventSeparator) // give system time to sync write change before delete - - // We expect this event to be received almost immediately, but let's wait 500 ms to be sure - time.Sleep(waitForEvents) - cReceived := createReceived.value() - if cReceived != 2 { - t.Fatalf("incorrect number of create events received after 500 ms (%d vs %d)", cReceived, 2) - } - mReceived := modifyReceived.value() - if mReceived != 1 { - t.Fatalf("incorrect number of modify events received after 500 ms (%d vs %d)", mReceived, 1) - } - dReceived := deleteReceived.value() - rReceived := renameReceived.value() - if dReceived+rReceived != 1 { - t.Fatalf("incorrect number of rename+delete events received after 500 ms (%d vs %d)", rReceived+dReceived, 1) - } - - // Try closing the fsnotify instance - t.Log("calling Close()") - watcher.Close() - t.Log("waiting for the event channel to become closed...") - select { - case <-done: - t.Log("event channel closed") - case <-time.After(2 * time.Second): - t.Fatal("event stream was not closed after 2 seconds") - } - - // wait for all groutines to finish. - wg.Wait() -} - -func TestFsnotifyMultipleCreates(t *testing.T) { - watcher := newWatcher(t) - - // Receive errors on the error channel on a separate goroutine - go func() { - for err := range watcher.Errors { - t.Errorf("error received: %s", err) - } - }() - - // Create directory to watch - testDir := tempMkdir(t) - defer os.RemoveAll(testDir) - - testFile := filepath.Join(testDir, "TestFsnotifySeq.testfile") - - addWatch(t, watcher, testDir) - - // Receive events on the event channel on a separate goroutine - eventstream := watcher.Events - var createReceived, modifyReceived, deleteReceived counter - done := make(chan bool) - go func() { - for event := range eventstream { - // Only count relevant events - if event.Name == filepath.Clean(testDir) || event.Name == filepath.Clean(testFile) { - t.Logf("event received: %s", event) - if event.Op&Remove == Remove { - deleteReceived.increment() - } - if event.Op&Create == Create { - createReceived.increment() - } - if event.Op&Write == Write { - modifyReceived.increment() - } - } else { - t.Logf("unexpected event received: %s", event) + unwatched := t.TempDir() + file := filepath.Join(tmp, "file") + renamed := filepath.Join(unwatched, "renamed") + + addWatch(t, w, tmp) + + cat(t, "data", file) + mv(t, file, renamed) + cat(t, "data", renamed) // Modify the file outside of the watched dir + touch(t, file) // Recreate the file that was moved + }, ` + create /file + write /file + rename /file + create /file + + # Windows has REMOVE /file, rather than CREATE /file + windows: + create /file + write /file + remove /file + create /file + `}, + + {"rename overwriting existing file", func(t *testing.T, w *Watcher, tmp string) { + switch runtime.GOOS { + case "windows": + t.Skipf("os.Rename over existing file does not create an event on %q", runtime.GOOS) } - } - done <- true - }() - - // Create a file - // This should add at least one event to the fsnotify event queue - var f *os.File - f, err := os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) - if err != nil { - t.Fatalf("creating test file failed: %s", err) - } - f.Sync() - - time.Sleep(eventSeparator) - f.WriteString("data") - f.Sync() - f.Close() - time.Sleep(eventSeparator) // give system time to sync write change before delete + touch(t, tmp, "renamed") + addWatch(t, w, tmp) - os.Remove(testFile) + unwatched := t.TempDir() + file := filepath.Join(unwatched, "file") + touch(t, file) + mv(t, file, tmp, "renamed") + }, ` + remove /renamed + create /renamed - time.Sleep(eventSeparator) // give system time to sync write change before delete - - // Recreate the file - f, err = os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) - if err != nil { - t.Fatalf("creating test file failed: %s", err) + # No remove event for Windows and Linux. + linux: + create /renamed + windows: + create /renamed + `}, } - f.Close() - time.Sleep(eventSeparator) // give system time to sync write change before delete - // Modify - f, err = os.OpenFile(testFile, os.O_WRONLY, 0666) - if err != nil { - t.Fatalf("creating test file failed: %s", err) - } - f.Sync() - - time.Sleep(eventSeparator) - f.WriteString("data") - f.Sync() - f.Close() - - time.Sleep(eventSeparator) // give system time to sync write change before delete - - // Modify - f, err = os.OpenFile(testFile, os.O_WRONLY, 0666) - if err != nil { - t.Fatalf("creating test file failed: %s", err) - } - f.Sync() - - time.Sleep(eventSeparator) - f.WriteString("data") - f.Sync() - f.Close() - - time.Sleep(eventSeparator) // give system time to sync write change before delete - - // We expect this event to be received almost immediately, but let's wait 500 ms to be sure - time.Sleep(waitForEvents) - cReceived := createReceived.value() - if cReceived != 2 { - t.Fatalf("incorrect number of create events received after 500 ms (%d vs %d)", cReceived, 2) - } - mReceived := modifyReceived.value() - if mReceived < 3 { - t.Fatalf("incorrect number of modify events received after 500 ms (%d vs atleast %d)", mReceived, 3) - } - dReceived := deleteReceived.value() - if dReceived != 1 { - t.Fatalf("incorrect number of rename+delete events received after 500 ms (%d vs %d)", dReceived, 1) - } - - // Try closing the fsnotify instance - t.Log("calling Close()") - watcher.Close() - t.Log("waiting for the event channel to become closed...") - select { - case <-done: - t.Log("event channel closed") - case <-time.After(2 * time.Second): - t.Fatal("event stream was not closed after 2 seconds") + for _, tt := range tests { + tt := tt + tt.run(t) } } -func TestFsnotifyDirOnly(t *testing.T) { - watcher := newWatcher(t) - - // Create directory to watch - testDir := tempMkdir(t) - defer os.RemoveAll(testDir) - - // Create a file before watching directory - // This should NOT add any events to the fsnotify event queue - testFileAlreadyExists := filepath.Join(testDir, "TestFsnotifyEventsExisting.testfile") - { - var f *os.File - f, err := os.OpenFile(testFileAlreadyExists, os.O_WRONLY|os.O_CREATE, 0666) - if err != nil { - t.Fatalf("creating test file failed: %s", err) - } - f.Sync() - f.Close() - } - - addWatch(t, watcher, testDir) - - // Receive errors on the error channel on a separate goroutine - go func() { - for err := range watcher.Errors { - t.Errorf("error received: %s", err) - } - }() - - testFile := filepath.Join(testDir, "TestFsnotifyDirOnly.testfile") - - // Receive events on the event channel on a separate goroutine - eventstream := watcher.Events - var createReceived, modifyReceived, deleteReceived counter - done := make(chan bool) - go func() { - for event := range eventstream { - // Only count relevant events - if event.Name == filepath.Clean(testDir) || event.Name == filepath.Clean(testFile) || event.Name == filepath.Clean(testFileAlreadyExists) { - t.Logf("event received: %s", event) - if event.Op&Remove == Remove { - deleteReceived.increment() - } - if event.Op&Write == Write { - modifyReceived.increment() - } - if event.Op&Create == Create { - createReceived.increment() - } - } else { - t.Logf("unexpected event received: %s", event) +func TestWatchSymlink(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("symlinks don't work on Windows") + } + + tests := []testCase{ + {"create unresolvable symlink", func(t *testing.T, w *Watcher, tmp string) { + addWatch(t, w, tmp) + + symlink(t, filepath.Join(tmp, "target"), tmp, "link") + }, ` + create /link + `}, + + {"cyclic symlink", func(t *testing.T, w *Watcher, tmp string) { + if runtime.GOOS == "darwin" { + // This test is borked on macOS; it reports events outside the + // watched directory: + // + // create "/private/.../testwatchsymlinkcyclic_symlink3681444267/001/link" + // create "/link" + // write "/link" + // write "/private/.../testwatchsymlinkcyclic_symlink3681444267/001/link" + // + // kqueue.go does a lot of weird things with symlinks that I + // don't think are necessarily correct, but need to test a bit + // more. + t.Skip() } - } - done <- true - }() - - // Create a file - // This should add at least one event to the fsnotify event queue - var f *os.File - f, err := os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) - if err != nil { - t.Fatalf("creating test file failed: %s", err) - } - f.Sync() - - time.Sleep(eventSeparator) - f.WriteString("data") - f.Sync() - f.Close() - time.Sleep(eventSeparator) // give system time to sync write change before delete + symlink(t, ".", tmp, "link") + addWatch(t, w, tmp) + rm(t, tmp, "link") + cat(t, "foo", tmp, "link") - os.Remove(testFile) - os.Remove(testFileAlreadyExists) + }, ` + write /link + create /link - // We expect this event to be received almost immediately, but let's wait 500 ms to be sure - time.Sleep(waitForEvents) - cReceived := createReceived.value() - if cReceived != 1 { - t.Fatalf("incorrect number of create events received after 500 ms (%d vs %d)", cReceived, 1) - } - mReceived := modifyReceived.value() - if mReceived != 1 { - t.Fatalf("incorrect number of modify events received after 500 ms (%d vs %d)", mReceived, 1) - } - dReceived := deleteReceived.value() - if dReceived != 2 { - t.Fatalf("incorrect number of delete events received after 500 ms (%d vs %d)", dReceived, 2) + linux: + remove /link + create /link + write /link + `}, } - // Try closing the fsnotify instance - t.Log("calling Close()") - watcher.Close() - t.Log("waiting for the event channel to become closed...") - select { - case <-done: - t.Log("event channel closed") - case <-time.After(2 * time.Second): - t.Fatal("event stream was not closed after 2 seconds") + for _, tt := range tests { + tt := tt + tt.run(t) } } -func TestFsnotifyDeleteWatchedDir(t *testing.T) { - watcher := newWatcher(t) - defer watcher.Close() - - // Create directory to watch - testDir := tempMkdir(t) - defer os.RemoveAll(testDir) - - // Create a file before watching directory - testFileAlreadyExists := filepath.Join(testDir, "TestFsnotifyEventsExisting.testfile") - { - var f *os.File - f, err := os.OpenFile(testFileAlreadyExists, os.O_WRONLY|os.O_CREATE, 0666) - if err != nil { - t.Fatalf("creating test file failed: %s", err) - } - f.Sync() - f.Close() +func TestWatchAttrib(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("attributes don't work on Windows") } - addWatch(t, watcher, testDir) - - // Add a watch for testFile - addWatch(t, watcher, testFileAlreadyExists) - - // Receive errors on the error channel on a separate goroutine - go func() { - for err := range watcher.Errors { - t.Errorf("error received: %s", err) - } - }() - - // Receive events on the event channel on a separate goroutine - eventstream := watcher.Events - var deleteReceived counter - go func() { - for event := range eventstream { - // Only count relevant events - if event.Name == filepath.Clean(testDir) || event.Name == filepath.Clean(testFileAlreadyExists) { - t.Logf("event received: %s", event) - if event.Op&Remove == Remove { - deleteReceived.increment() - } - } else { - t.Logf("unexpected event received: %s", event) - } - } - }() + tests := []testCase{ + {"chmod", func(t *testing.T, w *Watcher, tmp string) { + file := filepath.Join(tmp, "file") - os.RemoveAll(testDir) + cat(t, "data", file) + addWatch(t, w, file) + chmod(t, 0o700, file) + }, ` + CHMOD "/file" + `}, - // We expect this event to be received almost immediately, but let's wait 500 ms to be sure - time.Sleep(waitForEvents) - dReceived := deleteReceived.value() - if dReceived < 2 { - t.Fatalf("did not receive at least %d delete events, received %d after 500 ms", 2, dReceived) - } -} + {"write does not trigger CHMOD", func(t *testing.T, w *Watcher, tmp string) { + file := filepath.Join(tmp, "file") -func TestFsnotifySubDir(t *testing.T) { - watcher := newWatcher(t) + cat(t, "data", file) + addWatch(t, w, file) + chmod(t, 0o700, file) - // Create directory to watch - testDir := tempMkdir(t) - defer os.RemoveAll(testDir) + cat(t, "more data", file) + }, ` + CHMOD "/file" + WRITE "/file" + `}, - testFile1 := filepath.Join(testDir, "TestFsnotifyFile1.testfile") - testSubDir := filepath.Join(testDir, "sub") - testSubDirFile := filepath.Join(testDir, "sub/TestFsnotifyFile1.testfile") + {"chmod after write", func(t *testing.T, w *Watcher, tmp string) { + file := filepath.Join(tmp, "file") - // Receive errors on the error channel on a separate goroutine - go func() { - for err := range watcher.Errors { - t.Errorf("error received: %s", err) - } - }() - - // Receive events on the event channel on a separate goroutine - eventstream := watcher.Events - var createReceived, deleteReceived counter - done := make(chan bool) - go func() { - for event := range eventstream { - // Only count relevant events - if event.Name == filepath.Clean(testDir) || event.Name == filepath.Clean(testSubDir) || event.Name == filepath.Clean(testFile1) { - t.Logf("event received: %s", event) - if event.Op&Create == Create { - createReceived.increment() - } - if event.Op&Remove == Remove { - deleteReceived.increment() - } - } else { - t.Logf("unexpected event received: %s", event) - } - } - done <- true - }() - - addWatch(t, watcher, testDir) - - // Create sub-directory - if err := os.Mkdir(testSubDir, 0777); err != nil { - t.Fatalf("failed to create test sub-directory: %s", err) + cat(t, "data", file) + addWatch(t, w, file) + chmod(t, 0o700, file) + cat(t, "more data", file) + chmod(t, 0o600, file) + }, ` + CHMOD "/file" + WRITE "/file" + CHMOD "/file" + `}, } - // Create a file - var f *os.File - f, err := os.OpenFile(testFile1, os.O_WRONLY|os.O_CREATE, 0666) - if err != nil { - t.Fatalf("creating test file failed: %s", err) - } - f.Sync() - f.Close() - - // Create a file (Should not see this! we are not watching subdir) - var fs *os.File - fs, err = os.OpenFile(testSubDirFile, os.O_WRONLY|os.O_CREATE, 0666) - if err != nil { - t.Fatalf("creating test file failed: %s", err) - } - fs.Sync() - fs.Close() - - time.Sleep(200 * time.Millisecond) - - // Make sure receive deletes for both file and sub-directory - os.RemoveAll(testSubDir) - os.Remove(testFile1) - - // We expect this event to be received almost immediately, but let's wait 500 ms to be sure - time.Sleep(waitForEvents) - cReceived := createReceived.value() - if cReceived != 2 { - t.Fatalf("incorrect number of create events received after 500 ms (%d vs %d)", cReceived, 2) - } - dReceived := deleteReceived.value() - if dReceived != 2 { - t.Fatalf("incorrect number of delete events received after 500 ms (%d vs %d)", dReceived, 2) - } - - // Try closing the fsnotify instance - t.Log("calling Close()") - watcher.Close() - t.Log("waiting for the event channel to become closed...") - select { - case <-done: - t.Log("event channel closed") - case <-time.After(2 * time.Second): - t.Fatal("event stream was not closed after 2 seconds") + for _, tt := range tests { + tt := tt + tt.run(t) } } -func TestFsnotifyRename(t *testing.T) { - watcher := newWatcher(t) - - // Create directory to watch - testDir := tempMkdir(t) - defer os.RemoveAll(testDir) - - addWatch(t, watcher, testDir) - - // Receive errors on the error channel on a separate goroutine - go func() { - for err := range watcher.Errors { - t.Errorf("error received: %s", err) - } - }() - - testFile := filepath.Join(testDir, "TestFsnotifyEvents.testfile") - testFileRenamed := filepath.Join(testDir, "TestFsnotifyEvents.testfileRenamed") - - // Receive events on the event channel on a separate goroutine - eventstream := watcher.Events - var renameReceived counter - done := make(chan bool) - go func() { - for event := range eventstream { - // Only count relevant events - if event.Name == filepath.Clean(testDir) || event.Name == filepath.Clean(testFile) || event.Name == filepath.Clean(testFileRenamed) { - if event.Op&Rename == Rename { - renameReceived.increment() - } - t.Logf("event received: %s", event) - } else { - t.Logf("unexpected event received: %s", event) +func TestWatchRm(t *testing.T) { + tests := []testCase{ + {"remove watched directory", func(t *testing.T, w *Watcher, tmp string) { + if runtime.GOOS == "openbsd" || runtime.GOOS == "netbsd" { + t.Skip("behaviour is inconsistent on OpenBSD and NetBSD, and this test is flaky") } - } - done <- true - }() - - // Create a file - // This should add at least one event to the fsnotify event queue - var f *os.File - f, err := os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) - if err != nil { - t.Fatalf("creating test file failed: %s", err) - } - f.Sync() - - f.WriteString("data") - f.Sync() - f.Close() - - // Add a watch for testFile - addWatch(t, watcher, testFile) - - if err := testRename(testFile, testFileRenamed); err != nil { - t.Fatalf("rename failed: %s", err) - } - - // We expect this event to be received almost immediately, but let's wait 500 ms to be sure - time.Sleep(waitForEvents) - if renameReceived.value() == 0 { - t.Fatal("fsnotify rename events have not been received after 500 ms") - } - // Try closing the fsnotify instance - t.Log("calling Close()") - watcher.Close() - t.Log("waiting for the event channel to become closed...") - select { - case <-done: - t.Log("event channel closed") - case <-time.After(2 * time.Second): - t.Fatal("event stream was not closed after 2 seconds") + file := filepath.Join(tmp, "file") + + touch(t, file) + addWatch(t, w, tmp) + rmAll(t, tmp) + }, ` + # OpenBSD, NetBSD + remove /file + remove|write / + + freebsd: + remove|write "/" + remove "" + create "." + + darwin: + remove /file + remove|write / + linux: + remove /file + remove / + windows: + remove /file + remove / + `}, + } + + for _, tt := range tests { + tt := tt + tt.run(t) } - - os.Remove(testFileRenamed) } -func TestFsnotifyRenameToCreate(t *testing.T) { - watcher := newWatcher(t) - - // Create directory to watch - testDir := tempMkdir(t) - defer os.RemoveAll(testDir) - - // Create directory to get file - testDirFrom := tempMkdir(t) - defer os.RemoveAll(testDirFrom) - - addWatch(t, watcher, testDir) +func TestClose(t *testing.T) { + t.Run("close", func(t *testing.T) { + t.Parallel() - // Receive errors on the error channel on a separate goroutine - go func() { - for err := range watcher.Errors { - t.Errorf("error received: %s", err) - } - }() - - testFile := filepath.Join(testDirFrom, "TestFsnotifyEvents.testfile") - testFileRenamed := filepath.Join(testDir, "TestFsnotifyEvents.testfileRenamed") - - // Receive events on the event channel on a separate goroutine - eventstream := watcher.Events - var createReceived counter - done := make(chan bool) - go func() { - for event := range eventstream { - // Only count relevant events - if event.Name == filepath.Clean(testDir) || event.Name == filepath.Clean(testFile) || event.Name == filepath.Clean(testFileRenamed) { - if event.Op&Create == Create { - createReceived.increment() - } - t.Logf("event received: %s", event) - } else { - t.Logf("unexpected event received: %s", event) - } + w := newWatcher(t) + if err := w.Close(); err != nil { + t.Fatal(err) } - done <- true - }() - - // Create a file - // This should add at least one event to the fsnotify event queue - var f *os.File - f, err := os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) - if err != nil { - t.Fatalf("creating test file failed: %s", err) - } - f.Sync() - f.Close() - - if err := testRename(testFile, testFileRenamed); err != nil { - t.Fatalf("rename failed: %s", err) - } - // We expect this event to be received almost immediately, but let's wait 500 ms to be sure - time.Sleep(waitForEvents) - if createReceived.value() == 0 { - t.Fatal("fsnotify create events have not been received after 500 ms") - } - - // Try closing the fsnotify instance - t.Log("calling Close()") - watcher.Close() - t.Log("waiting for the event channel to become closed...") - select { - case <-done: - t.Log("event channel closed") - case <-time.After(2 * time.Second): - t.Fatal("event stream was not closed after 2 seconds") - } + var done int32 + go func() { + w.Close() + atomic.StoreInt32(&done, 1) + }() - os.Remove(testFileRenamed) -} - -func TestFsnotifyRenameToOverwrite(t *testing.T) { - switch runtime.GOOS { - case "plan9", "windows": - t.Skipf("skipping test on %q (os.Rename over existing file does not create event).", runtime.GOOS) - } - - watcher := newWatcher(t) - - // Create directory to watch - testDir := tempMkdir(t) - defer os.RemoveAll(testDir) - - // Create directory to get file - testDirFrom := tempMkdir(t) - defer os.RemoveAll(testDirFrom) - - testFile := filepath.Join(testDirFrom, "TestFsnotifyEvents.testfile") - testFileRenamed := filepath.Join(testDir, "TestFsnotifyEvents.testfileRenamed") - - // Create a file - var fr *os.File - fr, err := os.OpenFile(testFileRenamed, os.O_WRONLY|os.O_CREATE, 0666) - if err != nil { - t.Fatalf("creating test file failed: %s", err) - } - fr.Sync() - fr.Close() - - addWatch(t, watcher, testDir) - - // Receive errors on the error channel on a separate goroutine - go func() { - for err := range watcher.Errors { - t.Errorf("error received: %s", err) - } - }() - - // Receive events on the event channel on a separate goroutine - eventstream := watcher.Events - var eventReceived counter - done := make(chan bool) - go func() { - for event := range eventstream { - // Only count relevant events - if event.Name == filepath.Clean(testFileRenamed) { - eventReceived.increment() - t.Logf("event received: %s", event) - } else { - t.Logf("unexpected event received: %s", event) - } + eventSeparator() + if atomic.LoadInt32(&done) == 0 { + t.Fatal("double Close() test failed: second Close() call didn't return") } - done <- true - }() - - // Create a file - // This should add at least one event to the fsnotify event queue - var f *os.File - f, err = os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) - if err != nil { - t.Fatalf("creating test file failed: %s", err) - } - f.Sync() - f.Close() - - if err := testRename(testFile, testFileRenamed); err != nil { - t.Fatalf("rename failed: %s", err) - } - - // We expect this event to be received almost immediately, but let's wait 500 ms to be sure - time.Sleep(waitForEvents) - if eventReceived.value() == 0 { - t.Fatal("fsnotify events have not been received after 500 ms") - } - // Try closing the fsnotify instance - t.Log("calling Close()") - watcher.Close() - t.Log("waiting for the event channel to become closed...") - select { - case <-done: - t.Log("event channel closed") - case <-time.After(2 * time.Second): - t.Fatal("event stream was not closed after 2 seconds") - } - - os.Remove(testFileRenamed) -} - -func TestRemovalOfWatch(t *testing.T) { - // Create directory to watch - testDir := tempMkdir(t) - defer os.RemoveAll(testDir) - - // Create a file before watching directory - testFileAlreadyExists := filepath.Join(testDir, "TestFsnotifyEventsExisting.testfile") - { - var f *os.File - f, err := os.OpenFile(testFileAlreadyExists, os.O_WRONLY|os.O_CREATE, 0666) - if err != nil { - t.Fatalf("creating test file failed: %s", err) + if err := w.Add(t.TempDir()); err == nil { + t.Fatal("expected error on Watch() after Close(), got nil") } - f.Sync() - f.Close() - } + }) - watcher := newWatcher(t) - defer watcher.Close() + // Make sure that Close() works even when the Events channel isn't being + // read. + t.Run("events not read", func(t *testing.T) { + t.Parallel() - addWatch(t, watcher, testDir) - if err := watcher.Remove(testDir); err != nil { - t.Fatalf("Could not remove the watch: %v\n", err) - } + tmp := t.TempDir() + w := newWatcher(t, tmp) - var wg sync.WaitGroup - wg.Add(1) - go func() { - defer wg.Done() - select { - case ev := <-watcher.Events: - t.Errorf("We received event: %v\n", ev) - case <-time.After(500 * time.Millisecond): - t.Log("No event received, as expected.") + touch(t, tmp, "file") + rm(t, tmp, "file") + if err := w.Close(); err != nil { + t.Fatal(err) + } + }) + + // Make sure that calling Close() while REMOVE events are emitted doesn't race. + t.Run("close while removing files", func(t *testing.T) { + t.Parallel() + tmp := t.TempDir() + + files := make([]string, 0, 200) + for i := 0; i < 200; i++ { + f := filepath.Join(tmp, fmt.Sprintf("file-%03d", i)) + touch(t, f, noWait) + files = append(files, f) } - }() - - time.Sleep(200 * time.Millisecond) - // Modify the file outside of the watched dir - f, err := os.Open(testFileAlreadyExists) - if err != nil { - t.Fatalf("Open test file failed: %s", err) - } - f.WriteString("data") - f.Sync() - f.Close() - if err := os.Chmod(testFileAlreadyExists, 0700); err != nil { - t.Fatalf("chmod failed: %s", err) - } - - // wait for all groutines to finish. - wg.Wait() -} - -func TestFsnotifyAttrib(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("attributes don't work on Windows.") - } - - watcher := newWatcher(t) - // Create directory to watch - testDir := tempMkdir(t) - defer os.RemoveAll(testDir) + w := newWatcher(t, tmp) - // Receive errors on the error channel on a separate goroutine - go func() { - for err := range watcher.Errors { - t.Errorf("error received: %s", err) - } - }() - - testFile := filepath.Join(testDir, "TestFsnotifyAttrib.testfile") - - // Receive events on the event channel on a separate goroutine - eventstream := watcher.Events - // The modifyReceived counter counts IsModify events that are not IsAttrib, - // and the attribReceived counts IsAttrib events (which are also IsModify as - // a consequence). - var modifyReceived counter - var attribReceived counter - done := make(chan bool) - go func() { - for event := range eventstream { - // Only count relevant events - if event.Name == filepath.Clean(testDir) || event.Name == filepath.Clean(testFile) { - if event.Op&Write == Write { - modifyReceived.increment() - } - if event.Op&Chmod == Chmod { - attribReceived.increment() + startC, stopC, errC := make(chan struct{}), make(chan struct{}), make(chan error) + go func() { + for { + select { + case <-w.Errors: + case <-w.Events: + case <-stopC: + return } - t.Logf("event received: %s", event) - } else { - t.Logf("unexpected event received: %s", event) } - } - done <- true - }() - - // Create a file - // This should add at least one event to the fsnotify event queue - var f *os.File - f, err := os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) - if err != nil { - t.Fatalf("creating test file failed: %s", err) - } - f.Sync() - - f.WriteString("data") - f.Sync() - f.Close() - - // Add a watch for testFile - addWatch(t, watcher, testFile) - - if err := os.Chmod(testFile, 0700); err != nil { - t.Fatalf("chmod failed: %s", err) - } - - // We expect this event to be received almost immediately, but let's wait 500 ms to be sure - // Creating/writing a file changes also the mtime, so IsAttrib should be set to true here - time.Sleep(waitForEvents) - if modifyReceived.value() != 0 { - t.Fatal("received an unexpected modify event when creating a test file") - } - if attribReceived.value() == 0 { - t.Fatal("fsnotify attribute events have not received after 500 ms") - } - - // Modifying the contents of the file does not set the attrib flag (although eg. the mtime - // might have been modified). - modifyReceived.reset() - attribReceived.reset() - - f, err = os.OpenFile(testFile, os.O_WRONLY, 0) - if err != nil { - t.Fatalf("reopening test file failed: %s", err) - } - - f.WriteString("more data") - f.Sync() - f.Close() - - time.Sleep(waitForEvents) - - if modifyReceived.value() != 1 { - t.Fatal("didn't receive a modify event after changing test file contents") - } - - if attribReceived.value() != 0 { - t.Fatal("did receive an unexpected attrib event after changing test file contents") - } - - modifyReceived.reset() - attribReceived.reset() - - // Doing a chmod on the file should trigger an event with the "attrib" flag set (the contents - // of the file are not changed though) - if err := os.Chmod(testFile, 0600); err != nil { - t.Fatalf("chmod failed: %s", err) - } - - time.Sleep(waitForEvents) - - if attribReceived.value() != 1 { - t.Fatal("didn't receive an attribute change after 500ms") - } - - // Try closing the fsnotify instance - t.Log("calling Close()") - watcher.Close() - t.Log("waiting for the event channel to become closed...") - select { - case <-done: - t.Log("event channel closed") - case <-time.After(1e9): - t.Fatal("event stream was not closed after 1 second") - } - - os.Remove(testFile) -} - -func TestFsnotifyClose(t *testing.T) { - watcher := newWatcher(t) - watcher.Close() - - var done int32 - go func() { - watcher.Close() - atomic.StoreInt32(&done, 1) - }() - - time.Sleep(eventSeparator) - if atomic.LoadInt32(&done) == 0 { - t.Fatal("double Close() test failed: second Close() call didn't return") - } - - testDir := tempMkdir(t) - defer os.RemoveAll(testDir) - - if err := watcher.Add(testDir); err == nil { - t.Fatal("expected error on Watch() after Close(), got nil") - } -} - -func TestFsnotifyFakeSymlink(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("symlinks don't work on Windows.") - } - - watcher := newWatcher(t) - - // Create directory to watch - testDir := tempMkdir(t) - defer os.RemoveAll(testDir) - - var errorsReceived counter - // Receive errors on the error channel on a separate goroutine - go func() { - for errors := range watcher.Errors { - t.Logf("Received error: %s", errors) - errorsReceived.increment() - } - }() - - // Count the CREATE events received - var createEventsReceived, otherEventsReceived counter - go func() { - for ev := range watcher.Events { - t.Logf("event received: %s", ev) - if ev.Op&Create == Create { - createEventsReceived.increment() - } else { - otherEventsReceived.increment() + }() + rmDone := make(chan struct{}) + go func() { + <-startC + for _, f := range files { + rm(t, f, noWait) } + rmDone <- struct{}{} + }() + go func() { + <-startC + errC <- w.Close() + }() + close(startC) + defer close(stopC) + if err := <-errC; err != nil { + t.Fatal(err) } - }() - - addWatch(t, watcher, testDir) - - if err := os.Symlink(filepath.Join(testDir, "zzz"), filepath.Join(testDir, "zzznew")); err != nil { - t.Fatalf("Failed to create bogus symlink: %s", err) - } - t.Logf("Created bogus symlink") - - // We expect this event to be received almost immediately, but let's wait 500 ms to be sure - time.Sleep(waitForEvents) - // Should not be error, just no events for broken links (watching nothing) - if errorsReceived.value() > 0 { - t.Fatal("fsnotify errors have been received.") - } - if otherEventsReceived.value() > 0 { - t.Fatal("fsnotify other events received on the broken link") - } - - // Except for 1 create event (for the link itself) - if createEventsReceived.value() == 0 { - t.Fatal("fsnotify create events were not received after 500 ms") - } - if createEventsReceived.value() > 1 { - t.Fatal("fsnotify more create events received than expected") - } - - // Try closing the fsnotify instance - t.Log("calling Close()") - watcher.Close() -} - -func TestCyclicSymlink(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("symlinks don't work on Windows.") - } - - watcher := newWatcher(t) - - testDir := tempMkdir(t) - defer os.RemoveAll(testDir) - - link := path.Join(testDir, "link") - if err := os.Symlink(".", link); err != nil { - t.Fatalf("could not make symlink: %v", err) - } - addWatch(t, watcher, testDir) - - var createEventsReceived counter - go func() { - for ev := range watcher.Events { - if ev.Op&Create == Create { - createEventsReceived.increment() + <-rmDone + }) + + // Make sure Close() doesn't race when called more than once; hard to write + // a good reproducible test for this, but running it 150 times seems to + // reproduce it in ~75% of cases and isn't too slow (~0.06s on my system). + t.Run("double close", func(t *testing.T) { + t.Parallel() + + for i := 0; i < 150; i++ { + w, err := NewWatcher() + if err != nil { + if strings.Contains(err.Error(), "too many") { // syscall.EMFILE + time.Sleep(100 * time.Millisecond) + continue + } + t.Fatal(err) } + go w.Close() + go w.Close() + go w.Close() } - }() - - if err := os.Remove(link); err != nil { - t.Fatalf("Error removing link: %v", err) - } - - // It would be nice to be able to expect a delete event here, but kqueue has - // no way for us to get events on symlinks themselves, because opening them - // opens an fd to the file to which they point. - - if err := ioutil.WriteFile(link, []byte("foo"), 0700); err != nil { - t.Fatalf("could not make symlink: %v", err) - } - - // We expect this event to be received almost immediately, but let's wait 500 ms to be sure - time.Sleep(waitForEvents) - - if got := createEventsReceived.value(); got == 0 { - t.Errorf("want at least 1 create event got %v", got) - } - - watcher.Close() + }) } -// TestConcurrentRemovalOfWatch tests that concurrent calls to RemoveWatch do not race. -// See https://codereview.appspot.com/103300045/ -// go test -test.run=TestConcurrentRemovalOfWatch -test.cpu=1,1,1,1,1 -race -func TestConcurrentRemovalOfWatch(t *testing.T) { - if runtime.GOOS != "darwin" { - t.Skip("regression test for race only present on darwin") - } +func TestRemove(t *testing.T) { + t.Run("works", func(t *testing.T) { + t.Parallel() - // Create directory to watch - testDir := tempMkdir(t) - defer os.RemoveAll(testDir) - - // Create a file before watching directory - testFileAlreadyExists := filepath.Join(testDir, "TestFsnotifyEventsExisting.testfile") - { - var f *os.File - f, err := os.OpenFile(testFileAlreadyExists, os.O_WRONLY|os.O_CREATE, 0666) - if err != nil { - t.Fatalf("creating test file failed: %s", err) - } - f.Sync() - f.Close() - } - - watcher := newWatcher(t) - defer watcher.Close() - - addWatch(t, watcher, testDir) - - // Test that RemoveWatch can be invoked concurrently, with no data races. - removed1 := make(chan struct{}) - go func() { - defer close(removed1) - watcher.Remove(testDir) - }() - removed2 := make(chan struct{}) - go func() { - close(removed2) - watcher.Remove(testDir) - }() - <-removed1 - <-removed2 -} + tmp := t.TempDir() + touch(t, tmp, "file") -func TestClose(t *testing.T) { - // Regression test for #59 bad file descriptor from Close - testDir := tempMkdir(t) - defer os.RemoveAll(testDir) - - watcher := newWatcher(t) - if err := watcher.Add(testDir); err != nil { - t.Fatalf("Expected no error on Add, got %v", err) - } - err := watcher.Close() - if err != nil { - t.Fatalf("Expected no error on Close, got %v.", err) - } -} + w := newCollector(t) + w.collect(t) + addWatch(t, w.w, tmp) + if err := w.w.Remove(tmp); err != nil { + t.Fatal(err) + } -// TestRemoveWithClose tests if one can handle Remove events and, at the same -// time, close Watcher object without any data races. -func TestRemoveWithClose(t *testing.T) { - testDir := tempMkdir(t) - defer os.RemoveAll(testDir) + time.Sleep(200 * time.Millisecond) + cat(t, "data", tmp, "file") + chmod(t, 0o700, tmp, "file") - const fileN = 200 - tempFiles := make([]string, 0, fileN) - for i := 0; i < fileN; i++ { - tempFiles = append(tempFiles, tempMkFile(t, testDir)) - } - watcher := newWatcher(t) - if err := watcher.Add(testDir); err != nil { - t.Fatalf("Expected no error on Add, got %v", err) - } - startC, stopC := make(chan struct{}), make(chan struct{}) - errC := make(chan error) - go func() { - for { - select { - case <-watcher.Errors: - case <-watcher.Events: - case <-stopC: - return - } + have := w.stop(t) + if len(have) > 0 { + t.Errorf("received events; expected none:\n%s", have) } - }() - go func() { - <-startC - for _, fileName := range tempFiles { - os.Remove(fileName) - } - }() - go func() { - <-startC - errC <- watcher.Close() - }() - close(startC) - defer close(stopC) - if err := <-errC; err != nil { - t.Fatalf("Expected no error on Close, got %v.", err) - } -} - -// Make sure Close() doesn't race; hard to write a good reproducible test for -// this, but running it 150 times seems to reproduce it in ~75% of cases and -// isn't too slow (~0.06s on my system). -func TestCloseRace(t *testing.T) { - for i := 0; i < 150; i++ { - w, err := NewWatcher() - if err != nil { - if strings.Contains(err.Error(), "too many") { // syscall.EMFILE - time.Sleep(100 * time.Millisecond) - continue - } - t.Fatal(err) + }) + + // Make sure that concurrent calls to Remove() don't race. + t.Run("no race", func(t *testing.T) { + t.Parallel() + + tmp := t.TempDir() + touch(t, tmp, "file") + + for i := 0; i < 10; i++ { + w := newWatcher(t) + defer w.Close() + addWatch(t, w, tmp) + + done := make(chan struct{}) + go func() { + defer func() { done <- struct{}{} }() + w.Remove(tmp) + }() + go func() { + defer func() { done <- struct{}{} }() + w.Remove(tmp) + }() + <-done + <-done + w.Close() } - go w.Close() - go w.Close() - go w.Close() - } -} - -func testRename(file1, file2 string) error { - switch runtime.GOOS { - case "windows", "plan9": - return os.Rename(file1, file2) - default: - cmd := exec.Command("mv", file1, file2) - return cmd.Run() - } + }) } diff --git a/kqueue.go b/kqueue.go index 09cefd57..6b8b3389 100644 --- a/kqueue.go +++ b/kqueue.go @@ -212,11 +212,12 @@ func (w *Watcher) addWatch(name string, flags uint32) (string, error) { } // Follow Symlinks - // Unfortunately, Linux can add bogus symlinks to watch list without - // issue, and Windows can't do symlinks period (AFAIK). To maintain - // consistency, we will act like everything is fine. There will simply - // be no file events for broken symlinks. - // Hence the returns of nil on errors. + // + // Linux can add unresolvable symlinks to the watch list without issue, + // and Windows can't do symlinks period. To maintain consistency, we + // will act like everything is fine if the link can't be resolved. + // There will simply be no file events for broken symlinks. Hence the + // returns of nil on errors. if fi.Mode()&os.ModeSymlink == os.ModeSymlink { name, err = filepath.EvalSymlinks(name) if err != nil { @@ -332,7 +333,7 @@ loop: w.mu.Unlock() event := newEvent(path.name, mask) - if path.isDir && !(event.Op&Remove == Remove) { + if path.isDir && !event.Has(Remove) { // Double check to make sure the directory exists. This can happen when // we do a rm -fr on a recursively watched folders and we receive a // modification event first but the folder has been deleted and later @@ -343,14 +344,14 @@ loop: } } - if event.Op&Rename == Rename || event.Op&Remove == Remove { + if event.Has(Rename) || event.Has(Remove) { w.Remove(event.Name) w.mu.Lock() delete(w.fileExists, event.Name) w.mu.Unlock() } - if path.isDir && event.Op&Write == Write && !(event.Op&Remove == Remove) { + if path.isDir && event.Has(Write) && !event.Has(Remove) { w.sendDirectoryChangeEvents(event.Name) } else { // Send the event on the Events channel. @@ -361,7 +362,7 @@ loop: } } - if event.Op&Remove == Remove { + if event.Has(Remove) { // Look for a file that may have overwritten this. // For example, mv f1 f2 will delete f2, then create f2. if path.isDir {