github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/watch/notify_test.go (about)

     1  package watch
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"os"
     8  	"path/filepath"
     9  	"runtime"
    10  	"strings"
    11  	"testing"
    12  	"time"
    13  
    14  	"github.com/stretchr/testify/assert"
    15  	"github.com/stretchr/testify/require"
    16  
    17  	"github.com/tilt-dev/tilt/internal/dockerignore"
    18  	"github.com/tilt-dev/tilt/internal/testutils/tempdir"
    19  	"github.com/tilt-dev/tilt/pkg/logger"
    20  )
    21  
    22  // Each implementation of the notify interface should have the same basic
    23  // behavior.
    24  
    25  func TestWindowsBufferSize(t *testing.T) {
    26  	t.Run("empty", func(t *testing.T) {
    27  		t.Setenv(WindowsBufferSizeEnvVar, "")
    28  		require.Equal(t, defaultBufferSize, DesiredWindowsBufferSize())
    29  	})
    30  
    31  	t.Run("non-integer", func(t *testing.T) {
    32  		t.Setenv(WindowsBufferSizeEnvVar, "a")
    33  		require.Equal(t, defaultBufferSize, DesiredWindowsBufferSize())
    34  	})
    35  
    36  	t.Run("integer", func(t *testing.T) {
    37  		t.Setenv(WindowsBufferSizeEnvVar, "10")
    38  		require.Equal(t, 10, DesiredWindowsBufferSize())
    39  	})
    40  }
    41  
    42  func TestNoEvents(t *testing.T) {
    43  	f := newNotifyFixture(t)
    44  	f.assertEvents()
    45  }
    46  
    47  func TestNoWatches(t *testing.T) {
    48  	f := newNotifyFixture(t)
    49  	f.paths = nil
    50  	f.rebuildWatcher()
    51  	f.assertEvents()
    52  }
    53  
    54  func TestEventOrdering(t *testing.T) {
    55  	if runtime.GOOS == "windows" {
    56  		// https://qualapps.blogspot.com/2010/05/understanding-readdirectorychangesw_19.html
    57  		t.Skip("Windows doesn't make great guarantees about duplicate/out-of-order events")
    58  		return
    59  	}
    60  	f := newNotifyFixture(t)
    61  
    62  	count := 8
    63  	dirs := make([]string, count)
    64  	for i := range dirs {
    65  		dir := f.TempDir("watched")
    66  		dirs[i] = dir
    67  		f.watch(dir)
    68  	}
    69  
    70  	f.fsync()
    71  	f.events = nil
    72  
    73  	var expected []string
    74  	for i, dir := range dirs {
    75  		base := fmt.Sprintf("%d.txt", i)
    76  		p := filepath.Join(dir, base)
    77  		err := os.WriteFile(p, []byte(base), os.FileMode(0777))
    78  		if err != nil {
    79  			t.Fatal(err)
    80  		}
    81  		expected = append(expected, filepath.Join(dir, base))
    82  	}
    83  
    84  	f.assertEvents(expected...)
    85  }
    86  
    87  // Simulate a git branch switch that creates a bunch
    88  // of directories, creates files in them, then deletes
    89  // them all quickly. Make sure there are no errors.
    90  func TestGitBranchSwitch(t *testing.T) {
    91  	f := newNotifyFixture(t)
    92  
    93  	count := 10
    94  	dirs := make([]string, count)
    95  	for i := range dirs {
    96  		dir := f.TempDir("watched")
    97  		dirs[i] = dir
    98  		f.watch(dir)
    99  	}
   100  
   101  	f.fsync()
   102  	f.events = nil
   103  
   104  	// consume all the events in the background
   105  	ctx, cancel := context.WithCancel(context.Background())
   106  	done := f.consumeEventsInBackground(ctx)
   107  
   108  	for i, dir := range dirs {
   109  		for j := 0; j < count; j++ {
   110  			base := fmt.Sprintf("x/y/dir-%d/x.txt", j)
   111  			p := filepath.Join(dir, base)
   112  			f.WriteFile(p, "contents")
   113  		}
   114  
   115  		if i != 0 {
   116  			err := os.RemoveAll(dir)
   117  			require.NoError(t, err)
   118  		}
   119  	}
   120  
   121  	cancel()
   122  	err := <-done
   123  	if err != nil {
   124  		t.Fatal(err)
   125  	}
   126  
   127  	f.fsync()
   128  	f.events = nil
   129  
   130  	// Make sure the watch on the first dir still works.
   131  	dir := dirs[0]
   132  	path := filepath.Join(dir, "change")
   133  
   134  	f.WriteFile(path, "hello\n")
   135  	f.fsync()
   136  
   137  	f.assertEvents(path)
   138  
   139  	// Make sure there are no errors in the out stream
   140  	assert.Equal(t, "", f.out.String())
   141  }
   142  
   143  func TestWatchesAreRecursive(t *testing.T) {
   144  	f := newNotifyFixture(t)
   145  
   146  	root := f.TempDir("root")
   147  
   148  	// add a sub directory
   149  	subPath := filepath.Join(root, "sub")
   150  	f.MkdirAll(subPath)
   151  
   152  	// watch parent
   153  	f.watch(root)
   154  
   155  	f.fsync()
   156  	f.events = nil
   157  	// change sub directory
   158  	changeFilePath := filepath.Join(subPath, "change")
   159  	f.WriteFile(changeFilePath, "change")
   160  
   161  	f.assertEvents(changeFilePath)
   162  }
   163  
   164  func TestNewDirectoriesAreRecursivelyWatched(t *testing.T) {
   165  	f := newNotifyFixture(t)
   166  
   167  	root := f.TempDir("root")
   168  
   169  	// watch parent
   170  	f.watch(root)
   171  	f.fsync()
   172  	f.events = nil
   173  
   174  	// add a sub directory
   175  	subPath := filepath.Join(root, "sub")
   176  	f.MkdirAll(subPath)
   177  
   178  	// change something inside sub directory
   179  	changeFilePath := filepath.Join(subPath, "change")
   180  	file, err := os.OpenFile(changeFilePath, os.O_RDONLY|os.O_CREATE, 0666)
   181  	if err != nil {
   182  		t.Fatal(err)
   183  	}
   184  	_ = file.Close()
   185  	f.assertEvents(subPath, changeFilePath)
   186  }
   187  
   188  func TestWatchNonExistentPath(t *testing.T) {
   189  	f := newNotifyFixture(t)
   190  
   191  	root := f.TempDir("root")
   192  	path := filepath.Join(root, "change")
   193  
   194  	f.watch(path)
   195  	f.fsync()
   196  
   197  	d1 := "hello\ngo\n"
   198  	f.WriteFile(path, d1)
   199  	f.assertEvents(path)
   200  }
   201  
   202  func TestWatchDirectoryAndTouchIt(t *testing.T) {
   203  	f := newNotifyFixture(t)
   204  
   205  	cTime := time.Now()
   206  	root := f.TempDir("root")
   207  	path := filepath.Join(root, "change")
   208  	a := filepath.Join(path, "a.txt")
   209  	f.WriteFile(a, "a")
   210  
   211  	f.watch(path)
   212  	f.fsync()
   213  
   214  	err := os.Chtimes(path, cTime, time.Now())
   215  	assert.NoError(t, err)
   216  	err = os.Chmod(path, 0770)
   217  	assert.NoError(t, err)
   218  	f.assertEvents()
   219  }
   220  
   221  func TestWatchNonExistentPathDoesNotFireSiblingEvent(t *testing.T) {
   222  	f := newNotifyFixture(t)
   223  
   224  	root := f.TempDir("root")
   225  	watchedFile := filepath.Join(root, "a.txt")
   226  	unwatchedSibling := filepath.Join(root, "b.txt")
   227  
   228  	f.watch(watchedFile)
   229  	f.fsync()
   230  
   231  	d1 := "hello\ngo\n"
   232  	f.WriteFile(unwatchedSibling, d1)
   233  	f.assertEvents()
   234  }
   235  
   236  func TestRemove(t *testing.T) {
   237  	f := newNotifyFixture(t)
   238  
   239  	root := f.TempDir("root")
   240  	path := filepath.Join(root, "change")
   241  
   242  	d1 := "hello\ngo\n"
   243  	f.WriteFile(path, d1)
   244  
   245  	f.watch(path)
   246  	f.fsync()
   247  	f.events = nil
   248  	err := os.Remove(path)
   249  	if err != nil {
   250  		t.Fatal(err)
   251  	}
   252  	f.assertEvents(path)
   253  }
   254  
   255  func TestRemoveAndAddBack(t *testing.T) {
   256  	f := newNotifyFixture(t)
   257  
   258  	path := filepath.Join(f.paths[0], "change")
   259  
   260  	d1 := []byte("hello\ngo\n")
   261  	err := os.WriteFile(path, d1, 0644)
   262  	if err != nil {
   263  		t.Fatal(err)
   264  	}
   265  	f.watch(path)
   266  	f.assertEvents(path)
   267  
   268  	err = os.Remove(path)
   269  	if err != nil {
   270  		t.Fatal(err)
   271  	}
   272  
   273  	f.assertEvents(path)
   274  	f.events = nil
   275  
   276  	err = os.WriteFile(path, d1, 0644)
   277  	if err != nil {
   278  		t.Fatal(err)
   279  	}
   280  
   281  	f.assertEvents(path)
   282  }
   283  
   284  func TestSingleFile(t *testing.T) {
   285  	f := newNotifyFixture(t)
   286  
   287  	root := f.TempDir("root")
   288  	path := filepath.Join(root, "change")
   289  
   290  	d1 := "hello\ngo\n"
   291  	f.WriteFile(path, d1)
   292  
   293  	f.watch(path)
   294  	f.fsync()
   295  
   296  	d2 := []byte("hello\nworld\n")
   297  	err := os.WriteFile(path, d2, 0644)
   298  	if err != nil {
   299  		t.Fatal(err)
   300  	}
   301  	f.assertEvents(path)
   302  }
   303  
   304  func TestWriteBrokenLink(t *testing.T) {
   305  	if runtime.GOOS == "windows" {
   306  		t.Skip("no user-space symlinks on windows")
   307  	}
   308  	f := newNotifyFixture(t)
   309  
   310  	link := filepath.Join(f.paths[0], "brokenLink")
   311  	missingFile := filepath.Join(f.paths[0], "missingFile")
   312  	err := os.Symlink(missingFile, link)
   313  	if err != nil {
   314  		t.Fatal(err)
   315  	}
   316  
   317  	f.assertEvents(link)
   318  }
   319  
   320  func TestWriteGoodLink(t *testing.T) {
   321  	if runtime.GOOS == "windows" {
   322  		t.Skip("no user-space symlinks on windows")
   323  	}
   324  	f := newNotifyFixture(t)
   325  
   326  	goodFile := filepath.Join(f.paths[0], "goodFile")
   327  	err := os.WriteFile(goodFile, []byte("hello"), 0644)
   328  	if err != nil {
   329  		t.Fatal(err)
   330  	}
   331  
   332  	link := filepath.Join(f.paths[0], "goodFileSymlink")
   333  	err = os.Symlink(goodFile, link)
   334  	if err != nil {
   335  		t.Fatal(err)
   336  	}
   337  
   338  	f.assertEvents(goodFile, link)
   339  }
   340  
   341  func TestWatchBrokenLink(t *testing.T) {
   342  	if runtime.GOOS == "windows" {
   343  		t.Skip("no user-space symlinks on windows")
   344  	}
   345  	f := newNotifyFixture(t)
   346  
   347  	newRoot, err := NewDir(t.Name())
   348  	if err != nil {
   349  		t.Fatal(err)
   350  	}
   351  	defer func() {
   352  		err := newRoot.TearDown()
   353  		if err != nil {
   354  			fmt.Printf("error tearing down temp dir: %v\n", err)
   355  		}
   356  	}()
   357  
   358  	link := filepath.Join(newRoot.Path(), "brokenLink")
   359  	missingFile := filepath.Join(newRoot.Path(), "missingFile")
   360  	err = os.Symlink(missingFile, link)
   361  	if err != nil {
   362  		t.Fatal(err)
   363  	}
   364  
   365  	f.watch(newRoot.Path())
   366  	err = os.Remove(link)
   367  	require.NoError(t, err)
   368  	f.assertEvents(link)
   369  }
   370  
   371  func TestMoveAndReplace(t *testing.T) {
   372  	f := newNotifyFixture(t)
   373  
   374  	root := f.TempDir("root")
   375  	file := filepath.Join(root, "myfile")
   376  	f.WriteFile(file, "hello")
   377  
   378  	f.watch(file)
   379  	tmpFile := filepath.Join(root, ".myfile.swp")
   380  	f.WriteFile(tmpFile, "world")
   381  
   382  	err := os.Rename(tmpFile, file)
   383  	if err != nil {
   384  		t.Fatal(err)
   385  	}
   386  
   387  	f.assertEvents(file)
   388  }
   389  
   390  func TestWatchBothDirAndFile(t *testing.T) {
   391  	f := newNotifyFixture(t)
   392  
   393  	dir := f.JoinPath("foo")
   394  	fileA := f.JoinPath("foo", "a")
   395  	fileB := f.JoinPath("foo", "b")
   396  	f.WriteFile(fileA, "a")
   397  	f.WriteFile(fileB, "b")
   398  
   399  	f.watch(fileA)
   400  	f.watch(dir)
   401  	f.fsync()
   402  	f.events = nil
   403  
   404  	f.WriteFile(fileB, "b-new")
   405  	f.assertEvents(fileB)
   406  }
   407  
   408  func TestWatchNonexistentFileInNonexistentDirectoryCreatedSimultaneously(t *testing.T) {
   409  	f := newNotifyFixture(t)
   410  
   411  	root := f.JoinPath("root")
   412  	err := os.Mkdir(root, 0777)
   413  	if err != nil {
   414  		t.Fatal(err)
   415  	}
   416  	file := f.JoinPath("root", "parent", "a")
   417  
   418  	f.watch(file)
   419  	f.fsync()
   420  	f.events = nil
   421  	f.WriteFile(file, "hello")
   422  	f.assertEvents(file)
   423  }
   424  
   425  func TestWatchNonexistentDirectory(t *testing.T) {
   426  	f := newNotifyFixture(t)
   427  
   428  	root := f.JoinPath("root")
   429  	err := os.Mkdir(root, 0777)
   430  	if err != nil {
   431  		t.Fatal(err)
   432  	}
   433  	parent := f.JoinPath("parent")
   434  	file := f.JoinPath("parent", "a")
   435  
   436  	f.watch(parent)
   437  	f.fsync()
   438  	f.events = nil
   439  
   440  	err = os.Mkdir(parent, 0777)
   441  	if err != nil {
   442  		t.Fatal(err)
   443  	}
   444  
   445  	// for directories that were the root of an Add, we don't report creation, cf. watcher_darwin.go
   446  	f.assertEvents()
   447  
   448  	f.events = nil
   449  	f.WriteFile(file, "hello")
   450  
   451  	f.assertEvents(file)
   452  }
   453  
   454  func TestWatchNonexistentFileInNonexistentDirectory(t *testing.T) {
   455  	f := newNotifyFixture(t)
   456  
   457  	root := f.JoinPath("root")
   458  	err := os.Mkdir(root, 0777)
   459  	if err != nil {
   460  		t.Fatal(err)
   461  	}
   462  	parent := f.JoinPath("parent")
   463  	file := f.JoinPath("parent", "a")
   464  
   465  	f.watch(file)
   466  	f.assertEvents()
   467  
   468  	err = os.Mkdir(parent, 0777)
   469  	if err != nil {
   470  		t.Fatal(err)
   471  	}
   472  
   473  	f.assertEvents()
   474  	f.WriteFile(file, "hello")
   475  	f.assertEvents(file)
   476  }
   477  
   478  func TestWatchCountInnerFile(t *testing.T) {
   479  	f := newNotifyFixture(t)
   480  
   481  	root := f.paths[0]
   482  	a := f.JoinPath(root, "a")
   483  	b := f.JoinPath(a, "b")
   484  	file := f.JoinPath(b, "bigFile")
   485  	f.WriteFile(file, "hello")
   486  	f.assertEvents(a, b, file)
   487  
   488  	expectedWatches := 3
   489  	if isRecursiveWatcher() {
   490  		expectedWatches = 1
   491  	}
   492  	assert.Equal(t, expectedWatches, int(numberOfWatches.Value()))
   493  }
   494  
   495  func TestWatchCountInnerFileWithIgnore(t *testing.T) {
   496  	f := newNotifyFixture(t)
   497  
   498  	root := f.paths[0]
   499  	ignore, _ := dockerignore.NewDockerPatternMatcher(root, []string{
   500  		"a",
   501  		"!a/b",
   502  	})
   503  	f.setIgnore(ignore)
   504  
   505  	a := f.JoinPath(root, "a")
   506  	b := f.JoinPath(a, "b")
   507  	file := f.JoinPath(b, "bigFile")
   508  	f.WriteFile(file, "hello")
   509  	f.assertEvents(b, file)
   510  
   511  	expectedWatches := 3
   512  	if isRecursiveWatcher() {
   513  		expectedWatches = 1
   514  	}
   515  	assert.Equal(t, expectedWatches, int(numberOfWatches.Value()))
   516  }
   517  
   518  func TestIgnoreCreatedDir(t *testing.T) {
   519  	f := newNotifyFixture(t)
   520  
   521  	root := f.paths[0]
   522  	ignore, _ := dockerignore.NewDockerPatternMatcher(root, []string{"a/b"})
   523  	f.setIgnore(ignore)
   524  
   525  	a := f.JoinPath(root, "a")
   526  	b := f.JoinPath(a, "b")
   527  	file := f.JoinPath(b, "bigFile")
   528  	f.WriteFile(file, "hello")
   529  	f.assertEvents(a)
   530  
   531  	expectedWatches := 2
   532  	if isRecursiveWatcher() {
   533  		expectedWatches = 1
   534  	}
   535  	assert.Equal(t, expectedWatches, int(numberOfWatches.Value()))
   536  }
   537  
   538  func TestIgnoreCreatedDirWithExclusions(t *testing.T) {
   539  	f := newNotifyFixture(t)
   540  
   541  	root := f.paths[0]
   542  	ignore, _ := dockerignore.NewDockerPatternMatcher(root,
   543  		[]string{
   544  			"a/b",
   545  			"c",
   546  			"!c/d",
   547  		})
   548  	f.setIgnore(ignore)
   549  
   550  	a := f.JoinPath(root, "a")
   551  	b := f.JoinPath(a, "b")
   552  	file := f.JoinPath(b, "bigFile")
   553  	f.WriteFile(file, "hello")
   554  	f.assertEvents(a)
   555  
   556  	expectedWatches := 2
   557  	if isRecursiveWatcher() {
   558  		expectedWatches = 1
   559  	}
   560  	assert.Equal(t, expectedWatches, int(numberOfWatches.Value()))
   561  }
   562  
   563  func TestIgnoreInitialDir(t *testing.T) {
   564  	f := newNotifyFixture(t)
   565  
   566  	root := f.TempDir("root")
   567  	ignore, _ := dockerignore.NewDockerPatternMatcher(root, []string{"a/b"})
   568  	f.setIgnore(ignore)
   569  
   570  	a := f.JoinPath(root, "a")
   571  	b := f.JoinPath(a, "b")
   572  	file := f.JoinPath(b, "bigFile")
   573  	f.WriteFile(file, "hello")
   574  	f.watch(root)
   575  
   576  	f.assertEvents()
   577  
   578  	expectedWatches := 3
   579  	if isRecursiveWatcher() {
   580  		expectedWatches = 2
   581  	}
   582  	assert.Equal(t, expectedWatches, int(numberOfWatches.Value()))
   583  }
   584  
   585  func isRecursiveWatcher() bool {
   586  	return runtime.GOOS == "darwin" || runtime.GOOS == "windows"
   587  }
   588  
   589  type notifyFixture struct {
   590  	ctx    context.Context
   591  	cancel func()
   592  	out    *bytes.Buffer
   593  	*tempdir.TempDirFixture
   594  	notify Notify
   595  	ignore PathMatcher
   596  	paths  []string
   597  	events []FileEvent
   598  }
   599  
   600  func newNotifyFixture(t *testing.T) *notifyFixture {
   601  	out := bytes.NewBuffer(nil)
   602  	ctx, cancel := context.WithCancel(context.Background())
   603  	nf := &notifyFixture{
   604  		ctx:            ctx,
   605  		cancel:         cancel,
   606  		TempDirFixture: tempdir.NewTempDirFixture(t),
   607  		paths:          []string{},
   608  		ignore:         EmptyMatcher{},
   609  		out:            out,
   610  	}
   611  	nf.watch(nf.TempDir("watched"))
   612  	t.Cleanup(nf.tearDown)
   613  	return nf
   614  }
   615  
   616  func (f *notifyFixture) setIgnore(ignore PathMatcher) {
   617  	f.ignore = ignore
   618  	f.rebuildWatcher()
   619  }
   620  
   621  func (f *notifyFixture) watch(path string) {
   622  	f.paths = append(f.paths, path)
   623  	f.rebuildWatcher()
   624  }
   625  
   626  func (f *notifyFixture) rebuildWatcher() {
   627  	// sync any outstanding events and close the old watcher
   628  	if f.notify != nil {
   629  		f.fsync()
   630  		f.closeWatcher()
   631  	}
   632  
   633  	// create a new watcher
   634  	notify, err := NewWatcher(f.paths, f.ignore, logger.NewTestLogger(f.out))
   635  	if err != nil {
   636  		f.T().Fatal(err)
   637  	}
   638  	f.notify = notify
   639  	err = f.notify.Start()
   640  	if err != nil {
   641  		f.T().Fatal(err)
   642  	}
   643  }
   644  
   645  func (f *notifyFixture) assertEvents(expected ...string) {
   646  	f.fsync()
   647  	if runtime.GOOS == "windows" {
   648  		// NOTE(nick): It's unclear to me why an extra fsync() helps
   649  		// here, but it makes the I/O way more predictable.
   650  		f.fsync()
   651  	}
   652  
   653  	if len(f.events) != len(expected) {
   654  		f.T().Fatalf("Got %d events (expected %d): %v %v", len(f.events), len(expected), f.events, expected)
   655  	}
   656  
   657  	for i, actual := range f.events {
   658  		e := FileEvent{expected[i]}
   659  		if actual != e {
   660  			f.T().Fatalf("Got event %v (expected %v)", actual, e)
   661  		}
   662  	}
   663  }
   664  
   665  func (f *notifyFixture) consumeEventsInBackground(ctx context.Context) chan error {
   666  	done := make(chan error)
   667  	go func() {
   668  		for {
   669  			select {
   670  			case <-f.ctx.Done():
   671  				close(done)
   672  				return
   673  			case <-ctx.Done():
   674  				close(done)
   675  				return
   676  			case err := <-f.notify.Errors():
   677  				done <- err
   678  				close(done)
   679  				return
   680  			case <-f.notify.Events():
   681  			}
   682  		}
   683  	}()
   684  	return done
   685  }
   686  
   687  func (f *notifyFixture) fsync() {
   688  	f.fsyncWithRetryCount(3)
   689  }
   690  
   691  func (f *notifyFixture) fsyncWithRetryCount(retryCount int) {
   692  	if len(f.paths) == 0 {
   693  		return
   694  	}
   695  
   696  	syncPathBase := fmt.Sprintf("sync-%d.txt", time.Now().UnixNano())
   697  	syncPath := filepath.Join(f.paths[0], syncPathBase)
   698  	anySyncPath := filepath.Join(f.paths[0], "sync-")
   699  	timeout := time.After(250 * time.Millisecond)
   700  
   701  	f.WriteFile(syncPath, time.Now().String())
   702  
   703  F:
   704  	for {
   705  		select {
   706  		case <-f.ctx.Done():
   707  			return
   708  		case err := <-f.notify.Errors():
   709  			f.T().Fatal(err)
   710  
   711  		case event := <-f.notify.Events():
   712  			if strings.Contains(event.Path(), syncPath) {
   713  				break F
   714  			}
   715  			if strings.Contains(event.Path(), anySyncPath) {
   716  				continue
   717  			}
   718  
   719  			// Don't bother tracking duplicate changes to the same path
   720  			// for testing.
   721  			if len(f.events) > 0 && f.events[len(f.events)-1].Path() == event.Path() {
   722  				continue
   723  			}
   724  
   725  			f.events = append(f.events, event)
   726  
   727  		case <-timeout:
   728  			if retryCount <= 0 {
   729  				f.T().Fatalf("fsync: timeout")
   730  			} else {
   731  				f.fsyncWithRetryCount(retryCount - 1)
   732  			}
   733  			return
   734  		}
   735  	}
   736  }
   737  
   738  func (f *notifyFixture) closeWatcher() {
   739  	notify := f.notify
   740  	err := notify.Close()
   741  	if err != nil {
   742  		f.T().Fatal(err)
   743  	}
   744  
   745  	// drain channels from watcher
   746  	go func() {
   747  		for range notify.Events() {
   748  		}
   749  	}()
   750  
   751  	go func() {
   752  		for range notify.Errors() {
   753  		}
   754  	}()
   755  }
   756  
   757  func (f *notifyFixture) tearDown() {
   758  	f.cancel()
   759  	f.closeWatcher()
   760  	numberOfWatches.Set(0)
   761  }