github.com/anakojm/hugo-katex@v0.0.0-20231023141351-42d6f5de9c0b/watcher/filenotify/poller_test.go (about)

     1  package filenotify
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"path/filepath"
     7  	"runtime"
     8  	"testing"
     9  	"time"
    10  
    11  	qt "github.com/frankban/quicktest"
    12  	"github.com/fsnotify/fsnotify"
    13  	"github.com/gohugoio/hugo/htesting"
    14  )
    15  
    16  const (
    17  	subdir1       = "subdir1"
    18  	subdir2       = "subdir2"
    19  	watchWaitTime = 200 * time.Millisecond
    20  )
    21  
    22  var (
    23  	isMacOs   = runtime.GOOS == "darwin"
    24  	isWindows = runtime.GOOS == "windows"
    25  	isCI      = htesting.IsCI()
    26  )
    27  
    28  func TestPollerAddRemove(t *testing.T) {
    29  	c := qt.New(t)
    30  	w := NewPollingWatcher(watchWaitTime)
    31  
    32  	c.Assert(w.Add("foo"), qt.Not(qt.IsNil))
    33  	c.Assert(w.Remove("foo"), qt.Not(qt.IsNil))
    34  
    35  	f, err := os.CreateTemp("", "asdf")
    36  	if err != nil {
    37  		t.Fatal(err)
    38  	}
    39  	c.Cleanup(func() {
    40  		c.Assert(w.Close(), qt.IsNil)
    41  		os.Remove(f.Name())
    42  	})
    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(os.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(os.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 := os.CreateTemp("", "f1")
   131  	c.Assert(err, qt.IsNil)
   132  	defer os.Remove(f1.Name())
   133  	f2, err := os.CreateTemp("", "f2")
   134  	c.Assert(err, qt.IsNil)
   135  	filename1 := f1.Name()
   136  	filename2 := f2.Name()
   137  	f1.Close()
   138  	f2.Close()
   139  
   140  	c.Assert(w.Add(filename1), qt.IsNil)
   141  	c.Assert(w.Add(filename2), qt.IsNil)
   142  	c.Assert(w.Close(), qt.IsNil)
   143  	c.Assert(w.Close(), qt.IsNil)
   144  	c.Assert(os.WriteFile(filename1, []byte("new"), 0600), qt.IsNil)
   145  	c.Assert(os.WriteFile(filename2, []byte("new"), 0600), qt.IsNil)
   146  	// No more event as the watchers are closed.
   147  	assertEvents(c, w)
   148  
   149  	f2, err = os.CreateTemp("", "f2")
   150  	c.Assert(err, qt.IsNil)
   151  
   152  	defer os.Remove(f2.Name())
   153  
   154  	c.Assert(w.Add(f2.Name()), qt.Not(qt.IsNil))
   155  
   156  }
   157  
   158  func TestCheckChange(t *testing.T) {
   159  	c := qt.New(t)
   160  
   161  	dir := prepareTestDirWithSomeFiles(c, "check-change")
   162  
   163  	stat := func(s ...string) os.FileInfo {
   164  		fi, err := os.Stat(filepath.Join(append([]string{dir}, s...)...))
   165  		c.Assert(err, qt.IsNil)
   166  		return fi
   167  	}
   168  
   169  	f0, f1, f2 := stat(subdir2, "file0"), stat(subdir2, "file1"), stat(subdir2, "file2")
   170  	d1 := stat(subdir1)
   171  
   172  	// Note that on Windows, only the 0200 bit (owner writable) of mode is used.
   173  	c.Assert(os.Chmod(filepath.Join(filepath.Join(dir, subdir2, "file1")), 0400), qt.IsNil)
   174  	f1_2 := stat(subdir2, "file1")
   175  
   176  	c.Assert(os.WriteFile(filepath.Join(filepath.Join(dir, subdir2, "file2")), []byte("changed"), 0600), qt.IsNil)
   177  	f2_2 := stat(subdir2, "file2")
   178  
   179  	c.Assert(checkChange(f0, nil), qt.Equals, fsnotify.Remove)
   180  	c.Assert(checkChange(nil, f0), qt.Equals, fsnotify.Create)
   181  	c.Assert(checkChange(f1, f1_2), qt.Equals, fsnotify.Chmod)
   182  	c.Assert(checkChange(f2, f2_2), qt.Equals, fsnotify.Write)
   183  	c.Assert(checkChange(nil, nil), qt.Equals, fsnotify.Op(0))
   184  	c.Assert(checkChange(d1, f1), qt.Equals, fsnotify.Op(0))
   185  	c.Assert(checkChange(f1, d1), qt.Equals, fsnotify.Op(0))
   186  }
   187  
   188  func BenchmarkPoller(b *testing.B) {
   189  	runBench := func(b *testing.B, item *itemToWatch) {
   190  		b.ResetTimer()
   191  		for i := 0; i < b.N; i++ {
   192  			evs, err := item.checkForChanges()
   193  			if err != nil {
   194  				b.Fatal(err)
   195  			}
   196  			if len(evs) != 0 {
   197  				b.Fatal("got events")
   198  			}
   199  
   200  		}
   201  
   202  	}
   203  
   204  	b.Run("Check for changes in dir", func(b *testing.B) {
   205  		c := qt.New(b)
   206  		dir := prepareTestDirWithSomeFiles(c, "bench-check")
   207  		item, err := newItemToWatch(dir)
   208  		c.Assert(err, qt.IsNil)
   209  		runBench(b, item)
   210  
   211  	})
   212  
   213  	b.Run("Check for changes in file", func(b *testing.B) {
   214  		c := qt.New(b)
   215  		dir := prepareTestDirWithSomeFiles(c, "bench-check-file")
   216  		filename := filepath.Join(dir, subdir1, "file1")
   217  		item, err := newItemToWatch(filename)
   218  		c.Assert(err, qt.IsNil)
   219  		runBench(b, item)
   220  	})
   221  
   222  }
   223  
   224  func prepareTestDirWithSomeFiles(c *qt.C, id string) string {
   225  	dir := c.TB.TempDir()
   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(os.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(os.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  }