| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636 | 
							- // 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/.
 
- //go:build (!solaris && !darwin) || (solaris && cgo) || (darwin && cgo)
 
- // +build !solaris,!darwin solaris,cgo darwin,cgo
 
- package fs
 
- import (
 
- 	"context"
 
- 	"errors"
 
- 	"fmt"
 
- 	"os"
 
- 	"path/filepath"
 
- 	"strconv"
 
- 	"strings"
 
- 	"syscall"
 
- 	"testing"
 
- 	"time"
 
- 	"github.com/syncthing/notify"
 
- 	"github.com/syncthing/syncthing/lib/build"
 
- )
 
- 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 = evalSymlinks(dir)
 
- 	if err != nil {
 
- 		panic("Cannot get real path to working dir")
 
- 	}
 
- 	testDirAbs = filepath.Join(dir, testDir)
 
- 	if build.IsWindows {
 
- 		testDirAbs = longFilenameSupport(testDirAbs)
 
- 	}
 
- 	testFs = NewFilesystem(FilesystemTypeBasic, testDirAbs)
 
- 	backendBuffer = 10
 
- 	exitCode := m.Run()
 
- 	backendBuffer = 500
 
- 	os.RemoveAll(testDir)
 
- 	os.Exit(exitCode)
 
- }
 
- const (
 
- 	testDir        = "testdata"
 
- 	failsOnOpenBSD = "Fails on OpenBSD. See https://github.com/rjeczalik/notify/issues/172"
 
- )
 
- var (
 
- 	testDirAbs string
 
- 	testFs     Filesystem
 
- )
 
- func TestWatchIgnore(t *testing.T) {
 
- 	if build.IsOpenBSD {
 
- 		t.Skip(failsOnOpenBSD)
 
- 	}
 
- 	name := "ignore"
 
- 	file := "file"
 
- 	ignored := "ignored"
 
- 	testCase := func() {
 
- 		createTestFile(name, file)
 
- 		createTestFile(name, ignored)
 
- 	}
 
- 	expectedEvents := []Event{
 
- 		{file, NonRemove},
 
- 	}
 
- 	allowedEvents := []Event{
 
- 		{name, NonRemove},
 
- 	}
 
- 	testScenario(t, name, testCase, expectedEvents, allowedEvents, fakeMatcher{ignore: filepath.Join(name, ignored), skipIgnoredDirs: true}, false)
 
- }
 
- func TestWatchInclude(t *testing.T) {
 
- 	if build.IsOpenBSD {
 
- 		t.Skip(failsOnOpenBSD)
 
- 	}
 
- 	name := "include"
 
- 	file := "file"
 
- 	ignored := "ignored"
 
- 	testFs.MkdirAll(filepath.Join(name, ignored), 0777)
 
- 	included := filepath.Join(ignored, "included")
 
- 	testCase := func() {
 
- 		createTestFile(name, file)
 
- 		createTestFile(name, included)
 
- 	}
 
- 	expectedEvents := []Event{
 
- 		{file, NonRemove},
 
- 		{included, NonRemove},
 
- 	}
 
- 	allowedEvents := []Event{
 
- 		{name, NonRemove},
 
- 	}
 
- 	testScenario(t, name, testCase, expectedEvents, allowedEvents, fakeMatcher{ignore: filepath.Join(name, ignored), include: filepath.Join(name, included)}, false)
 
- }
 
- func TestWatchRename(t *testing.T) {
 
- 	if build.IsOpenBSD {
 
- 		t.Skip(failsOnOpenBSD)
 
- 	}
 
- 	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 build.IsWindows || build.IsLinux || build.IsSolaris || build.IsIllumos || build.IsFreeBSD {
 
- 		destEvent = Event{new, NonRemove}
 
- 	}
 
- 	expectedEvents := []Event{
 
- 		{old, Remove},
 
- 		destEvent,
 
- 	}
 
- 	allowedEvents := []Event{
 
- 		{name, NonRemove},
 
- 	}
 
- 	// set the "allow others" flag because we might get the create of
 
- 	// "oldfile" initially
 
- 	testScenario(t, name, testCase, expectedEvents, allowedEvents, fakeMatcher{}, false)
 
- }
 
