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