blob: ec1c3bcbc5e96efdd34c04f2183144071b1cad1d [file] [log] [blame]
package fsnotify
import (
"fmt"
"io/fs"
"os"
"path/filepath"
"runtime"
"sort"
"strings"
"sync"
"testing"
"time"
"github.com/fsnotify/fsnotify/internal"
)
type testCase struct {
name string
ops func(*testing.T, *Watcher, string)
want string
}
func (tt testCase) run(t *testing.T) {
t.Helper()
t.Run(tt.name, func(t *testing.T) {
t.Helper()
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, w *Watcher, path ...string) {
t.Helper()
if len(path) < 1 {
t.Fatalf("addWatch: path must have at least one element: %s", path)
}
err := w.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
}
// Create n empty files with the prefix in the directory dir.
func createFiles(t *testing.T, dir, prefix string, n int, d time.Duration) int {
t.Helper()
if d == 0 {
d = 9 * time.Minute
}
fmtNum := func(n int) string {
s := fmt.Sprintf("%09d", n)
return s[:3] + "_" + s[3:6] + "_" + s[6:]
}
var (
max = time.After(d)
created int
)
for i := 0; i < n; i++ {
select {
case <-max:
t.Logf("createFiles: stopped at %s files because it took longer than %s", fmtNum(created), d)
return created
default:
fp, err := os.Create(filepath.Join(dir, prefix+fmtNum(i)))
if err != nil {
t.Errorf("create failed for %s: %s", fmtNum(i), err)
continue
}
if err := fp.Close(); err != nil {
t.Errorf("close failed for %s: %s", fmtNum(i), err)
}
if i%10_000 == 0 {
t.Logf("createFiles: %s", fmtNum(i))
}
created++
}
}
return created
}
// 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()
}
}
// mkfifo
func mkfifo(t *testing.T, path ...string) {
t.Helper()
if len(path) < 1 {
t.Fatalf("mkfifo: path must have at least one element: %s", path)
}
err := internal.Mkfifo(filepath.Join(path...), 0o644)
if err != nil {
t.Fatalf("mkfifo(%q): %s", filepath.Join(path...), err)
}
if shouldWait(path...) {
eventSeparator()
}
}
// mknod
func mknod(t *testing.T, dev int, path ...string) {
t.Helper()
if len(path) < 1 {
t.Fatalf("mknod: path must have at least one element: %s", path)
}
err := internal.Mknod(filepath.Join(path...), 0o644, dev)
if err != nil {
t.Fatalf("mknod(%d, %q): %s", dev, filepath.Join(path...), err)
}
if shouldWait(path...) {
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)
}
err := os.Rename(src, filepath.Join(dst...))
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
e Events
mu sync.Mutex
done chan struct{}
}
func newCollector(t *testing.T, add ...string) *eventCollector {
return &eventCollector{
w: newWatcher(t, add...),
done: make(chan struct{}),
e: make(Events, 0, 8),
}
}
// stop collecting events and return what we've got.
func (w *eventCollector) stop(t *testing.T) Events {
return w.stopWait(t, time.Second)
}
func (w *eventCollector) stopWait(t *testing.T, waitFor time.Duration) Events {
waitForEvents()
go func() {
err := w.w.Close()
if err != nil {
t.Error(err)
}
}()
select {
case <-time.After(waitFor):
t.Fatalf("event stream was not closed after %s", waitFor)
case <-w.done:
}
w.mu.Lock()
defer w.mu.Unlock()
return w.e
}
// Get all events we've found up to now and clear the event buffer.
func (w *eventCollector) events(t *testing.T) Events {
w.mu.Lock()
defer w.mu.Unlock()
e := make(Events, len(w.e))
copy(e, w.e)
w.e = make(Events, 0, 16)
return e
}
// Start collecting 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.e = append(w.e, 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
//
// You can specify multiple platforms with a comma (e.g. "windows, linux:").
// "kqueue" is a shortcut for all kqueue systems (BSD, macOS).
func newEvents(t *testing.T, s string) Events {
t.Helper()
var (
lines = strings.Split(s, "\n")
groups = []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, ":") {
groups = strings.Split(strings.TrimRight(line, ":"), ",")
for i := range groups {
groups[i] = strings.TrimSpace(groups[i])
}
continue
}
fields := strings.Fields(line)
if len(fields) < 2 {
if strings.ToUpper(fields[0]) == "EMPTY" {
for _, g := range groups {
events[g] = Events{}
}
continue
}
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)
}
}
}
for _, g := range groups {
events[g] = append(events[g], Event{Name: path, Op: op})
}
}
if e, ok := events[runtime.GOOS]; ok {
return e
}
switch runtime.GOOS {
// kqueue shortcut
case "freebsd", "netbsd", "openbsd", "dragonfly", "darwin":
if e, ok := events["kqueue"]; ok {
return e
}
// Fall back to solaris for illumos, and vice versa.
case "solaris":
if e, ok := events["illumos"]; ok {
return e
}
case "illumos":
if e, ok := events["solaris"]; 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.Error("\n" + ztest.Diff(indent(haveSort), indent(wantSort)))
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")
}
func isCI() bool {
_, ok := os.LookupEnv("CI")
return ok
}