From fdf41a303e4f147cde32bbb3e813f04551cd76f3 Mon Sep 17 00:00:00 2001 From: Martin Tournoij Date: Sat, 6 Aug 2022 20:24:59 +0200 Subject: [PATCH] Move some files around This will be useful once we start splitting out "systems" and "backends" a bit more, e.g. when polling support is added. --- fen.go => backend_fen.go | 0 inotify.go => backend_inotify.go | 0 inotify_test.go => backend_inotify_test.go | 0 kqueue.go => backend_kqueue.go | 0 fsnotify_unsupported.go => backend_other.go | 0 windows.go => backend_windows.go | 0 fsnotify_test.go | 574 ++++++++++++++++- integration_test.go | 578 ------------------ open_mode_bsd.go => system_bsd.go | 0 open_mode_darwin.go => system_darwin.go | 0 ...on_darwin_test.go => system_darwin_test.go | 3 + 11 files changed, 575 insertions(+), 580 deletions(-) rename fen.go => backend_fen.go (100%) rename inotify.go => backend_inotify.go (100%) rename inotify_test.go => backend_inotify_test.go (100%) rename kqueue.go => backend_kqueue.go (100%) rename fsnotify_unsupported.go => backend_other.go (100%) rename windows.go => backend_windows.go (100%) delete mode 100644 integration_test.go rename open_mode_bsd.go => system_bsd.go (100%) rename open_mode_darwin.go => system_darwin.go (100%) rename integration_darwin_test.go => system_darwin_test.go (98%) diff --git a/fen.go b/backend_fen.go similarity index 100% rename from fen.go rename to backend_fen.go diff --git a/inotify.go b/backend_inotify.go similarity index 100% rename from inotify.go rename to backend_inotify.go diff --git a/inotify_test.go b/backend_inotify_test.go similarity index 100% rename from inotify_test.go rename to backend_inotify_test.go diff --git a/kqueue.go b/backend_kqueue.go similarity index 100% rename from kqueue.go rename to backend_kqueue.go diff --git a/fsnotify_unsupported.go b/backend_other.go similarity index 100% rename from fsnotify_unsupported.go rename to backend_other.go diff --git a/windows.go b/backend_windows.go similarity index 100% rename from windows.go rename to backend_windows.go diff --git a/fsnotify_test.go b/fsnotify_test.go index 5f6c882..6e369d6 100644 --- a/fsnotify_test.go +++ b/fsnotify_test.go @@ -1,12 +1,582 @@ -//go:build !plan9 -// +build !plan9 +//go:build !plan9 && !solaris +// +build !plan9,!solaris package fsnotify import ( + "errors" + "fmt" + "path/filepath" + "runtime" + "strings" + "sync/atomic" "testing" + "time" + + "github.com/fsnotify/fsnotify/internal" ) +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 + `}, + + {"file in directory is not readable", func(t *testing.T, w *Watcher, tmp string) { + if runtime.GOOS == "windows" { + t.Skip("attributes don't work on Windows") + } + + touch(t, tmp, "file-unreadable") + chmod(t, 0, tmp, "file-unreadable") + touch(t, tmp, "file") + addWatch(t, w, tmp) + + cat(t, "hello", tmp, "file") + rm(t, tmp, "file") + rm(t, tmp, "file-unreadable") + }, ` + WRITE "/file" + REMOVE "/file" + REMOVE "/file-unreadable" + + # We never set up a watcher on the unreadable file, so we don't get + # the REMOVE. + kqueue: + WRITE "/file" + REMOVE "/file" + `}, + } + + for _, tt := range tests { + tt := tt + tt.run(t) + } +} + +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" && isCI() { + t.Skip("fails in CI; see #488") + } + + 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 # cat data >file + write /file # ^ + rename /file # mv file ../renamed + create /file # touch 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) { + touch(t, tmp, "renamed") + addWatch(t, w, tmp) + + unwatched := t.TempDir() + file := filepath.Join(unwatched, "file") + touch(t, file) + mv(t, file, tmp, "renamed") + }, ` + remove /renamed + create /renamed + + # No remove event for inotify; inotify just sends MOVE_SELF. + linux: + create /renamed + `}, + + {"rename watched directory", func(t *testing.T, w *Watcher, tmp string) { + addWatch(t, w, tmp) + + dir := filepath.Join(tmp, "dir") + mkdir(t, dir) + addWatch(t, w, dir) + + mv(t, dir, tmp, "dir-renamed") + touch(t, tmp, "dir-renamed/file") + }, ` + CREATE "/dir" # mkdir + RENAME "/dir" # mv + CREATE "/dir-renamed" + RENAME "/dir" + CREATE "/dir/file" # touch + + windows: + CREATE "/dir" # mkdir + RENAME "/dir" # mv + CREATE "/dir-renamed" + CREATE "/dir-renamed/file" # touch + + # TODO: no results for the touch; this is probably a bug; windows + # was fixed in #370. + kqueue: + CREATE "/dir" # mkdir + CREATE "/dir-renamed" # mv + REMOVE|RENAME "/dir" + `}, + } + + for _, tt := range tests { + tt := tt + tt.run(t) + } +} + +func TestWatchSymlink(t *testing.T) { + 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 + + windows: + create /link + write /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() + } + + symlink(t, ".", tmp, "link") + addWatch(t, w, tmp) + rm(t, tmp, "link") + cat(t, "foo", tmp, "link") + + }, ` + write /link + create /link + + linux, windows: + remove /link + create /link + write /link + `}, + } + + for _, tt := range tests { + tt := tt + tt.run(t) + } +} + +func TestWatchAttrib(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("attributes don't work on Windows") + } + + tests := []testCase{ + {"chmod", func(t *testing.T, w *Watcher, tmp string) { + file := filepath.Join(tmp, "file") + + cat(t, "data", file) + addWatch(t, w, file) + chmod(t, 0o700, file) + }, ` + CHMOD "/file" + `}, + + {"write does not trigger CHMOD", func(t *testing.T, w *Watcher, tmp string) { + file := filepath.Join(tmp, "file") + + cat(t, "data", file) + addWatch(t, w, file) + chmod(t, 0o700, file) + + cat(t, "more data", file) + }, ` + CHMOD "/file" + WRITE "/file" + `}, + + {"chmod after write", func(t *testing.T, w *Watcher, tmp string) { + file := filepath.Join(tmp, "file") + + 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" + `}, + } + + for _, tt := range tests { + tt := tt + tt.run(t) + } +} + +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") + } + + 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) + } +} + +func TestClose(t *testing.T) { + t.Run("close", func(t *testing.T) { + t.Parallel() + + w := newWatcher(t) + if err := w.Close(); err != nil { + t.Fatal(err) + } + + var done int32 + go func() { + w.Close() + atomic.StoreInt32(&done, 1) + }() + + eventSeparator() + if atomic.LoadInt32(&done) == 0 { + t.Fatal("double Close() test failed: second Close() call didn't return") + } + + if err := w.Add(t.TempDir()); err == nil { + t.Fatal("expected error on Watch() after Close(), got nil") + } + }) + + // 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() + + tmp := t.TempDir() + w := newWatcher(t, tmp) + + 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) + } + + w := newWatcher(t, tmp) + + 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 + } + } + }() + 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) + } + + <-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() + } + }) +} + +func TestAdd(t *testing.T) { + t.Run("permission denied", func(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("attributes don't work on Windows") + } + + t.Parallel() + + tmp := t.TempDir() + dir := filepath.Join(tmp, "dir-unreadable") + mkdir(t, dir) + touch(t, dir, "/file") + chmod(t, 0, dir) + + w := newWatcher(t) + defer func() { + w.Close() + chmod(t, 0o755, dir) // Make TempDir() cleanup work + }() + err := w.Add(dir) + if err == nil { + t.Fatal("error is nil") + } + if !errors.Is(err, internal.UnixEACCES) { + t.Errorf("not unix.EACCESS: %T %#[1]v", err) + } + if !errors.Is(err, internal.SyscallEACCES) { + t.Errorf("not syscall.EACCESS: %T %#[1]v", err) + } + }) +} + +// TODO: should also check internal state is correct/cleaned up; e.g. no +// left-over file descriptors or whatnot. +func TestRemove(t *testing.T) { + t.Run("works", func(t *testing.T) { + t.Parallel() + + tmp := t.TempDir() + touch(t, tmp, "file") + + w := newCollector(t) + w.collect(t) + addWatch(t, w.w, tmp) + if err := w.w.Remove(tmp); err != nil { + t.Fatal(err) + } + + time.Sleep(200 * time.Millisecond) + cat(t, "data", tmp, "file") + chmod(t, 0o700, tmp, "file") + + have := w.stop(t) + if len(have) > 0 { + t.Errorf("received events; expected none:\n%s", have) + } + }) + + t.Run("remove same dir twice", func(t *testing.T) { + tmp := t.TempDir() + + touch(t, tmp, "file") + + w := newWatcher(t) + defer w.Close() + + addWatch(t, w, tmp) + + if err := w.Remove(tmp); err != nil { + t.Fatal(err) + } + if err := w.Remove(tmp); err == nil { + t.Fatal("no error") + } + }) + + // 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() + } + }) +} + func TestEventString(t *testing.T) { tests := []struct { in Event diff --git a/integration_test.go b/integration_test.go deleted file mode 100644 index da9bb20..0000000 --- a/integration_test.go +++ /dev/null @@ -1,578 +0,0 @@ -//go:build !plan9 && !solaris -// +build !plan9,!solaris - -package fsnotify - -import ( - "errors" - "fmt" - "path/filepath" - "runtime" - "strings" - "sync/atomic" - "testing" - "time" - - "github.com/fsnotify/fsnotify/internal" -) - -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 - `}, - - {"file in directory is not readable", func(t *testing.T, w *Watcher, tmp string) { - if runtime.GOOS == "windows" { - t.Skip("attributes don't work on Windows") - } - - touch(t, tmp, "file-unreadable") - chmod(t, 0, tmp, "file-unreadable") - touch(t, tmp, "file") - addWatch(t, w, tmp) - - cat(t, "hello", tmp, "file") - rm(t, tmp, "file") - rm(t, tmp, "file-unreadable") - }, ` - WRITE "/file" - REMOVE "/file" - REMOVE "/file-unreadable" - - # We never set up a watcher on the unreadable file, so we don't get - # the REMOVE. - kqueue: - WRITE "/file" - REMOVE "/file" - `}, - } - - for _, tt := range tests { - tt := tt - tt.run(t) - } -} - -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" && isCI() { - t.Skip("fails in CI; see #488") - } - - 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 # cat data >file - write /file # ^ - rename /file # mv file ../renamed - create /file # touch 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) { - touch(t, tmp, "renamed") - addWatch(t, w, tmp) - - unwatched := t.TempDir() - file := filepath.Join(unwatched, "file") - touch(t, file) - mv(t, file, tmp, "renamed") - }, ` - remove /renamed - create /renamed - - # No remove event for inotify; inotify just sends MOVE_SELF. - linux: - create /renamed - `}, - - {"rename watched directory", func(t *testing.T, w *Watcher, tmp string) { - addWatch(t, w, tmp) - - dir := filepath.Join(tmp, "dir") - mkdir(t, dir) - addWatch(t, w, dir) - - mv(t, dir, tmp, "dir-renamed") - touch(t, tmp, "dir-renamed/file") - }, ` - CREATE "/dir" # mkdir - RENAME "/dir" # mv - CREATE "/dir-renamed" - RENAME "/dir" - CREATE "/dir/file" # touch - - windows: - CREATE "/dir" # mkdir - RENAME "/dir" # mv - CREATE "/dir-renamed" - CREATE "/dir-renamed/file" # touch - - # TODO: no results for the touch; this is probably a bug; windows - # was fixed in #370. - kqueue: - CREATE "/dir" # mkdir - CREATE "/dir-renamed" # mv - REMOVE|RENAME "/dir" - `}, - } - - for _, tt := range tests { - tt := tt - tt.run(t) - } -} - -func TestWatchSymlink(t *testing.T) { - 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 - - windows: - create /link - write /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() - } - - symlink(t, ".", tmp, "link") - addWatch(t, w, tmp) - rm(t, tmp, "link") - cat(t, "foo", tmp, "link") - - }, ` - write /link - create /link - - linux, windows: - remove /link - create /link - write /link - `}, - } - - for _, tt := range tests { - tt := tt - tt.run(t) - } -} - -func TestWatchAttrib(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("attributes don't work on Windows") - } - - tests := []testCase{ - {"chmod", func(t *testing.T, w *Watcher, tmp string) { - file := filepath.Join(tmp, "file") - - cat(t, "data", file) - addWatch(t, w, file) - chmod(t, 0o700, file) - }, ` - CHMOD "/file" - `}, - - {"write does not trigger CHMOD", func(t *testing.T, w *Watcher, tmp string) { - file := filepath.Join(tmp, "file") - - cat(t, "data", file) - addWatch(t, w, file) - chmod(t, 0o700, file) - - cat(t, "more data", file) - }, ` - CHMOD "/file" - WRITE "/file" - `}, - - {"chmod after write", func(t *testing.T, w *Watcher, tmp string) { - file := filepath.Join(tmp, "file") - - 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" - `}, - } - - for _, tt := range tests { - tt := tt - tt.run(t) - } -} - -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") - } - - 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) - } -} - -func TestClose(t *testing.T) { - t.Run("close", func(t *testing.T) { - t.Parallel() - - w := newWatcher(t) - if err := w.Close(); err != nil { - t.Fatal(err) - } - - var done int32 - go func() { - w.Close() - atomic.StoreInt32(&done, 1) - }() - - eventSeparator() - if atomic.LoadInt32(&done) == 0 { - t.Fatal("double Close() test failed: second Close() call didn't return") - } - - if err := w.Add(t.TempDir()); err == nil { - t.Fatal("expected error on Watch() after Close(), got nil") - } - }) - - // 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() - - tmp := t.TempDir() - w := newWatcher(t, tmp) - - 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) - } - - w := newWatcher(t, tmp) - - 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 - } - } - }() - 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) - } - - <-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() - } - }) -} - -func TestAdd(t *testing.T) { - t.Run("permission denied", func(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("attributes don't work on Windows") - } - - t.Parallel() - - tmp := t.TempDir() - dir := filepath.Join(tmp, "dir-unreadable") - mkdir(t, dir) - touch(t, dir, "/file") - chmod(t, 0, dir) - - w := newWatcher(t) - defer func() { - w.Close() - chmod(t, 0o755, dir) // Make TempDir() cleanup work - }() - err := w.Add(dir) - if err == nil { - t.Fatal("error is nil") - } - if !errors.Is(err, internal.UnixEACCES) { - t.Errorf("not unix.EACCESS: %T %#[1]v", err) - } - if !errors.Is(err, internal.SyscallEACCES) { - t.Errorf("not syscall.EACCESS: %T %#[1]v", err) - } - }) -} - -// TODO: should also check internal state is correct/cleaned up; e.g. no -// left-over file descriptors or whatnot. -func TestRemove(t *testing.T) { - t.Run("works", func(t *testing.T) { - t.Parallel() - - tmp := t.TempDir() - touch(t, tmp, "file") - - w := newCollector(t) - w.collect(t) - addWatch(t, w.w, tmp) - if err := w.w.Remove(tmp); err != nil { - t.Fatal(err) - } - - time.Sleep(200 * time.Millisecond) - cat(t, "data", tmp, "file") - chmod(t, 0o700, tmp, "file") - - have := w.stop(t) - if len(have) > 0 { - t.Errorf("received events; expected none:\n%s", have) - } - }) - - t.Run("remove same dir twice", func(t *testing.T) { - tmp := t.TempDir() - - touch(t, tmp, "file") - - w := newWatcher(t) - defer w.Close() - - addWatch(t, w, tmp) - - if err := w.Remove(tmp); err != nil { - t.Fatal(err) - } - if err := w.Remove(tmp); err == nil { - t.Fatal("no error") - } - }) - - // 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() - } - }) -} diff --git a/open_mode_bsd.go b/system_bsd.go similarity index 100% rename from open_mode_bsd.go rename to system_bsd.go diff --git a/open_mode_darwin.go b/system_darwin.go similarity index 100% rename from open_mode_darwin.go rename to system_darwin.go diff --git a/integration_darwin_test.go b/system_darwin_test.go similarity index 98% rename from integration_darwin_test.go rename to system_darwin_test.go index a5dfb08..48cabe9 100644 --- a/integration_darwin_test.go +++ b/system_darwin_test.go @@ -1,3 +1,6 @@ +//go:build darwin +// +build darwin + package fsnotify import ( -- 2.50.1