all: Add filesystem notification support
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3986
This commit is contained in:
committed by
Audrius Butkevicius
parent
c704ba9ef9
commit
f98c21b68e
295
lib/fs/basicfs_watch_test.go
Normal file
295
lib/fs/basicfs_watch_test.go
Normal file
@@ -0,0 +1,295 @@
|
||||
// Copyright (C) 2016 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
// +build !solaris,!darwin solaris,cgo darwin,cgo
|
||||
|
||||
package fs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/zillode/notify"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if err := os.RemoveAll(testDir); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
dir, err := filepath.Abs(".")
|
||||
if err != nil {
|
||||
panic("Cannot get absolute path to working dir")
|
||||
}
|
||||
dir, err = filepath.EvalSymlinks(dir)
|
||||
if err != nil {
|
||||
panic("Cannot get real path to working dir")
|
||||
}
|
||||
testDirAbs = filepath.Join(dir, testDir)
|
||||
testFs = newBasicFilesystem(testDirAbs)
|
||||
if l.ShouldDebug("filesystem") {
|
||||
testFs = &logFilesystem{testFs}
|
||||
}
|
||||
|
||||
backendBuffer = 10
|
||||
defer func() {
|
||||
backendBuffer = 500
|
||||
}()
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
const (
|
||||
testDir = "temporary_test_root"
|
||||
)
|
||||
|
||||
var (
|
||||
testDirAbs string
|
||||
testFs Filesystem
|
||||
)
|
||||
|
||||
func TestWatchIgnore(t *testing.T) {
|
||||
name := "ignore"
|
||||
|
||||
file := "file"
|
||||
ignored := "ignored"
|
||||
|
||||
testCase := func() {
|
||||
createTestFile(name, file)
|
||||
createTestFile(name, ignored)
|
||||
}
|
||||
|
||||
expectedEvents := []Event{
|
||||
{file, NonRemove},
|
||||
}
|
||||
|
||||
testScenario(t, name, testCase, expectedEvents, false, ignored)
|
||||
}
|
||||
|
||||
func TestWatchRename(t *testing.T) {
|
||||
name := "rename"
|
||||
|
||||
old := createTestFile(name, "oldfile")
|
||||
new := "newfile"
|
||||
|
||||
testCase := func() {
|
||||
renameTestFile(name, old, new)
|
||||
}
|
||||
|
||||
destEvent := Event{new, Remove}
|
||||
// Only on these platforms the removed file can be differentiated from
|
||||
// the created file during renaming
|
||||
if runtime.GOOS == "windows" || runtime.GOOS == "linux" || runtime.GOOS == "solaris" {
|
||||
destEvent = Event{new, NonRemove}
|
||||
}
|
||||
expectedEvents := []Event{
|
||||
{old, Remove},
|
||||
destEvent,
|
||||
}
|
||||
|
||||
testScenario(t, name, testCase, expectedEvents, false, "")
|
||||
}
|
||||
|
||||
// TestWatchOutside checks that no changes from outside the folder make it in
|
||||
func TestWatchOutside(t *testing.T) {
|
||||
outChan := make(chan Event)
|
||||
backendChan := make(chan notify.EventInfo, backendBuffer)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// testFs is Filesystem, but we need BasicFilesystem here
|
||||
fs := newBasicFilesystem(testDirAbs)
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
if recover() == nil {
|
||||
t.Fatalf("Watch did not panic on receiving event outside of folder")
|
||||
}
|
||||
cancel()
|
||||
}()
|
||||
fs.watchLoop(".", testDirAbs, backendChan, outChan, fakeMatcher{}, ctx)
|
||||
}()
|
||||
|
||||
backendChan <- fakeEventInfo(filepath.Join(filepath.Dir(testDirAbs), "outside"))
|
||||
}
|
||||
|
||||
func TestWatchSubpath(t *testing.T) {
|
||||
outChan := make(chan Event)
|
||||
backendChan := make(chan notify.EventInfo, backendBuffer)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// testFs is Filesystem, but we need BasicFilesystem here
|
||||
fs := newBasicFilesystem(testDirAbs)
|
||||
|
||||
abs, _ := fs.rooted("sub")
|
||||
go fs.watchLoop("sub", abs, backendChan, outChan, fakeMatcher{}, ctx)
|
||||
|
||||
backendChan <- fakeEventInfo(filepath.Join(abs, "file"))
|
||||
|
||||
timeout := time.NewTimer(2 * time.Second)
|
||||
select {
|
||||
case <-timeout.C:
|
||||
t.Errorf("Timed out before receiving an event")
|
||||
cancel()
|
||||
case ev := <-outChan:
|
||||
if ev.Name != filepath.Join("sub", "file") {
|
||||
t.Errorf("While watching a subfolder, received an event with unexpected path %v", ev.Name)
|
||||
}
|
||||
}
|
||||
|
||||
cancel()
|
||||
}
|
||||
|
||||
// TestWatchOverflow checks that an event at the root is sent when maxFiles is reached
|
||||
func TestWatchOverflow(t *testing.T) {
|
||||
name := "overflow"
|
||||
|
||||
testCase := func() {
|
||||
for i := 0; i < 5*backendBuffer; i++ {
|
||||
createTestFile(name, "file"+strconv.Itoa(i))
|
||||
}
|
||||
}
|
||||
|
||||
expectedEvents := []Event{
|
||||
{".", NonRemove},
|
||||
}
|
||||
|
||||
testScenario(t, name, testCase, expectedEvents, true, "")
|
||||
}
|
||||
|
||||
// path relative to folder root, also creates parent dirs if necessary
|
||||
func createTestFile(name string, file string) string {
|
||||
joined := filepath.Join(name, file)
|
||||
if err := testFs.MkdirAll(filepath.Dir(joined), 0755); err != nil {
|
||||
panic(fmt.Sprintf("Failed to create parent directory for %s: %s", joined, err))
|
||||
}
|
||||
handle, err := testFs.Create(joined)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Failed to create test file %s: %s", joined, err))
|
||||
}
|
||||
handle.Close()
|
||||
return file
|
||||
}
|
||||
|
||||
func renameTestFile(name string, old string, new string) {
|
||||
old = filepath.Join(name, old)
|
||||
new = filepath.Join(name, new)
|
||||
if err := testFs.Rename(old, new); err != nil {
|
||||
panic(fmt.Sprintf("Failed to rename %s to %s: %s", old, new, err))
|
||||
}
|
||||
}
|
||||
|
||||
func sleepMs(ms int) {
|
||||
time.Sleep(time.Duration(ms) * time.Millisecond)
|
||||
}
|
||||
|
||||
func testScenario(t *testing.T, name string, testCase func(), expectedEvents []Event, allowOthers bool, ignored string) {
|
||||
if err := testFs.MkdirAll(name, 0755); err != nil {
|
||||
panic(fmt.Sprintf("Failed to create directory %s: %s", name, err))
|
||||
}
|
||||
|
||||
// Tests pick up the previously created files/dirs, probably because
|
||||
// they get flushed to disk with a delay.
|
||||
initDelayMs := 500
|
||||
if runtime.GOOS == "darwin" {
|
||||
initDelayMs = 900
|
||||
}
|
||||
sleepMs(initDelayMs)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
if ignored != "" {
|
||||
ignored = filepath.Join(name, ignored)
|
||||
}
|
||||
|
||||
eventChan, err := testFs.Watch(name, fakeMatcher{ignored}, ctx, false)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
go testWatchOutput(t, name, eventChan, expectedEvents, allowOthers, ctx, cancel)
|
||||
|
||||
timeout := time.NewTimer(2 * time.Second)
|
||||
|
||||
testCase()
|
||||
|
||||
select {
|
||||
case <-timeout.C:
|
||||
t.Errorf("Timed out before receiving all expected events")
|
||||
cancel()
|
||||
case <-ctx.Done():
|
||||
}
|
||||
|
||||
if err := testFs.RemoveAll(name); err != nil {
|
||||
panic(fmt.Sprintf("Failed to remove directory %s: %s", name, err))
|
||||
}
|
||||
}
|
||||
|
||||
func testWatchOutput(t *testing.T, name string, in <-chan Event, expectedEvents []Event, allowOthers bool, ctx context.Context, cancel context.CancelFunc) {
|
||||
var expected = make(map[Event]struct{})
|
||||
for _, ev := range expectedEvents {
|
||||
ev.Name = filepath.Join(name, ev.Name)
|
||||
expected[ev] = struct{}{}
|
||||
}
|
||||
|
||||
var received Event
|
||||
var last Event
|
||||
for {
|
||||
if len(expected) == 0 {
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case received = <-in:
|
||||
}
|
||||
|
||||
// apparently the backend sometimes sends repeat events
|
||||
if last == received {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := expected[received]; !ok {
|
||||
if allowOthers {
|
||||
sleepMs(100) // To facilitate overflow
|
||||
continue
|
||||
}
|
||||
t.Errorf("Received unexpected event %v expected one of %v", received, expected)
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
delete(expected, received)
|
||||
last = received
|
||||
}
|
||||
}
|
||||
|
||||
type fakeMatcher struct{ match string }
|
||||
|
||||
func (fm fakeMatcher) ShouldIgnore(name string) bool {
|
||||
return name == fm.match
|
||||
}
|
||||
|
||||
type fakeEventInfo string
|
||||
|
||||
func (e fakeEventInfo) Path() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func (e fakeEventInfo) Event() notify.Event {
|
||||
return notify.Write
|
||||
}
|
||||
|
||||
func (e fakeEventInfo) Sys() interface{} {
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user