github.com/linchen2chris/hugo@v0.0.0-20230307053224-cec209389705/watcher/filenotify/poller_test.go (about) 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 "os" 8 "path/filepath" 9 "runtime" 10 "testing" 11 "time" 12 13 qt "github.com/frankban/quicktest" 14 "github.com/fsnotify/fsnotify" 15 "github.com/gohugoio/hugo/htesting" 16 ) 17 18 const ( 19 subdir1 = "subdir1" 20 subdir2 = "subdir2" 21 watchWaitTime = 200 * time.Millisecond 22 ) 23 24 var ( 25 isMacOs = runtime.GOOS == "darwin" 26 isWindows = runtime.GOOS == "windows" 27 isCI = htesting.IsCI() 28 ) 29 30 func TestPollerAddRemove(t *testing.T) { 31 c := qt.New(t) 32 w := NewPollingWatcher(watchWaitTime) 33 34 c.Assert(w.Add("foo"), qt.Not(qt.IsNil)) 35 c.Assert(w.Remove("foo"), qt.Not(qt.IsNil)) 36 37 f, err := os.CreateTemp("", "asdf") 38 if err != nil { 39 t.Fatal(err) 40 } 41 c.Cleanup(func() { 42 c.Assert(w.Close(), qt.IsNil) 43 os.Remove(f.Name()) 44 }) 45 c.Assert(w.Add(f.Name()), qt.IsNil) 46 c.Assert(w.Remove(f.Name()), qt.IsNil) 47 48 } 49 50 func TestPollerEvent(t *testing.T) { 51 c := qt.New(t) 52 53 for _, poll := range []bool{true, false} { 54 if !(poll || isMacOs) || isCI { 55 // Only run the fsnotify tests on MacOS locally. 56 continue 57 } 58 method := "fsnotify" 59 if poll { 60 method = "poll" 61 } 62 63 c.Run(fmt.Sprintf("%s, Watch dir", method), func(c *qt.C) { 64 dir, w := preparePollTest(c, poll) 65 subdir := filepath.Join(dir, subdir1) 66 c.Assert(w.Add(subdir), qt.IsNil) 67 68 filename := filepath.Join(subdir, "file1") 69 70 // Write to one file. 71 c.Assert(os.WriteFile(filename, []byte("changed"), 0600), qt.IsNil) 72 73 var expected []fsnotify.Event 74 75 if poll { 76 expected = append(expected, fsnotify.Event{Name: filename, Op: fsnotify.Write}) 77 assertEvents(c, w, expected...) 78 } else { 79 // fsnotify sometimes emits Chmod before Write, 80 // which is hard to test, so skip it here. 81 drainEvents(c, w) 82 } 83 84 // Remove one file. 85 filename = filepath.Join(subdir, "file2") 86 c.Assert(os.Remove(filename), qt.IsNil) 87 assertEvents(c, w, fsnotify.Event{Name: filename, Op: fsnotify.Remove}) 88 89 // Add one file. 90 filename = filepath.Join(subdir, "file3") 91 c.Assert(os.WriteFile(filename, []byte("new"), 0600), qt.IsNil) 92 assertEvents(c, w, fsnotify.Event{Name: filename, Op: fsnotify.Create}) 93 94 // Remove entire directory. 95 subdir = filepath.Join(dir, subdir2) 96 c.Assert(w.Add(subdir), qt.IsNil) 97 98 c.Assert(os.RemoveAll(subdir), qt.IsNil) 99 100 expected = expected[:0] 101 102 // This looks like a bug in fsnotify on MacOS. There are 103 // 3 files in this directory, yet we get Remove events 104 // for one of them + the directory. 105 if !poll { 106 expected = append(expected, fsnotify.Event{Name: filepath.Join(subdir, "file2"), Op: fsnotify.Remove}) 107 } 108 expected = append(expected, fsnotify.Event{Name: subdir, Op: fsnotify.Remove}) 109 assertEvents(c, w, expected...) 110 111 }) 112 113 c.Run(fmt.Sprintf("%s, Add should not trigger event", method), func(c *qt.C) { 114 dir, w := preparePollTest(c, poll) 115 subdir := filepath.Join(dir, subdir1) 116 w.Add(subdir) 117 assertEvents(c, w) 118 // Create a new sub directory and add it to the watcher. 119 subdir = filepath.Join(dir, subdir1, subdir2) 120 c.Assert(os.Mkdir(subdir, 0777), qt.IsNil) 121 w.Add(subdir) 122 // This should create only one event. 123 assertEvents(c, w, fsnotify.Event{Name: subdir, Op: fsnotify.Create}) 124 }) 125 126 } 127 } 128 129 func TestPollerClose(t *testing.T) { 130 c := qt.New(t) 131 w := NewPollingWatcher(watchWaitTime) 132 f1, err := os.CreateTemp("", "f1") 133 c.Assert(err, qt.IsNil) 134 defer os.Remove(f1.Name()) 135 f2, err := os.CreateTemp("", "f2") 136 c.Assert(err, qt.IsNil) 137 filename1 := f1.Name() 138 filename2 := f2.Name() 139 f1.Close() 140 f2.Close() 141 142 c.Assert(w.Add(filename1), qt.IsNil) 143 c.Assert(w.Add(filename2), qt.IsNil) 144 c.Assert(w.Close(), qt.IsNil) 145 c.Assert(w.Close(), qt.IsNil) 146 c.Assert(os.WriteFile(filename1, []byte("new"), 0600), qt.IsNil) 147 c.Assert(os.WriteFile(filename2, []byte("new"), 0600), qt.IsNil) 148 // No more event as the watchers are closed. 149 assertEvents(c, w) 150 151 f2, err = os.CreateTemp("", "f2") 152 c.Assert(err, qt.IsNil) 153 154 defer os.Remove(f2.Name()) 155 156 c.Assert(w.Add(f2.Name()), qt.Not(qt.IsNil)) 157 158 } 159 160 func TestCheckChange(t *testing.T) { 161 c := qt.New(t) 162 163 dir := prepareTestDirWithSomeFiles(c, "check-change") 164 165 stat := func(s ...string) os.FileInfo { 166 fi, err := os.Stat(filepath.Join(append([]string{dir}, s...)...)) 167 c.Assert(err, qt.IsNil) 168 return fi 169 } 170 171 f0, f1, f2 := stat(subdir2, "file0"), stat(subdir2, "file1"), stat(subdir2, "file2") 172 d1 := stat(subdir1) 173 174 // Note that on Windows, only the 0200 bit (owner writable) of mode is used. 175 c.Assert(os.Chmod(filepath.Join(filepath.Join(dir, subdir2, "file1")), 0400), qt.IsNil) 176 f1_2 := stat(subdir2, "file1") 177 178 c.Assert(os.WriteFile(filepath.Join(filepath.Join(dir, subdir2, "file2")), []byte("changed"), 0600), qt.IsNil) 179 f2_2 := stat(subdir2, "file2") 180 181 c.Assert(checkChange(f0, nil), qt.Equals, fsnotify.Remove) 182 c.Assert(checkChange(nil, f0), qt.Equals, fsnotify.Create) 183 c.Assert(checkChange(f1, f1_2), qt.Equals, fsnotify.Chmod) 184 c.Assert(checkChange(f2, f2_2), qt.Equals, fsnotify.Write) 185 c.Assert(checkChange(nil, nil), qt.Equals, fsnotify.Op(0)) 186 c.Assert(checkChange(d1, f1), qt.Equals, fsnotify.Op(0)) 187 c.Assert(checkChange(f1, d1), qt.Equals, fsnotify.Op(0)) 188 } 189 190 func BenchmarkPoller(b *testing.B) { 191 runBench := func(b *testing.B, item *itemToWatch) { 192 b.ResetTimer() 193 for i := 0; i < b.N; i++ { 194 evs, err := item.checkForChanges() 195 if err != nil { 196 b.Fatal(err) 197 } 198 if len(evs) != 0 { 199 b.Fatal("got events") 200 } 201 202 } 203 204 } 205 206 b.Run("Check for changes in dir", func(b *testing.B) { 207 c := qt.New(b) 208 dir := prepareTestDirWithSomeFiles(c, "bench-check") 209 item, err := newItemToWatch(dir) 210 c.Assert(err, qt.IsNil) 211 runBench(b, item) 212 213 }) 214 215 b.Run("Check for changes in file", func(b *testing.B) { 216 c := qt.New(b) 217 dir := prepareTestDirWithSomeFiles(c, "bench-check-file") 218 filename := filepath.Join(dir, subdir1, "file1") 219 item, err := newItemToWatch(filename) 220 c.Assert(err, qt.IsNil) 221 runBench(b, item) 222 }) 223 224 } 225 226 func prepareTestDirWithSomeFiles(c *qt.C, id string) string { 227 dir := c.TB.TempDir() 228 c.Assert(os.MkdirAll(filepath.Join(dir, subdir1), 0777), qt.IsNil) 229 c.Assert(os.MkdirAll(filepath.Join(dir, subdir2), 0777), qt.IsNil) 230 231 for i := 0; i < 3; i++ { 232 c.Assert(os.WriteFile(filepath.Join(dir, subdir1, fmt.Sprintf("file%d", i)), []byte("hello1"), 0600), qt.IsNil) 233 } 234 235 for i := 0; i < 3; i++ { 236 c.Assert(os.WriteFile(filepath.Join(dir, subdir2, fmt.Sprintf("file%d", i)), []byte("hello2"), 0600), qt.IsNil) 237 } 238 239 c.Cleanup(func() { 240 os.RemoveAll(dir) 241 }) 242 243 return dir 244 } 245 246 func preparePollTest(c *qt.C, poll bool) (string, FileWatcher) { 247 var w FileWatcher 248 if poll { 249 w = NewPollingWatcher(watchWaitTime) 250 } else { 251 var err error 252 w, err = NewEventWatcher() 253 c.Assert(err, qt.IsNil) 254 } 255 256 dir := prepareTestDirWithSomeFiles(c, fmt.Sprint(poll)) 257 258 c.Cleanup(func() { 259 w.Close() 260 }) 261 return dir, w 262 } 263 264 func assertEvents(c *qt.C, w FileWatcher, evs ...fsnotify.Event) { 265 c.Helper() 266 i := 0 267 check := func() error { 268 for { 269 select { 270 case got := <-w.Events(): 271 if i > len(evs)-1 { 272 return fmt.Errorf("got too many event(s): %q", got) 273 } 274 expected := evs[i] 275 i++ 276 if expected.Name != got.Name { 277 return fmt.Errorf("got wrong filename, expected %q: %v", expected.Name, got.Name) 278 } else if got.Op&expected.Op != expected.Op { 279 return fmt.Errorf("got wrong event type, expected %q: %v", expected.Op, got.Op) 280 } 281 case e := <-w.Errors(): 282 return fmt.Errorf("got unexpected error waiting for events %v", e) 283 case <-time.After(watchWaitTime + (watchWaitTime / 2)): 284 return nil 285 } 286 } 287 } 288 c.Assert(check(), qt.IsNil) 289 c.Assert(i, qt.Equals, len(evs)) 290 } 291 292 func drainEvents(c *qt.C, w FileWatcher) { 293 c.Helper() 294 check := func() error { 295 for { 296 select { 297 case <-w.Events(): 298 case e := <-w.Errors(): 299 return fmt.Errorf("got unexpected error waiting for events %v", e) 300 case <-time.After(watchWaitTime * 2): 301 return nil 302 } 303 } 304 } 305 c.Assert(check(), qt.IsNil) 306 }