mirror of https://github.com/docker/compose.git
755 lines
15 KiB
Go
755 lines
15 KiB
Go
/*
|
|
Copyright 2020 Docker Compose CLI authors
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package watch
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// Each implementation of the notify interface should have the same basic
|
|
// behavior.
|
|
|
|
func TestWindowsBufferSize(t *testing.T) {
|
|
orig := os.Getenv(WindowsBufferSizeEnvVar)
|
|
defer os.Setenv(WindowsBufferSizeEnvVar, orig) //nolint:errcheck
|
|
|
|
err := os.Setenv(WindowsBufferSizeEnvVar, "")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, defaultBufferSize, DesiredWindowsBufferSize())
|
|
|
|
err = os.Setenv(WindowsBufferSizeEnvVar, "a")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, defaultBufferSize, DesiredWindowsBufferSize())
|
|
|
|
err = os.Setenv(WindowsBufferSizeEnvVar, "10")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 10, DesiredWindowsBufferSize())
|
|
}
|
|
|
|
func TestNoEvents(t *testing.T) {
|
|
f := newNotifyFixture(t)
|
|
f.assertEvents()
|
|
}
|
|
|
|
func TestNoWatches(t *testing.T) {
|
|
f := newNotifyFixture(t)
|
|
f.paths = nil
|
|
f.rebuildWatcher()
|
|
f.assertEvents()
|
|
}
|
|
|
|
func TestEventOrdering(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
// https://qualapps.blogspot.com/2010/05/understanding-readdirectorychangesw_19.html
|
|
t.Skip("Windows doesn't make great guarantees about duplicate/out-of-order events")
|
|
return
|
|
}
|
|
f := newNotifyFixture(t)
|
|
|
|
count := 8
|
|
dirs := make([]string, count)
|
|
for i := range dirs {
|
|
dir := f.TempDir("watched")
|
|
dirs[i] = dir
|
|
f.watch(dir)
|
|
}
|
|
|
|
f.fsync()
|
|
f.events = nil
|
|
|
|
var expected []string
|
|
for i, dir := range dirs {
|
|
base := fmt.Sprintf("%d.txt", i)
|
|
p := filepath.Join(dir, base)
|
|
err := os.WriteFile(p, []byte(base), os.FileMode(0o777))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
expected = append(expected, filepath.Join(dir, base))
|
|
}
|
|
|
|
f.assertEvents(expected...)
|
|
}
|
|
|
|
// Simulate a git branch switch that creates a bunch
|
|
// of directories, creates files in them, then deletes
|
|
// them all quickly. Make sure there are no errors.
|
|
func TestGitBranchSwitch(t *testing.T) {
|
|
f := newNotifyFixture(t)
|
|
|
|
count := 10
|
|
dirs := make([]string, count)
|
|
for i := range dirs {
|
|
dir := f.TempDir("watched")
|
|
dirs[i] = dir
|
|
f.watch(dir)
|
|
}
|
|
|
|
f.fsync()
|
|
f.events = nil
|
|
|
|
// consume all the events in the background
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
done := f.consumeEventsInBackground(ctx)
|
|
|
|
for i, dir := range dirs {
|
|
for j := 0; j < count; j++ {
|
|
base := fmt.Sprintf("x/y/dir-%d/x.txt", j)
|
|
p := filepath.Join(dir, base)
|
|
f.WriteFile(p, "contents")
|
|
}
|
|
|
|
if i != 0 {
|
|
err := os.RemoveAll(dir)
|
|
require.NoError(t, err)
|
|
}
|
|
}
|
|
|
|
cancel()
|
|
err := <-done
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
f.fsync()
|
|
f.events = nil
|
|
|
|
// Make sure the watch on the first dir still works.
|
|
dir := dirs[0]
|
|
path := filepath.Join(dir, "change")
|
|
|
|
f.WriteFile(path, "hello\n")
|
|
f.fsync()
|
|
|
|
f.assertEvents(path)
|
|
|
|
// Make sure there are no errors in the out stream
|
|
assert.Equal(t, "", f.out.String())
|
|
}
|
|
|
|
func TestWatchesAreRecursive(t *testing.T) {
|
|
f := newNotifyFixture(t)
|
|
|
|
root := f.TempDir("root")
|
|
|
|
// add a sub directory
|
|
subPath := filepath.Join(root, "sub")
|
|
f.MkdirAll(subPath)
|
|
|
|
// watch parent
|
|
f.watch(root)
|
|
|
|
f.fsync()
|
|
f.events = nil
|
|
// change sub directory
|
|
changeFilePath := filepath.Join(subPath, "change")
|
|
f.WriteFile(changeFilePath, "change")
|
|
|
|
f.assertEvents(changeFilePath)
|
|
}
|
|
|
|
func TestNewDirectoriesAreRecursivelyWatched(t *testing.T) {
|
|
f := newNotifyFixture(t)
|
|
|
|
root := f.TempDir("root")
|
|
|
|
// watch parent
|
|
f.watch(root)
|
|
f.fsync()
|
|
f.events = nil
|
|
|
|
// add a sub directory
|
|
subPath := filepath.Join(root, "sub")
|
|
f.MkdirAll(subPath)
|
|
|
|
// change something inside sub directory
|
|
changeFilePath := filepath.Join(subPath, "change")
|
|
file, err := os.OpenFile(changeFilePath, os.O_RDONLY|os.O_CREATE, 0o666)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_ = file.Close()
|
|
f.assertEvents(subPath, changeFilePath)
|
|
}
|
|
|
|
func TestWatchNonExistentPath(t *testing.T) {
|
|
f := newNotifyFixture(t)
|
|
|
|
root := f.TempDir("root")
|
|
path := filepath.Join(root, "change")
|
|
|
|
f.watch(path)
|
|
f.fsync()
|
|
|
|
d1 := "hello\ngo\n"
|
|
f.WriteFile(path, d1)
|
|
f.assertEvents(path)
|
|
}
|
|
|
|
func TestWatchNonExistentPathDoesNotFireSiblingEvent(t *testing.T) {
|
|
f := newNotifyFixture(t)
|
|
|
|
root := f.TempDir("root")
|
|
watchedFile := filepath.Join(root, "a.txt")
|
|
unwatchedSibling := filepath.Join(root, "b.txt")
|
|
|
|
f.watch(watchedFile)
|
|
f.fsync()
|
|
|
|
d1 := "hello\ngo\n"
|
|
f.WriteFile(unwatchedSibling, d1)
|
|
f.assertEvents()
|
|
}
|
|
|
|
func TestRemove(t *testing.T) {
|
|
f := newNotifyFixture(t)
|
|
|
|
root := f.TempDir("root")
|
|
path := filepath.Join(root, "change")
|
|
|
|
d1 := "hello\ngo\n"
|
|
f.WriteFile(path, d1)
|
|
|
|
f.watch(path)
|
|
f.fsync()
|
|
f.events = nil
|
|
err := os.Remove(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f.assertEvents(path)
|
|
}
|
|
|
|
func TestRemoveAndAddBack(t *testing.T) {
|
|
f := newNotifyFixture(t)
|
|
|
|
path := filepath.Join(f.paths[0], "change")
|
|
|
|
d1 := []byte("hello\ngo\n")
|
|
err := os.WriteFile(path, d1, 0o644)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f.watch(path)
|
|
f.assertEvents(path)
|
|
|
|
err = os.Remove(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
f.assertEvents(path)
|
|
f.events = nil
|
|
|
|
err = os.WriteFile(path, d1, 0o644)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
f.assertEvents(path)
|
|
}
|
|
|
|
func TestSingleFile(t *testing.T) {
|
|
f := newNotifyFixture(t)
|
|
|
|
root := f.TempDir("root")
|
|
path := filepath.Join(root, "change")
|
|
|
|
d1 := "hello\ngo\n"
|
|
f.WriteFile(path, d1)
|
|
|
|
f.watch(path)
|
|
f.fsync()
|
|
|
|
d2 := []byte("hello\nworld\n")
|
|
err := os.WriteFile(path, d2, 0o644)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f.assertEvents(path)
|
|
}
|
|
|
|
func TestWriteBrokenLink(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("no user-space symlinks on windows")
|
|
}
|
|
f := newNotifyFixture(t)
|
|
|
|
link := filepath.Join(f.paths[0], "brokenLink")
|
|
missingFile := filepath.Join(f.paths[0], "missingFile")
|
|
err := os.Symlink(missingFile, link)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
f.assertEvents(link)
|
|
}
|
|
|
|
func TestWriteGoodLink(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("no user-space symlinks on windows")
|
|
}
|
|
f := newNotifyFixture(t)
|
|
|
|
goodFile := filepath.Join(f.paths[0], "goodFile")
|
|
err := os.WriteFile(goodFile, []byte("hello"), 0o644)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
link := filepath.Join(f.paths[0], "goodFileSymlink")
|
|
err = os.Symlink(goodFile, link)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
f.assertEvents(goodFile, link)
|
|
}
|
|
|
|
func TestWatchBrokenLink(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("no user-space symlinks on windows")
|
|
}
|
|
f := newNotifyFixture(t)
|
|
|
|
newRoot, err := NewDir(t.Name())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() {
|
|
err := newRoot.TearDown()
|
|
if err != nil {
|
|
fmt.Printf("error tearing down temp dir: %v\n", err)
|
|
}
|
|
}()
|
|
|
|
link := filepath.Join(newRoot.Path(), "brokenLink")
|
|
missingFile := filepath.Join(newRoot.Path(), "missingFile")
|
|
err = os.Symlink(missingFile, link)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
f.watch(newRoot.Path())
|
|
err = os.Remove(link)
|
|
require.NoError(t, err)
|
|
f.assertEvents(link)
|
|
}
|
|
|
|
func TestMoveAndReplace(t *testing.T) {
|
|
f := newNotifyFixture(t)
|
|
|
|
root := f.TempDir("root")
|
|
file := filepath.Join(root, "myfile")
|
|
f.WriteFile(file, "hello")
|
|
|
|
f.watch(file)
|
|
tmpFile := filepath.Join(root, ".myfile.swp")
|
|
f.WriteFile(tmpFile, "world")
|
|
|
|
err := os.Rename(tmpFile, file)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
f.assertEvents(file)
|
|
}
|
|
|
|
func TestWatchBothDirAndFile(t *testing.T) {
|
|
f := newNotifyFixture(t)
|
|
|
|
dir := f.JoinPath("foo")
|
|
fileA := f.JoinPath("foo", "a")
|
|
fileB := f.JoinPath("foo", "b")
|
|
f.WriteFile(fileA, "a")
|
|
f.WriteFile(fileB, "b")
|
|
|
|
f.watch(fileA)
|
|
f.watch(dir)
|
|
f.fsync()
|
|
f.events = nil
|
|
|
|
f.WriteFile(fileB, "b-new")
|
|
f.assertEvents(fileB)
|
|
}
|
|
|
|
func TestWatchNonexistentFileInNonexistentDirectoryCreatedSimultaneously(t *testing.T) {
|
|
f := newNotifyFixture(t)
|
|
|
|
root := f.JoinPath("root")
|
|
err := os.Mkdir(root, 0o777)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
file := f.JoinPath("root", "parent", "a")
|
|
|
|
f.watch(file)
|
|
f.fsync()
|
|
f.events = nil
|
|
f.WriteFile(file, "hello")
|
|
f.assertEvents(file)
|
|
}
|
|
|
|
func TestWatchNonexistentDirectory(t *testing.T) {
|
|
f := newNotifyFixture(t)
|
|
|
|
root := f.JoinPath("root")
|
|
err := os.Mkdir(root, 0o777)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
parent := f.JoinPath("parent")
|
|
file := f.JoinPath("parent", "a")
|
|
|
|
f.watch(parent)
|
|
f.fsync()
|
|
f.events = nil
|
|
|
|
err = os.Mkdir(parent, 0o777)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// for directories that were the root of an Add, we don't report creation, cf. watcher_darwin.go
|
|
f.assertEvents()
|
|
|
|
f.events = nil
|
|
f.WriteFile(file, "hello")
|
|
|
|
f.assertEvents(file)
|
|
}
|
|
|
|
func TestWatchNonexistentFileInNonexistentDirectory(t *testing.T) {
|
|
f := newNotifyFixture(t)
|
|
|
|
root := f.JoinPath("root")
|
|
err := os.Mkdir(root, 0o777)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
parent := f.JoinPath("parent")
|
|
file := f.JoinPath("parent", "a")
|
|
|
|
f.watch(file)
|
|
f.assertEvents()
|
|
|
|
err = os.Mkdir(parent, 0o777)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
f.assertEvents()
|
|
f.WriteFile(file, "hello")
|
|
f.assertEvents(file)
|
|
}
|
|
|
|
func TestWatchCountInnerFile(t *testing.T) {
|
|
f := newNotifyFixture(t)
|
|
|
|
root := f.paths[0]
|
|
a := f.JoinPath(root, "a")
|
|
b := f.JoinPath(a, "b")
|
|
file := f.JoinPath(b, "bigFile")
|
|
f.WriteFile(file, "hello")
|
|
f.assertEvents(a, b, file)
|
|
|
|
expectedWatches := 3
|
|
if isRecursiveWatcher() {
|
|
expectedWatches = 1
|
|
}
|
|
assert.Equal(t, expectedWatches, int(numberOfWatches.Value()))
|
|
}
|
|
|
|
func TestWatchCountInnerFileWithIgnore(t *testing.T) {
|
|
f := newNotifyFixture(t)
|
|
|
|
root := f.paths[0]
|
|
ignore, _ := NewDockerPatternMatcher(root, []string{
|
|
"a",
|
|
"!a/b",
|
|
})
|
|
f.setIgnore(ignore)
|
|
|
|
a := f.JoinPath(root, "a")
|
|
b := f.JoinPath(a, "b")
|
|
file := f.JoinPath(b, "bigFile")
|
|
f.WriteFile(file, "hello")
|
|
f.assertEvents(b, file)
|
|
|
|
expectedWatches := 3
|
|
if isRecursiveWatcher() {
|
|
expectedWatches = 1
|
|
}
|
|
assert.Equal(t, expectedWatches, int(numberOfWatches.Value()))
|
|
}
|
|
|
|
func TestIgnoreCreatedDir(t *testing.T) {
|
|
f := newNotifyFixture(t)
|
|
|
|
root := f.paths[0]
|
|
ignore, _ := NewDockerPatternMatcher(root, []string{"a/b"})
|
|
f.setIgnore(ignore)
|
|
|
|
a := f.JoinPath(root, "a")
|
|
b := f.JoinPath(a, "b")
|
|
file := f.JoinPath(b, "bigFile")
|
|
f.WriteFile(file, "hello")
|
|
f.assertEvents(a)
|
|
|
|
expectedWatches := 2
|
|
if isRecursiveWatcher() {
|
|
expectedWatches = 1
|
|
}
|
|
assert.Equal(t, expectedWatches, int(numberOfWatches.Value()))
|
|
}
|
|
|
|
func TestIgnoreCreatedDirWithExclusions(t *testing.T) {
|
|
f := newNotifyFixture(t)
|
|
|
|
root := f.paths[0]
|
|
ignore, _ := NewDockerPatternMatcher(root,
|
|
[]string{
|
|
"a/b",
|
|
"c",
|
|
"!c/d",
|
|
})
|
|
f.setIgnore(ignore)
|
|
|
|
a := f.JoinPath(root, "a")
|
|
b := f.JoinPath(a, "b")
|
|
file := f.JoinPath(b, "bigFile")
|
|
f.WriteFile(file, "hello")
|
|
f.assertEvents(a)
|
|
|
|
expectedWatches := 2
|
|
if isRecursiveWatcher() {
|
|
expectedWatches = 1
|
|
}
|
|
assert.Equal(t, expectedWatches, int(numberOfWatches.Value()))
|
|
}
|
|
|
|
func TestIgnoreInitialDir(t *testing.T) {
|
|
f := newNotifyFixture(t)
|
|
|
|
root := f.TempDir("root")
|
|
ignore, _ := NewDockerPatternMatcher(root, []string{"a/b"})
|
|
f.setIgnore(ignore)
|
|
|
|
a := f.JoinPath(root, "a")
|
|
b := f.JoinPath(a, "b")
|
|
file := f.JoinPath(b, "bigFile")
|
|
f.WriteFile(file, "hello")
|
|
f.watch(root)
|
|
|
|
f.assertEvents()
|
|
|
|
expectedWatches := 3
|
|
if isRecursiveWatcher() {
|
|
expectedWatches = 2
|
|
}
|
|
assert.Equal(t, expectedWatches, int(numberOfWatches.Value()))
|
|
}
|
|
|
|
func isRecursiveWatcher() bool {
|
|
return runtime.GOOS == "darwin" || runtime.GOOS == "windows"
|
|
}
|
|
|
|
type notifyFixture struct {
|
|
ctx context.Context
|
|
cancel func()
|
|
out *bytes.Buffer
|
|
*TempDirFixture
|
|
notify Notify
|
|
ignore PathMatcher
|
|
paths []string
|
|
events []FileEvent
|
|
}
|
|
|
|
func newNotifyFixture(t *testing.T) *notifyFixture {
|
|
out := bytes.NewBuffer(nil)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
nf := ¬ifyFixture{
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
TempDirFixture: NewTempDirFixture(t),
|
|
paths: []string{},
|
|
ignore: EmptyMatcher{},
|
|
out: out,
|
|
}
|
|
nf.watch(nf.TempDir("watched"))
|
|
t.Cleanup(nf.tearDown)
|
|
return nf
|
|
}
|
|
|
|
func (f *notifyFixture) setIgnore(ignore PathMatcher) {
|
|
f.ignore = ignore
|
|
f.rebuildWatcher()
|
|
}
|
|
|
|
func (f *notifyFixture) watch(path string) {
|
|
f.paths = append(f.paths, path)
|
|
f.rebuildWatcher()
|
|
}
|
|
|
|
func (f *notifyFixture) rebuildWatcher() {
|
|
// sync any outstanding events and close the old watcher
|
|
if f.notify != nil {
|
|
f.fsync()
|
|
f.closeWatcher()
|
|
}
|
|
|
|
// create a new watcher
|
|
notify, err := NewWatcher(f.paths, f.ignore)
|
|
if err != nil {
|
|
f.T().Fatal(err)
|
|
}
|
|
f.notify = notify
|
|
err = f.notify.Start()
|
|
if err != nil {
|
|
f.T().Fatal(err)
|
|
}
|
|
}
|
|
|
|
func (f *notifyFixture) assertEvents(expected ...string) {
|
|
f.fsync()
|
|
if runtime.GOOS == "windows" {
|
|
// NOTE(nick): It's unclear to me why an extra fsync() helps
|
|
// here, but it makes the I/O way more predictable.
|
|
f.fsync()
|
|
}
|
|
|
|
if len(f.events) != len(expected) {
|
|
f.T().Fatalf("Got %d events (expected %d): %v %v", len(f.events), len(expected), f.events, expected)
|
|
}
|
|
|
|
for i, actual := range f.events {
|
|
e := FileEvent{expected[i]}
|
|
if actual != e {
|
|
f.T().Fatalf("Got event %v (expected %v)", actual, e)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (f *notifyFixture) consumeEventsInBackground(ctx context.Context) chan error {
|
|
done := make(chan error)
|
|
go func() {
|
|
for {
|
|
select {
|
|
case <-f.ctx.Done():
|
|
close(done)
|
|
return
|
|
case <-ctx.Done():
|
|
close(done)
|
|
return
|
|
case err := <-f.notify.Errors():
|
|
done <- err
|
|
close(done)
|
|
return
|
|
case <-f.notify.Events():
|
|
}
|
|
}
|
|
}()
|
|
return done
|
|
}
|
|
|
|
func (f *notifyFixture) fsync() {
|
|
f.fsyncWithRetryCount(3)
|
|
}
|
|
|
|
func (f *notifyFixture) fsyncWithRetryCount(retryCount int) {
|
|
if len(f.paths) == 0 {
|
|
return
|
|
}
|
|
|
|
syncPathBase := fmt.Sprintf("sync-%d.txt", time.Now().UnixNano())
|
|
syncPath := filepath.Join(f.paths[0], syncPathBase)
|
|
anySyncPath := filepath.Join(f.paths[0], "sync-")
|
|
timeout := time.After(250 * time.Second)
|
|
|
|
f.WriteFile(syncPath, time.Now().String())
|
|
|
|
F:
|
|
for {
|
|
select {
|
|
case <-f.ctx.Done():
|
|
return
|
|
case err := <-f.notify.Errors():
|
|
f.T().Fatal(err)
|
|
|
|
case event := <-f.notify.Events():
|
|
if strings.Contains(event.Path(), syncPath) {
|
|
break F
|
|
}
|
|
if strings.Contains(event.Path(), anySyncPath) {
|
|
continue
|
|
}
|
|
|
|
// Don't bother tracking duplicate changes to the same path
|
|
// for testing.
|
|
if len(f.events) > 0 && f.events[len(f.events)-1].Path() == event.Path() {
|
|
continue
|
|
}
|
|
|
|
f.events = append(f.events, event)
|
|
|
|
case <-timeout:
|
|
if retryCount <= 0 {
|
|
f.T().Fatalf("fsync: timeout")
|
|
} else {
|
|
f.fsyncWithRetryCount(retryCount - 1)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (f *notifyFixture) closeWatcher() {
|
|
notify := f.notify
|
|
err := notify.Close()
|
|
if err != nil {
|
|
f.T().Fatal(err)
|
|
}
|
|
|
|
// drain channels from watcher
|
|
go func() {
|
|
for range notify.Events() {
|
|
}
|
|
}()
|
|
|
|
go func() {
|
|
for range notify.Errors() {
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (f *notifyFixture) tearDown() {
|
|
f.cancel()
|
|
f.closeWatcher()
|
|
numberOfWatches.Set(0)
|
|
}
|