github.com/graemephi/kahugo@v0.62.3-0.20211121071557-d78c0423784d/watcher/filenotify/poller_test.go (about)

     1  // Package filenotify is adapted from https://github.com/moby/moby/tree/master/pkg/filenotify, Apache-2.0 License.
     2  // Hopefully this can be replaced with an external package sometime in the future, see https://github.com/fsnotify/fsnotify/issues/9
     3  package filenotify
     4  
     5  import (
     6  	"fmt"
     7  	"io/ioutil"
     8  	"os"
     9  	"path/filepath"
    10  	"runtime"
    11  	"testing"
    12  	"time"
    13  
    14  	qt "github.com/frankban/quicktest"
    15  	"github.com/fsnotify/fsnotify"
    16  	"github.com/gohugoio/hugo/htesting"
    17  )
    18  
    19  const (
    20  	subdir1       = "subdir1"
    21  	subdir2       = "subdir2"
    22  	watchWaitTime = 200 * time.Millisecond
    23  )
    24  
    25  var (
    26  	isMacOs   = runtime.GOOS == "darwin"
    27  	isWindows = runtime.GOOS == "windows"
    28  	isCI      = htesting.IsCI()
    29  )
    30  
    31  func TestPollerAddRemove(t *testing.T) {
    32  	c := qt.New(t)
    33  	w := NewPollingWatcher(watchWaitTime)
    34  
    35  	c.Assert(w.Add("foo"), qt.Not(qt.IsNil))
    36  	c.Assert(w.Remove("foo"), qt.Not(qt.IsNil))
    37  
    38  	f, err := ioutil.TempFile("", "asdf")
    39  	if err != nil {
    40  		t.Fatal(err)
    41  	}
    42  	defer os.RemoveAll(f.Name())
    43  	c.Assert(w.Add(f.Name()), qt.IsNil)
    44  	c.Assert(w.Remove(f.Name()), qt.IsNil)
    45  
    46  }
    47  
    48  func TestPollerEvent(t *testing.T) {
    49  	c := qt.New(t)
    50  
    51  	for _, poll := range []bool{true, false} {
    52  		if !(poll || isMacOs) || isCI {
    53  			// Only run the fsnotify tests on MacOS locally.
    54  			continue
    55  		}
    56  		method := "fsnotify"
    57  		if poll {
    58  			method = "poll"
    59  		}
    60  
    61  		c.Run(fmt.Sprintf("%s, Watch dir", method), func(c *qt.C) {
    62  			dir, w := preparePollTest(c, poll)
    63  			subdir := filepath.Join(dir, subdir1)
    64  			c.Assert(w.Add(subdir), qt.IsNil)
    65  
    66  			filename := filepath.Join(subdir, "file1")
    67  
    68  			// Write to one file.
    69  			c.Assert(ioutil.WriteFile(filename, []byte("changed"), 0600), qt.IsNil)
    70  
    71  			var expected []fsnotify.Event
    72  
    73  			if poll {
    74  				expected = append(expected, fsnotify.Event{Name: filename, Op: fsnotify.Write})
    75  				assertEvents(c, w, expected...)
    76  			} else {
    77  				// fsnotify sometimes emits Chmod before Write,
    78  				// which is hard to test, so skip it here.
    79  				drainEvents(c, w)
    80  			}
    81  
    82  			// Remove one file.
    83  			filename = filepath.Join(subdir, "file2")
    84  			c.Assert(os.Remove(filename), qt.IsNil)
    85  			assertEvents(c, w, fsnotify.Event{Name: filename, Op: fsnotify.Remove})
    86  
    87  			// Add one file.
    88  			filename = filepath.Join(subdir, "file3")
    89  			c.Assert(ioutil.WriteFile(filename, []byte("new"), 0600), qt.IsNil)
    90  			assertEvents(c, w, fsnotify.Event{Name: filename, Op: fsnotify.Create})
    91  
    92  			// Remove entire directory.
    93  			subdir = filepath.Join(dir, subdir2)
    94  			c.Assert(w.Add(subdir), qt.IsNil)
    95  
    96  			c.Assert(os.RemoveAll(subdir), qt.IsNil)
    97  
    98  			expected = expected[:0]
    99  
   100  			// This looks like a bug in fsnotify on MacOS. There are
   101  			// 3 files in this directory, yet we get Remove events
   102  			// for one of them + the directory.
   103  			if !poll {
   104  				expected = append(expected, fsnotify.Event{Name: filepath.Join(subdir, "file2"), Op: fsnotify.Remove})
   105  			}
   106  			expected = append(expected, fsnotify.Event{Name: subdir, Op: fsnotify.Remove})
   107  			assertEvents(c, w, expected...)
   108  
   109  		})
   110  
   111  		c.Run(fmt.Sprintf("%s, Add should not trigger event", method), func(c *qt.C) {
   112  			dir, w := preparePollTest(c, poll)
   113  			subdir := filepath.Join(dir, subdir1)
   114  			w.Add(subdir)
   115  			assertEvents(c, w)
   116  			// Create a new sub directory and add it to the watcher.
   117  			subdir = filepath.Join(dir, subdir1, subdir2)
   118  			c.Assert(os.Mkdir(subdir, 0777), qt.IsNil)
   119  			w.Add(subdir)
   120  			// This should create only one event.
   121  			assertEvents(c, w, fsnotify.Event{Name: subdir, Op: fsnotify.Create})
   122  		})
   123  
   124  	}
   125  }
   126  
   127  func TestPollerClose(t *testing.T) {
   128  	c := qt.New(t)
   129  	w := NewPollingWatcher(watchWaitTime)
   130  	f1, err := ioutil.TempFile("", "f1")
   131  	c.Assert(err, qt.IsNil)
   132  	f2, err := ioutil.TempFile("", "f2")
   133  	c.Assert(err, qt.IsNil)
   134  	filename1 := f1.Name()
   135  	filename2 := f2.Name()
   136  	f1.Close()
   137  	f2.Close()
   138  
   139  	c.Assert(w.Add(filename1), qt.IsNil)
   140  	c.Assert(w.Add(filename2), qt.IsNil)
   141  	c.Assert(w.Close(), qt.IsNil)
   142  	c.Assert(w.Close(), qt.IsNil)
   143  	c.Assert(ioutil.WriteFile(filename1, []byte("new"), 0600), qt.IsNil)
   144  	c.Assert(ioutil.WriteFile(filename2, []byte("new"), 0600), qt.IsNil)
   145  	// No more event as the watchers are closed.
   146  	assertEvents(c, w)
   147  
   148  	f2, err = ioutil.TempFile("", "f2")
   149  	c.Assert(err, qt.IsNil)
   150  
   151  	defer os.Remove(f2.Name())
   152  
   153  	c.Assert(w.Add(f2.Name()), qt.Not(qt.IsNil))
   154  
   155  }
   156  
   157  func TestCheckChange(t *testing.T) {
   158  	c := qt.New(t)
   159  
   160  	dir := prepareTestDirWithSomeFiles(c, "check-change")
   161  
   162  	stat := func(s ...string) os.FileInfo {
   163  		fi, err := os.Stat(filepath.Join(append([]string{dir}, s...)...))
   164  		c.Assert(err, qt.IsNil)
   165  		return fi
   166  	}
   167  
   168  	f0, f1, f2 := stat(subdir2, "file0"), stat(subdir2, "file1"), stat(subdir2, "file2")
   169  	d1 := stat(subdir1)
   170  
   171  	// Note that on Windows, only the 0200 bit (owner writable) of mode is used.
   172  	c.Assert(os.Chmod(filepath.Join(filepath.Join(dir, subdir2, "file1")), 0400), qt.IsNil)
   173  	f1_2 := stat(subdir2, "file1")
   174  
   175  	c.Assert(ioutil.WriteFile(filepath.Join(filepath.Join(dir, subdir2, "file2")), []byte("changed"), 0600), qt.IsNil)
   176  	f2_2 := stat(subdir2, "file2")
   177  
   178  	c.Assert(checkChange(f0, nil), qt.Equals, fsnotify.Remove)
   179  	c.Assert(checkChange(nil, f0), qt.Equals, fsnotify.Create)
   180  	c.Assert(checkChange(f1, f1_2), qt.Equals, fsnotify.Chmod)
   181  	c.Assert(checkChange(f2, f2_2), qt.Equals, fsnotify.Write)
   182  	c.Assert(checkChange(nil, nil), qt.Equals, fsnotify.Op(0))
   183  	c.Assert(checkChange(d1, f1), qt.Equals, fsnotify.Op(0))
   184  	c.Assert(checkChange(f1, d1), qt.Equals, fsnotify.Op(0))
   185  }
   186  
   187  func BenchmarkPoller(b *testing.B) {
   188  	runBench := func(b *testing.B, item *itemToWatch) {
   189  		b.ResetTimer()
   190  		for i := 0; i < b.N; i++ {
   191  			evs, err := item.checkForChanges()
   192  			if err != nil {
   193  				b.Fatal(err)
   194  			}
   195  			if len(evs) != 0 {
   196  				b.Fatal("got events")
   197  			}
   198  
   199  		}
   200  
   201  	}
   202  
   203  	b.Run("Check for changes in dir", func(b *testing.B) {
   204  		c := qt.New(b)
   205  		dir := prepareTestDirWithSomeFiles(c, "bench-check")
   206  		item, err := newItemToWatch(dir)
   207  		c.Assert(err, qt.IsNil)
   208  		runBench(b, item)
   209  
   210  	})
   211  
   212  	b.Run("Check for changes in file", func(b *testing.B) {
   213  		c := qt.New(b)
   214  		dir := prepareTestDirWithSomeFiles(c, "bench-check-file")
   215  		filename := filepath.Join(dir, subdir1, "file1")
   216  		item, err := newItemToWatch(filename)
   217  		c.Assert(err, qt.IsNil)
   218  		runBench(b, item)
   219  	})
   220  
   221  }
   222  
   223  func prepareTestDirWithSomeFiles(c *qt.C, id string) string {
   224  	dir, err := ioutil.TempDir("", fmt.Sprintf("test-poller-dir-%s", id))
   225  	c.Assert(err, qt.IsNil)
   226  	c.Assert(os.MkdirAll(filepath.Join(dir, subdir1), 0777), qt.IsNil)
   227  	c.Assert(os.MkdirAll(filepath.Join(dir, subdir2), 0777), qt.IsNil)
   228  
   229  	for i := 0; i < 3; i++ {
   230  		c.Assert(ioutil.WriteFile(filepath.Join(dir, subdir1, fmt.Sprintf("file%d", i)), []byte("hello1"), 0600), qt.IsNil)
   231  	}
   232  
   233  	for i := 0; i < 3; i++ {
   234  		c.Assert(ioutil.WriteFile(filepath.Join(dir, subdir2, fmt.Sprintf("file%d", i)), []byte("hello2"), 0600), qt.IsNil)
   235  	}
   236  
   237  	c.Cleanup(func() {
   238  		os.RemoveAll(dir)
   239  	})
   240  
   241  	return dir
   242  }
   243  
   244  func preparePollTest(c *qt.C, poll bool) (string, FileWatcher) {
   245  	var w FileWatcher
   246  	if poll {
   247  		w = NewPollingWatcher(watchWaitTime)
   248  	} else {
   249  		var err error
   250  		w, err = NewEventWatcher()
   251  		c.Assert(err, qt.IsNil)
   252  	}
   253  
   254  	dir := prepareTestDirWithSomeFiles(c, fmt.Sprint(poll))
   255  
   256  	c.Cleanup(func() {
   257  		w.Close()
   258  	})
   259  	return dir, w
   260  }
   261  
   262  func assertEvents(c *qt.C, w FileWatcher, evs ...fsnotify.Event) {
   263  	c.Helper()
   264  	i := 0
   265  	check := func() error {
   266  		for {
   267  			select {
   268  			case got := <-w.Events():
   269  				if i > len(evs)-1 {
   270  					return fmt.Errorf("got too many event(s): %q", got)
   271  				}
   272  				expected := evs[i]
   273  				i++
   274  				if expected.Name != got.Name {
   275  					return fmt.Errorf("got wrong filename, expected %q: %v", expected.Name, got.Name)
   276  				} else if got.Op&expected.Op != expected.Op {
   277  					return fmt.Errorf("got wrong event type, expected %q: %v", expected.Op, got.Op)
   278  				}
   279  			case e := <-w.Errors():
   280  				return fmt.Errorf("got unexpected error waiting for events %v", e)
   281  			case <-time.After(watchWaitTime + (watchWaitTime / 2)):
   282  				return nil
   283  			}
   284  		}
   285  	}
   286  	c.Assert(check(), qt.IsNil)
   287  	c.Assert(i, qt.Equals, len(evs))
   288  }
   289  
   290  func drainEvents(c *qt.C, w FileWatcher) {
   291  	c.Helper()
   292  	check := func() error {
   293  		for {
   294  			select {
   295  			case <-w.Events():
   296  			case e := <-w.Errors():
   297  				return fmt.Errorf("got unexpected error waiting for events %v", e)
   298  			case <-time.After(watchWaitTime * 2):
   299  				return nil
   300  			}
   301  		}
   302  	}
   303  	c.Assert(check(), qt.IsNil)
   304  }