github.com/cortesi/moddwatch@v0.1.0/watch_test.go (about) 1 package moddwatch 2 3 import ( 4 "fmt" 5 "io/ioutil" 6 "net/url" 7 "os" 8 "path" 9 "path/filepath" 10 "reflect" 11 "runtime" 12 "strings" 13 "testing" 14 "time" 15 16 "github.com/google/go-cmp/cmp" 17 "github.com/rjeczalik/notify" 18 ) 19 20 var alwaysEqual = cmp.Comparer(func(_, _ interface{}) bool { return true }) 21 var cmpOptions = cmp.Options{ 22 cmp.FilterValues( 23 func(x, y interface{}) bool { 24 vx, vy := reflect.ValueOf(x), reflect.ValueOf(y) 25 return (vx.IsValid() && vy.IsValid() && 26 vx.Type() == vy.Type()) && 27 (vx.Kind() == reflect.Slice || vx.Kind() == reflect.Map) && 28 (vx.Len() == 0 && vy.Len() == 0) 29 }, 30 alwaysEqual, 31 ), 32 cmp.FilterPath( 33 func(p cmp.Path) bool { 34 if p.String() == "URL.RawQuery" { 35 return true 36 } 37 return false 38 }, 39 cmp.Comparer(func(a, b interface{}) bool { 40 qa, _ := url.ParseQuery(a.(string)) 41 qb, _ := url.ParseQuery(b.(string)) 42 return cmp.Equal(qa, qb) 43 }), 44 ), 45 } 46 47 // WithTempDir creates a temp directory, changes the current working directory 48 // to it, and returns a function that can be called to clean up. Use it like 49 // this: 50 // defer WithTempDir(t)() 51 func WithTempDir(t *testing.T) func() { 52 cwd, err := os.Getwd() 53 if err != nil { 54 t.Fatalf("TempDir: %v", err) 55 } 56 tmpdir, err := ioutil.TempDir("", "") 57 if err != nil { 58 t.Fatalf("TempDir: %v", err) 59 } 60 err = os.Chdir(tmpdir) 61 if err != nil { 62 t.Fatalf("Chdir: %v", err) 63 } 64 return func() { 65 err := os.Chdir(cwd) 66 if err != nil { 67 t.Fatalf("Chdir: %v", err) 68 } 69 err = os.RemoveAll(tmpdir) 70 if err != nil { 71 t.Fatalf("Removing tmpdir: %s", err) 72 } 73 } 74 } 75 76 type TEventInfo struct { 77 event notify.Event 78 path string 79 } 80 81 func (te TEventInfo) Path() string { 82 return te.path 83 } 84 85 func (te TEventInfo) Event() notify.Event { 86 return te.event 87 } 88 89 func (te TEventInfo) Sys() interface{} { 90 return nil 91 } 92 93 type testExistenceChecker struct { 94 paths map[string]bool 95 } 96 97 func (e *testExistenceChecker) Check(p string) bool { 98 _, ok := e.paths[p] 99 return ok 100 } 101 102 func exists(paths ...string) *testExistenceChecker { 103 et := testExistenceChecker{make(map[string]bool)} 104 for _, p := range paths { 105 et.paths[p] = true 106 } 107 return &et 108 } 109 110 var batchTests = []struct { 111 events []TEventInfo 112 exists *testExistenceChecker 113 expected Mod 114 }{ 115 { 116 []TEventInfo{ 117 TEventInfo{notify.Create, "foo"}, 118 TEventInfo{notify.Create, "bar"}, 119 }, 120 exists("bar", "foo"), 121 Mod{Added: []string{"bar", "foo"}}, 122 }, 123 { 124 []TEventInfo{ 125 TEventInfo{notify.Rename, "foo"}, 126 TEventInfo{notify.Rename, "bar"}, 127 }, 128 exists("foo"), 129 Mod{Added: []string{"foo"}, Deleted: []string{"bar"}}, 130 }, 131 { 132 []TEventInfo{ 133 TEventInfo{notify.Write, "foo"}, 134 }, 135 exists("foo"), 136 Mod{Changed: []string{"foo"}}, 137 }, 138 { 139 []TEventInfo{ 140 TEventInfo{notify.Write, "foo"}, 141 TEventInfo{notify.Remove, "foo"}, 142 }, 143 exists(), 144 Mod{Deleted: []string{"foo"}}, 145 }, 146 { 147 []TEventInfo{ 148 TEventInfo{notify.Remove, "foo"}, 149 }, 150 exists("foo"), 151 Mod{}, 152 }, 153 { 154 []TEventInfo{ 155 TEventInfo{notify.Create, "foo"}, 156 TEventInfo{notify.Create, "bar"}, 157 TEventInfo{notify.Remove, "bar"}, 158 }, 159 exists("bar", "foo"), 160 Mod{Added: []string{"bar", "foo"}}, 161 }, 162 { 163 []TEventInfo{ 164 TEventInfo{notify.Create, "foo"}, 165 }, 166 exists(), 167 Mod{}, 168 }, 169 } 170 171 func TestBatch(t *testing.T) { 172 for i, tst := range batchTests { 173 input := make(chan notify.EventInfo, len(tst.events)) 174 for _, e := range tst.events { 175 input <- e 176 } 177 ret := batch(time.Millisecond*10, MaxLullWait, tst.exists, input) 178 if !reflect.DeepEqual(*ret, tst.expected) { 179 t.Errorf("Test %d: expected\n%#v\ngot\n%#v", i, tst.expected, ret) 180 } 181 } 182 } 183 184 func abs(path string) string { 185 wd, err := os.Getwd() 186 if err != nil { 187 panic("Could not get current working directory") 188 } 189 return filepath.ToSlash(filepath.Join(wd, path)) 190 } 191 192 var isUnderTests = []struct { 193 parent string 194 child string 195 expected bool 196 }{ 197 {"/foo", "/foo/bar", true}, 198 {"/foo", "/foo", true}, 199 {"/foo", "/foobar/bar", false}, 200 } 201 202 func TestIsUnder(t *testing.T) { 203 for i, tst := range isUnderTests { 204 ret := isUnder(tst.parent, tst.child) 205 if ret != tst.expected { 206 t.Errorf("Test %d: expected %#v, got %#v", i, tst.expected, ret) 207 } 208 } 209 } 210 211 func TestMod(t *testing.T) { 212 if !(Mod{}.Empty()) { 213 t.Error("Expected mod to be empty.") 214 } 215 m := Mod{ 216 Added: []string{"add"}, 217 Deleted: []string{"rm"}, 218 Changed: []string{"change"}, 219 } 220 if m.Empty() { 221 t.Error("Expected mod not to be empty") 222 } 223 if !reflect.DeepEqual(m.All(), []string{"add", "change"}) { 224 t.Error("Unexpeced return from Mod.All") 225 } 226 227 m = Mod{ 228 Added: []string{abs("add")}, 229 Deleted: []string{abs("rm")}, 230 Changed: []string{abs("change")}, 231 } 232 if _, err := m.normPaths("."); err != nil { 233 t.Error(err) 234 } 235 } 236 237 func testListBasic(t *testing.T) { 238 var findTests = []struct { 239 include []string 240 exclude []string 241 expected []string 242 }{ 243 { 244 []string{"**"}, 245 []string{}, 246 []string{"a/a.test1", "a/b.test2", "b/a.test1", "b/b.test2", "x", "x.test1"}, 247 }, 248 { 249 []string{"**/*.test1"}, 250 []string{}, 251 []string{"a/a.test1", "b/a.test1", "x.test1"}, 252 }, 253 { 254 []string{"a"}, 255 []string{}, 256 []string{}, 257 }, 258 { 259 []string{"x"}, 260 []string{}, 261 []string{"x"}, 262 }, 263 { 264 []string{"a/a.test1"}, 265 []string{}, 266 []string{"a/a.test1"}, 267 }, 268 { 269 []string{"**"}, 270 []string{"*.test1"}, 271 []string{"a/a.test1", "a/b.test2", "b/a.test1", "b/b.test2", "x"}, 272 }, 273 { 274 []string{"**"}, 275 []string{"a/**"}, 276 []string{"b/a.test1", "b/b.test2", "x", "x.test1"}, 277 }, 278 { 279 []string{"**"}, 280 []string{"a/*"}, 281 []string{"b/a.test1", "b/b.test2", "x", "x.test1"}, 282 }, 283 { 284 []string{"**"}, 285 []string{"**/*.test1", "**/*.test2"}, 286 []string{"x"}, 287 }, 288 } 289 290 defer WithTempDir(t)() 291 paths := []string{ 292 "a/a.test1", 293 "a/b.test2", 294 "b/a.test1", 295 "b/b.test2", 296 "x", 297 "x.test1", 298 } 299 for _, p := range paths { 300 dst := filepath.Join(".", p) 301 err := os.MkdirAll(filepath.Dir(dst), 0777) 302 if err != nil { 303 t.Fatalf("Error creating test dir: %v", err) 304 } 305 err = ioutil.WriteFile(dst, []byte("test"), 0777) 306 if err != nil { 307 t.Fatalf("Error writing test file: %v", err) 308 } 309 } 310 311 for i, tt := range findTests { 312 ret, err := List(".", tt.include, tt.exclude) 313 if err != nil { 314 t.Fatal(err) 315 } 316 expected := tt.expected 317 for i := range ret { 318 ret[i] = filepath.ToSlash(ret[i]) 319 } 320 if !reflect.DeepEqual(ret, expected) { 321 t.Errorf( 322 "%d: %#v, %#v - Expected\n%#v\ngot:\n%#v", 323 i, tt.include, tt.exclude, expected, ret, 324 ) 325 } 326 } 327 } 328 329 func testList(t *testing.T) { 330 var findTests = []struct { 331 include []string 332 exclude []string 333 expected []string 334 }{ 335 { 336 []string{"**"}, 337 []string{}, 338 []string{"a/a.test1", "a/b.test2", "a/sub/c.test2", "b/a.test1", "b/b.test2", "x", "x.test1"}, 339 }, 340 { 341 []string{"**/*.test1"}, 342 []string{}, 343 []string{"a/a.test1", "b/a.test1", "x.test1"}, 344 }, 345 { 346 []string{"**"}, 347 []string{"*.test1"}, 348 []string{"a/a.test1", "a/b.test2", "a/sub/c.test2", "b/a.test1", "b/b.test2", "x"}, 349 }, 350 { 351 []string{"**"}, 352 []string{"a/**"}, 353 []string{"b/a.test1", "b/b.test2", "x", "x.test1"}, 354 }, 355 { 356 []string{"**"}, 357 []string{"a/**"}, 358 []string{"b/a.test1", "b/b.test2", "x", "x.test1"}, 359 }, 360 { 361 []string{"**"}, 362 []string{"**/*.test1", "**/*.test2"}, 363 []string{"x"}, 364 }, 365 { 366 []string{"a/relsymlink"}, 367 []string{}, 368 []string{}, 369 }, 370 { 371 []string{"a/relfilesymlink"}, 372 []string{}, 373 []string{"x"}, 374 }, 375 { 376 []string{"a/relsymlink/**"}, 377 []string{}, 378 []string{"b/a.test1", "b/b.test2"}, 379 }, 380 { 381 []string{"a/**", "a/relsymlink/**"}, 382 []string{}, 383 []string{"a/a.test1", "a/b.test2", "a/sub/c.test2", "b/a.test1", "b/b.test2"}, 384 }, 385 { 386 []string{"a/abssymlink/**"}, 387 []string{}, 388 []string{"b/a.test1", "b/b.test2"}, 389 }, 390 { 391 []string{"a/**", "a/abssymlink/**"}, 392 []string{}, 393 []string{"a/a.test1", "a/b.test2", "a/sub/c.test2", "b/a.test1", "b/b.test2"}, 394 }, 395 } 396 397 defer WithTempDir(t)() 398 paths := []string{ 399 "a/a.test1", 400 "a/b.test2", 401 "a/sub/c.test2", 402 "b/a.test1", 403 "b/b.test2", 404 "x", 405 "x.test1", 406 } 407 for _, p := range paths { 408 dst := path.Join(".", p) 409 err := os.MkdirAll(path.Dir(dst), 0777) 410 if err != nil { 411 t.Fatalf("Error creating test dir: %v", err) 412 } 413 err = ioutil.WriteFile(dst, []byte("test"), 0777) 414 if err != nil { 415 t.Fatalf("Error writing test file: %v", err) 416 } 417 } 418 if err := os.Symlink("../../b", "./a/relsymlink"); err != nil { 419 t.Fatal(err) 420 return 421 } 422 if err := os.Symlink("../../x", "./a/relfilesymlink"); err != nil { 423 t.Fatal(err) 424 return 425 } 426 427 sabs, err := filepath.Abs(filepath.FromSlash("./b")) 428 if err != nil { 429 t.Fatal(err) 430 return 431 } 432 if err = os.Symlink(sabs, "./a/abssymlink"); err != nil { 433 t.Fatal(err) 434 return 435 } 436 437 for i, tt := range findTests { 438 t.Run( 439 fmt.Sprintf("%.3d", i), 440 func(t *testing.T) { 441 ret, err := List(".", tt.include, tt.exclude) 442 if err != nil { 443 t.Fatal(err) 444 } 445 expected := tt.expected 446 for i := range ret { 447 if filepath.IsAbs(ret[i]) { 448 wd, err := os.Getwd() 449 rel, err := filepath.Rel(wd, filepath.ToSlash(ret[i])) 450 if err != nil { 451 t.Fatal(err) 452 return 453 } 454 ret[i] = rel 455 } else { 456 ret[i] = filepath.ToSlash(ret[i]) 457 } 458 } 459 if !reflect.DeepEqual(ret, expected) { 460 t.Errorf( 461 "%d: %#v, %#v - Expected\n%#v\ngot:\n%#v", 462 i, tt.include, tt.exclude, expected, ret, 463 ) 464 } 465 }, 466 ) 467 } 468 } 469 470 func TestList(t *testing.T) { 471 testListBasic(t) 472 if runtime.GOOS != "windows" { 473 testList(t) 474 } 475 } 476 477 const timeout = 2 * time.Second 478 479 func wait(p string) { 480 p = filepath.FromSlash(p) 481 for { 482 _, err := os.Stat(p) 483 if err != nil { 484 continue 485 } else { 486 break 487 } 488 } 489 } 490 491 func touch(p string) { 492 p = filepath.FromSlash(p) 493 d := filepath.Dir(p) 494 err := os.MkdirAll(d, 0777) 495 if err != nil { 496 panic(err) 497 } 498 499 f, err := os.OpenFile(p, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0777) 500 if err != nil { 501 panic(err) 502 } 503 if _, err := f.Write([]byte("teststring")); err != nil { 504 panic(err) 505 } 506 if err := f.Close(); err != nil { 507 panic(err) 508 } 509 ioutil.ReadFile(p) 510 } 511 512 func events(p string) []string { 513 parts := []string{} 514 for _, p := range strings.Split(p, "\n") { 515 if strings.HasPrefix(p, ":") { 516 p = strings.TrimSpace(p) 517 if !strings.HasSuffix(p, ":") { 518 parts = append(parts, strings.TrimSpace(p)) 519 } 520 } 521 } 522 return parts 523 } 524 525 func _testWatch( 526 t *testing.T, 527 modfunc func(), 528 includes []string, 529 excludes []string, 530 expected Mod, 531 ) { 532 defer WithTempDir(t)() 533 534 err := os.MkdirAll("a", 0777) 535 if err != nil { 536 t.Fatal(err) 537 } 538 539 err = os.MkdirAll("b", 0777) 540 if err != nil { 541 t.Fatal(err) 542 } 543 544 ch := make(chan *Mod, 1024) 545 cwd, err := os.Getwd() 546 if err != nil { 547 t.Fatal(err) 548 return 549 } 550 watcher, err := Watch( 551 cwd, 552 includes, 553 excludes, 554 time.Millisecond*200, 555 ch, 556 ) 557 if err != nil { 558 t.Fatal(err) 559 return 560 } 561 defer watcher.Stop() 562 go func() { 563 time.Sleep(2 * time.Second) 564 watcher.Stop() 565 }() 566 567 // There's some race condition in rjeczalik/notify. If we don't wait a bit 568 // here, we sometimes don't receive notifications for the initial event. 569 go func() { 570 touch("a/initial") 571 }() 572 for { 573 evt, more := <-ch 574 if !more { 575 t.Errorf("Never saw initial sync event") 576 return 577 } 578 if cmp.Equal(evt.Added, []string{"a/initial"}) { 579 break 580 } else { 581 t.Errorf("Unexpected initial sync event:\n%#v", evt) 582 return 583 } 584 } 585 586 go modfunc() 587 ret := Mod{} 588 for { 589 evt, more := <-ch 590 if more { 591 ret = ret.Join(*evt) 592 if cmp.Equal(ret, expected, cmpOptions) { 593 watcher.Stop() 594 return 595 } 596 } else { 597 break 598 } 599 } 600 t.Errorf("Never saw expected result, did see\n%s", ret) 601 } 602 603 func TestWatch(t *testing.T) { 604 t.Run( 605 "simple", 606 func(t *testing.T) { 607 _testWatch( 608 t, 609 func() { 610 touch("a/touched") 611 touch("a/initial") 612 }, 613 []string{"**"}, 614 []string{}, 615 Mod{ 616 Added: []string{"a/touched"}, 617 Changed: []string{"a/initial"}, 618 }, 619 ) 620 }, 621 ) 622 t.Run( 623 "direct", 624 func(t *testing.T) { 625 _testWatch( 626 t, 627 func() { 628 touch("a/direct") 629 }, 630 []string{"a/initial", "a/direct"}, 631 []string{}, 632 Mod{ 633 Added: []string{"a/direct"}, 634 }, 635 ) 636 }, 637 ) 638 t.Run( 639 "directprexisting", 640 func(t *testing.T) { 641 _testWatch( 642 t, 643 func() { 644 touch("a/initial") 645 }, 646 []string{"a/initial"}, 647 []string{}, 648 Mod{ 649 Changed: []string{"a/initial"}, 650 }, 651 ) 652 }, 653 ) 654 t.Run( 655 "deepdirect", 656 func(t *testing.T) { 657 // On Linux, We can't currently pick up changes within directories 658 // created after the watch started. See here for more: 659 // 660 // https://github.com/cortesi/modd/issues/44 661 if runtime.GOOS != "linux" { 662 _testWatch( 663 t, 664 func() { 665 touch("a/deep/directory/direct") 666 }, 667 []string{"a/initial", "a/deep/directory/direct"}, 668 []string{}, 669 Mod{ 670 Added: []string{"a/deep/directory/direct"}, 671 }, 672 ) 673 } 674 }, 675 ) 676 }