- // TestWatchWinRoot checks that a watch at a drive letter does not panic due to
 
- // out of root event on every event.
 
- // https://github.com/syncthing/syncthing/issues/5695
 
- func TestWatchWinRoot(t *testing.T) {
 
- 	if !build.IsWindows {
 
- 		t.Skip("Windows specific test")
 
- 	}
 
- 	outChan := make(chan Event)
 
- 	backendChan := make(chan notify.EventInfo, backendBuffer)
 
- 	errChan := make(chan error)
 
- 	ctx, cancel := context.WithCancel(context.Background())
 
- 	// testFs is Filesystem, but we need BasicFilesystem here
 
- 	root := `D:\`
 
- 	fs := newBasicFilesystem(root)
 
- 	watch, roots, err := fs.watchPaths(".")
 
- 	if err != nil {
 
- 		t.Fatal(err)
 
- 	}
 
- 	done := make(chan struct{})
 
- 	defer func() {
 
- 		cancel()
 
- 		<-done
 
- 	}()
 
- 	go func() {
 
- 		defer func() {
 
- 			if r := recover(); r != nil {
 
- 				t.Error(r)
 
- 			}
 
- 			cancel()
 
- 		}()
 
- 		fs.watchLoop(ctx, ".", roots, backendChan, outChan, errChan, fakeMatcher{})
 
- 		close(done)
 
- 	}()
 
- 	// filepath.Dir as watch has a /... suffix
 
- 	name := "foo"
 
- 	backendChan <- fakeEventInfo(filepath.Join(filepath.Dir(watch), name))
 
- 	select {
 
- 	case <-time.After(10 * time.Second):
 
- 		cancel()
 
- 		t.Errorf("Timed out before receiving event")
 
- 	case ev := <-outChan:
 
- 		if ev.Name != name {
 
- 			t.Errorf("Unexpected event %v, expected %v", ev.Name, name)
 
- 		}
 
- 	case err := <-errChan:
 
- 		t.Error("Received fatal watch error:", err)
 
- 	case <-ctx.Done():
 
- 	}
 
- }
 
- // TestWatchOutside checks that no changes from outside the folder make it in
 
- func TestWatchOutside(t *testing.T) {
 
- 	expectErrorForPath(t, filepath.Join(filepath.Dir(testDirAbs), "outside"))
 
- 	rootWithoutSlash := strings.TrimRight(filepath.ToSlash(testDirAbs), "/")
 
- 	expectErrorForPath(t, rootWithoutSlash+"outside")
 
- 	expectErrorForPath(t, rootWithoutSlash+"outside/thing")
 
- }
 
- func expectErrorForPath(t *testing.T, path string) {
 
- 	outChan := make(chan Event)
 
- 	backendChan := make(chan notify.EventInfo, backendBuffer)
 
- 	errChan := make(chan error)
 
- 	ctx, cancel := context.WithCancel(context.Background())
 
- 	// testFs is Filesystem, but we need BasicFilesystem here
 
- 	fs := newBasicFilesystem(testDirAbs)
 
- 	done := make(chan struct{})
 
- 	go func() {
 
- 		fs.watchLoop(ctx, ".", []string{testDirAbs}, backendChan, outChan, errChan, fakeMatcher{})
 
- 		close(done)
 
- 	}()
 
- 	defer func() {
 
- 		cancel()
 
- 		<-done
 
- 	}()
 
- 	backendChan <- fakeEventInfo(path)
 
- 	select {
 
- 	case <-time.After(10 * time.Second):
 
- 		t.Errorf("Timed out before receiving error")
 
- 	case e := <-outChan:
 
- 		t.Errorf("Unexpected passed through event %v", e)
 
- 	case <-errChan:
 
- 	case <-ctx.Done():
 
- 	}
 
- }
 
- func TestWatchSubpath(t *testing.T) {
 
- 	outChan := make(chan Event)
 
- 	backendChan := make(chan notify.EventInfo, backendBuffer)
 
- 	errChan := make(chan error)
 
- 	ctx, cancel := context.WithCancel(context.Background())
 
- 	// testFs is Filesystem, but we need BasicFilesystem here
 
- 	fs := newBasicFilesystem(testDirAbs)
 
- 	abs, _ := fs.rooted("sub")
 
- 	done := make(chan struct{})
 
- 	go func() {
 
- 		fs.watchLoop(ctx, "sub", []string{testDirAbs}, backendChan, outChan, errChan, fakeMatcher{})
 
- 		close(done)
 
- 	}()
 
- 	defer func() {
 
- 		cancel()
 
- 		<-done
 
- 	}()
 
- 	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)
 
- 		}
 
- 	case err := <-errChan:
 
- 		t.Error("Received fatal watch error:", err)
 
- 	}
 
- 	cancel()
 
- }
 
- // TestWatchOverflow checks that an event at the root is sent when maxFiles is reached
 
- func TestWatchOverflow(t *testing.T) {
 
- 	if build.IsOpenBSD {
 
- 		t.Skip(failsOnOpenBSD)
 
- 	}
 
- 	name := "overflow"
 
- 	expectedEvents := []Event{
 
- 		{".", NonRemove},
 
- 	}
 
- 	allowedEvents := []Event{
 
- 		{name, NonRemove},
 
- 	}
 
- 	testCase := func() {
 
- 		for i := 0; i < 5*backendBuffer; i++ {
 
- 			file := "file" + strconv.Itoa(i)
 
- 			createTestFile(name, file)
 
- 			allowedEvents = append(allowedEvents, Event{file, NonRemove})
 
- 		}
 
- 	}
 
- 	testScenario(t, name, testCase, expectedEvents, allowedEvents, fakeMatcher{}, false)
 
- }
 
- func TestWatchErrorLinuxInterpretation(t *testing.T) {
 
- 	if !build.IsLinux {
 
- 		t.Skip("testing of linux specific error codes")
 
- 	}
 
- 	var errTooManyFiles = &os.PathError{
 
- 		Op:   "error while traversing",
 
- 		Path: "foo",
 
- 		Err:  syscall.Errno(24),
 
- 	}
 
- 	var errNoSpace = &os.PathError{
 
- 		Op:   "error while traversing",
 
- 		Path: "bar",
 
- 		Err:  syscall.Errno(28),
 
- 	}
 
- 	if !reachedMaxUserWatches(errTooManyFiles) {
 
- 		t.Error("Underlying error syscall.Errno(24) should be recognised to be about inotify limits.")
 
- 	}
 
- 	if !reachedMaxUserWatches(errNoSpace) {
 
- 		t.Error("Underlying error syscall.Errno(28) should be recognised to be about inotify limits.")
 
- 	}
 
- 	err := errors.New("Another error")
 
- 	if reachedMaxUserWatches(err) {
 
- 		t.Errorf("This error does not concern inotify limits: %#v", err)
 
- 	}
 
- }
 
- func TestWatchSymlinkedRoot(t *testing.T) {
 
- 	if build.IsWindows {
 
- 		t.Skip("Involves symlinks")
 
- 	}
 
- 	name := "symlinkedRoot"
 
- 	if err := testFs.MkdirAll(name, 0755); err != nil {
 
- 		panic(fmt.Sprintf("Failed to create directory %s: %s", name, err))
 
- 	}
 
- 	defer testFs.RemoveAll(name)
 
- 	root := filepath.Join(name, "root")
 
- 	if err := testFs.MkdirAll(root, 0777); err != nil {
 
- 		panic(err)
 
- 	}
 
- 	link := filepath.Join(name, "link")
 
- 	if err := testFs.CreateSymlink(filepath.Join(testFs.URI(), root), link); err != nil {
 
- 		panic(err)
 
- 	}
 
- 	linkedFs := NewFilesystem(FilesystemTypeBasic, filepath.Join(testFs.URI(), link))
 
- 	ctx, cancel := context.WithCancel(context.Background())
 
- 	defer cancel()
 
- 	if _, _, err := linkedFs.Watch(".", fakeMatcher{}, ctx, false); err != nil {
 
- 		panic(err)
 
- 	}
 
- 	if err := linkedFs.MkdirAll("foo", 0777); err != nil {
 
- 		panic(err)
 
- 	}
 
- 	// Give the panic some time to happen
 
- 	sleepMs(100)
 
- }
 
- func TestUnrootedChecked(t *testing.T) {
 
- 	fs := newBasicFilesystem(testDirAbs)
 
- 	if unrooted, err := fs.unrootedChecked("/random/other/path", []string{testDirAbs}); err == nil {
 
- 		t.Error("unrootedChecked did not return an error on outside path, but returned", unrooted)
 
- 	}
 
- }
 
- func TestWatchIssue4877(t *testing.T) {
 
- 	if !build.IsWindows {
 
- 		t.Skip("Windows specific test")
 
- 	}
 
- 	name := "Issue4877"
 
- 	file := "file"
 
- 	testCase := func() {
 
- 		createTestFile(name, file)
 
- 	}
 
- 	expectedEvents := []Event{
 
- 		{file, NonRemove},
 
- 	}
 
- 	allowedEvents := []Event{
 
- 		{name, NonRemove},
 
- 	}
 
- 	volName := filepath.VolumeName(testDirAbs)
 
- 	if volName == "" {
 
- 		t.Fatalf("Failed to get volume name for path %v", testDirAbs)
 
- 	}
 
- 	origTestFs := testFs
 
- 	testFs = NewFilesystem(FilesystemTypeBasic, strings.ToLower(volName)+strings.ToUpper(testDirAbs[len(volName):]))
 
- 	defer func() {
 
- 		testFs = origTestFs
 
- 	}()
 
- 	testScenario(t, name, testCase, expectedEvents, allowedEvents, fakeMatcher{}, false)
 
- }
 
- func TestWatchModTime(t *testing.T) {
 
- 	name := "modtime"
 
- 	file := createTestFile(name, "foo")
 
- 	path := filepath.Join(name, file)
 
- 	now := time.Now()
 
- 	before := now.Add(-10 * time.Second)
 
- 	if err := testFs.Chtimes(path, before, before); err != nil {
 
- 		t.Fatal(err)
 
- 	}
 
- 	testCase := func() {
 
- 		if err := testFs.Chtimes(path, now, now); err != nil {
 
- 			t.Error(err)
 
- 		}
 
- 	}
 
- 	expectedEvents := []Event{
 
- 		{file, NonRemove},
 
- 	}
 
- 	var allowedEvents []Event
 
- 	// Apparently an event for the parent is also sent on mac
 
- 	if build.IsDarwin {
 
- 		allowedEvents = []Event{
 
- 			{name, NonRemove},
 
- 		}
 
- 	}
 
- 	testScenario(t, name, testCase, expectedEvents, allowedEvents, fakeMatcher{}, false)
 
- }
 
- func TestModifyFile(t *testing.T) {
 
- 	name := "modify"
 
- 	old := createTestFile(name, "file")
 
- 	modifyTestFile(name, old, "syncthing")
 
- 	testCase := func() {
 
- 		modifyTestFile(name, old, "modified")
 
- 	}
 
- 	expectedEvents := []Event{
 
- 		{old, NonRemove},
 
- 	}
 
- 	allowedEvents := []Event{
 
- 		{name, NonRemove},
 
- 	}
 
- 	sleepMs(1000)
 
- 	testScenario(t, name, testCase, expectedEvents, allowedEvents, fakeMatcher{}, false)
 
- }
 
- func TestTruncateFileOnly(t *testing.T) {
 
- 	name := "truncate"
 
- 	file := createTestFile(name, "file")
 
- 	modifyTestFile(name, file, "syncthing")
 
- 	// modified the content to empty use os.WriteFile will first truncate the file
 
- 	// (/os/file.go:696) then write nothing. This logic is also used in many editors,
 
- 	// such as when emptying a file in VSCode or JetBrain
 
- 	//
 
- 	// darwin will only modified the inode's metadata, such us mtime, file size, etc.
 
- 	// but would not modified the file directly, so FSEvent 'FSEventsModified' will not
 
- 	// be received
 
- 	//
 
- 	// we should watch the FSEvent 'FSEventsInodeMetaMod' to watch the Inode metadata,
 
- 	// and that should be considered as an NonRemove Event
 
- 	//
 
- 	// notify also considered FSEventsInodeMetaMod as Write Event
 
- 	// /watcher_fsevents.go:89
 
- 	testCase := func() {
 
- 		modifyTestFile(name, file, "")
 
- 	}
 
- 	expectedEvents := []Event{
 
- 		{file, NonRemove},
 
- 	}
 
- 	allowedEvents := []Event{
 
- 		{file, NonRemove},
 
- 	}
 
- 	sleepMs(1000)
 
- 	testScenario(t, name, testCase, expectedEvents, allowedEvents, fakeMatcher{}, 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 modifyTestFile(name string, file string, content string) {
 
- 	joined := filepath.Join(testDirAbs, name, file)
 
- 	err := os.WriteFile(joined, []byte(content), 0755)
 
- 	if err != nil {
 
- 		panic(fmt.Sprintf("Failed to modify test file %s: %s", joined, err))
 
- 	}
 
- }
 
- func sleepMs(ms int) {
 
- 	time.Sleep(time.Duration(ms) * time.Millisecond)
 
- }
 
- func testScenario(t *testing.T, name string, testCase func(), expectedEvents, allowedEvents []Event, fm fakeMatcher, ignorePerms bool) {
 
- 	if err := testFs.MkdirAll(name, 0755); err != nil {
 
- 		panic(fmt.Sprintf("Failed to create directory %s: %s", name, err))
 
- 	}
 
- 	defer testFs.RemoveAll(name)
 
- 	ctx, cancel := context.WithCancel(context.Background())
 
- 	defer cancel()
 
- 	eventChan, errChan, err := testFs.Watch(name, fm, ctx, ignorePerms)
 
- 	if err != nil {
 
- 		panic(err)
 
- 	}
 
- 	go testWatchOutput(t, name, eventChan, expectedEvents, allowedEvents, ctx, cancel)
 
- 	testCase()
 
- 	select {
 
- 	case <-time.After(10 * time.Second):
 
- 		t.Error("Timed out before receiving all expected events")
 
- 	case err := <-errChan:
 
- 		t.Error("Received fatal watch error:", err)
 
- 	case <-ctx.Done():
 
- 	}
 
- }
 
- func testWatchOutput(t *testing.T, name string, in <-chan Event, expectedEvents, allowedEvents []Event, 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 len(allowedEvents) > 0 {
 
- 				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
 
- 	}
 
- }
 
- // Matches are done via direct comparison against both ignore and include
 
- type fakeMatcher struct {
 
- 	ignore          string
 
- 	include         string
 
- 	skipIgnoredDirs bool
 
- }
 
- func (fm fakeMatcher) ShouldIgnore(name string) bool {
 
- 	return name != fm.include && name == fm.ignore
 
- }
 
- func (fm fakeMatcher) SkipIgnoredDirs() bool {
 
- 	return fm.skipIgnoredDirs
 
- }
 
- type fakeEventInfo string
 
- func (e fakeEventInfo) Path() string {
 
- 	return string(e)
 
- }
 
- func (fakeEventInfo) Event() notify.Event {
 
- 	return notify.Write
 
- }
 
- func (fakeEventInfo) Sys() interface{} {
 
- 	return nil
 
- }
 
 
  |