vfstest: make VFS test suite support symlinks

This commit is contained in:
Filipe Azevedo 2022-12-14 23:02:10 +01:00 committed by Nick Craig-Wood
parent a5abe4b8b3
commit f1d2f2b2c8
3 changed files with 304 additions and 6 deletions

View File

@ -6,6 +6,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/vfs" "github.com/rclone/rclone/vfs"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -72,3 +73,230 @@ func TestFileModTimeWithOpenWriters(t *testing.T) {
run.rm(t, "cp-archive-test") run.rm(t, "cp-archive-test")
} }
// TestSymlinks tests all the api of the VFS / Mount symlinks support
func TestSymlinks(t *testing.T) {
run.skipIfNoFUSE(t)
if !run.vfsOpt.Links {
t.Skip("No symlinks configured")
}
if runtime.GOOS == "windows" {
t.Skip("Skipping test on Windows")
}
fs.Logf(nil, "Links: %v, useVFS: %v, suffix: %v", run.vfsOpt.Links, run.useVFS, fs.LinkSuffix)
// Create initial setup of test files and directories we will create links to
run.mkdir(t, "dir1")
run.mkdir(t, "dir1/sub1dir1")
run.createFile(t, "dir1/file1", "potato")
run.mkdir(t, "dir2")
run.mkdir(t, "dir2/sub1dir2")
run.createFile(t, "dir2/file1", "chicken")
// base state all the tests will be build off
baseState := "dir1/|dir1/sub1dir1/|dir1/file1 6|dir2/|dir2/sub1dir2/|dir2/file1 7"
// Check the tests return to the base state
checkBaseState := func() {
run.checkDir(t, baseState)
}
checkBaseState()
t.Run("FileLink", func(t *testing.T) {
// Link to a file
run.symlink(t, "dir1/file1", "dir1file1_link")
run.checkDir(t, baseState+"|dir1file1_link 10")
run.checkMode(t, "dir1file1_link", os.FileMode(run.vfsOpt.LinkPerms), os.FileMode(run.vfsOpt.FilePerms))
assert.Equal(t, "dir1/file1", run.readlink(t, "dir1file1_link"))
// Read through a symlink
assert.Equal(t, "potato", run.readFile(t, "dir1file1_link"))
// Write through a symlink
err := writeFile(run.path("dir1file1_link"), []byte("carrot"), 0600)
require.NoError(t, err)
assert.Equal(t, "carrot", run.readFile(t, "dir1file1_link"))
assert.Equal(t, "carrot", run.readFile(t, "dir1/file1"))
// Rename a symlink
err = run.os.Rename(run.path("dir1file1_link"), run.path("dir1file1_link")+"_bla")
require.NoError(t, err)
run.checkDir(t, baseState+"|dir1file1_link_bla 10")
assert.Equal(t, "dir1/file1", run.readlink(t, "dir1file1_link_bla"))
// Delete a symlink
run.rm(t, "dir1file1_link_bla")
checkBaseState()
})
t.Run("DirLink", func(t *testing.T) {
// Link to a dir
run.symlink(t, "dir1", "dir1_link")
run.checkDir(t, baseState+"|dir1_link 4")
run.checkMode(t, "dir1_link", os.FileMode(run.vfsOpt.LinkPerms), os.FileMode(run.vfsOpt.DirPerms))
assert.Equal(t, "dir1", run.readlink(t, "dir1_link"))
// Check you can't open a directory symlink
_, err := run.os.OpenFile(run.path("dir1_link"), os.O_WRONLY, 0600)
require.Error(t, err)
// Our symlink resolution is very simple when using the VFS as when using the
// mount the OS will resolve the symlinks, so we don't recurse here
// Read entries directly
dir1Entries := make(dirMap)
run.readLocalEx(t, dir1Entries, "dir1", false)
assert.Equal(t, newDirMap("dir1/sub1dir1/|dir1/file1 6"), dir1Entries)
// Read entries through the directory symlink
dir1EntriesSymlink := make(dirMap)
run.readLocalEx(t, dir1EntriesSymlink, "dir1_link", false)
assert.Equal(t, newDirMap("dir1_link/sub1dir1/|dir1_link/file1 6"), dir1EntriesSymlink)
// Rename directory symlink
err = run.os.Rename(run.path("dir1_link"), run.path("dir1_link")+"_bla")
require.NoError(t, err)
run.checkDir(t, baseState+"|dir1_link_bla 4")
assert.Equal(t, "dir1", run.readlink(t, "dir1_link_bla"))
// Remove directory symlink
run.rm(t, "dir1_link_bla")
checkBaseState()
})
// Corner case #1 - We do not allow creating regular and symlink files having the same name (ie, test.txt and test.txt.rclonelink)
// Symlink first, then regular
t.Run("OverwriteSymlinkWithRegular", func(t *testing.T) {
link1Name := "link1.txt"
run.symlink(t, "dir1/file1", link1Name)
run.checkDir(t, baseState+"|link1.txt 10")
fh, err := run.os.OpenFile(run.path(link1Name), os.O_WRONLY|os.O_CREATE, os.FileMode(run.vfsOpt.FilePerms))
// On real mount with links enabled, that open the symlink target as expected, else that fails to create a new file
assert.NoError(t, err)
// Don't care about the result, in some cache mode the file can't be opened for writing, so closing would trigger an err
_ = fh.Close()
run.rm(t, link1Name)
checkBaseState()
})
// Regular first, then symlink
t.Run("OverwriteRegularWithSymlink", func(t *testing.T) {
link1Name := "link1.txt"
run.createFile(t, link1Name, "")
run.checkDir(t, baseState+"|link1.txt 0")
err := run.os.Symlink(".", run.path(link1Name))
assert.Error(t, err)
run.rm(t, link1Name)
checkBaseState()
})
// Corner case #2 - We do not allow creating directory and symlink file having the same name (ie, test and test.rclonelink)
// Symlink first, then directory
t.Run("OverwriteSymlinkWithDirectory", func(t *testing.T) {
link1Name := "link1"
run.symlink(t, ".", link1Name)
run.checkDir(t, baseState+"|link1 1")
err := run.os.Mkdir(run.path(link1Name), os.FileMode(run.vfsOpt.DirPerms))
assert.Error(t, err)
run.rm(t, link1Name)
checkBaseState()
})
// Directory first, then symlink
t.Run("OverwriteDirectoryWithSymlink", func(t *testing.T) {
link1Name := "link1"
run.mkdir(t, link1Name)
run.checkDir(t, baseState+"|link1/")
err := run.os.Symlink(".", run.path(link1Name))
assert.Error(t, err)
run.rm(t, link1Name)
checkBaseState()
})
// Corner case #3 - We do not allow moving directory or file having the same name in a target (ie, test and test.rclonelink)
// Move symlink -> regular file
t.Run("MoveSymlinkToFile", func(t *testing.T) {
t.Skip("FIXME not implemented")
link1Name := "link1.txt"
run.symlink(t, ".", link1Name)
run.createFile(t, "dir1/link1.txt", "")
run.checkDir(t, baseState+"|link1.txt 1|dir1/link1.txt 0")
err := run.os.Rename(run.path(link1Name), run.path("dir1/"+link1Name))
assert.Error(t, err)
run.rm(t, link1Name)
run.rm(t, "dir1/link1.txt")
checkBaseState()
})
// Move regular file -> symlink
t.Run("MoveFileToSymlink", func(t *testing.T) {
t.Skip("FIXME not implemented")
link1Name := "link1.txt"
run.createFile(t, link1Name, "")
run.symlink(t, ".", "dir1/"+link1Name)
run.checkDir(t, baseState+"|link1.txt 0|dir1/link1.txt 1")
err := run.os.Rename(run.path(link1Name), run.path("dir1/link1.txt"))
assert.Error(t, err)
run.rm(t, link1Name)
run.rm(t, "dir1/"+link1Name)
checkBaseState()
})
// Move symlink -> directory
t.Run("MoveSymlinkToDirectory", func(t *testing.T) {
t.Skip("FIXME not implemented")
link1Name := "link1"
run.symlink(t, ".", link1Name)
run.mkdir(t, "dir1/link1")
run.checkDir(t, baseState+"|link1 1|dir1/link1/")
err := run.os.Rename(run.path(link1Name), run.path("dir1/"+link1Name))
assert.Error(t, err)
run.rm(t, link1Name)
run.rm(t, "dir1/link1")
checkBaseState()
})
// Move directory -> symlink
t.Run("MoveDirectoryToSymlink", func(t *testing.T) {
t.Skip("FIXME not implemented")
link1Name := "dir1/link1"
run.mkdir(t, "link1")
run.symlink(t, ".", link1Name)
run.checkDir(t, baseState+"|link1/|dir1/link1 1")
err := run.os.Rename(run.path("link1"), run.path("dir1/link1"))
assert.Error(t, err)
run.rm(t, "link1")
run.rm(t, link1Name)
checkBaseState()
})
}

