basicfs_watch_test.go 15 KB

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