gitlab.com/xtgo/livefile@v0.0.2/watcher_test.go (about) 1 package livefile_test 2 3 import ( 4 "fmt" 5 "io/fs" 6 "os" 7 "strings" 8 "sync" 9 "testing" 10 "time" 11 12 "gitlab.com/xtgo/livefile" 13 ) 14 15 func TestWatcher(t *testing.T) { 16 cwd, err := os.Getwd() 17 if err != nil { 18 t.Skip("unable to find current directory:", err) 19 } 20 21 t.Cleanup(func() { os.Chdir(cwd) }) 22 23 tests := []struct { 24 name string 25 backup bool 26 restore bool 27 callOnWatch bool 28 steps []operation 29 }{ 30 { 31 name: "watch_existing_file", 32 callOnWatch: true, 33 steps: []operation{ 34 update("filename", 0600, "filedata"), 35 watch("filename", func(string) bool { return true }), 36 assertCallbackSuccess("filename"), 37 assertRegular("filename", 0600, "filedata"), 38 }, 39 }, 40 { 41 name: "watch_nonexistant_file", 42 steps: []operation{ 43 watch("filename", func(string) bool { return true }), 44 update("filename", 0600, "filedata"), 45 assertCallbackSuccess("filename"), 46 assertRegular("filename", 0600, "filedata"), 47 }, 48 }, 49 { 50 name: "watch_existing_file_backup", 51 backup: true, 52 callOnWatch: true, 53 steps: []operation{ 54 update("filename", 0600, "filedata"), 55 watch("filename", func(string) bool { return true }), 56 assertCallbackSuccess("filename"), 57 assertRegular("filename", 0600, "filedata"), 58 assertRegular("filename~", 0600, "filedata"), 59 }, 60 }, 61 { 62 name: "watch_nonexistant_file_backup", 63 backup: true, 64 callOnWatch: true, 65 steps: []operation{ 66 watch("filename", func(string) bool { return true }), 67 assertCallbackSuccess("filename"), 68 assertMissing("filename"), 69 assertMissing("filename~"), 70 }, 71 }, 72 } 73 74 for _, tt := range tests { 75 t.Run(tt.name, func(t *testing.T) { 76 dir := t.TempDir() 77 78 err := os.Chdir(dir) 79 if err != nil { 80 t.Skip("unable to change directory:", err) 81 } 82 83 w := livefile.Watcher{ 84 Backup: tt.backup, 85 Restore: tt.restore, 86 CallOnWatch: tt.callOnWatch, 87 88 OnError: func(err error) { 89 t.Fatal("async error:", err) 90 }, 91 } 92 93 defer w.Close() 94 95 tc := testContext{ 96 watcher: &w, 97 events: make(map[string]chan bool), 98 } 99 100 for _, step := range tt.steps { 101 err := step(&tc) 102 if err != nil { 103 t.Fatal(err) 104 } 105 } 106 }) 107 } 108 } 109 110 type testContext struct { 111 watcher *livefile.Watcher 112 mu sync.RWMutex 113 events map[string]chan bool 114 } 115 116 type operation func(tc *testContext) error 117 118 func assertCallbackSuccess(path string) operation { 119 return assertCallback(path, true) 120 } 121 122 func assertCallbackFailure(path string) operation { 123 return assertCallback(path, false) 124 } 125 126 func assertCallback(path string, want bool) operation { 127 return func(tc *testContext) error { 128 const timeout = 10 * time.Second 129 130 tc.mu.RLock() 131 ch := tc.events[path] 132 tc.mu.RUnlock() 133 134 select { 135 case <-time.After(timeout): 136 return fmt.Errorf("timeout exceeded waiting for inotify event") 137 138 case got := <-ch: 139 if got == want { 140 return nil 141 } 142 143 msg := "callback on %q returned %v, want %v" 144 145 return fmt.Errorf(msg, path, got, want) 146 } 147 } 148 } 149 150 func assertMissing(path string) operation { 151 return func(tc *testContext) error { 152 _, err := os.Lstat(path) 153 if err == nil { 154 return os.ErrExist 155 } 156 157 if os.IsNotExist(err) { 158 return nil 159 } 160 161 return err 162 } 163 } 164 165 func assertRegular(path string, perm fs.FileMode, data string) operation { 166 return func(tc *testContext) error { 167 const ( 168 timeout = 1500 * time.Millisecond 169 interval = 25 * time.Millisecond 170 ) 171 172 deadline := time.Now().Add(timeout) 173 174 var info os.FileInfo 175 176 for { 177 var err error 178 179 info, err = os.Lstat(path) 180 if err == nil { 181 break 182 } 183 184 if !os.IsNotExist(err) || time.Now().After(deadline) { 185 return err 186 } 187 188 time.Sleep(interval) 189 } 190 191 mode := info.Mode() 192 if !mode.IsRegular() { 193 return fmt.Errorf("%s is not a regular file", path) 194 } 195 196 mode = mode.Perm() 197 if mode != perm { 198 return fmt.Errorf("%s has perm %v, want %v", path, mode, perm) 199 } 200 201 buf, err := os.ReadFile(path) 202 if err != nil { 203 return err 204 } 205 206 if string(buf) != data { 207 return fmt.Errorf("%s has content %#q, want %#q", path, buf, data) 208 } 209 210 return nil 211 } 212 } 213 214 func watch(path string, callback livefile.Callback) operation { 215 return func(tc *testContext) error { 216 tc.mu.Lock() 217 defer tc.mu.Unlock() 218 219 ch := tc.events[path] 220 if ch == nil { 221 ch = make(chan bool) 222 tc.events[path] = ch 223 } 224 225 return tc.watcher.Watch(path, 226 func(key string) bool { 227 ok := callback(key) 228 ch <- ok 229 230 return ok 231 }) 232 } 233 } 234 235 func mkdir(path string) operation { 236 return func(*testContext) error { 237 return os.Mkdir(path, 0700) 238 } 239 } 240 241 func rename(src, dst string) operation { 242 return func(*testContext) error { 243 return os.Rename(src, dst) 244 } 245 } 246 247 func update(path string, mode fs.FileMode, data string) operation { 248 return func(*testContext) error { 249 return livefile.Update(path, mode, strings.NewReader(data)) 250 } 251 } 252 253 func symlink(src, dst string) operation { 254 return func(tc *testContext) error { 255 return livefile.Symlink(src, dst) 256 } 257 } 258 259 func unlink(path string) operation { 260 return func(tc *testContext) error { 261 return livefile.Remove(path) 262 } 263 }