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 }