basicfs_watch_test.go 13 KB

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