poller_test.go (8206B)
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 }