notify_test.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754
  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. assert.Nil(t, err)
  34. assert.Equal(t, defaultBufferSize, DesiredWindowsBufferSize())
  35. err = os.Setenv(WindowsBufferSizeEnvVar, "a")
  36. assert.Nil(t, err)
  37. assert.Equal(t, defaultBufferSize, DesiredWindowsBufferSize())
  38. err = os.Setenv(WindowsBufferSizeEnvVar, "10")
  39. assert.Nil(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.Equal(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 TestWatchCountInnerFileWithIgnore(t *testing.T) {
  385. f := newNotifyFixture(t)
  386. root := f.paths[0]
  387. ignore, _ := NewDockerPatternMatcher(root, []string{
  388. "a",
  389. "!a/b",
  390. })
  391. f.setIgnore(ignore)
  392. a := f.JoinPath(root, "a")
  393. b := f.JoinPath(a, "b")
  394. file := f.JoinPath(b, "bigFile")
  395. f.WriteFile(file, "hello")
  396. f.assertEvents(b, file)
  397. expectedWatches := 3
  398. if isRecursiveWatcher() {
  399. expectedWatches = 1
  400. }
  401. assert.Equal(t, expectedWatches, int(numberOfWatches.Value()))
  402. }
  403. func TestIgnoreCreatedDir(t *testing.T) {
  404. f := newNotifyFixture(t)
  405. root := f.paths[0]
  406. ignore, _ := NewDockerPatternMatcher(root, []string{"a/b"})
  407. f.setIgnore(ignore)
  408. a := f.JoinPath(root, "a")
  409. b := f.JoinPath(a, "b")
  410. file := f.JoinPath(b, "bigFile")
  411. f.WriteFile(file, "hello")
  412. f.assertEvents(a)
  413. expectedWatches := 2
  414. if isRecursiveWatcher() {
  415. expectedWatches = 1
  416. }
  417. assert.Equal(t, expectedWatches, int(numberOfWatches.Value()))
  418. }
  419. func TestIgnoreCreatedDirWithExclusions(t *testing.T) {
  420. f := newNotifyFixture(t)
  421. root := f.paths[0]
  422. ignore, _ := NewDockerPatternMatcher(root,
  423. []string{
  424. "a/b",
  425. "c",
  426. "!c/d",
  427. })
  428. f.setIgnore(ignore)
  429. a := f.JoinPath(root, "a")
  430. b := f.JoinPath(a, "b")
  431. file := f.JoinPath(b, "bigFile")
  432. f.WriteFile(file, "hello")
  433. f.assertEvents(a)
  434. expectedWatches := 2
  435. if isRecursiveWatcher() {
  436. expectedWatches = 1
  437. }
  438. assert.Equal(t, expectedWatches, int(numberOfWatches.Value()))
  439. }
  440. func TestIgnoreInitialDir(t *testing.T) {
  441. f := newNotifyFixture(t)
  442. root := f.TempDir("root")
  443. ignore, _ := NewDockerPatternMatcher(root, []string{"a/b"})
  444. f.setIgnore(ignore)
  445. a := f.JoinPath(root, "a")
  446. b := f.JoinPath(a, "b")
  447. file := f.JoinPath(b, "bigFile")
  448. f.WriteFile(file, "hello")
  449. f.watch(root)
  450. f.assertEvents()
  451. expectedWatches := 3
  452. if isRecursiveWatcher() {
  453. expectedWatches = 2
  454. }
  455. assert.Equal(t, expectedWatches, int(numberOfWatches.Value()))
  456. }
  457. func isRecursiveWatcher() bool {
  458. return runtime.GOOS == "darwin" || runtime.GOOS == "windows"
  459. }
  460. type notifyFixture struct {
  461. ctx context.Context
  462. cancel func()
  463. out *bytes.Buffer
  464. *TempDirFixture
  465. notify Notify
  466. ignore PathMatcher
  467. paths []string
  468. events []FileEvent
  469. }
  470. func newNotifyFixture(t *testing.T) *notifyFixture {
  471. out := bytes.NewBuffer(nil)
  472. ctx, cancel := context.WithCancel(context.Background())
  473. nf := &notifyFixture{
  474. ctx: ctx,
  475. cancel: cancel,
  476. TempDirFixture: NewTempDirFixture(t),
  477. paths: []string{},
  478. ignore: EmptyMatcher{},
  479. out: out,
  480. }
  481. nf.watch(nf.TempDir("watched"))
  482. t.Cleanup(nf.tearDown)
  483. return nf
  484. }
  485. func (f *notifyFixture) setIgnore(ignore PathMatcher) {
  486. f.ignore = ignore
  487. f.rebuildWatcher()
  488. }
  489. func (f *notifyFixture) watch(path string) {
  490. f.paths = append(f.paths, path)
  491. f.rebuildWatcher()
  492. }
  493. func (f *notifyFixture) rebuildWatcher() {
  494. // sync any outstanding events and close the old watcher
  495. if f.notify != nil {
  496. f.fsync()
  497. f.closeWatcher()
  498. }
  499. // create a new watcher
  500. notify, err := NewWatcher(f.paths, f.ignore)
  501. if err != nil {
  502. f.T().Fatal(err)
  503. }
  504. f.notify = notify
  505. err = f.notify.Start()
  506. if err != nil {
  507. f.T().Fatal(err)
  508. }
  509. }
  510. func (f *notifyFixture) assertEvents(expected ...string) {
  511. f.fsync()
  512. if runtime.GOOS == "windows" {
  513. // NOTE(nick): It's unclear to me why an extra fsync() helps
  514. // here, but it makes the I/O way more predictable.
  515. f.fsync()
  516. }
  517. if len(f.events) != len(expected) {
  518. f.T().Fatalf("Got %d events (expected %d): %v %v", len(f.events), len(expected), f.events, expected)
  519. }
  520. for i, actual := range f.events {
  521. e := FileEvent{expected[i]}
  522. if actual != e {
  523. f.T().Fatalf("Got event %v (expected %v)", actual, e)
  524. }
  525. }
  526. }
  527. func (f *notifyFixture) consumeEventsInBackground(ctx context.Context) chan error {
  528. done := make(chan error)
  529. go func() {
  530. for {
  531. select {
  532. case <-f.ctx.Done():
  533. close(done)
  534. return
  535. case <-ctx.Done():
  536. close(done)
  537. return
  538. case err := <-f.notify.Errors():
  539. done <- err
  540. close(done)
  541. return
  542. case <-f.notify.Events():
  543. }
  544. }
  545. }()
  546. return done
  547. }
  548. func (f *notifyFixture) fsync() {
  549. f.fsyncWithRetryCount(3)
  550. }
  551. func (f *notifyFixture) fsyncWithRetryCount(retryCount int) {
  552. if len(f.paths) == 0 {
  553. return
  554. }
  555. syncPathBase := fmt.Sprintf("sync-%d.txt", time.Now().UnixNano())
  556. syncPath := filepath.Join(f.paths[0], syncPathBase)
  557. anySyncPath := filepath.Join(f.paths[0], "sync-")
  558. timeout := time.After(250 * time.Second)
  559. f.WriteFile(syncPath, time.Now().String())
  560. F:
  561. for {
  562. select {
  563. case <-f.ctx.Done():
  564. return
  565. case err := <-f.notify.Errors():
  566. f.T().Fatal(err)
  567. case event := <-f.notify.Events():
  568. if strings.Contains(event.Path(), syncPath) {
  569. break F
  570. }
  571. if strings.Contains(event.Path(), anySyncPath) {
  572. continue
  573. }
  574. // Don't bother tracking duplicate changes to the same path
  575. // for testing.
  576. if len(f.events) > 0 && f.events[len(f.events)-1].Path() == event.Path() {
  577. continue
  578. }
  579. f.events = append(f.events, event)
  580. case <-timeout:
  581. if retryCount <= 0 {
  582. f.T().Fatalf("fsync: timeout")
  583. } else {
  584. f.fsyncWithRetryCount(retryCount - 1)
  585. }
  586. return
  587. }
  588. }
  589. }
  590. func (f *notifyFixture) closeWatcher() {
  591. notify := f.notify
  592. err := notify.Close()
  593. if err != nil {
  594. f.T().Fatal(err)
  595. }
  596. // drain channels from watcher
  597. go func() {
  598. for range notify.Events() {
  599. }
  600. }()
  601. go func() {
  602. for range notify.Errors() {
  603. }
  604. }()
  605. }
  606. func (f *notifyFixture) tearDown() {
  607. f.cancel()
  608. f.closeWatcher()
  609. numberOfWatches.Set(0)
  610. }