notify_test.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657
  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. "bytes"
  16. "context"
  17. "fmt"
  18. "os"
  19. "path/filepath"
  20. "runtime"
  21. "strings"
  22. "testing"
  23. "time"
  24. "github.com/stretchr/testify/assert"
  25. "github.com/stretchr/testify/require"
  26. )
  27. // Each implementation of the notify interface should have the same basic
  28. // behavior.
  29. func TestWindowsBufferSize(t *testing.T) {
  30. orig := os.Getenv(WindowsBufferSizeEnvVar)
  31. defer os.Setenv(WindowsBufferSizeEnvVar, orig) //nolint:errcheck
  32. err := os.Setenv(WindowsBufferSizeEnvVar, "")
  33. require.NoError(t, err)
  34. assert.Equal(t, defaultBufferSize, DesiredWindowsBufferSize())
  35. err = os.Setenv(WindowsBufferSizeEnvVar, "a")
  36. require.NoError(t, err)
  37. assert.Equal(t, defaultBufferSize, DesiredWindowsBufferSize())
  38. err = os.Setenv(WindowsBufferSizeEnvVar, "10")
  39. require.NoError(t, err)
  40. assert.Equal(t, 10, DesiredWindowsBufferSize())
  41. }
  42. func TestNoEvents(t *testing.T) {
  43. f := newNotifyFixture(t)
  44. f.assertEvents()
  45. }
  46. func TestNoWatches(t *testing.T) {
  47. f := newNotifyFixture(t)
  48. f.paths = nil
  49. f.rebuildWatcher()
  50. f.assertEvents()
  51. }
  52. func TestEventOrdering(t *testing.T) {
  53. if runtime.GOOS == "windows" {
  54. // https://qualapps.blogspot.com/2010/05/understanding-readdirectorychangesw_19.html
  55. t.Skip("Windows doesn't make great guarantees about duplicate/out-of-order events")
  56. return
  57. }
  58. f := newNotifyFixture(t)
  59. count := 8
  60. dirs := make([]string, count)
  61. for i := range dirs {
  62. dir := f.TempDir("watched")
  63. dirs[i] = dir
  64. f.watch(dir)
  65. }
  66. f.fsync()
  67. f.events = nil
  68. var expected []string
  69. for i, dir := range dirs {
  70. base := fmt.Sprintf("%d.txt", i)
  71. p := filepath.Join(dir, base)
  72. err := os.WriteFile(p, []byte(base), os.FileMode(0o777))
  73. if err != nil {
  74. t.Fatal(err)
  75. }
  76. expected = append(expected, filepath.Join(dir, base))
  77. }
  78. f.assertEvents(expected...)
  79. }
  80. // Simulate a git branch switch that creates a bunch
  81. // of directories, creates files in them, then deletes
  82. // them all quickly. Make sure there are no errors.
  83. func TestGitBranchSwitch(t *testing.T) {
  84. f := newNotifyFixture(t)
  85. count := 10
  86. dirs := make([]string, count)
  87. for i := range dirs {
  88. dir := f.TempDir("watched")
  89. dirs[i] = dir
  90. f.watch(dir)
  91. }
  92. f.fsync()
  93. f.events = nil
  94. // consume all the events in the background
  95. ctx, cancel := context.WithCancel(context.Background())
  96. done := f.consumeEventsInBackground(ctx)
  97. for i, dir := range dirs {
  98. for j := 0; j < count; j++ {
  99. base := fmt.Sprintf("x/y/dir-%d/x.txt", j)
  100. p := filepath.Join(dir, base)
  101. f.WriteFile(p, "contents")
  102. }
  103. if i != 0 {
  104. err := os.RemoveAll(dir)
  105. require.NoError(t, err)
  106. }
  107. }
  108. cancel()
  109. err := <-done
  110. if err != nil {
  111. t.Fatal(err)
  112. }
  113. f.fsync()
  114. f.events = nil
  115. // Make sure the watch on the first dir still works.
  116. dir := dirs[0]
  117. path := filepath.Join(dir, "change")
  118. f.WriteFile(path, "hello\n")
  119. f.fsync()
  120. f.assertEvents(path)
  121. // Make sure there are no errors in the out stream
  122. assert.Empty(t, f.out.String())
  123. }
  124. func TestWatchesAreRecursive(t *testing.T) {
  125. f := newNotifyFixture(t)
  126. root := f.TempDir("root")
  127. // add a sub directory
  128. subPath := filepath.Join(root, "sub")
  129. f.MkdirAll(subPath)
  130. // watch parent
  131. f.watch(root)
  132. f.fsync()
  133. f.events = nil
  134. // change sub directory
  135. changeFilePath := filepath.Join(subPath, "change")
  136. f.WriteFile(changeFilePath, "change")
  137. f.assertEvents(changeFilePath)
  138. }
  139. func TestNewDirectoriesAreRecursivelyWatched(t *testing.T) {
  140. f := newNotifyFixture(t)
  141. root := f.TempDir("root")
  142. // watch parent
  143. f.watch(root)
  144. f.fsync()
  145. f.events = nil
  146. // add a sub directory
  147. subPath := filepath.Join(root, "sub")
  148. f.MkdirAll(subPath)
  149. // change something inside sub directory
  150. changeFilePath := filepath.Join(subPath, "change")
  151. file, err := os.OpenFile(changeFilePath, os.O_RDONLY|os.O_CREATE, 0o666)
  152. if err != nil {
  153. t.Fatal(err)
  154. }
  155. _ = file.Close()
  156. f.assertEvents(subPath, changeFilePath)
  157. }
  158. func TestWatchNonExistentPath(t *testing.T) {
  159. f := newNotifyFixture(t)
  160. root := f.TempDir("root")
  161. path := filepath.Join(root, "change")
  162. f.watch(path)
  163. f.fsync()
  164. d1 := "hello\ngo\n"
  165. f.WriteFile(path, d1)
  166. f.assertEvents(path)
  167. }
  168. func TestWatchNonExistentPathDoesNotFireSiblingEvent(t *testing.T) {
  169. f := newNotifyFixture(t)
  170. root := f.TempDir("root")
  171. watchedFile := filepath.Join(root, "a.txt")
  172. unwatchedSibling := filepath.Join(root, "b.txt")
  173. f.watch(watchedFile)
  174. f.fsync()
  175. d1 := "hello\ngo\n"
  176. f.WriteFile(unwatchedSibling, d1)
  177. f.assertEvents()
  178. }
  179. func TestRemove(t *testing.T) {
  180. f := newNotifyFixture(t)
  181. root := f.TempDir("root")
  182. path := filepath.Join(root, "change")
  183. d1 := "hello\ngo\n"
  184. f.WriteFile(path, d1)
  185. f.watch(path)
  186. f.fsync()
  187. f.events = nil
  188. err := os.Remove(path)
  189. if err != nil {
  190. t.Fatal(err)
  191. }
  192. f.assertEvents(path)
  193. }
  194. func TestRemoveAndAddBack(t *testing.T) {
  195. f := newNotifyFixture(t)
  196. path := filepath.Join(f.paths[0], "change")
  197. d1 := []byte("hello\ngo\n")
  198. err := os.WriteFile(path, d1, 0o644)
  199. if err != nil {
  200. t.Fatal(err)
  201. }
  202. f.watch(path)
  203. f.assertEvents(path)
  204. err = os.Remove(path)
  205. if err != nil {
  206. t.Fatal(err)
  207. }
  208. f.assertEvents(path)
  209. f.events = nil
  210. err = os.WriteFile(path, d1, 0o644)
  211. if err != nil {
  212. t.Fatal(err)
  213. }
  214. f.assertEvents(path)
  215. }
  216. func TestSingleFile(t *testing.T) {
  217. f := newNotifyFixture(t)
  218. root := f.TempDir("root")
  219. path := filepath.Join(root, "change")
  220. d1 := "hello\ngo\n"
  221. f.WriteFile(path, d1)
  222. f.watch(path)
  223. f.fsync()
  224. d2 := []byte("hello\nworld\n")
  225. err := os.WriteFile(path, d2, 0o644)
  226. if err != nil {
  227. t.Fatal(err)
  228. }
  229. f.assertEvents(path)
  230. }
  231. func TestWriteBrokenLink(t *testing.T) {
  232. if runtime.GOOS == "windows" {
  233. t.Skip("no user-space symlinks on windows")
  234. }
  235. f := newNotifyFixture(t)
  236. link := filepath.Join(f.paths[0], "brokenLink")
  237. missingFile := filepath.Join(f.paths[0], "missingFile")
  238. err := os.Symlink(missingFile, link)
  239. if err != nil {
  240. t.Fatal(err)
  241. }
  242. f.assertEvents(link)
  243. }
  244. func TestWriteGoodLink(t *testing.T) {
  245. if runtime.GOOS == "windows" {
  246. t.Skip("no user-space symlinks on windows")
  247. }
  248. f := newNotifyFixture(t)
  249. goodFile := filepath.Join(f.paths[0], "goodFile")
  250. err := os.WriteFile(goodFile, []byte("hello"), 0o644)
  251. if err != nil {
  252. t.Fatal(err)
  253. }
  254. link := filepath.Join(f.paths[0], "goodFileSymlink")
  255. err = os.Symlink(goodFile, link)
  256. if err != nil {
  257. t.Fatal(err)
  258. }
  259. f.assertEvents(goodFile, link)
  260. }
  261. func TestWatchBrokenLink(t *testing.T) {
  262. if runtime.GOOS == "windows" {
  263. t.Skip("no user-space symlinks on windows")
  264. }
  265. f := newNotifyFixture(t)
  266. newRoot, err := NewDir(t.Name())
  267. if err != nil {
  268. t.Fatal(err)
  269. }
  270. defer func() {
  271. err := newRoot.TearDown()
  272. if err != nil {
  273. fmt.Printf("error tearing down temp dir: %v\n", err)
  274. }
  275. }()
  276. link := filepath.Join(newRoot.Path(), "brokenLink")
  277. missingFile := filepath.Join(newRoot.Path(), "missingFile")
  278. err = os.Symlink(missingFile, link)
  279. if err != nil {
  280. t.Fatal(err)
  281. }
  282. f.watch(newRoot.Path())
  283. err = os.Remove(link)
  284. require.NoError(t, err)
  285. f.assertEvents(link)
  286. }
  287. func TestMoveAndReplace(t *testing.T) {
  288. f := newNotifyFixture(t)
  289. root := f.TempDir("root")
  290. file := filepath.Join(root, "myfile")
  291. f.WriteFile(file, "hello")
  292. f.watch(file)
  293. tmpFile := filepath.Join(root, ".myfile.swp")
  294. f.WriteFile(tmpFile, "world")
  295. err := os.Rename(tmpFile, file)
  296. if err != nil {
  297. t.Fatal(err)
  298. }
  299. f.assertEvents(file)
  300. }
  301. func TestWatchBothDirAndFile(t *testing.T) {
  302. f := newNotifyFixture(t)
  303. dir := f.JoinPath("foo")
  304. fileA := f.JoinPath("foo", "a")
  305. fileB := f.JoinPath("foo", "b")
  306. f.WriteFile(fileA, "a")
  307. f.WriteFile(fileB, "b")
  308. f.watch(fileA)
  309. f.watch(dir)
  310. f.fsync()
  311. f.events = nil
  312. f.WriteFile(fileB, "b-new")
  313. f.assertEvents(fileB)
  314. }
  315. func TestWatchNonexistentFileInNonexistentDirectoryCreatedSimultaneously(t *testing.T) {
  316. f := newNotifyFixture(t)
  317. root := f.JoinPath("root")
  318. err := os.Mkdir(root, 0o777)
  319. if err != nil {
  320. t.Fatal(err)
  321. }
  322. file := f.JoinPath("root", "parent", "a")
  323. f.watch(file)
  324. f.fsync()
  325. f.events = nil
  326. f.WriteFile(file, "hello")
  327. f.assertEvents(file)
  328. }
  329. func TestWatchNonexistentDirectory(t *testing.T) {
  330. f := newNotifyFixture(t)
  331. root := f.JoinPath("root")
  332. err := os.Mkdir(root, 0o777)
  333. if err != nil {
  334. t.Fatal(err)
  335. }
  336. parent := f.JoinPath("parent")
  337. file := f.JoinPath("parent", "a")
  338. f.watch(parent)
  339. f.fsync()
  340. f.events = nil
  341. err = os.Mkdir(parent, 0o777)
  342. if err != nil {
  343. t.Fatal(err)
  344. }
  345. // for directories that were the root of an Add, we don't report creation, cf. watcher_darwin.go
  346. f.assertEvents()
  347. f.events = nil
  348. f.WriteFile(file, "hello")
  349. f.assertEvents(file)
  350. }
  351. func TestWatchNonexistentFileInNonexistentDirectory(t *testing.T) {
  352. f := newNotifyFixture(t)
  353. root := f.JoinPath("root")
  354. err := os.Mkdir(root, 0o777)
  355. if err != nil {
  356. t.Fatal(err)
  357. }
  358. parent := f.JoinPath("parent")
  359. file := f.JoinPath("parent", "a")
  360. f.watch(file)
  361. f.assertEvents()
  362. err = os.Mkdir(parent, 0o777)
  363. if err != nil {
  364. t.Fatal(err)
  365. }
  366. f.assertEvents()
  367. f.WriteFile(file, "hello")
  368. f.assertEvents(file)
  369. }
  370. func TestWatchCountInnerFile(t *testing.T) {
  371. f := newNotifyFixture(t)
  372. root := f.paths[0]
  373. a := f.JoinPath(root, "a")
  374. b := f.JoinPath(a, "b")
  375. file := f.JoinPath(b, "bigFile")
  376. f.WriteFile(file, "hello")
  377. f.assertEvents(a, b, file)
  378. expectedWatches := 3
  379. if isRecursiveWatcher() {
  380. expectedWatches = 1
  381. }
  382. assert.Equal(t, expectedWatches, int(numberOfWatches.Value()))
  383. }
  384. func isRecursiveWatcher() bool {
  385. return runtime.GOOS == "darwin" || runtime.GOOS == "windows"
  386. }
  387. type notifyFixture struct {
  388. ctx context.Context
  389. cancel func()
  390. out *bytes.Buffer
  391. *TempDirFixture
  392. notify Notify
  393. paths []string
  394. events []FileEvent
  395. }
  396. func newNotifyFixture(t *testing.T) *notifyFixture {
  397. out := bytes.NewBuffer(nil)
  398. ctx, cancel := context.WithCancel(context.Background())
  399. nf := &notifyFixture{
  400. ctx: ctx,
  401. cancel: cancel,
  402. TempDirFixture: NewTempDirFixture(t),
  403. paths: []string{},
  404. out: out,
  405. }
  406. nf.watch(nf.TempDir("watched"))
  407. t.Cleanup(nf.tearDown)
  408. return nf
  409. }
  410. func (f *notifyFixture) watch(path string) {
  411. f.paths = append(f.paths, path)
  412. f.rebuildWatcher()
  413. }
  414. func (f *notifyFixture) rebuildWatcher() {
  415. // sync any outstanding events and close the old watcher
  416. if f.notify != nil {
  417. f.fsync()
  418. f.closeWatcher()
  419. }
  420. // create a new watcher
  421. notify, err := NewWatcher(f.paths)
  422. if err != nil {
  423. f.T().Fatal(err)
  424. }
  425. f.notify = notify
  426. err = f.notify.Start()
  427. if err != nil {
  428. f.T().Fatal(err)
  429. }
  430. }
  431. func (f *notifyFixture) assertEvents(expected ...string) {
  432. f.fsync()
  433. if runtime.GOOS == "windows" {
  434. // NOTE(nick): It's unclear to me why an extra fsync() helps
  435. // here, but it makes the I/O way more predictable.
  436. f.fsync()
  437. }
  438. if len(f.events) != len(expected) {
  439. f.T().Fatalf("Got %d events (expected %d): %v %v", len(f.events), len(expected), f.events, expected)
  440. }
  441. for i, actual := range f.events {
  442. e := FileEvent(expected[i])
  443. if actual != e {
  444. f.T().Fatalf("Got event %v (expected %v)", actual, e)
  445. }
  446. }
  447. }
  448. func (f *notifyFixture) consumeEventsInBackground(ctx context.Context) chan error {
  449. done := make(chan error)
  450. go func() {
  451. for {
  452. select {
  453. case <-f.ctx.Done():
  454. close(done)
  455. return
  456. case <-ctx.Done():
  457. close(done)
  458. return
  459. case err := <-f.notify.Errors():
  460. done <- err
  461. close(done)
  462. return
  463. case <-f.notify.Events():
  464. }
  465. }
  466. }()
  467. return done
  468. }
  469. func (f *notifyFixture) fsync() {
  470. f.fsyncWithRetryCount(3)
  471. }
  472. func (f *notifyFixture) fsyncWithRetryCount(retryCount int) {
  473. if len(f.paths) == 0 {
  474. return
  475. }
  476. syncPathBase := fmt.Sprintf("sync-%d.txt", time.Now().UnixNano())
  477. syncPath := filepath.Join(f.paths[0], syncPathBase)
  478. anySyncPath := filepath.Join(f.paths[0], "sync-")
  479. timeout := time.After(250 * time.Second)
  480. f.WriteFile(syncPath, time.Now().String())
  481. F:
  482. for {
  483. select {
  484. case <-f.ctx.Done():
  485. return
  486. case err := <-f.notify.Errors():
  487. f.T().Fatal(err)
  488. case event := <-f.notify.Events():
  489. if strings.Contains(string(event), syncPath) {
  490. break F
  491. }
  492. if strings.Contains(string(event), anySyncPath) {
  493. continue
  494. }
  495. // Don't bother tracking duplicate changes to the same path
  496. // for testing.
  497. if len(f.events) > 0 && f.events[len(f.events)-1] == event {
  498. continue
  499. }
  500. f.events = append(f.events, event)
  501. case <-timeout:
  502. if retryCount <= 0 {
  503. f.T().Fatalf("fsync: timeout")
  504. } else {
  505. f.fsyncWithRetryCount(retryCount - 1)
  506. }
  507. return
  508. }
  509. }
  510. }
  511. func (f *notifyFixture) closeWatcher() {
  512. notify := f.notify
  513. err := notify.Close()
  514. if err != nil {
  515. f.T().Fatal(err)
  516. }
  517. // drain channels from watcher
  518. go func() {
  519. for range notify.Events() {
  520. }
  521. }()
  522. go func() {
  523. for range notify.Errors() {
  524. }
  525. }()
  526. }
  527. func (f *notifyFixture) tearDown() {
  528. f.cancel()
  529. f.closeWatcher()
  530. numberOfWatches.Set(0)
  531. }