basicfs_watch_test.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636
  1. // Copyright (C) 2016 The Syncthing Authors.
  2. //
  3. // This Source Code Form is subject to the terms of the Mozilla Public
  4. // License, v. 2.0. If a copy of the MPL was not distributed with this file,
  5. // You can obtain one at http://mozilla.org/MPL/2.0/.
  6. //go:build (!solaris && !darwin) || (solaris && cgo) || (darwin && cgo)
  7. // +build !solaris,!darwin solaris,cgo darwin,cgo
  8. package fs
  9. import (
  10. "context"
  11. "errors"
  12. "fmt"
  13. "os"
  14. "path/filepath"
  15. "runtime"
  16. "strconv"
  17. "strings"
  18. "syscall"
  19. "testing"
  20. "time"
  21. "github.com/syncthing/notify"
  22. )
  23. func TestMain(m *testing.M) {
  24. if err := os.RemoveAll(testDir); err != nil {
  25. panic(err)
  26. }
  27. dir, err := filepath.Abs(".")
  28. if err != nil {
  29. panic("Cannot get absolute path to working dir")
  30. }
  31. dir, err = evalSymlinks(dir)
  32. if err != nil {
  33. panic("Cannot get real path to working dir")
  34. }
  35. testDirAbs = filepath.Join(dir, testDir)
  36. if runtime.GOOS == "windows" {
  37. testDirAbs = longFilenameSupport(testDirAbs)
  38. }
  39. testFs = NewFilesystem(FilesystemTypeBasic, testDirAbs)
  40. backendBuffer = 10
  41. exitCode := m.Run()
  42. backendBuffer = 500
  43. os.RemoveAll(testDir)
  44. os.Exit(exitCode)
  45. }
  46. const (
  47. testDir = "testdata"
  48. failsOnOpenBSD = "Fails on OpenBSD. See https://github.com/rjeczalik/notify/issues/172"
  49. )
  50. var (
  51. testDirAbs string
  52. testFs Filesystem
  53. )
  54. func TestWatchIgnore(t *testing.T) {
  55. if runtime.GOOS == "openbsd" {
  56. t.Skip(failsOnOpenBSD)
  57. }
  58. name := "ignore"
  59. file := "file"
  60. ignored := "ignored"
  61. testCase := func() {
  62. createTestFile(name, file)
  63. createTestFile(name, ignored)
  64. }
  65. expectedEvents := []Event{
  66. {file, NonRemove},
  67. }
  68. allowedEvents := []Event{
  69. {name, NonRemove},
  70. }
  71. testScenario(t, name, testCase, expectedEvents, allowedEvents, fakeMatcher{ignore: filepath.Join(name, ignored), skipIgnoredDirs: true}, false)
  72. }
  73. func TestWatchInclude(t *testing.T) {
  74. if runtime.GOOS == "openbsd" {
  75. t.Skip(failsOnOpenBSD)
  76. }
  77. name := "include"
  78. file := "file"
  79. ignored := "ignored"
  80. testFs.MkdirAll(filepath.Join(name, ignored), 0777)
  81. included := filepath.Join(ignored, "included")
  82. testCase := func() {
  83. createTestFile(name, file)
  84. createTestFile(name, included)
  85. }
  86. expectedEvents := []Event{
  87. {file, NonRemove},
  88. {included, NonRemove},
  89. }
  90. allowedEvents := []Event{
  91. {name, NonRemove},
  92. }
  93. testScenario(t, name, testCase, expectedEvents, allowedEvents, fakeMatcher{ignore: filepath.Join(name, ignored), include: filepath.Join(name, included)}, false)
  94. }
  95. func TestWatchRename(t *testing.T) {
  96. if runtime.GOOS == "openbsd" {
  97. t.Skip(failsOnOpenBSD)
  98. }
  99. name := "rename"
  100. old := createTestFile(name, "oldfile")
  101. new := "newfile"
  102. testCase := func() {
  103. renameTestFile(name, old, new)
  104. }
  105. destEvent := Event{new, Remove}
  106. // Only on these platforms the removed file can be differentiated from
  107. // the created file during renaming
  108. if runtime.GOOS == "windows" || runtime.GOOS == "linux" || runtime.GOOS == "solaris" || runtime.GOOS == "freebsd" {
  109. destEvent = Event{new, NonRemove}
  110. }
  111. expectedEvents := []Event{
  112. {old, Remove},
  113. destEvent,
  114. }
  115. allowedEvents := []Event{
  116. {name, NonRemove},
  117. }
  118. // set the "allow others" flag because we might get the create of
  119. // "oldfile" initially
  120. testScenario(t, name, testCase, expectedEvents, allowedEvents, fakeMatcher{}, false)
  121. }
  122. // TestWatchWinRoot checks that a watch at a drive letter does not panic due to
  123. // out of root event on every event.
  124. // https://github.com/syncthing/syncthing/issues/5695
  125. func TestWatchWinRoot(t *testing.T) {
  126. if runtime.GOOS != "windows" {
  127. t.Skip("Windows specific test")
  128. }
  129. outChan := make(chan Event)
  130. backendChan := make(chan notify.EventInfo, backendBuffer)
  131. errChan := make(chan error)
  132. ctx, cancel := context.WithCancel(context.Background())
  133. // testFs is Filesystem, but we need BasicFilesystem here
  134. root := `D:\`
  135. fs := newBasicFilesystem(root)
  136. watch, roots, err := fs.watchPaths(".")
  137. if err != nil {
  138. t.Fatal(err)
  139. }
  140. done := make(chan struct{})
  141. defer func() {
  142. cancel()
  143. <-done
  144. }()
  145. go func() {
  146. defer func() {
  147. if r := recover(); r != nil {
  148. t.Error(r)
  149. }
  150. cancel()
  151. }()
  152. fs.watchLoop(ctx, ".", roots, backendChan, outChan, errChan, fakeMatcher{})
  153. close(done)
  154. }()
  155. // filepath.Dir as watch has a /... suffix
  156. name := "foo"
  157. backendChan <- fakeEventInfo(filepath.Join(filepath.Dir(watch), name))
  158. select {
  159. case <-time.After(10 * time.Second):
  160. cancel()
  161. t.Errorf("Timed out before receiving event")
  162. case ev := <-outChan:
  163. if ev.Name != name {
  164. t.Errorf("Unexpected event %v, expected %v", ev.Name, name)
  165. }
  166. case err := <-errChan:
  167. t.Error("Received fatal watch error:", err)
  168. case <-ctx.Done():
  169. }
  170. }
  171. // TestWatchOutside checks that no changes from outside the folder make it in
  172. func TestWatchOutside(t *testing.T) {
  173. expectErrorForPath(t, filepath.Join(filepath.Dir(testDirAbs), "outside"))
  174. rootWithoutSlash := strings.TrimRight(filepath.ToSlash(testDirAbs), "/")
  175. expectErrorForPath(t, rootWithoutSlash+"outside")
  176. expectErrorForPath(t, rootWithoutSlash+"outside/thing")
  177. }
  178. func expectErrorForPath(t *testing.T, path string) {
  179. outChan := make(chan Event)
  180. backendChan := make(chan notify.EventInfo, backendBuffer)
  181. errChan := make(chan error)
  182. ctx, cancel := context.WithCancel(context.Background())
  183. // testFs is Filesystem, but we need BasicFilesystem here
  184. fs := newBasicFilesystem(testDirAbs)
  185. done := make(chan struct{})
  186. go func() {
  187. fs.watchLoop(ctx, ".", []string{testDirAbs}, backendChan, outChan, errChan, fakeMatcher{})
  188. close(done)
  189. }()
  190. defer func() {
  191. cancel()
  192. <-done
  193. }()
  194. backendChan <- fakeEventInfo(path)
  195. select {
  196. case <-time.After(10 * time.Second):
  197. t.Errorf("Timed out before receiving error")
  198. case e := <-outChan:
  199. t.Errorf("Unexpected passed through event %v", e)
  200. case <-errChan:
  201. case <-ctx.Done():
  202. }
  203. }
  204. func TestWatchSubpath(t *testing.T) {
  205. outChan := make(chan Event)
  206. backendChan := make(chan notify.EventInfo, backendBuffer)
  207. errChan := make(chan error)
  208. ctx, cancel := context.WithCancel(context.Background())
  209. // testFs is Filesystem, but we need BasicFilesystem here
  210. fs := newBasicFilesystem(testDirAbs)
  211. abs, _ := fs.rooted("sub")
  212. done := make(chan struct{})
  213. go func() {
  214. fs.watchLoop(ctx, "sub", []string{testDirAbs}, backendChan, outChan, errChan, fakeMatcher{})
  215. close(done)
  216. }()
  217. defer func() {
  218. cancel()
  219. <-done
  220. }()
  221. backendChan <- fakeEventInfo(filepath.Join(abs, "file"))
  222. timeout := time.NewTimer(2 * time.Second)
  223. select {
  224. case <-timeout.C:
  225. t.Errorf("Timed out before receiving an event")
  226. cancel()
  227. case ev := <-outChan:
  228. if ev.Name != filepath.Join("sub", "file") {
  229. t.Errorf("While watching a subfolder, received an event with unexpected path %v", ev.Name)
  230. }
  231. case err := <-errChan:
  232. t.Error("Received fatal watch error:", err)
  233. }
  234. cancel()
  235. }
  236. // TestWatchOverflow checks that an event at the root is sent when maxFiles is reached
  237. func TestWatchOverflow(t *testing.T) {
  238. if runtime.GOOS == "openbsd" {
  239. t.Skip(failsOnOpenBSD)
  240. }
  241. name := "overflow"
  242. expectedEvents := []Event{
  243. {".", NonRemove},
  244. }
  245. allowedEvents := []Event{
  246. {name, NonRemove},
  247. }
  248. testCase := func() {
  249. for i := 0; i < 5*backendBuffer; i++ {
  250. file := "file" + strconv.Itoa(i)
  251. createTestFile(name, file)
  252. allowedEvents = append(allowedEvents, Event{file, NonRemove})
  253. }
  254. }
  255. testScenario(t, name, testCase, expectedEvents, allowedEvents, fakeMatcher{}, false)
  256. }
  257. func TestWatchErrorLinuxInterpretation(t *testing.T) {
  258. if runtime.GOOS != "linux" {
  259. t.Skip("testing of linux specific error codes")
  260. }
  261. var errTooManyFiles = &os.PathError{
  262. Op: "error while traversing",
  263. Path: "foo",
  264. Err: syscall.Errno(24),
  265. }
  266. var errNoSpace = &os.PathError{
  267. Op: "error while traversing",
  268. Path: "bar",
  269. Err: syscall.Errno(28),
  270. }
  271. if !reachedMaxUserWatches(errTooManyFiles) {
  272. t.Error("Underlying error syscall.Errno(24) should be recognised to be about inotify limits.")
  273. }
  274. if !reachedMaxUserWatches(errNoSpace) {
  275. t.Error("Underlying error syscall.Errno(28) should be recognised to be about inotify limits.")
  276. }
  277. err := errors.New("Another error")
  278. if reachedMaxUserWatches(err) {
  279. t.Errorf("This error does not concern inotify limits: %#v", err)
  280. }
  281. }
  282. func TestWatchSymlinkedRoot(t *testing.T) {
  283. if runtime.GOOS == "windows" {
  284. t.Skip("Involves symlinks")
  285. }
  286. name := "symlinkedRoot"
  287. if err := testFs.MkdirAll(name, 0755); err != nil {
  288. panic(fmt.Sprintf("Failed to create directory %s: %s", name, err))
  289. }
  290. defer testFs.RemoveAll(name)
  291. root := filepath.Join(name, "root")
  292. if err := testFs.MkdirAll(root, 0777); err != nil {
  293. panic(err)
  294. }
  295. link := filepath.Join(name, "link")
  296. if err := testFs.CreateSymlink(filepath.Join(testFs.URI(), root), link); err != nil {
  297. panic(err)
  298. }
  299. linkedFs := NewFilesystem(FilesystemTypeBasic, filepath.Join(testFs.URI(), link))
  300. ctx, cancel := context.WithCancel(context.Background())
  301. defer cancel()
  302. if _, _, err := linkedFs.Watch(".", fakeMatcher{}, ctx, false); err != nil {
  303. panic(err)
  304. }
  305. if err := linkedFs.MkdirAll("foo", 0777); err != nil {
  306. panic(err)
  307. }
  308. // Give the panic some time to happen
  309. sleepMs(100)
  310. }
  311. func TestUnrootedChecked(t *testing.T) {
  312. fs := newBasicFilesystem(testDirAbs)
  313. if unrooted, err := fs.unrootedChecked("/random/other/path", []string{testDirAbs}); err == nil {
  314. t.Error("unrootedChecked did not return an error on outside path, but returned", unrooted)
  315. }
  316. }
  317. func TestWatchIssue4877(t *testing.T) {
  318. if runtime.GOOS != "windows" {
  319. t.Skip("Windows specific test")
  320. }
  321. name := "Issue4877"
  322. file := "file"
  323. testCase := func() {
  324. createTestFile(name, file)
  325. }
  326. expectedEvents := []Event{
  327. {file, NonRemove},
  328. }
  329. allowedEvents := []Event{
  330. {name, NonRemove},
  331. }
  332. volName := filepath.VolumeName(testDirAbs)
  333. if volName == "" {
  334. t.Fatalf("Failed to get volume name for path %v", testDirAbs)
  335. }
  336. origTestFs := testFs
  337. testFs = NewFilesystem(FilesystemTypeBasic, strings.ToLower(volName)+strings.ToUpper(testDirAbs[len(volName):]))
  338. defer func() {
  339. testFs = origTestFs
  340. }()
  341. testScenario(t, name, testCase, expectedEvents, allowedEvents, fakeMatcher{}, false)
  342. }
  343. func TestWatchModTime(t *testing.T) {
  344. name := "modtime"
  345. file := createTestFile(name, "foo")
  346. path := filepath.Join(name, file)
  347. now := time.Now()
  348. before := now.Add(-10 * time.Second)
  349. if err := testFs.Chtimes(path, before, before); err != nil {
  350. t.Fatal(err)
  351. }
  352. testCase := func() {
  353. if err := testFs.Chtimes(path, now, now); err != nil {
  354. t.Error(err)
  355. }
  356. }
  357. expectedEvents := []Event{
  358. {file, NonRemove},
  359. }
  360. var allowedEvents []Event
  361. // Apparently an event for the parent is also sent on mac
  362. if runtime.GOOS == "darwin" {
  363. allowedEvents = []Event{
  364. {name, NonRemove},
  365. }
  366. }
  367. testScenario(t, name, testCase, expectedEvents, allowedEvents, fakeMatcher{}, false)
  368. }
  369. func TestModifyFile(t *testing.T) {
  370. name := "modify"
  371. old := createTestFile(name, "file")
  372. modifyTestFile(name, old, "syncthing")
  373. testCase := func() {
  374. modifyTestFile(name, old, "modified")
  375. }
  376. expectedEvents := []Event{
  377. {old, NonRemove},
  378. }
  379. allowedEvents := []Event{
  380. {name, NonRemove},
  381. }
  382. sleepMs(1000)
  383. testScenario(t, name, testCase, expectedEvents, allowedEvents, fakeMatcher{}, false)
  384. }
  385. func TestTruncateFileOnly(t *testing.T) {
  386. name := "truncate"
  387. file := createTestFile(name, "file")
  388. modifyTestFile(name, file, "syncthing")
  389. // modified the content to empty use os.WriteFile will first truncate the file
  390. // (/os/file.go:696) then write nothing. This logic is also used in many editors,
  391. // such as when emptying a file in VSCode or JetBrain
  392. //
  393. // darwin will only modified the inode's metadata, such us mtime, file size, etc.
  394. // but would not modified the file directly, so FSEvent 'FSEventsModified' will not
  395. // be received
  396. //
  397. // we should watch the FSEvent 'FSEventsInodeMetaMod' to watch the Inode metadata,
  398. // and that should be considered as an NonRemove Event
  399. //
  400. // notify also considered FSEventsInodeMetaMod as Write Event
  401. // /watcher_fsevents.go:89
  402. testCase := func() {
  403. modifyTestFile(name, file, "")
  404. }
  405. expectedEvents := []Event{
  406. {file, NonRemove},
  407. }
  408. allowedEvents := []Event{
  409. {file, NonRemove},
  410. }
  411. sleepMs(1000)
  412. testScenario(t, name, testCase, expectedEvents, allowedEvents, fakeMatcher{}, true)
  413. }
  414. // path relative to folder root, also creates parent dirs if necessary
  415. func createTestFile(name string, file string) string {
  416. joined := filepath.Join(name, file)
  417. if err := testFs.MkdirAll(filepath.Dir(joined), 0755); err != nil {
  418. panic(fmt.Sprintf("Failed to create parent directory for %s: %s", joined, err))
  419. }
  420. handle, err := testFs.Create(joined)
  421. if err != nil {
  422. panic(fmt.Sprintf("Failed to create test file %s: %s", joined, err))
  423. }
  424. handle.Close()
  425. return file
  426. }
  427. func renameTestFile(name string, old string, new string) {
  428. old = filepath.Join(name, old)
  429. new = filepath.Join(name, new)
  430. if err := testFs.Rename(old, new); err != nil {
  431. panic(fmt.Sprintf("Failed to rename %s to %s: %s", old, new, err))
  432. }
  433. }
  434. func modifyTestFile(name string, file string, content string) {
  435. joined := filepath.Join(testDirAbs, name, file)
  436. err := os.WriteFile(joined, []byte(content), 0755)
  437. if err != nil {
  438. panic(fmt.Sprintf("Failed to modify test file %s: %s", joined, err))
  439. }
  440. }
  441. func sleepMs(ms int) {
  442. time.Sleep(time.Duration(ms) * time.Millisecond)
  443. }
  444. func testScenario(t *testing.T, name string, testCase func(), expectedEvents, allowedEvents []Event, fm fakeMatcher, ignorePerms bool) {
  445. if err := testFs.MkdirAll(name, 0755); err != nil {
  446. panic(fmt.Sprintf("Failed to create directory %s: %s", name, err))
  447. }
  448. defer testFs.RemoveAll(name)
  449. ctx, cancel := context.WithCancel(context.Background())
  450. defer cancel()
  451. eventChan, errChan, err := testFs.Watch(name, fm, ctx, ignorePerms)
  452. if err != nil {
  453. panic(err)
  454. }
  455. go testWatchOutput(t, name, eventChan, expectedEvents, allowedEvents, ctx, cancel)
  456. testCase()
  457. select {
  458. case <-time.After(10 * time.Second):
  459. t.Error("Timed out before receiving all expected events")
  460. case err := <-errChan:
  461. t.Error("Received fatal watch error:", err)
  462. case <-ctx.Done():
  463. }
  464. }
  465. func testWatchOutput(t *testing.T, name string, in <-chan Event, expectedEvents, allowedEvents []Event, ctx context.Context, cancel context.CancelFunc) {
  466. var expected = make(map[Event]struct{})
  467. for _, ev := range expectedEvents {
  468. ev.Name = filepath.Join(name, ev.Name)
  469. expected[ev] = struct{}{}
  470. }
  471. var received Event
  472. var last Event
  473. for {
  474. if len(expected) == 0 {
  475. cancel()
  476. return
  477. }
  478. select {
  479. case <-ctx.Done():
  480. return
  481. case received = <-in:
  482. }
  483. // apparently the backend sometimes sends repeat events
  484. if last == received {
  485. continue
  486. }
  487. if _, ok := expected[received]; !ok {
  488. if len(allowedEvents) > 0 {
  489. sleepMs(100) // To facilitate overflow
  490. continue
  491. }
  492. t.Errorf("Received unexpected event %v expected one of %v", received, expected)
  493. cancel()
  494. return
  495. }
  496. delete(expected, received)
  497. last = received
  498. }
  499. }
  500. // Matches are done via direct comparison against both ignore and include
  501. type fakeMatcher struct {
  502. ignore string
  503. include string
  504. skipIgnoredDirs bool
  505. }
  506. func (fm fakeMatcher) ShouldIgnore(name string) bool {
  507. return name != fm.include && name == fm.ignore
  508. }
  509. func (fm fakeMatcher) SkipIgnoredDirs() bool {
  510. return fm.skipIgnoredDirs
  511. }
  512. type fakeEventInfo string
  513. func (e fakeEventInfo) Path() string {
  514. return string(e)
  515. }
  516. func (fakeEventInfo) Event() notify.Event {
  517. return notify.Write
  518. }
  519. func (fakeEventInfo) Sys() interface{} {
  520. return nil
  521. }