mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-29 21:32:06 -05:00
24ce98b6d1
Fixes #8720 Fixes #6849 Fixes #7930
304 lines
8 KiB
Go
304 lines
8 KiB
Go
// Package filenotify is adapted from https://github.com/moby/moby/tree/master/pkg/filenotify, Apache-2.0 License.
|
|
// Hopefully this can be replaced with an external package sometime in the future, see https://github.com/fsnotify/fsnotify/issues/9
|
|
package filenotify
|
|
|
|
import (
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"testing"
|
|
"time"
|
|
|
|
qt "github.com/frankban/quicktest"
|
|
"github.com/fsnotify/fsnotify"
|
|
"github.com/gohugoio/hugo/htesting"
|
|
)
|
|
|
|
const (
|
|
subdir1 = "subdir1"
|
|
subdir2 = "subdir2"
|
|
watchWaitTime = 200 * time.Millisecond
|
|
)
|
|
|
|
var (
|
|
isMacOs = runtime.GOOS == "darwin"
|
|
isWindows = runtime.GOOS == "windows"
|
|
isCI = htesting.IsCI()
|
|
)
|
|
|
|
func TestPollerAddRemove(t *testing.T) {
|
|
c := qt.New(t)
|
|
w := NewPollingWatcher(watchWaitTime)
|
|
|
|
c.Assert(w.Add("foo"), qt.Not(qt.IsNil))
|
|
c.Assert(w.Remove("foo"), qt.Not(qt.IsNil))
|
|
|
|
f, err := ioutil.TempFile("", "asdf")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(f.Name())
|
|
c.Assert(w.Add(f.Name()), qt.IsNil)
|
|
c.Assert(w.Remove(f.Name()), qt.IsNil)
|
|
|
|
}
|
|
|
|
func TestPollerEvent(t *testing.T) {
|
|
c := qt.New(t)
|
|
|
|
for _, poll := range []bool{true, false} {
|
|
if !(poll || isMacOs) || isCI {
|
|
// Only run the fsnotify tests on MacOS locally.
|
|
continue
|
|
}
|
|
method := "fsnotify"
|
|
if poll {
|
|
method = "poll"
|
|
}
|
|
|
|
c.Run(fmt.Sprintf("%s, Watch dir", method), func(c *qt.C) {
|
|
dir, w := preparePollTest(c, poll)
|
|
subdir := filepath.Join(dir, subdir1)
|
|
c.Assert(w.Add(subdir), qt.IsNil)
|
|
|
|
filename := filepath.Join(subdir, "file1")
|
|
|
|
// Write to one file.
|
|
c.Assert(ioutil.WriteFile(filename, []byte("changed"), 0600), qt.IsNil)
|
|
|
|
var expected []fsnotify.Event
|
|
|
|
if poll {
|
|
expected = append(expected, fsnotify.Event{Name: filename, Op: fsnotify.Write})
|
|
assertEvents(c, w, expected...)
|
|
} else {
|
|
// fsnotify sometimes emits Chmod before Write,
|
|
// which is hard to test, so skip it here.
|
|
drainEvents(c, w)
|
|
}
|
|
|
|
// Remove one file.
|
|
filename = filepath.Join(subdir, "file2")
|
|
c.Assert(os.Remove(filename), qt.IsNil)
|
|
assertEvents(c, w, fsnotify.Event{Name: filename, Op: fsnotify.Remove})
|
|
|
|
// Add one file.
|
|
filename = filepath.Join(subdir, "file3")
|
|
c.Assert(ioutil.WriteFile(filename, []byte("new"), 0600), qt.IsNil)
|
|
assertEvents(c, w, fsnotify.Event{Name: filename, Op: fsnotify.Create})
|
|
|
|
// Remove entire directory.
|
|
subdir = filepath.Join(dir, subdir2)
|
|
c.Assert(w.Add(subdir), qt.IsNil)
|
|
|
|
c.Assert(os.RemoveAll(subdir), qt.IsNil)
|
|
|
|
expected = expected[:0]
|
|
|
|
// This looks like a bug in fsnotify on MacOS. There are
|
|
// 3 files in this directory, yet we get Remove events
|
|
// for one of them + the directory.
|
|
if !poll {
|
|
expected = append(expected, fsnotify.Event{Name: filepath.Join(subdir, "file2"), Op: fsnotify.Remove})
|
|
}
|
|
expected = append(expected, fsnotify.Event{Name: subdir, Op: fsnotify.Remove})
|
|
assertEvents(c, w, expected...)
|
|
|
|
})
|
|
|
|
c.Run(fmt.Sprintf("%s, Add should not trigger event", method), func(c *qt.C) {
|
|
dir, w := preparePollTest(c, poll)
|
|
subdir := filepath.Join(dir, subdir1)
|
|
w.Add(subdir)
|
|
assertEvents(c, w)
|
|
// Create a new sub directory and add it to the watcher.
|
|
subdir = filepath.Join(dir, subdir1, subdir2)
|
|
c.Assert(os.Mkdir(subdir, 0777), qt.IsNil)
|
|
w.Add(subdir)
|
|
// This should create only one event.
|
|
assertEvents(c, w, fsnotify.Event{Name: subdir, Op: fsnotify.Create})
|
|
})
|
|
|
|
}
|
|
}
|
|
|
|
func TestPollerClose(t *testing.T) {
|
|
c := qt.New(t)
|
|
w := NewPollingWatcher(watchWaitTime)
|
|
f1, err := ioutil.TempFile("", "f1")
|
|
c.Assert(err, qt.IsNil)
|
|
f2, err := ioutil.TempFile("", "f2")
|
|
c.Assert(err, qt.IsNil)
|
|
filename1 := f1.Name()
|
|
filename2 := f2.Name()
|
|
f1.Close()
|
|
f2.Close()
|
|
|
|
c.Assert(w.Add(filename1), qt.IsNil)
|
|
c.Assert(w.Add(filename2), qt.IsNil)
|
|
c.Assert(w.Close(), qt.IsNil)
|
|
c.Assert(w.Close(), qt.IsNil)
|
|
c.Assert(ioutil.WriteFile(filename1, []byte("new"), 0600), qt.IsNil)
|
|
c.Assert(ioutil.WriteFile(filename2, []byte("new"), 0600), qt.IsNil)
|
|
// No more event as the watchers are closed.
|
|
assertEvents(c, w)
|
|
|
|
f2, err = ioutil.TempFile("", "f2")
|
|
c.Assert(err, qt.IsNil)
|
|
|
|
defer os.Remove(f2.Name())
|
|
|
|
c.Assert(w.Add(f2.Name()), qt.Not(qt.IsNil))
|
|
|
|
}
|
|
|
|
func TestCheckChange(t *testing.T) {
|
|
c := qt.New(t)
|
|
|
|
dir := prepareTestDirWithSomeFiles(c, "check-change")
|
|
|
|
stat := func(s ...string) os.FileInfo {
|
|
fi, err := os.Stat(filepath.Join(append([]string{dir}, s...)...))
|
|
c.Assert(err, qt.IsNil)
|
|
return fi
|
|
}
|
|
|
|
f0, f1, f2 := stat(subdir2, "file0"), stat(subdir2, "file1"), stat(subdir2, "file2")
|
|
d1 := stat(subdir1)
|
|
|
|
// Note that on Windows, only the 0200 bit (owner writable) of mode is used.
|
|
c.Assert(os.Chmod(filepath.Join(filepath.Join(dir, subdir2, "file1")), 0400), qt.IsNil)
|
|
f1_2 := stat(subdir2, "file1")
|
|
|
|
c.Assert(ioutil.WriteFile(filepath.Join(filepath.Join(dir, subdir2, "file2")), []byte("changed"), 0600), qt.IsNil)
|
|
f2_2 := stat(subdir2, "file2")
|
|
|
|
c.Assert(checkChange(f0, nil), qt.Equals, fsnotify.Remove)
|
|
c.Assert(checkChange(nil, f0), qt.Equals, fsnotify.Create)
|
|
c.Assert(checkChange(f1, f1_2), qt.Equals, fsnotify.Chmod)
|
|
c.Assert(checkChange(f2, f2_2), qt.Equals, fsnotify.Write)
|
|
c.Assert(checkChange(nil, nil), qt.Equals, fsnotify.Op(0))
|
|
c.Assert(checkChange(d1, f1), qt.Equals, fsnotify.Op(0))
|
|
c.Assert(checkChange(f1, d1), qt.Equals, fsnotify.Op(0))
|
|
}
|
|
|
|
func BenchmarkPoller(b *testing.B) {
|
|
runBench := func(b *testing.B, item *itemToWatch) {
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
evs, err := item.checkForChanges()
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
if len(evs) != 0 {
|
|
b.Fatal("got events")
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
b.Run("Check for changes in dir", func(b *testing.B) {
|
|
c := qt.New(b)
|
|
dir := prepareTestDirWithSomeFiles(c, "bench-check")
|
|
item, err := newItemToWatch(dir)
|
|
c.Assert(err, qt.IsNil)
|
|
runBench(b, item)
|
|
|
|
})
|
|
|
|
b.Run("Check for changes in file", func(b *testing.B) {
|
|
c := qt.New(b)
|
|
dir := prepareTestDirWithSomeFiles(c, "bench-check-file")
|
|
filename := filepath.Join(dir, subdir1, "file1")
|
|
item, err := newItemToWatch(filename)
|
|
c.Assert(err, qt.IsNil)
|
|
runBench(b, item)
|
|
})
|
|
|
|
}
|
|
|
|
func prepareTestDirWithSomeFiles(c *qt.C, id string) string {
|
|
dir, err := ioutil.TempDir("", fmt.Sprintf("test-poller-dir-%s", id))
|
|
c.Assert(err, qt.IsNil)
|
|
c.Assert(os.MkdirAll(filepath.Join(dir, subdir1), 0777), qt.IsNil)
|
|
c.Assert(os.MkdirAll(filepath.Join(dir, subdir2), 0777), qt.IsNil)
|
|
|
|
for i := 0; i < 3; i++ {
|
|
c.Assert(ioutil.WriteFile(filepath.Join(dir, subdir1, fmt.Sprintf("file%d", i)), []byte("hello1"), 0600), qt.IsNil)
|
|
}
|
|
|
|
for i := 0; i < 3; i++ {
|
|
c.Assert(ioutil.WriteFile(filepath.Join(dir, subdir2, fmt.Sprintf("file%d", i)), []byte("hello2"), 0600), qt.IsNil)
|
|
}
|
|
|
|
c.Cleanup(func() {
|
|
os.RemoveAll(dir)
|
|
})
|
|
|
|
return dir
|
|
}
|
|
|
|
func preparePollTest(c *qt.C, poll bool) (string, FileWatcher) {
|
|
var w FileWatcher
|
|
if poll {
|
|
w = NewPollingWatcher(watchWaitTime)
|
|
} else {
|
|
var err error
|
|
w, err = NewEventWatcher()
|
|
c.Assert(err, qt.IsNil)
|
|
}
|
|
|
|
dir := prepareTestDirWithSomeFiles(c, fmt.Sprint(poll))
|
|
|
|
c.Cleanup(func() {
|
|
w.Close()
|
|
})
|
|
return dir, w
|
|
}
|
|
|
|
func assertEvents(c *qt.C, w FileWatcher, evs ...fsnotify.Event) {
|
|
c.Helper()
|
|
i := 0
|
|
check := func() error {
|
|
for {
|
|
select {
|
|
case got := <-w.Events():
|
|
if i > len(evs)-1 {
|
|
return fmt.Errorf("got too many event(s): %q", got)
|
|
}
|
|
expected := evs[i]
|
|
i++
|
|
if expected.Name != got.Name {
|
|
return fmt.Errorf("got wrong filename, expected %q: %v", expected.Name, got.Name)
|
|
} else if got.Op&expected.Op != expected.Op {
|
|
return fmt.Errorf("got wrong event type, expected %q: %v", expected.Op, got.Op)
|
|
}
|
|
case e := <-w.Errors():
|
|
return fmt.Errorf("got unexpected error waiting for events %v", e)
|
|
case <-time.After(watchWaitTime + (watchWaitTime / 2)):
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
c.Assert(check(), qt.IsNil)
|
|
c.Assert(i, qt.Equals, len(evs))
|
|
}
|
|
|
|
func drainEvents(c *qt.C, w FileWatcher) {
|
|
c.Helper()
|
|
check := func() error {
|
|
for {
|
|
select {
|
|
case <-w.Events():
|
|
case e := <-w.Errors():
|
|
return fmt.Errorf("got unexpected error waiting for events %v", e)
|
|
case <-time.After(watchWaitTime * 2):
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
c.Assert(check(), qt.IsNil)
|
|
}
|