View File

@ -49,12 +49,15 @@ func RunTests(t *testing.T, useVFS bool, minimumRequiredCacheMode vfscommon.Cach
tests := []struct { tests := []struct {
cacheMode vfscommon.CacheMode cacheMode vfscommon.CacheMode
writeBack fs.Duration writeBack fs.Duration
links bool
}{ }{
{cacheMode: vfscommon.CacheModeOff}, {cacheMode: vfscommon.CacheModeOff},
{cacheMode: vfscommon.CacheModeOff, links: true},
{cacheMode: vfscommon.CacheModeMinimal}, {cacheMode: vfscommon.CacheModeMinimal},
{cacheMode: vfscommon.CacheModeWrites}, {cacheMode: vfscommon.CacheModeWrites},
{cacheMode: vfscommon.CacheModeFull}, {cacheMode: vfscommon.CacheModeFull},
{cacheMode: vfscommon.CacheModeFull, writeBack: fs.Duration(100 * time.Millisecond)}, {cacheMode: vfscommon.CacheModeFull, writeBack: fs.Duration(100 * time.Millisecond)},
{cacheMode: vfscommon.CacheModeFull, writeBack: fs.Duration(100 * time.Millisecond), links: true},
} }
for _, test := range tests { for _, test := range tests {
if test.cacheMode < minimumRequiredCacheMode { if test.cacheMode < minimumRequiredCacheMode {
@ -63,11 +66,15 @@ func RunTests(t *testing.T, useVFS bool, minimumRequiredCacheMode vfscommon.Cach
vfsOpt := vfscommon.Opt vfsOpt := vfscommon.Opt
vfsOpt.CacheMode = test.cacheMode vfsOpt.CacheMode = test.cacheMode
vfsOpt.WriteBack = test.writeBack vfsOpt.WriteBack = test.writeBack
vfsOpt.Links = test.links
run = newRun(useVFS, &vfsOpt, mountFn) run = newRun(useVFS, &vfsOpt, mountFn)
what := fmt.Sprintf("CacheMode=%v", test.cacheMode) what := fmt.Sprintf("CacheMode=%v", test.cacheMode)
if test.writeBack > 0 { if test.writeBack > 0 {
what += fmt.Sprintf(",WriteBack=%v", test.writeBack) what += fmt.Sprintf(",WriteBack=%v", test.writeBack)
} }
if test.links {
what += fmt.Sprintf(",Links=%v", test.links)
}
fs.Logf(nil, "Starting test run with %s", what) fs.Logf(nil, "Starting test run with %s", what)
ok := t.Run(what, func(t *testing.T) { ok := t.Run(what, func(t *testing.T) {
t.Run("TestTouchAndDelete", TestTouchAndDelete) t.Run("TestTouchAndDelete", TestTouchAndDelete)
@ -98,6 +105,7 @@ func RunTests(t *testing.T, useVFS bool, minimumRequiredCacheMode vfscommon.Cach
t.Run("TestWriteFileFsync", TestWriteFileFsync) t.Run("TestWriteFileFsync", TestWriteFileFsync)
t.Run("TestWriteFileDup", TestWriteFileDup) t.Run("TestWriteFileDup", TestWriteFileDup)
t.Run("TestWriteFileAppend", TestWriteFileAppend) t.Run("TestWriteFileAppend", TestWriteFileAppend)
t.Run("TestSymlinks", TestSymlinks)
}) })
fs.Logf(nil, "Finished test run with %s (ok=%v)", what, ok) fs.Logf(nil, "Finished test run with %s (ok=%v)", what, ok)
run.Finalise() run.Finalise()
@ -213,10 +221,16 @@ func newDirMap(dirString string) (dm dirMap) {
} }
// Returns a dirmap with only the files in // Returns a dirmap with only the files in
func (dm dirMap) filesOnly() dirMap { func (dm dirMap) filesOnly(stripLinksSuffix bool) dirMap {
newDm := make(dirMap) newDm := make(dirMap)
for name := range dm { for name := range dm {
if !strings.HasSuffix(name, "/") { if !strings.HasSuffix(name, "/") {
if stripLinksSuffix {
index := strings.LastIndex(name, " ")
if index != -1 {
name = strings.TrimSuffix(name[0:index], fs.LinkSuffix) + name[index:]
}
}
newDm[name] = struct{}{} newDm[name] = struct{}{}
} }
} }
@ -224,7 +238,9 @@ func (dm dirMap) filesOnly() dirMap {
} }
// reads the local tree into dir // reads the local tree into dir
func (r *Run) readLocal(t *testing.T, dir dirMap, filePath string) { //
// If recurse it set it will recurse into subdirectories
func (r *Run) readLocalEx(t *testing.T, dir dirMap, filePath string, recurse bool) {
realPath := r.path(filePath) realPath := r.path(filePath)
files, err := r.os.ReadDir(realPath) files, err := r.os.ReadDir(realPath)
require.NoError(t, err) require.NoError(t, err)
@ -232,15 +248,26 @@ func (r *Run) readLocal(t *testing.T, dir dirMap, filePath string) {
name := path.Join(filePath, fi.Name()) name := path.Join(filePath, fi.Name())
if fi.IsDir() { if fi.IsDir() {
dir[name+"/"] = struct{}{} dir[name+"/"] = struct{}{}
r.readLocal(t, dir, name) if recurse {
r.readLocalEx(t, dir, name, recurse)
}
assert.Equal(t, os.FileMode(r.vfsOpt.DirPerms)&os.ModePerm, fi.Mode().Perm()) assert.Equal(t, os.FileMode(r.vfsOpt.DirPerms)&os.ModePerm, fi.Mode().Perm())
} else { } else {
dir[fmt.Sprintf("%s %d", name, fi.Size())] = struct{}{} dir[fmt.Sprintf("%s %d", name, fi.Size())] = struct{}{}
assert.Equal(t, os.FileMode(r.vfsOpt.FilePerms)&os.ModePerm, fi.Mode().Perm()) if fi.Mode()&os.ModeSymlink != 0 {
assert.Equal(t, os.FileMode(r.vfsOpt.LinkPerms)&os.ModePerm, fi.Mode().Perm())
} else {
assert.Equal(t, os.FileMode(r.vfsOpt.FilePerms)&os.ModePerm, fi.Mode().Perm())
}
} }
} }
} }
// reads the local tree into dir
func (r *Run) readLocal(t *testing.T, dir dirMap, filePath string) {
r.readLocalEx(t, dir, filePath, true)
}
// reads the remote tree into dir // reads the remote tree into dir
func (r *Run) readRemote(t *testing.T, dir dirMap, filepath string) { func (r *Run) readRemote(t *testing.T, dir dirMap, filepath string) {
objs, dirs, err := walk.GetAll(context.Background(), r.fremote, filepath, true, 1) objs, dirs, err := walk.GetAll(context.Background(), r.fremote, filepath, true, 1)
@ -271,7 +298,7 @@ func (r *Run) checkDir(t *testing.T, dirString string) {
remoteDm = make(dirMap) remoteDm = make(dirMap)
r.readRemote(t, remoteDm, "") r.readRemote(t, remoteDm, "")
// Ignore directories for remote compare // Ignore directories for remote compare
remoteOK = reflect.DeepEqual(dm.filesOnly(), remoteDm.filesOnly()) remoteOK = reflect.DeepEqual(dm.filesOnly(run.vfsOpt.Links), remoteDm.filesOnly(run.vfsOpt.Links))
fuseOK = reflect.DeepEqual(dm, localDm) fuseOK = reflect.DeepEqual(dm, localDm)
if remoteOK && fuseOK { if remoteOK && fuseOK {
return return
@ -280,7 +307,7 @@ func (r *Run) checkDir(t *testing.T, dirString string) {
t.Logf("Sleeping for %v for list eventual consistency: %d/%d", sleep, i, retries) t.Logf("Sleeping for %v for list eventual consistency: %d/%d", sleep, i, retries)
time.Sleep(sleep) time.Sleep(sleep)
} }
assert.Equal(t, dm.filesOnly(), remoteDm.filesOnly(), "expected vs remote") assert.Equal(t, dm.filesOnly(run.vfsOpt.Links), remoteDm.filesOnly(run.vfsOpt.Links), "expected vs remote")
assert.Equal(t, dm, localDm, "expected vs fuse mount") assert.Equal(t, dm, localDm, "expected vs fuse mount")
} }
@ -353,6 +380,37 @@ func (r *Run) rmdir(t *testing.T, filepath string) {
require.NoError(t, err) require.NoError(t, err)
} }
func (r *Run) symlink(t *testing.T, oldname, newname string) {
newname = r.path(newname)
err := r.os.Symlink(oldname, newname)
require.NoError(t, err)
}
func (r *Run) checkMode(t *testing.T, name string, lexpected os.FileMode, expected os.FileMode) {
if r.useVFS {
info, err := run.os.Stat(run.path(name))
require.NoError(t, err)
assert.Equal(t, lexpected, info.Mode())
assert.Equal(t, name, info.Name())
} else {
info, err := os.Lstat(run.path(name))
require.NoError(t, err)
assert.Equal(t, lexpected, info.Mode())
assert.Equal(t, name, info.Name())
info, err = run.os.Stat(run.path(name))
require.NoError(t, err)
assert.Equal(t, expected, info.Mode())
assert.Equal(t, name, info.Name())
}
}
func (r *Run) readlink(t *testing.T, name string) string {
result, err := r.os.Readlink(r.path(name))
require.NoError(t, err)
return result
}
// TestMount checks that the Fs is mounted by seeing if the mountpoint // TestMount checks that the Fs is mounted by seeing if the mountpoint
// is in the mount output // is in the mount output
func TestMount(t *testing.T) { func TestMount(t *testing.T) {

View File

@ -22,6 +22,8 @@ type Oser interface {
Remove(name string) error Remove(name string) error
Rename(oldName, newName string) error Rename(oldName, newName string) error
Stat(path string) (os.FileInfo, error) Stat(path string) (os.FileInfo, error)
Symlink(oldname, newname string) error
Readlink(name string) (s string, err error)
} }
// realOs is an implementation of Oser backed by the "os" package // realOs is an implementation of Oser backed by the "os" package
@ -130,6 +132,16 @@ func (r realOs) Stat(path string) (os.FileInfo, error) {
return os.Stat(path) return os.Stat(path)
} }
// Symlink
func (r realOs) Symlink(oldname, newname string) error {
return os.Symlink(oldname, newname)
}
// Readlink
func (r realOs) Readlink(name string) (s string, err error) {
return os.Readlink(name)
}
// Check interfaces // Check interfaces
var _ Oser = &realOs{} var _ Oser = &realOs{}
var _ vfs.Handle = &realOsFile{} var _ vfs.Handle = &realOsFile{}