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