"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.
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)
}
}
}
-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() {
}
}()
- 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
}
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 {
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 {
}
}
-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()
}
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 {
}
}
- // 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)
- }
-}
-// 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()
- }
+ })
}