watcher_naive_test.go 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. /*
  2. Copyright 2020 Docker Compose CLI authors
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package watch
  14. import (
  15. "fmt"
  16. "os"
  17. "os/exec"
  18. "path/filepath"
  19. "runtime"
  20. "strconv"
  21. "strings"
  22. "testing"
  23. "github.com/stretchr/testify/require"
  24. )
  25. func TestDontWatchEachFile(t *testing.T) {
  26. if runtime.GOOS != "linux" {
  27. t.Skip("This test uses linux-specific inotify checks")
  28. }
  29. // fsnotify is not recursive, so we need to watch each directory
  30. // you can watch individual files with fsnotify, but that is more prone to exhaust resources
  31. // this test uses a Linux way to get the number of watches to make sure we're watching
  32. // per-directory, not per-file
  33. f := newNotifyFixture(t)
  34. watched := f.TempDir("watched")
  35. // there are a few different cases we want to test for because the code paths are slightly
  36. // different:
  37. // 1) initial: data there before we ever call watch
  38. // 2) inplace: data we create while the watch is happening
  39. // 3) staged: data we create in another directory and then atomically move into place
  40. // initial
  41. f.WriteFile(f.JoinPath(watched, "initial.txt"), "initial data")
  42. initialDir := f.JoinPath(watched, "initial_dir")
  43. if err := os.Mkdir(initialDir, 0o777); err != nil {
  44. t.Fatal(err)
  45. }
  46. for i := 0; i < 100; i++ {
  47. f.WriteFile(f.JoinPath(initialDir, fmt.Sprintf("%d", i)), "initial data")
  48. }
  49. f.watch(watched)
  50. f.fsync()
  51. if len(f.events) != 0 {
  52. t.Fatalf("expected 0 initial events; got %d events: %v", len(f.events), f.events)
  53. }
  54. f.events = nil
  55. // inplace
  56. inplace := f.JoinPath(watched, "inplace")
  57. if err := os.Mkdir(inplace, 0o777); err != nil {
  58. t.Fatal(err)
  59. }
  60. f.WriteFile(f.JoinPath(inplace, "inplace.txt"), "inplace data")
  61. inplaceDir := f.JoinPath(inplace, "inplace_dir")
  62. if err := os.Mkdir(inplaceDir, 0o777); err != nil {
  63. t.Fatal(err)
  64. }
  65. for i := 0; i < 100; i++ {
  66. f.WriteFile(f.JoinPath(inplaceDir, fmt.Sprintf("%d", i)), "inplace data")
  67. }
  68. f.fsync()
  69. if len(f.events) < 100 {
  70. t.Fatalf("expected >100 inplace events; got %d events: %v", len(f.events), f.events)
  71. }
  72. f.events = nil
  73. // staged
  74. staged := f.TempDir("staged")
  75. f.WriteFile(f.JoinPath(staged, "staged.txt"), "staged data")
  76. stagedDir := f.JoinPath(staged, "staged_dir")
  77. if err := os.Mkdir(stagedDir, 0o777); err != nil {
  78. t.Fatal(err)
  79. }
  80. for i := 0; i < 100; i++ {
  81. f.WriteFile(f.JoinPath(stagedDir, fmt.Sprintf("%d", i)), "staged data")
  82. }
  83. if err := os.Rename(staged, f.JoinPath(watched, "staged")); err != nil {
  84. t.Fatal(err)
  85. }
  86. f.fsync()
  87. if len(f.events) < 100 {
  88. t.Fatalf("expected >100 staged events; got %d events: %v", len(f.events), f.events)
  89. }
  90. f.events = nil
  91. n, err := inotifyNodes()
  92. require.NoError(t, err)
  93. if n > 10 {
  94. t.Fatalf("watching more than 10 files: %d", n)
  95. }
  96. }
  97. func inotifyNodes() (int, error) {
  98. pid := os.Getpid()
  99. output, err := exec.Command("/bin/sh", "-c", fmt.Sprintf(
  100. "find /proc/%d/fd -lname anon_inode:inotify -printf '%%hinfo/%%f\n' | xargs cat | grep -c '^inotify'", pid)).Output()
  101. if err != nil {
  102. return 0, fmt.Errorf("error running command to determine number of watched files: %w\n %s", err, output)
  103. }
  104. n, err := strconv.Atoi(strings.TrimSpace(string(output)))
  105. if err != nil {
  106. return 0, fmt.Errorf("couldn't parse number of watched files: %w", err)
  107. }
  108. return n, nil
  109. }
  110. func TestDontRecurseWhenWatchingParentsOfNonExistentFiles(t *testing.T) {
  111. if runtime.GOOS != "linux" {
  112. t.Skip("This test uses linux-specific inotify checks")
  113. }
  114. f := newNotifyFixture(t)
  115. watched := f.TempDir("watched")
  116. f.watch(filepath.Join(watched, ".tiltignore"))
  117. excludedDir := f.JoinPath(watched, "excluded")
  118. for i := 0; i < 10; i++ {
  119. f.WriteFile(f.JoinPath(excludedDir, fmt.Sprintf("%d", i), "data.txt"), "initial data")
  120. }
  121. f.fsync()
  122. n, err := inotifyNodes()
  123. require.NoError(t, err)
  124. if n > 5 {
  125. t.Fatalf("watching more than 5 files: %d", n)
  126. }
  127. }