]> go.fuhry.dev Git - fsnotify.git/commitdiff
Rewrite tests (#478)
authorMartin Tournoij <martin@arp242.net>
Sun, 31 Jul 2022 18:03:10 +0000 (20:03 +0200)
committerGitHub <noreply@github.com>
Sun, 31 Jul 2022 18:03:10 +0000 (20:03 +0200)
Rewrite tests

This rewrites quite a lot of tests to be much more easily readable.

While working on #472 I wanted to check "how do renames behave now?",
and found this quite hard as most test cases were >90% "plumbing", and
seeing "what file operations does this do?" and "what events do we get?"
was not very easy.

So refactor the lot, based on some work I did in #472:

- Add a bunch of "shell-like" helper functions so you're not forever
  typing error checks, filepath.Join(), and eventSeparator(). Just
  touch(t, tmp, "file") will create a file in tmp.

- Add eventCollector type which will collect all events in in a slice,
  replacing the previous "counter". This also ensures that the Watcher
  is closed within a second (this removes a lot of duplicate code).

  This is also much more precise than merely counting events; before
  random events could get emitted but if you weren't counting those then
  you'd never know.

  Downside is that some tests are a bit more flaky now as some
  behaviours are not always consistent in various edge cases; these are
  pre-existing bugs.

- Add Events (plural) type (only for tests), and helper function to
  create this from a string like:

      REMOVE  /link
      CREATE  /link
      WRITE   /link

  Which makes seeing which events are received, diffing them, etc. much
  easier.

- Add Parallel() for most tests; reduces runtime on my system from ~12
  seconds to ~6 seconds.

All in all it reduces the integrations_test.go from 1279 lines to 405
lines and it's quite easy to see which events are expected for which
operations, which should make things a lot easier going forward.

fsnotify.go
fsnotify_test.go
helpers_test.go [new file with mode: 0644]
inotify.go
inotify_test.go
integration_darwin_test.go
integration_test.go
kqueue.go

index 1610d663a35a64efc926ce9067ff35cc282124cd..4ff4e3db73638087a1e18724e3f635eebaba9a02 100644 (file)
@@ -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")
-)
index 51aa49c58f551258efc51bd8b68f43ee3688e15a..5f6c8827fd0de93189bf69bfeeeb4d484352b589 100644 (file)
@@ -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 (file)
index 0000000..cb2f121
--- /dev/null
@@ -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")
+}
index 71254996b218c97c2d1b16d81adbdbe94403a37f..454e81e27ce072e328d0e35e241977e4d98e8a8e 100644 (file)
@@ -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
index 9aed1bec52525641dc1163a5ac1b5bf5b804bc6f..785888bc29c609a7dc98c10bcda1c61805fecbb0 100644 (file)
@@ -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)
-       }
-}
index c43d00c6b5fd40737e51f4de24a633290d03c00c..06fd665c0ece34ab9107ba4b61e605b714d0de0c 100644 (file)
@@ -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()
-}
index c4e1d3e68ecb87f5335d8955009a6f510eb12ef5..6ae1ae076603fb46652c189fe1f4b26419d26e50 100644 (file)
-// 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()
-       }
+       })
 }
index 09cefd574199e80528a8e8e565c7719ba11757d7..6b8b3389e5cb64f6f9dffa3f658f8a69f3d28c41 100644 (file)
--- 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 {