rclone/fstest/fstests/fstests.go
Nick Craig-Wood 089c168fb9 fstests: attempt to fix flaky serve s3 test
Sometimes (particularly on macOS amd64) the serve s3 test fails with
TestIntegration/FsMkdir/FsPutError where it wasn't expecting to get an
object but it did.

This is likely caused by a race between the serve s3 goroutine
deleting the half uploaded file and the fstests code looking for it to
not exist.

This fix treats it like any other eventual consistency problem and
retries the check using the test framework.
2024-08-15 16:30:29 +01:00

2720 lines
90 KiB
Go

// Package fstests provides generic integration tests for the Fs and
// Object interfaces.
//
// These tests are concerned with the basic functionality of a
// backend. The tests in fs/sync and fs/operations tests more
// cornercases that these tests don't.
package fstests
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"math/bits"
"os"
"path"
"path/filepath"
"reflect"
"sort"
"strconv"
"strings"
"testing"
"time"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/cache"
"github.com/rclone/rclone/fs/config"
"github.com/rclone/rclone/fs/fserrors"
"github.com/rclone/rclone/fs/fspath"
"github.com/rclone/rclone/fs/hash"
"github.com/rclone/rclone/fs/object"
"github.com/rclone/rclone/fs/operations"
"github.com/rclone/rclone/fs/walk"
"github.com/rclone/rclone/fstest"
"github.com/rclone/rclone/fstest/testserver"
"github.com/rclone/rclone/lib/encoder"
"github.com/rclone/rclone/lib/random"
"github.com/rclone/rclone/lib/readers"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// InternalTester is an optional interface for Fs which allows to execute internal tests
//
// This interface should be implemented in 'backend'_internal_test.go and not in 'backend'.go
type InternalTester interface {
InternalTest(*testing.T)
}
// ChunkedUploadConfig contains the values used by TestFsPutChunked
// to determine the limits of chunked uploading
type ChunkedUploadConfig struct {
// Minimum allowed chunk size
MinChunkSize fs.SizeSuffix
// Maximum allowed chunk size, 0 is no limit
MaxChunkSize fs.SizeSuffix
// Rounds the given chunk size up to the next valid value
// nil will disable rounding
// e.g. the next power of 2
CeilChunkSize func(fs.SizeSuffix) fs.SizeSuffix
// More than one chunk is required on upload
NeedMultipleChunks bool
// Skip this particular remote
Skip bool
}
// SetUploadChunkSizer is a test only interface to change the upload chunk size at runtime
type SetUploadChunkSizer interface {
// Change the configured UploadChunkSize.
// Will only be called while no transfer is in progress.
SetUploadChunkSize(fs.SizeSuffix) (fs.SizeSuffix, error)
}
// SetUploadCutoffer is a test only interface to change the upload cutoff size at runtime
type SetUploadCutoffer interface {
// Change the configured UploadCutoff.
// Will only be called while no transfer is in progress.
SetUploadCutoff(fs.SizeSuffix) (fs.SizeSuffix, error)
}
// SetCopyCutoffer is a test only interface to change the copy cutoff size at runtime
type SetCopyCutoffer interface {
// Change the configured CopyCutoff.
// Will only be called while no transfer is in progress.
// Return fs.ErrorNotImplemented if you can't implement this
SetCopyCutoff(fs.SizeSuffix) (fs.SizeSuffix, error)
}
// NextPowerOfTwo returns the current or next bigger power of two.
// All values less or equal 0 will return 0
func NextPowerOfTwo(i fs.SizeSuffix) fs.SizeSuffix {
return 1 << uint(64-bits.LeadingZeros64(uint64(i)-1))
}
// NextMultipleOf returns a function that can be used as a CeilChunkSize function.
// This function will return the next multiple of m that is equal or bigger than i.
// All values less or equal 0 will return 0.
func NextMultipleOf(m fs.SizeSuffix) func(fs.SizeSuffix) fs.SizeSuffix {
if m <= 0 {
panic(fmt.Sprintf("invalid multiplier %s", m))
}
return func(i fs.SizeSuffix) fs.SizeSuffix {
if i <= 0 {
return 0
}
return (((i - 1) / m) + 1) * m
}
}
// dirsToNames returns a sorted list of names
func dirsToNames(dirs []fs.Directory) []string {
names := []string{}
for _, dir := range dirs {
names = append(names, fstest.Normalize(dir.Remote()))
}
sort.Strings(names)
return names
}
// objsToNames returns a sorted list of object names
func objsToNames(objs []fs.Object) []string {
names := []string{}
for _, obj := range objs {
names = append(names, fstest.Normalize(obj.Remote()))
}
sort.Strings(names)
return names
}
// retry f() until no retriable error
func retry(t *testing.T, what string, f func() error) {
const maxTries = 10
var err error
for tries := 1; tries <= maxTries; tries++ {
err = f()
// exit if no error, or error is not retriable
if err == nil || !fserrors.IsRetryError(err) {
break
}
t.Logf("%s error: %v - low level retry %d/%d", what, err, tries, maxTries)
time.Sleep(2 * time.Second)
}
require.NoError(t, err, what)
}
// check interface
// PutTestContentsMetadata puts file with given contents to the remote and checks it but unlike TestPutLarge doesn't remove
//
// It uploads the object with the mimeType and metadata passed in if set.
//
// It returns the object which will have been checked if check is set
func PutTestContentsMetadata(ctx context.Context, t *testing.T, f fs.Fs, file *fstest.Item, useFileHashes bool, contents string, check bool, mimeType string, metadata fs.Metadata, options ...fs.OpenOption) fs.Object {
var (
err error
obj fs.Object
uploadHash *hash.MultiHasher
)
retry(t, "Put", func() error {
buf := bytes.NewBufferString(contents)
uploadHash = hash.NewMultiHasher()
in := io.TeeReader(buf, uploadHash)
file.Size = int64(buf.Len())
// The caller explicitly indicates whether the hashes in the file parameter should be used. If hashes is nil,
// then NewStaticObjectInfo will calculate default hashes for use in the check.
hashes := file.Hashes
if !useFileHashes {
hashes = nil
}
obji := object.NewStaticObjectInfo(file.Path, file.ModTime, file.Size, true, hashes, nil)
if mimeType != "" || metadata != nil {
// force the --metadata flag on temporarily
if metadata != nil {
ci := fs.GetConfig(ctx)
previousMetadata := ci.Metadata
ci.Metadata = true
defer func() {
ci.Metadata = previousMetadata
}()
}
obji.WithMetadata(metadata).WithMimeType(mimeType)
}
obj, err = f.Put(ctx, in, obji, options...)
return err
})
file.Hashes = uploadHash.Sums()
if check {
// Overwrite time with that in metadata if it is already specified
mtime, ok := metadata["mtime"]
if ok {
modTime, err := time.Parse(time.RFC3339Nano, mtime)
require.NoError(t, err)
file.ModTime = modTime
}
file.Check(t, obj, f.Precision())
// Re-read the object and check again
obj = fstest.NewObject(ctx, t, f, file.Path)
file.Check(t, obj, f.Precision())
}
return obj
}
// PutTestContents puts file with given contents to the remote and checks it but unlike TestPutLarge doesn't remove
func PutTestContents(ctx context.Context, t *testing.T, f fs.Fs, file *fstest.Item, contents string, check bool) fs.Object {
return PutTestContentsMetadata(ctx, t, f, file, false, contents, check, "", nil)
}
// testPut puts file with random contents to the remote
func testPut(ctx context.Context, t *testing.T, f fs.Fs, file *fstest.Item) (string, fs.Object) {
return testPutMimeType(ctx, t, f, file, "", nil)
}
// testPutMimeType puts file with random contents to the remote and the mime type given
func testPutMimeType(ctx context.Context, t *testing.T, f fs.Fs, file *fstest.Item, mimeType string, metadata fs.Metadata) (string, fs.Object) {
contents := random.String(100)
// We just generated new contents, but file may contain hashes generated by a previous operation
if len(file.Hashes) > 0 {
file.Hashes = make(map[hash.Type]string)
}
return contents, PutTestContentsMetadata(ctx, t, f, file, false, contents, true, mimeType, metadata)
}
// testPutLarge puts file to the remote, checks it and removes it on success.
//
// If stream is set, then it uploads the file with size -1
func testPutLarge(ctx context.Context, t *testing.T, f fs.Fs, file *fstest.Item, stream bool) {
var (
err error
obj fs.Object
uploadHash *hash.MultiHasher
)
retry(t, "PutLarge", func() error {
r := readers.NewPatternReader(file.Size)
uploadHash = hash.NewMultiHasher()
in := io.TeeReader(r, uploadHash)
size := file.Size
if stream {
size = -1
}
obji := object.NewStaticObjectInfo(file.Path, file.ModTime, size, true, nil, nil)
obj, err = f.Put(ctx, in, obji)
if file.Size == 0 && err == fs.ErrorCantUploadEmptyFiles {
t.Skip("Can't upload zero length files")
}
return err
})
file.Hashes = uploadHash.Sums()
file.Check(t, obj, f.Precision())
// Re-read the object and check again
obj = fstest.NewObject(ctx, t, f, file.Path)
file.Check(t, obj, f.Precision())
// Download the object and check it is OK
downloadHash := hash.NewMultiHasher()
download, err := obj.Open(ctx)
require.NoError(t, err)
n, err := io.Copy(downloadHash, download)
require.NoError(t, err)
assert.Equal(t, file.Size, n)
require.NoError(t, download.Close())
assert.Equal(t, file.Hashes, downloadHash.Sums())
// Remove the object
require.NoError(t, obj.Remove(ctx))
}
// TestPutLarge puts file to the remote, checks it and removes it on success.
func TestPutLarge(ctx context.Context, t *testing.T, f fs.Fs, file *fstest.Item) {
testPutLarge(ctx, t, f, file, false)
}
// TestPutLargeStreamed puts file of unknown size to the remote, checks it and removes it on success.
func TestPutLargeStreamed(ctx context.Context, t *testing.T, f fs.Fs, file *fstest.Item) {
testPutLarge(ctx, t, f, file, true)
}
// ReadObject reads the contents of an object as a string
func ReadObject(ctx context.Context, t *testing.T, obj fs.Object, limit int64, options ...fs.OpenOption) string {
what := fmt.Sprintf("readObject(%q) limit=%d, options=%+v", obj, limit, options)
in, err := obj.Open(ctx, options...)
require.NoError(t, err, what)
var r io.Reader = in
if limit >= 0 {
r = &io.LimitedReader{R: r, N: limit}
}
contents, err := io.ReadAll(r)
require.NoError(t, err, what)
err = in.Close()
require.NoError(t, err, what)
return string(contents)
}
// ExtraConfigItem describes a config item for the tests
type ExtraConfigItem struct{ Name, Key, Value string }
// Opt is options for Run
type Opt struct {
RemoteName string
NilObject fs.Object
ExtraConfig []ExtraConfigItem
SkipBadWindowsCharacters bool // skips unusable characters for windows if set
SkipFsMatch bool // if set skip exact matching of Fs value
TiersToTest []string // List of tiers which can be tested in setTier test
ChunkedUpload ChunkedUploadConfig
UnimplementableFsMethods []string // List of Fs methods which can't be implemented in this wrapping Fs
UnimplementableObjectMethods []string // List of Object methods which can't be implemented in this wrapping Fs
UnimplementableDirectoryMethods []string // List of Directory methods which can't be implemented in this wrapping Fs
SkipFsCheckWrap bool // if set skip FsCheckWrap
SkipObjectCheckWrap bool // if set skip ObjectCheckWrap
SkipDirectoryCheckWrap bool // if set skip DirectoryCheckWrap
SkipInvalidUTF8 bool // if set skip invalid UTF-8 checks
SkipLeadingDot bool // if set skip leading dot checks
QuickTestOK bool // if set, run this test with make quicktest
}
// returns true if x is found in ss
func stringsContains(x string, ss []string) bool {
for _, s := range ss {
if x == s {
return true
}
}
return false
}
// toUpperASCII returns a copy of the string s with all Unicode
// letters mapped to their upper case.
func toUpperASCII(s string) string {
return strings.Map(func(r rune) rune {
if 'a' <= r && r <= 'z' {
r -= 'a' - 'A'
}
return r
}, s)
}
// removeConfigID removes any {xyz} parts of the name put in for
// config disambiguation
func removeConfigID(s string) string {
bra := strings.IndexRune(s, '{')
ket := strings.IndexRune(s, '}')
if bra >= 0 && ket > bra {
s = s[:bra] + s[ket+1:]
}
return s
}
// InternalTestFiles is the state of the remote at the moment the internal tests are called
var InternalTestFiles []fstest.Item
// Run runs the basic integration tests for a remote using the options passed in.
//
// They are structured in a hierarchical way so that dependencies for the tests can be created.
//
// For example some tests require the directory to be created - these
// are inside the "FsMkdir" test. Some tests require some tests files
// - these are inside the "FsPutFiles" test.
func Run(t *testing.T, opt *Opt) {
var (
f fs.Fs
remoteName = opt.RemoteName
subRemoteName string
subRemoteLeaf string
file1 = fstest.Item{
ModTime: fstest.Time("2001-02-03T04:05:06.499999999Z"),
Path: "file name.txt",
}
file1Contents string
file1MimeType = "text/csv"
file1Metadata = fs.Metadata{"rclone-test": "potato"}
file2 = fstest.Item{
ModTime: fstest.Time("2001-02-03T04:05:10.123123123Z"),
Path: `hello? sausage/êé/Hello, 世界/ " ' @ < > & ? + ≠/z.txt`,
}
isLocalRemote bool
purged bool // whether the dir has been purged or not
ctx = context.Background()
ci = fs.GetConfig(ctx)
unwrappableFsMethods = []string{"Command"} // these Fs methods don't need to be wrapped ever
)
if strings.HasSuffix(os.Getenv("RCLONE_CONFIG"), "/notfound") && *fstest.RemoteName == "" && !opt.QuickTestOK {
t.Skip("quicktest only")
}
// Skip the test if the remote isn't configured
skipIfNotOk := func(t *testing.T) {
if f == nil {
t.Skipf("WARN: %q not configured", remoteName)
}
}
// Skip if remote is not ListR capable, otherwise set the useListR
// flag, returning a function to restore its value
skipIfNotListR := func(t *testing.T) func() {
skipIfNotOk(t)
if f.Features().ListR == nil {
t.Skip("FS has no ListR interface")
}
previous := ci.UseListR
ci.UseListR = true
return func() {
ci.UseListR = previous
}
}
// Skip if remote is not SetTier and GetTier capable
skipIfNotSetTier := func(t *testing.T) {
skipIfNotOk(t)
if !f.Features().SetTier || !f.Features().GetTier {
t.Skip("FS has no SetTier & GetTier interfaces")
}
}
// Return true if f (or any of the things it wraps) is bucket
// based but not at the root.
isBucketBasedButNotRoot := func(f fs.Fs) bool {
f = fs.UnWrapFs(f)
return f.Features().BucketBased && strings.Contains(strings.Trim(f.Root(), "/"), "/")
}
// Initialise the remote
fstest.Initialise()
// Set extra config if supplied
for _, item := range opt.ExtraConfig {
config.FileSetValue(item.Name, item.Key, item.Value)
}
if *fstest.RemoteName != "" {
remoteName = *fstest.RemoteName
}
oldFstestRemoteName := fstest.RemoteName
fstest.RemoteName = &remoteName
defer func() {
fstest.RemoteName = oldFstestRemoteName
}()
t.Logf("Using remote %q", remoteName)
var err error
if remoteName == "" {
remoteName, err = fstest.LocalRemote()
require.NoError(t, err)
isLocalRemote = true
}
// Start any test servers if required
finish, err := testserver.Start(remoteName)
require.NoError(t, err)
defer finish()
// Make the Fs we are testing with, initialising the local variables
// subRemoteName - name of the remote after the TestRemote:
// subRemoteLeaf - a subdirectory to use under that
// remote - the result of fs.NewFs(TestRemote:subRemoteName)
subRemoteName, subRemoteLeaf, err = fstest.RandomRemoteName(remoteName)
require.NoError(t, err)
f, err = fs.NewFs(context.Background(), subRemoteName)
if err == fs.ErrorNotFoundInConfigFile {
t.Logf("Didn't find %q in config file - skipping tests", remoteName)
return
}
require.NoError(t, err, fmt.Sprintf("unexpected error: %v", err))
// Get fsInfo which contains type, etc. of the fs
fsInfo, _, _, _, err := fs.ConfigFs(subRemoteName)
require.NoError(t, err, fmt.Sprintf("unexpected error: %v", err))
// Skip the rest if it failed
skipIfNotOk(t)
// Check to see if Fs that wrap other Fs implement all the optional methods
t.Run("FsCheckWrap", func(t *testing.T) {
skipIfNotOk(t)
if opt.SkipFsCheckWrap {
t.Skip("Skipping FsCheckWrap on this Fs")
}
ft := new(fs.Features).Fill(ctx, f)
if ft.UnWrap == nil && !f.Features().Overlay {
t.Skip("Not a wrapping Fs")
}
v := reflect.ValueOf(ft).Elem()
vType := v.Type()
for i := 0; i < v.NumField(); i++ {
vName := vType.Field(i).Name
if stringsContains(vName, opt.UnimplementableFsMethods) {
continue
}
if stringsContains(vName, unwrappableFsMethods) {
continue
}
field := v.Field(i)
// skip the bools
if field.Type().Kind() == reflect.Bool {
continue
}
if field.IsNil() {
t.Errorf("Missing Fs wrapper for %s", vName)
}
}
})
// Check to see if Fs advertises commands and they work and have docs
t.Run("FsCommand", func(t *testing.T) {
skipIfNotOk(t)
doCommand := f.Features().Command
if doCommand == nil {
t.Skip("No commands in this remote")
}
// Check the correct error is generated
_, err := doCommand(context.Background(), "NOTFOUND", nil, nil)
assert.Equal(t, fs.ErrorCommandNotFound, err, "Incorrect error generated on command not found")
// Check there are some commands in the fsInfo
fsInfo, _, _, _, err := fs.ConfigFs(remoteName)
require.NoError(t, err)
assert.True(t, len(fsInfo.CommandHelp) > 0, "Command is declared, must return some help in CommandHelp")
})
// TestFsRmdirNotFound tests deleting a nonexistent directory
t.Run("FsRmdirNotFound", func(t *testing.T) {
skipIfNotOk(t)
if isBucketBasedButNotRoot(f) {
t.Skip("Skipping test as non root bucket-based remote")
}
err := f.Rmdir(ctx, "")
assert.Error(t, err, "Expecting error on Rmdir nonexistent")
})
// Make the directory
err = f.Mkdir(ctx, "")
require.NoError(t, err)
fstest.CheckListing(t, f, []fstest.Item{})
// TestFsString tests the String method
t.Run("FsString", func(t *testing.T) {
skipIfNotOk(t)
str := f.String()
require.NotEqual(t, "", str)
})
// TestFsName tests the Name method
t.Run("FsName", func(t *testing.T) {
skipIfNotOk(t)
got := removeConfigID(f.Name())
var want string
if isLocalRemote {
want = "local"
} else {
want = remoteName[:strings.LastIndex(remoteName, ":")]
comma := strings.IndexRune(remoteName, ',')
if comma >= 0 {
want = want[:comma]
}
}
require.Equal(t, want, got)
})
// TestFsRoot tests the Root method
t.Run("FsRoot", func(t *testing.T) {
skipIfNotOk(t)
got := f.Root()
want := subRemoteName
colon := strings.LastIndex(want, ":")
if colon >= 0 {
want = want[colon+1:]
}
if isLocalRemote {
// only check last path element on local
require.Equal(t, filepath.Base(subRemoteName), filepath.Base(got))
} else {
require.Equal(t, want, got)
}
})
// TestFsRmdirEmpty tests deleting an empty directory
t.Run("FsRmdirEmpty", func(t *testing.T) {
skipIfNotOk(t)
err := f.Rmdir(ctx, "")
require.NoError(t, err)
})
// TestFsMkdir tests making a directory
//
// Tests that require the directory to be made are within this
t.Run("FsMkdir", func(t *testing.T) {
skipIfNotOk(t)
err := f.Mkdir(ctx, "")
require.NoError(t, err)
fstest.CheckListing(t, f, []fstest.Item{})
err = f.Mkdir(ctx, "")
require.NoError(t, err)
// TestFsMkdirRmdirSubdir tests making and removing a sub directory
t.Run("FsMkdirRmdirSubdir", func(t *testing.T) {
skipIfNotOk(t)
dir := "dir/subdir"
err := operations.Mkdir(ctx, f, dir)
require.NoError(t, err)
fstest.CheckListingWithPrecision(t, f, []fstest.Item{}, []string{"dir", "dir/subdir"}, fs.GetModifyWindow(ctx, f))
err = operations.Rmdir(ctx, f, dir)
require.NoError(t, err)
fstest.CheckListingWithPrecision(t, f, []fstest.Item{}, []string{"dir"}, fs.GetModifyWindow(ctx, f))
err = operations.Rmdir(ctx, f, "dir")
require.NoError(t, err)
fstest.CheckListingWithPrecision(t, f, []fstest.Item{}, []string{}, fs.GetModifyWindow(ctx, f))
})
// TestFsListEmpty tests listing an empty directory
t.Run("FsListEmpty", func(t *testing.T) {
skipIfNotOk(t)
fstest.CheckListing(t, f, []fstest.Item{})
})
// TestFsListDirEmpty tests listing the directories from an empty directory
TestFsListDirEmpty := func(t *testing.T) {
skipIfNotOk(t)
objs, dirs, err := walk.GetAll(ctx, f, "", true, 1)
if !f.Features().CanHaveEmptyDirectories {
if err != fs.ErrorDirNotFound {
require.NoError(t, err)
}
} else {
require.NoError(t, err)
}
assert.Equal(t, []string{}, objsToNames(objs))
assert.Equal(t, []string{}, dirsToNames(dirs))
}
t.Run("FsListDirEmpty", TestFsListDirEmpty)
// TestFsListRDirEmpty tests listing the directories from an empty directory using ListR
t.Run("FsListRDirEmpty", func(t *testing.T) {
defer skipIfNotListR(t)()
TestFsListDirEmpty(t)
})
// TestFsListDirNotFound tests listing the directories from an empty directory
TestFsListDirNotFound := func(t *testing.T) {
skipIfNotOk(t)
objs, dirs, err := walk.GetAll(ctx, f, "does not exist", true, 1)
if !f.Features().CanHaveEmptyDirectories {
if err != fs.ErrorDirNotFound {
assert.NoError(t, err)
assert.Equal(t, 0, len(objs)+len(dirs))
}
} else {
assert.Equal(t, fs.ErrorDirNotFound, err)
}
}
t.Run("FsListDirNotFound", TestFsListDirNotFound)
// TestFsListRDirNotFound tests listing the directories from an empty directory using ListR
t.Run("FsListRDirNotFound", func(t *testing.T) {
defer skipIfNotListR(t)()
TestFsListDirNotFound(t)
})
// FsEncoding tests that file name encodings are
// working by uploading a series of unusual files
// Must be run in an empty directory
t.Run("FsEncoding", func(t *testing.T) {
skipIfNotOk(t)
if testing.Short() {
t.Skip("not running with -short")
}
// check no files or dirs as pre-requisite
fstest.CheckListingWithPrecision(t, f, []fstest.Item{}, []string{}, fs.GetModifyWindow(ctx, f))
for _, test := range []struct {
name string
path string
}{
// See lib/encoder/encoder.go for list of things that go here
{"control chars", "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F\x7F"},
{"dot", "."},
{"dot dot", ".."},
{"punctuation", "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"},
{"leading space", " leading space"},
{"leading tilde", "~leading tilde"},
{"leading CR", "\rleading CR"},
{"leading LF", "\nleading LF"},
{"leading HT", "\tleading HT"},
{"leading VT", "\vleading VT"},
{"leading dot", ".leading dot"},
{"trailing space", "trailing space "},
{"trailing CR", "trailing CR\r"},
{"trailing LF", "trailing LF\n"},
{"trailing HT", "trailing HT\t"},
{"trailing VT", "trailing VT\v"},
{"trailing dot", "trailing dot."},
{"invalid UTF-8", "invalid utf-8\xfe"},
{"URL encoding", "test%46.txt"},
} {
t.Run(test.name, func(t *testing.T) {
if opt.SkipInvalidUTF8 && test.name == "invalid UTF-8" {
t.Skip("Skipping " + test.name)
}
if opt.SkipLeadingDot && test.name == "leading dot" {
t.Skip("Skipping " + test.name)
}
// turn raw strings into Standard encoding
fileName := encoder.Standard.Encode(test.path)
dirName := fileName
t.Logf("testing %q", fileName)
assert.NoError(t, f.Mkdir(ctx, dirName))
file := fstest.Item{
ModTime: time.Now(),
Path: dirName + "/" + fileName, // test creating a file and dir with that name
}
_, o := testPut(context.Background(), t, f, &file)
fstest.CheckListingWithPrecision(t, f, []fstest.Item{file}, []string{dirName}, fs.GetModifyWindow(ctx, f))
assert.NoError(t, o.Remove(ctx))
assert.NoError(t, f.Rmdir(ctx, dirName))
fstest.CheckListingWithPrecision(t, f, []fstest.Item{}, []string{}, fs.GetModifyWindow(ctx, f))
})
}
})
// TestFsNewObjectNotFound tests not finding an object
t.Run("FsNewObjectNotFound", func(t *testing.T) {
skipIfNotOk(t)
// Object in an existing directory
o, err := f.NewObject(ctx, "potato")
assert.Nil(t, o)
assert.Equal(t, fs.ErrorObjectNotFound, err)
// Now try an object in a nonexistent directory
o, err = f.NewObject(ctx, "directory/not/found/potato")
assert.Nil(t, o)
assert.Equal(t, fs.ErrorObjectNotFound, err)
})
// TestFsPutError tests uploading a file where there is an error
//
// It makes sure that aborting a file half way through does not create
// a file on the remote.
//
// go test -v -run 'TestIntegration/Test(Setup|Init|FsMkdir|FsPutError)$'
t.Run("FsPutError", func(t *testing.T) {
skipIfNotOk(t)
var N int64 = 5 * 1024
if *fstest.SizeLimit > 0 && N > *fstest.SizeLimit {
N = *fstest.SizeLimit
t.Logf("Reduce file size due to limit %d", N)
}
// Read N bytes then produce an error
contents := random.String(int(N))
buf := bytes.NewBufferString(contents)
er := &readers.ErrorReader{Err: errors.New("potato")}
in := io.MultiReader(buf, er)
obji := object.NewStaticObjectInfo(file2.Path, file2.ModTime, 2*N, true, nil, nil)
_, err := f.Put(ctx, in, obji)
// assert.Nil(t, obj) - FIXME some remotes return the object even on nil
assert.NotNil(t, err)
retry(t, "FsPutError: test object does not exist", func() error {
obj, err := f.NewObject(ctx, file2.Path)
if err == nil {
return fserrors.RetryErrorf("object is present")
}
assert.Nil(t, obj)
assert.Equal(t, fs.ErrorObjectNotFound, err)
return nil
})
})
t.Run("FsPutZeroLength", func(t *testing.T) {
skipIfNotOk(t)
TestPutLarge(ctx, t, f, &fstest.Item{
ModTime: fstest.Time("2001-02-03T04:05:06.499999999Z"),
Path: "zero-length-file",
Size: int64(0),
})
})
t.Run("FsOpenWriterAt", func(t *testing.T) {
skipIfNotOk(t)
openWriterAt := f.Features().OpenWriterAt
if openWriterAt == nil {
t.Skip("FS has no OpenWriterAt interface")
}
path := "writer-at-subdir/writer-at-file"
out, err := openWriterAt(ctx, path, -1)
require.NoError(t, err)
var n int
n, err = out.WriteAt([]byte("def"), 3)
assert.NoError(t, err)
assert.Equal(t, 3, n)
n, err = out.WriteAt([]byte("ghi"), 6)
assert.NoError(t, err)
assert.Equal(t, 3, n)
n, err = out.WriteAt([]byte("abc"), 0)
assert.NoError(t, err)
assert.Equal(t, 3, n)
assert.NoError(t, out.Close())
obj := fstest.NewObject(ctx, t, f, path)
assert.Equal(t, "abcdefghi", ReadObject(ctx, t, obj, -1), "contents of file differ")
assert.NoError(t, obj.Remove(ctx))
assert.NoError(t, f.Rmdir(ctx, "writer-at-subdir"))
})
// TestFsOpenChunkWriter tests writing in chunks to fs
// then reads back the contents and check if they match
// go test -v -run 'TestIntegration/FsMkdir/FsOpenChunkWriter'
t.Run("FsOpenChunkWriter", func(t *testing.T) {
skipIfNotOk(t)
openChunkWriter := f.Features().OpenChunkWriter
if openChunkWriter == nil {
t.Skip("FS has no OpenChunkWriter interface")
}
size5MBs := 5 * 1024 * 1024
contents1 := random.String(size5MBs)
contents2 := random.String(size5MBs)
size1MB := 1 * 1024 * 1024
contents3 := random.String(size1MB)
path := "writer-at-subdir/writer-at-file"
objSrc := object.NewStaticObjectInfo(path+"-WRONG-REMOTE", file1.ModTime, -1, true, nil, nil)
_, out, err := openChunkWriter(ctx, path, objSrc, &fs.ChunkOption{
ChunkSize: int64(size5MBs),
})
require.NoError(t, err)
var n int64
n, err = out.WriteChunk(ctx, 1, strings.NewReader(contents2))
assert.NoError(t, err)
assert.Equal(t, int64(size5MBs), n)
n, err = out.WriteChunk(ctx, 2, strings.NewReader(contents3))
assert.NoError(t, err)
assert.Equal(t, int64(size1MB), n)
n, err = out.WriteChunk(ctx, 0, strings.NewReader(contents1))
assert.NoError(t, err)
assert.Equal(t, int64(size5MBs), n)
assert.NoError(t, out.Close(ctx))
obj := fstest.NewObject(ctx, t, f, path)
originalContents := contents1 + contents2 + contents3
fileContents := ReadObject(ctx, t, obj, -1)
isEqual := originalContents == fileContents
assert.True(t, isEqual, "contents of file differ")
assert.NoError(t, obj.Remove(ctx))
assert.NoError(t, f.Rmdir(ctx, "writer-at-subdir"))
})
// TestFsChangeNotify tests that changes are properly
// propagated
//
// go test -v -remote TestDrive: -run '^Test(Setup|Init|FsChangeNotify)$' -verbose
t.Run("FsChangeNotify", func(t *testing.T) {
skipIfNotOk(t)
// Check have ChangeNotify
doChangeNotify := f.Features().ChangeNotify
if doChangeNotify == nil {
t.Skip("FS has no ChangeNotify interface")
}
err := operations.Mkdir(ctx, f, "dir")
require.NoError(t, err)
pollInterval := make(chan time.Duration)
dirChanges := map[string]struct{}{}
objChanges := map[string]struct{}{}
doChangeNotify(ctx, func(x string, e fs.EntryType) {
fs.Debugf(nil, "doChangeNotify(%q, %+v)", x, e)
if strings.HasPrefix(x, file1.Path[:5]) || strings.HasPrefix(x, file2.Path[:5]) {
fs.Debugf(nil, "Ignoring notify for file1 or file2: %q, %v", x, e)
return
}
if e == fs.EntryDirectory {
dirChanges[x] = struct{}{}
} else if e == fs.EntryObject {
objChanges[x] = struct{}{}
}
}, pollInterval)
defer func() { close(pollInterval) }()
pollInterval <- time.Second
var dirs []string
for _, idx := range []int{1, 3, 2} {
dir := fmt.Sprintf("dir/subdir%d", idx)
err = operations.Mkdir(ctx, f, dir)
require.NoError(t, err)
dirs = append(dirs, dir)
}
var objs []fs.Object
for _, idx := range []int{2, 4, 3} {
file := fstest.Item{
ModTime: time.Now(),
Path: fmt.Sprintf("dir/file%d", idx),
}
_, o := testPut(ctx, t, f, &file)
objs = append(objs, o)
}
// Looks for each item in wants in changes -
// if they are all found it returns true
contains := func(changes map[string]struct{}, wants []string) bool {
for _, want := range wants {
_, ok := changes[want]
if !ok {
return false
}
}
return true
}
// Wait a little while for the changes to come in
wantDirChanges := []string{"dir/subdir1", "dir/subdir3", "dir/subdir2"}
wantObjChanges := []string{"dir/file2", "dir/file4", "dir/file3"}
ok := false
for tries := 1; tries < 10; tries++ {
ok = contains(dirChanges, wantDirChanges) && contains(objChanges, wantObjChanges)
if ok {
break
}
t.Logf("Try %d/10 waiting for dirChanges and objChanges", tries)
time.Sleep(3 * time.Second)
}
if !ok {
t.Errorf("%+v does not contain %+v or \n%+v does not contain %+v", dirChanges, wantDirChanges, objChanges, wantObjChanges)
}
// tidy up afterwards
for _, o := range objs {
assert.NoError(t, o.Remove(ctx))
}
dirs = append(dirs, "dir")
for _, dir := range dirs {
assert.NoError(t, f.Rmdir(ctx, dir))
}
})
// TestFsPut files writes file1, file2 and tests an update
//
// Tests that require file1, file2 are within this
t.Run("FsPutFiles", func(t *testing.T) {
skipIfNotOk(t)
file1Contents, _ = testPut(ctx, t, f, &file1)
/* file2Contents = */ testPut(ctx, t, f, &file2)
file1Contents, _ = testPutMimeType(ctx, t, f, &file1, file1MimeType, file1Metadata)
// Note that the next test will check there are no duplicated file names
// TestFsListDirFile2 tests the files are correctly uploaded by doing
// Depth 1 directory listings
TestFsListDirFile2 := func(t *testing.T) {
skipIfNotOk(t)
list := func(dir string, expectedDirNames, expectedObjNames []string) {
var objNames, dirNames []string
for i := 1; i <= *fstest.ListRetries; i++ {
objs, dirs, err := walk.GetAll(ctx, f, dir, true, 1)
if errors.Is(err, fs.ErrorDirNotFound) {
objs, dirs, err = walk.GetAll(ctx, f, dir, true, 1)
}
require.NoError(t, err)
objNames = objsToNames(objs)
dirNames = dirsToNames(dirs)
if len(objNames) >= len(expectedObjNames) && len(dirNames) >= len(expectedDirNames) {
break
}
t.Logf("Sleeping for 1 second for TestFsListDirFile2 eventual consistency: %d/%d", i, *fstest.ListRetries)
time.Sleep(1 * time.Second)
}
assert.Equal(t, expectedDirNames, dirNames)
assert.Equal(t, expectedObjNames, objNames)
}
dir := file2.Path
deepest := true
for dir != "" {
expectedObjNames := []string{}
expectedDirNames := []string{}
child := dir
dir = path.Dir(dir)
if dir == "." {
dir = ""
expectedObjNames = append(expectedObjNames, file1.Path)
}
if deepest {
expectedObjNames = append(expectedObjNames, file2.Path)
deepest = false
} else {
expectedDirNames = append(expectedDirNames, child)
}
list(dir, expectedDirNames, expectedObjNames)
}
}
t.Run("FsListDirFile2", TestFsListDirFile2)
// TestFsListRDirFile2 tests the files are correctly uploaded by doing
// Depth 1 directory listings using ListR
t.Run("FsListRDirFile2", func(t *testing.T) {
defer skipIfNotListR(t)()
TestFsListDirFile2(t)
})
// Test the files are all there with walk.ListR recursive listings
t.Run("FsListR", func(t *testing.T) {
skipIfNotOk(t)
objs, dirs, err := walk.GetAll(ctx, f, "", true, -1)
require.NoError(t, err)
assert.Equal(t, []string{
"hello? sausage",
"hello? sausage/êé",
"hello? sausage/êé/Hello, 世界",
"hello? sausage/êé/Hello, 世界/ \" ' @ < > & ? + ≠",
}, dirsToNames(dirs))
assert.Equal(t, []string{
"file name.txt",
"hello? sausage/êé/Hello, 世界/ \" ' @ < > & ? + ≠/z.txt",
}, objsToNames(objs))
})
// Test the files are all there with
// walk.ListR recursive listings on a sub dir
t.Run("FsListRSubdir", func(t *testing.T) {
skipIfNotOk(t)
objs, dirs, err := walk.GetAll(ctx, f, path.Dir(path.Dir(path.Dir(path.Dir(file2.Path)))), true, -1)
require.NoError(t, err)
assert.Equal(t, []string{
"hello? sausage/êé",
"hello? sausage/êé/Hello, 世界",
"hello? sausage/êé/Hello, 世界/ \" ' @ < > & ? + ≠",
}, dirsToNames(dirs))
assert.Equal(t, []string{
"hello? sausage/êé/Hello, 世界/ \" ' @ < > & ? + ≠/z.txt",
}, objsToNames(objs))
})
// TestFsListDirRoot tests that DirList works in the root
TestFsListDirRoot := func(t *testing.T) {
skipIfNotOk(t)
rootRemote, err := fs.NewFs(context.Background(), remoteName)
require.NoError(t, err)
_, dirs, err := walk.GetAll(ctx, rootRemote, "", true, 1)
require.NoError(t, err)
assert.Contains(t, dirsToNames(dirs), subRemoteLeaf, "Remote leaf not found")
}
t.Run("FsListDirRoot", TestFsListDirRoot)
// TestFsListRDirRoot tests that DirList works in the root using ListR
t.Run("FsListRDirRoot", func(t *testing.T) {
defer skipIfNotListR(t)()
TestFsListDirRoot(t)
})
// TestFsListSubdir tests List works for a subdirectory
TestFsListSubdir := func(t *testing.T) {
skipIfNotOk(t)
fileName := file2.Path
var err error
var objs []fs.Object
var dirs []fs.Directory
for i := 0; i < 2; i++ {
dir, _ := path.Split(fileName)
dir = dir[:len(dir)-1]
objs, dirs, err = walk.GetAll(ctx, f, dir, true, -1)
}
require.NoError(t, err)
require.Len(t, objs, 1)
assert.Equal(t, fileName, objs[0].Remote())
require.Len(t, dirs, 0)
}
t.Run("FsListSubdir", TestFsListSubdir)
// TestFsListRSubdir tests List works for a subdirectory using ListR
t.Run("FsListRSubdir", func(t *testing.T) {
defer skipIfNotListR(t)()
TestFsListSubdir(t)
})
// TestFsListLevel2 tests List works for 2 levels
TestFsListLevel2 := func(t *testing.T) {
skipIfNotOk(t)
objs, dirs, err := walk.GetAll(ctx, f, "", true, 2)
if err == fs.ErrorLevelNotSupported {
return
}
require.NoError(t, err)
assert.Equal(t, []string{file1.Path}, objsToNames(objs))
assert.Equal(t, []string{"hello? sausage", "hello? sausage/êé"}, dirsToNames(dirs))
}
t.Run("FsListLevel2", TestFsListLevel2)
// TestFsListRLevel2 tests List works for 2 levels using ListR
t.Run("FsListRLevel2", func(t *testing.T) {
defer skipIfNotListR(t)()
TestFsListLevel2(t)
})
// TestFsListFile1 tests file present
t.Run("FsListFile1", func(t *testing.T) {
skipIfNotOk(t)
fstest.CheckListing(t, f, []fstest.Item{file1, file2})
})
// TestFsNewObject tests NewObject
t.Run("FsNewObject", func(t *testing.T) {
skipIfNotOk(t)
obj := fstest.NewObject(ctx, t, f, file1.Path)
file1.Check(t, obj, f.Precision())
})
// FsNewObjectCaseInsensitive tests NewObject on a case insensitive file system
t.Run("FsNewObjectCaseInsensitive", func(t *testing.T) {
skipIfNotOk(t)
if !f.Features().CaseInsensitive {
t.Skip("Not Case Insensitive")
}
obj := fstest.NewObject(ctx, t, f, toUpperASCII(file1.Path))
file1.Check(t, obj, f.Precision())
t.Run("Dir", func(t *testing.T) {
obj := fstest.NewObject(ctx, t, f, toUpperASCII(file2.Path))
file2.Check(t, obj, f.Precision())
})
})
// TestFsListFile1and2 tests two files present
t.Run("FsListFile1and2", func(t *testing.T) {
skipIfNotOk(t)
fstest.CheckListing(t, f, []fstest.Item{file1, file2})
})
// TestFsNewObjectDir tests NewObject on a directory which should produce fs.ErrorIsDir if possible or fs.ErrorObjectNotFound if not
t.Run("FsNewObjectDir", func(t *testing.T) {
skipIfNotOk(t)
dir := path.Dir(file2.Path)
obj, err := f.NewObject(ctx, dir)
assert.Nil(t, obj)
assert.True(t, err == fs.ErrorIsDir || err == fs.ErrorObjectNotFound, fmt.Sprintf("Wrong error: expecting fs.ErrorIsDir or fs.ErrorObjectNotFound but got: %#v", err))
})
// TestFsPurge tests Purge
t.Run("FsPurge", func(t *testing.T) {
skipIfNotOk(t)
// Check have Purge
doPurge := f.Features().Purge
if doPurge == nil {
t.Skip("FS has no Purge interface")
}
// put up a file to purge
fileToPurge := fstest.Item{
ModTime: fstest.Time("2001-02-03T04:05:06.499999999Z"),
Path: "dirToPurge/fileToPurge.txt",
}
_, _ = testPut(ctx, t, f, &fileToPurge)
fstest.CheckListingWithPrecision(t, f, []fstest.Item{file1, file2, fileToPurge}, []string{
"dirToPurge",
"hello? sausage",
"hello? sausage/êé",
"hello? sausage/êé/Hello, 世界",
"hello? sausage/êé/Hello, 世界/ \" ' @ < > & ? + ≠",
}, fs.GetModifyWindow(ctx, f))
// Now purge it
err = operations.Purge(ctx, f, "dirToPurge")
require.NoError(t, err)
fstest.CheckListingWithPrecision(t, f, []fstest.Item{file1, file2}, []string{
"hello? sausage",
"hello? sausage/êé",
"hello? sausage/êé/Hello, 世界",
"hello? sausage/êé/Hello, 世界/ \" ' @ < > & ? + ≠",
}, fs.GetModifyWindow(ctx, f))
})
// TestFsPurge tests Purge on the Root
t.Run("FsPurgeRoot", func(t *testing.T) {
skipIfNotOk(t)
// Check have Purge
doPurge := f.Features().Purge
if doPurge == nil {
t.Skip("FS has no Purge interface")
}
// put up a file to purge
fileToPurge := fstest.Item{
ModTime: fstest.Time("2001-02-03T04:05:06.499999999Z"),
Path: "dirToPurgeFromRoot/fileToPurgeFromRoot.txt",
}
_, _ = testPut(ctx, t, f, &fileToPurge)
fstest.CheckListingWithPrecision(t, f, []fstest.Item{file1, file2, fileToPurge}, []string{
"dirToPurgeFromRoot",
"hello? sausage",
"hello? sausage/êé",
"hello? sausage/êé/Hello, 世界",
"hello? sausage/êé/Hello, 世界/ \" ' @ < > & ? + ≠",
}, fs.GetModifyWindow(ctx, f))
// Create a new Fs pointing at the directory
remoteName := subRemoteName + "/" + "dirToPurgeFromRoot"
fPurge, err := fs.NewFs(context.Background(), remoteName)
require.NoError(t, err)
// Now purge it from the root
err = operations.Purge(ctx, fPurge, "")
require.NoError(t, err)
fstest.CheckListingWithPrecision(t, f, []fstest.Item{file1, file2}, []string{
"hello? sausage",
"hello? sausage/êé",
"hello? sausage/êé/Hello, 世界",
"hello? sausage/êé/Hello, 世界/ \" ' @ < > & ? + ≠",
}, fs.GetModifyWindow(ctx, f))
})
// TestFsListRootedSubdir tests putting and listing with an Fs that is rooted at a subdirectory 2 levels down
TestFsListRootedSubdir := func(t *testing.T) {
skipIfNotOk(t)
newF, err := cache.Get(ctx, subRemoteName+"/hello? sausage/êé")
assert.NoError(t, err)
nestedFile := fstest.Item{
ModTime: fstest.Time("2001-02-03T04:05:06.499999999Z"),
Path: "a/b/c/d/e.txt",
}
_, _ = testPut(ctx, t, newF, &nestedFile)
objs, dirs, err := walk.GetAll(ctx, newF, "", true, 10)
require.NoError(t, err)
assert.Equal(t, []string{`Hello, 世界/ " ' @ < > & ? + ≠/z.txt`, nestedFile.Path}, objsToNames(objs))
assert.Equal(t, []string{`Hello, 世界`, `Hello, 世界/ " ' @ < > & ? + ≠`, "a", "a/b", "a/b/c", "a/b/c/d"}, dirsToNames(dirs))
// cleanup
err = operations.Purge(ctx, newF, "a")
require.NoError(t, err)
}
t.Run("FsListRootedSubdir", TestFsListRootedSubdir)
// TestFsCopy tests Copy
t.Run("FsCopy", func(t *testing.T) {
skipIfNotOk(t)
// Check have Copy
doCopy := f.Features().Copy
if doCopy == nil {
t.Skip("FS has no Copier interface")
}
// Test with file2 so have + and ' ' in file name
var file2Copy = file2
file2Copy.Path += "-copy"
// do the copy
src := fstest.NewObject(ctx, t, f, file2.Path)
dst, err := doCopy(ctx, src, file2Copy.Path)
if err == fs.ErrorCantCopy {
t.Skip("FS can't copy")
}
require.NoError(t, err, fmt.Sprintf("Error: %#v", err))
// check file exists in new listing
fstest.CheckListing(t, f, []fstest.Item{file1, file2, file2Copy})
// Check dst lightly - list above has checked ModTime/Hashes
assert.Equal(t, file2Copy.Path, dst.Remote())
// check that mutating dst does not mutate src
err = dst.SetModTime(ctx, fstest.Time("2004-03-03T04:05:06.499999999Z"))
if err != fs.ErrorCantSetModTimeWithoutDelete && err != fs.ErrorCantSetModTime {
assert.NoError(t, err)
assert.False(t, src.ModTime(ctx).Equal(dst.ModTime(ctx)), "mutating dst should not mutate src -- is it Copying by pointer?")
}
// Delete copy
err = dst.Remove(ctx)
require.NoError(t, err)
// Test that server side copying files does the correct thing with metadata
t.Run("Metadata", func(t *testing.T) {
if !f.Features().WriteMetadata {
t.Skip("Skipping test as can't write metadata")
}
ctx, ci := fs.AddConfig(ctx)
ci.Metadata = true
// Create file with metadata
const srcName = "test metadata copy.txt"
const dstName = "test metadata copied.txt"
t1 := fstest.Time("2003-02-03T04:05:06.499999999Z")
t2 := fstest.Time("2004-03-03T04:05:06.499999999Z")
contents := random.String(100)
fileSrc := fstest.NewItem(srcName, contents, t1)
var testMetadata = fs.Metadata{
// System metadata supported by all backends
"mtime": t1.Format(time.RFC3339Nano),
// User metadata
"potato": "jersey",
}
oSrc := PutTestContentsMetadata(ctx, t, f, &fileSrc, false, contents, true, "text/plain", testMetadata)
fstest.CheckEntryMetadata(ctx, t, f, oSrc, testMetadata)
// Copy it with --metadata-set
ci.MetadataSet = fs.Metadata{
// System metadata supported by all backends
"mtime": t2.Format(time.RFC3339Nano),
// User metadata
"potato": "royal",
}
oDst, err := doCopy(ctx, oSrc, dstName)
require.NoError(t, err)
fileDst := fileSrc
fileDst.Path = dstName
fileDst.ModTime = t2
fstest.CheckListing(t, f, []fstest.Item{file1, file2, fileSrc, fileDst})
// Check metadata is correct
fstest.CheckEntryMetadata(ctx, t, f, oDst, ci.MetadataSet)
oDst = fstest.NewObject(ctx, t, f, dstName)
fstest.CheckEntryMetadata(ctx, t, f, oDst, ci.MetadataSet)
// Remove test files
require.NoError(t, oSrc.Remove(ctx))
require.NoError(t, oDst.Remove(ctx))
})
})
// TestFsMove tests Move
t.Run("FsMove", func(t *testing.T) {
skipIfNotOk(t)
// Check have Move
doMove := f.Features().Move
if doMove == nil {
t.Skip("FS has no Mover interface")
}
// state of files now:
// 1: file name.txt
// 2: hello sausage?/../z.txt
var file1Move = file1
var file2Move = file2
// check happy path, i.e. no naming conflicts when rename and move are two
// separate operations
file2Move.Path = "other.txt"
src := fstest.NewObject(ctx, t, f, file2.Path)
dst, err := doMove(ctx, src, file2Move.Path)
if err == fs.ErrorCantMove {
t.Skip("FS can't move")
}
require.NoError(t, err)
// check file exists in new listing
fstest.CheckListing(t, f, []fstest.Item{file1, file2Move})
// Check dst lightly - list above has checked ModTime/Hashes
assert.Equal(t, file2Move.Path, dst.Remote())
// 1: file name.txt
// 2: other.txt
// Check conflict on "rename, then move"
file1Move.Path = "moveTest/other.txt"
src = fstest.NewObject(ctx, t, f, file1.Path)
_, err = doMove(ctx, src, file1Move.Path)
require.NoError(t, err)
fstest.CheckListing(t, f, []fstest.Item{file1Move, file2Move})
// 1: moveTest/other.txt
// 2: other.txt
// Check conflict on "move, then rename"
src = fstest.NewObject(ctx, t, f, file1Move.Path)
_, err = doMove(ctx, src, file1.Path)
require.NoError(t, err)
fstest.CheckListing(t, f, []fstest.Item{file1, file2Move})
// 1: file name.txt
// 2: other.txt
src = fstest.NewObject(ctx, t, f, file2Move.Path)
_, err = doMove(ctx, src, file2.Path)
require.NoError(t, err)
fstest.CheckListing(t, f, []fstest.Item{file1, file2})
// 1: file name.txt
// 2: hello sausage?/../z.txt
// Tidy up moveTest directory
require.NoError(t, f.Rmdir(ctx, "moveTest"))
// Test that server side moving files does the correct thing with metadata
t.Run("Metadata", func(t *testing.T) {
if !f.Features().WriteMetadata {
t.Skip("Skipping test as can't write metadata")
}
ctx, ci := fs.AddConfig(ctx)
ci.Metadata = true
// Create file with metadata
const name = "test metadata move.txt"
const newName = "test metadata moved.txt"
t1 := fstest.Time("2003-02-03T04:05:06.499999999Z")
t2 := fstest.Time("2004-03-03T04:05:06.499999999Z")
file := fstest.NewItem(name, name, t1)
contents := random.String(100)
var testMetadata = fs.Metadata{
// System metadata supported by all backends
"mtime": t1.Format(time.RFC3339Nano),
// User metadata
"potato": "jersey",
}
o := PutTestContentsMetadata(ctx, t, f, &file, false, contents, true, "text/plain", testMetadata)
fstest.CheckEntryMetadata(ctx, t, f, o, testMetadata)
// Move it with --metadata-set
ci.MetadataSet = fs.Metadata{
// System metadata supported by all backends
"mtime": t2.Format(time.RFC3339Nano),
// User metadata
"potato": "royal",
}
newO, err := doMove(ctx, o, newName)
require.NoError(t, err)
file.Path = newName
file.ModTime = t2
fstest.CheckListing(t, f, []fstest.Item{file1, file2, file})
// Check metadata is correct
fstest.CheckEntryMetadata(ctx, t, f, newO, ci.MetadataSet)
newO = fstest.NewObject(ctx, t, f, newName)
fstest.CheckEntryMetadata(ctx, t, f, newO, ci.MetadataSet)
// Remove test file
require.NoError(t, newO.Remove(ctx))
})
})
// Move src to this remote using server-side move operations.
//
// Will only be called if src.Fs().Name() == f.Name()
//
// If it isn't possible then return fs.ErrorCantDirMove
//
// If destination exists then return fs.ErrorDirExists
// TestFsDirMove tests DirMove
//
// go test -v -run 'TestIntegration/Test(Setup|Init|FsMkdir|FsPutFile1|FsPutFile2|FsUpdateFile1|FsDirMove)$
t.Run("FsDirMove", func(t *testing.T) {
skipIfNotOk(t)
// Check have DirMove
doDirMove := f.Features().DirMove
if doDirMove == nil {
t.Skip("FS has no DirMover interface")
}
// Check it can't move onto itself
err := doDirMove(ctx, f, "", "")
require.Equal(t, fs.ErrorDirExists, err)
// new remote
newRemote, _, removeNewRemote, err := fstest.RandomRemote()
require.NoError(t, err)
defer removeNewRemote()
const newName = "new_name/sub_new_name"
// try the move
err = newRemote.Features().DirMove(ctx, f, "", newName)
require.NoError(t, err)
// check remotes
// remote should not exist here
_, err = f.List(ctx, "")
assert.True(t, errors.Is(err, fs.ErrorDirNotFound))
//fstest.CheckListingWithPrecision(t, remote, []fstest.Item{}, []string{}, remote.Precision())
file1Copy := file1
file1Copy.Path = path.Join(newName, file1.Path)
file2Copy := file2
file2Copy.Path = path.Join(newName, file2.Path)
fstest.CheckListingWithPrecision(t, newRemote, []fstest.Item{file2Copy, file1Copy}, []string{
"new_name",
"new_name/sub_new_name",
"new_name/sub_new_name/hello? sausage",
"new_name/sub_new_name/hello? sausage/êé",
"new_name/sub_new_name/hello? sausage/êé/Hello, 世界",
"new_name/sub_new_name/hello? sausage/êé/Hello, 世界/ \" ' @ < > & ? + ≠",
}, newRemote.Precision())
// move it back
err = doDirMove(ctx, newRemote, newName, "")
require.NoError(t, err)
// check remotes
fstest.CheckListingWithPrecision(t, f, []fstest.Item{file2, file1}, []string{
"hello? sausage",
"hello? sausage/êé",
"hello? sausage/êé/Hello, 世界",
"hello? sausage/êé/Hello, 世界/ \" ' @ < > & ? + ≠",
}, f.Precision())
fstest.CheckListingWithPrecision(t, newRemote, []fstest.Item{}, []string{
"new_name",
}, newRemote.Precision())
})
// TestFsRmdirFull tests removing a non empty directory
t.Run("FsRmdirFull", func(t *testing.T) {
skipIfNotOk(t)
if isBucketBasedButNotRoot(f) {
t.Skip("Skipping test as non root bucket-based remote")
}
err := f.Rmdir(ctx, "")
require.Error(t, err, "Expecting error on RMdir on non empty remote")
})
// TestFsPrecision tests the Precision of the Fs
t.Run("FsPrecision", func(t *testing.T) {
skipIfNotOk(t)
precision := f.Precision()
if precision == fs.ModTimeNotSupported {
return
}
if precision > time.Second || precision < 0 {
t.Fatalf("Precision out of range %v", precision)
}
// FIXME check expected precision
})
// TestObjectString tests the Object String method
t.Run("ObjectString", func(t *testing.T) {
skipIfNotOk(t)
obj := fstest.NewObject(ctx, t, f, file1.Path)
assert.Equal(t, file1.Path, obj.String())
if opt.NilObject != nil {
assert.Equal(t, "<nil>", opt.NilObject.String())
}
})
// TestObjectFs tests the object can be found
t.Run("ObjectFs", func(t *testing.T) {
skipIfNotOk(t)
obj := fstest.NewObject(ctx, t, f, file1.Path)
// If this is set we don't do the direct comparison of
// the Fs from the object as it may be different
if opt.SkipFsMatch {
return
}
testRemote := f
if obj.Fs() != testRemote {
// Check to see if this wraps something else
if doUnWrap := testRemote.Features().UnWrap; doUnWrap != nil {
testRemote = doUnWrap()
}
}
assert.Equal(t, obj.Fs(), testRemote)
})
// TestObjectRemote tests the Remote is correct
t.Run("ObjectRemote", func(t *testing.T) {
skipIfNotOk(t)
obj := fstest.NewObject(ctx, t, f, file1.Path)
assert.Equal(t, file1.Path, obj.Remote())
})
// TestObjectHashes checks all the hashes the object supports
t.Run("ObjectHashes", func(t *testing.T) {
skipIfNotOk(t)
obj := fstest.NewObject(ctx, t, f, file1.Path)
file1.CheckHashes(t, obj)
})
// TestObjectModTime tests the ModTime of the object is correct
TestObjectModTime := func(t *testing.T) {
skipIfNotOk(t)
obj := fstest.NewObject(ctx, t, f, file1.Path)
file1.CheckModTime(t, obj, obj.ModTime(ctx), f.Precision())
}
t.Run("ObjectModTime", TestObjectModTime)
// TestObjectMimeType tests the MimeType of the object is correct
t.Run("ObjectMimeType", func(t *testing.T) {
skipIfNotOk(t)
features := f.Features()
obj := fstest.NewObject(ctx, t, f, file1.Path)
do, ok := obj.(fs.MimeTyper)
if !ok {
require.False(t, features.ReadMimeType, "Features.ReadMimeType is set but Object.MimeType method not found")
t.Skip("MimeType method not supported")
}
mimeType := do.MimeType(ctx)
if !features.ReadMimeType {
require.Equal(t, "", mimeType, "Features.ReadMimeType is not set but Object.MimeType returned a non-empty MimeType")
} else if features.WriteMimeType {
assert.Equal(t, file1MimeType, mimeType, "can read and write mime types but failed")
} else {
if strings.ContainsRune(mimeType, ';') {
assert.Equal(t, "text/plain; charset=utf-8", mimeType)
} else {
assert.Equal(t, "text/plain", mimeType)
}
}
})
// TestObjectMetadata tests the Metadata of the object is correct
t.Run("ObjectMetadata", func(t *testing.T) {
skipIfNotOk(t)
ctx, ci := fs.AddConfig(ctx)
ci.Metadata = true
features := f.Features()
obj := fstest.NewObject(ctx, t, f, file1.Path)
do, objectHasMetadata := obj.(fs.Metadataer)
if objectHasMetadata || features.ReadMetadata || features.WriteMetadata || features.UserMetadata {
fsInfo := fs.FindFromFs(f)
require.NotNil(t, fsInfo)
require.NotNil(t, fsInfo.MetadataInfo, "Object declares metadata support but no MetadataInfo in RegInfo")
}
if !objectHasMetadata {
require.False(t, features.ReadMetadata, "Features.ReadMetadata is set but Object.Metadata method not found")
t.Skip("Metadata method not supported")
}
metadata, err := do.Metadata(ctx)
require.NoError(t, err)
// check standard metadata
for k, v := range metadata {
switch k {
case "atime", "btime", "mtime":
mtime, err := time.Parse(time.RFC3339Nano, v)
require.NoError(t, err)
if k == "mtime" {
fstest.AssertTimeEqualWithPrecision(t, file1.Path, file1.ModTime, mtime, f.Precision())
}
}
}
if !features.ReadMetadata {
if metadata != nil && !features.Overlay {
require.Equal(t, "", metadata, "Features.ReadMetadata is not set but Object.Metadata returned a non nil Metadata: %#v", metadata)
}
} else if features.WriteMetadata {
require.NotNil(t, metadata)
if features.UserMetadata {
// check all the metadata bits we uploaded are present - there may be more we didn't write
for k, v := range file1Metadata {
assert.Equal(t, v, metadata[k], "can read and write metadata but failed on key %q (want=%+v, got=%+v)", k, file1Metadata, metadata)
}
}
// Now test we can set the mtime and content-type via the metadata and these take precedence
t.Run("mtime", func(t *testing.T) {
path := "metadatatest"
mtimeModTime := fstest.Time("2002-02-03T04:05:06.499999999Z")
modTime := fstest.Time("2003-02-03T04:05:06.499999999Z")
item := fstest.NewItem(path, path, modTime)
metaMimeType := "application/zip"
mimeType := "application/gzip"
metadata := fs.Metadata{
"mtime": mtimeModTime.Format(time.RFC3339Nano),
"content-type": metaMimeType,
}
// This checks the mtime is correct also and returns the re-read object
_, obj := testPutMimeType(ctx, t, f, &item, mimeType, metadata)
defer func() {
assert.NoError(t, obj.Remove(ctx))
}()
// Check content-type got updated too
if features.ReadMimeType && features.WriteMimeType {
// read the object from scratch
o, err := f.NewObject(ctx, path)
require.NoError(t, err)
// Check the mimetype is correct
do, ok := o.(fs.MimeTyper)
require.True(t, ok)
gotMimeType := do.MimeType(ctx)
assert.Equal(t, metaMimeType, gotMimeType)
}
})
} // else: Have some metadata here we didn't write - can't really check it!
})
// TestObjectSetMetadata tests the SetMetadata of the object
t.Run("ObjectSetMetadata", func(t *testing.T) {
skipIfNotOk(t)
ctx, ci := fs.AddConfig(ctx)
ci.Metadata = true
features := f.Features()
// Test to see if SetMetadata is supported on an existing object before creating a new one
obj := fstest.NewObject(ctx, t, f, file1.Path)
_, objectHasSetMetadata := obj.(fs.SetMetadataer)
if !objectHasSetMetadata {
t.Skip("SetMetadata method not supported")
}
if !features.Overlay {
require.True(t, features.WriteMetadata, "Features.WriteMetadata is false but Object.SetMetadata found")
}
if !features.ReadMetadata {
t.Skip("SetMetadata can't be tested without ReadMetadata")
}
// Create file with metadata
const fileName = "test set metadata.txt"
t1 := fstest.Time("2003-02-03T04:05:06.499999999Z")
t2 := fstest.Time("2004-03-03T04:05:06.499999999Z")
contents := random.String(100)
file := fstest.NewItem(fileName, contents, t1)
var testMetadata = fs.Metadata{
// System metadata supported by all backends
"mtime": t1.Format(time.RFC3339Nano),
// User metadata
"potato": "jersey",
}
obj = PutTestContentsMetadata(ctx, t, f, &file, true, contents, true, "text/plain", testMetadata)
fstest.CheckEntryMetadata(ctx, t, f, obj, testMetadata)
do, objectHasSetMetadata := obj.(fs.SetMetadataer)
require.True(t, objectHasSetMetadata)
// Set new metadata
err := do.SetMetadata(ctx, fs.Metadata{
// System metadata supported by all backends
"mtime": t2.Format(time.RFC3339Nano),
// User metadata
"potato": "royal",
})
if err == fs.ErrorNotImplemented {
t.Log("SetMetadata returned fs.ErrorNotImplemented")
} else {
require.NoError(t, err)
file.ModTime = t2
fstest.CheckListing(t, f, []fstest.Item{file1, file2, file})
// Check metadata is correct
fstest.CheckEntryMetadata(ctx, t, f, obj, ci.MetadataSet)
obj = fstest.NewObject(ctx, t, f, fileName)
fstest.CheckEntryMetadata(ctx, t, f, obj, ci.MetadataSet)
}
// Remove test file
require.NoError(t, obj.Remove(ctx))
})
// TestObjectSetModTime tests that SetModTime works
t.Run("ObjectSetModTime", func(t *testing.T) {
skipIfNotOk(t)
newModTime := fstest.Time("2011-12-13T14:15:16.999999999Z")
obj := fstest.NewObject(ctx, t, f, file1.Path)
err := obj.SetModTime(ctx, newModTime)
if err == fs.ErrorCantSetModTime || err == fs.ErrorCantSetModTimeWithoutDelete {
t.Log(err)
return
}
require.NoError(t, err)
file1.ModTime = newModTime
file1.CheckModTime(t, obj, obj.ModTime(ctx), f.Precision())
// And make a new object and read it from there too
TestObjectModTime(t)
})
// TestObjectSize tests that Size works
t.Run("ObjectSize", func(t *testing.T) {
skipIfNotOk(t)
obj := fstest.NewObject(ctx, t, f, file1.Path)
assert.Equal(t, file1.Size, obj.Size())
})
// TestObjectOpen tests that Open works
t.Run("ObjectOpen", func(t *testing.T) {
skipIfNotOk(t)
obj := fstest.NewObject(ctx, t, f, file1.Path)
assert.Equal(t, file1Contents, ReadObject(ctx, t, obj, -1), "contents of file1 differ")
})
// TestObjectOpenSeek tests that Open works with SeekOption
t.Run("ObjectOpenSeek", func(t *testing.T) {
skipIfNotOk(t)
obj := fstest.NewObject(ctx, t, f, file1.Path)
assert.Equal(t, file1Contents[50:], ReadObject(ctx, t, obj, -1, &fs.SeekOption{Offset: 50}), "contents of file1 differ after seek")
})
// TestObjectOpenRange tests that Open works with RangeOption
//
// go test -v -run 'TestIntegration/Test(Setup|Init|FsMkdir|FsPutFile1|FsPutFile2|FsUpdateFile1|ObjectOpenRange)$'
t.Run("ObjectOpenRange", func(t *testing.T) {
skipIfNotOk(t)
obj := fstest.NewObject(ctx, t, f, file1.Path)
for _, test := range []struct {
ro fs.RangeOption
wantStart, wantEnd int
}{
{fs.RangeOption{Start: 5, End: 15}, 5, 16},
{fs.RangeOption{Start: 80, End: -1}, 80, 100},
{fs.RangeOption{Start: 81, End: 100000}, 81, 100},
{fs.RangeOption{Start: -1, End: 20}, 80, 100}, // if start is omitted this means get the final bytes
// {fs.RangeOption{Start: -1, End: -1}, 0, 100}, - this seems to work but the RFC doesn't define it
} {
got := ReadObject(ctx, t, obj, -1, &test.ro)
foundAt := strings.Index(file1Contents, got)
help := fmt.Sprintf("%#v failed want [%d:%d] got [%d:%d]", test.ro, test.wantStart, test.wantEnd, foundAt, foundAt+len(got))
assert.Equal(t, file1Contents[test.wantStart:test.wantEnd], got, help)
}
})
// TestObjectPartialRead tests that reading only part of the object does the correct thing
t.Run("ObjectPartialRead", func(t *testing.T) {
skipIfNotOk(t)
obj := fstest.NewObject(ctx, t, f, file1.Path)
assert.Equal(t, file1Contents[:50], ReadObject(ctx, t, obj, 50), "contents of file1 differ after limited read")
})
// TestObjectUpdate tests that Update works
t.Run("ObjectUpdate", func(t *testing.T) {
skipIfNotOk(t)
contents := random.String(200)
var h *hash.MultiHasher
file1.Size = int64(len(contents))
obj := fstest.NewObject(ctx, t, f, file1.Path)
remoteBefore := obj.Remote()
obji := object.NewStaticObjectInfo(file1.Path+"-should-be-ignored.bin", file1.ModTime, int64(len(contents)), true, nil, obj.Fs())
retry(t, "Update object", func() error {
buf := bytes.NewBufferString(contents)
h = hash.NewMultiHasher()
in := io.TeeReader(buf, h)
return obj.Update(ctx, in, obji)
})
remoteAfter := obj.Remote()
assert.Equal(t, remoteBefore, remoteAfter, "Remote should not change")
file1.Hashes = h.Sums()
// check the object has been updated
file1.Check(t, obj, f.Precision())
// Re-read the object and check again
obj = fstest.NewObject(ctx, t, f, file1.Path)
file1.Check(t, obj, f.Precision())
// check contents correct
assert.Equal(t, contents, ReadObject(ctx, t, obj, -1), "contents of updated file1 differ")
file1Contents = contents
})
// TestObjectStorable tests that Storable works
t.Run("ObjectStorable", func(t *testing.T) {
skipIfNotOk(t)
obj := fstest.NewObject(ctx, t, f, file1.Path)
require.NotNil(t, !obj.Storable(), "Expecting object to be storable")
})
// TestFsIsFile tests that an error is returned along with a valid fs
// which points to the parent directory.
t.Run("FsIsFile", func(t *testing.T) {
skipIfNotOk(t)
remoteName := subRemoteName + "/" + file2.Path
file2Copy := file2
file2Copy.Path = "z.txt"
fileRemote, err := fs.NewFs(context.Background(), remoteName)
require.NotNil(t, fileRemote)
assert.Equal(t, fs.ErrorIsFile, err)
// Check Fs.Root returns the right thing
t.Run("FsRoot", func(t *testing.T) {
skipIfNotOk(t)
got := fileRemote.Root()
remoteDir := path.Dir(remoteName)
want := remoteDir
colon := strings.LastIndex(want, ":")
if colon >= 0 {
want = want[colon+1:]
}
if isLocalRemote {
// only check last path element on local
require.Equal(t, filepath.Base(remoteDir), filepath.Base(got))
} else {
require.Equal(t, want, got)
}
})
if strings.HasPrefix(remoteName, "TestChunker") && strings.Contains(remoteName, "Nometa") {
// TODO fix chunker and remove this bypass
t.Logf("Skip listing check -- chunker can't yet handle this tricky case")
return
}
fstest.CheckListing(t, fileRemote, []fstest.Item{file2Copy})
})
// TestFsIsFileNotFound tests that an error is not returned if no object is found
t.Run("FsIsFileNotFound", func(t *testing.T) {
skipIfNotOk(t)
remoteName := subRemoteName + "/not found.txt"
fileRemote, err := fs.NewFs(context.Background(), remoteName)
require.NoError(t, err)
fstest.CheckListing(t, fileRemote, []fstest.Item{})
})
// Test that things work from the root
t.Run("FromRoot", func(t *testing.T) {
if features := f.Features(); features.BucketBased && !features.BucketBasedRootOK {
t.Skip("Can't list from root on this remote")
}
parsed, err := fspath.Parse(subRemoteName)
require.NoError(t, err)
configName, configLeaf := parsed.ConfigString, parsed.Path
if configName == "" {
configName, configLeaf = path.Split(subRemoteName)
} else {
configName += ":"
}
t.Logf("Opening root remote %q path %q from %q", configName, configLeaf, subRemoteName)
rootRemote, err := fs.NewFs(context.Background(), configName)
require.NoError(t, err)
file1Root := file1
file1Root.Path = path.Join(configLeaf, file1Root.Path)
file2Root := file2
file2Root.Path = path.Join(configLeaf, file2Root.Path)
var dirs []string
dir := file2.Path
for {
dir = path.Dir(dir)
if dir == "" || dir == "." || dir == "/" {
break
}
dirs = append(dirs, path.Join(configLeaf, dir))
}
// Check that we can see file1 and file2 from the root
t.Run("List", func(t *testing.T) {
fstest.CheckListingWithRoot(t, rootRemote, configLeaf, []fstest.Item{file1Root, file2Root}, dirs, rootRemote.Precision())
})
// Check that listing the entries is OK
t.Run("ListEntries", func(t *testing.T) {
entries, err := rootRemote.List(context.Background(), configLeaf)
require.NoError(t, err)
fstest.CompareItems(t, entries, []fstest.Item{file1Root}, dirs[len(dirs)-1:], rootRemote.Precision(), "ListEntries")
})
// List the root with ListR
t.Run("ListR", func(t *testing.T) {
doListR := rootRemote.Features().ListR
if doListR == nil {
t.Skip("FS has no ListR interface")
}
file1Found, file2Found := false, false
stopTime := time.Now().Add(10 * time.Second)
errTooMany := errors.New("too many files")
errFound := errors.New("found")
err := doListR(context.Background(), "", func(entries fs.DirEntries) error {
for _, entry := range entries {
remote := entry.Remote()
if remote == file1Root.Path {
file1Found = true
}
if remote == file2Root.Path {
file2Found = true
}
if file1Found && file2Found {
return errFound
}
}
if time.Now().After(stopTime) {
return errTooMany
}
return nil
})
if !errors.Is(err, errFound) && !errors.Is(err, errTooMany) {
assert.NoError(t, err)
}
if !errors.Is(err, errTooMany) {
assert.True(t, file1Found, "file1Root %q not found", file1Root.Path)
assert.True(t, file2Found, "file2Root %q not found", file2Root.Path)
} else {
t.Logf("Too many files to list - giving up")
}
})
// Create a new file
t.Run("Put", func(t *testing.T) {
file3Root := fstest.Item{
ModTime: time.Now(),
Path: path.Join(configLeaf, "created from root.txt"),
}
_, file3Obj := testPut(ctx, t, rootRemote, &file3Root)
fstest.CheckListingWithRoot(t, rootRemote, configLeaf, []fstest.Item{file1Root, file2Root, file3Root}, nil, rootRemote.Precision())
// And then remove it
t.Run("Remove", func(t *testing.T) {
require.NoError(t, file3Obj.Remove(context.Background()))
fstest.CheckListingWithRoot(t, rootRemote, configLeaf, []fstest.Item{file1Root, file2Root}, nil, rootRemote.Precision())
})
})
})
// TestPublicLink tests creation of sharable, public links
// go test -v -run 'TestIntegration/Test(Setup|Init|FsMkdir|FsPutFile1|FsPutFile2|FsUpdateFile1|PublicLink)$'
t.Run("PublicLink", func(t *testing.T) {
skipIfNotOk(t)
publicLinkFunc := f.Features().PublicLink
if publicLinkFunc == nil {
t.Skip("FS has no PublicLinker interface")
}
type PublicLinkFunc func(ctx context.Context, remote string, expire fs.Duration, unlink bool) (link string, err error)
wrapPublicLinkFunc := func(f PublicLinkFunc) PublicLinkFunc {
return func(ctx context.Context, remote string, expire fs.Duration, unlink bool) (link string, err error) {
link, err = publicLinkFunc(ctx, remote, expire, unlink)
if err == nil {
return
}
// For OneDrive Personal, link expiry is a premium feature
// Don't let it fail the test (https://github.com/rclone/rclone/issues/5420)
if fsInfo.Name == "onedrive" && strings.Contains(err.Error(), "accountUpgradeRequired") {
t.Log("treating accountUpgradeRequired as success for PublicLink")
link, err = "bogus link to "+remote, nil
}
return
}
}
expiry := fs.Duration(120 * time.Second)
doPublicLink := wrapPublicLinkFunc(publicLinkFunc)
// if object not found
link, err := doPublicLink(ctx, file1.Path+"_does_not_exist", expiry, false)
require.Error(t, err, "Expected to get error when file doesn't exist")
require.Equal(t, "", link, "Expected link to be empty on error")
// sharing file for the first time
link1, err := doPublicLink(ctx, file1.Path, expiry, false)
require.NoError(t, err)
require.NotEqual(t, "", link1, "Link should not be empty")
link2, err := doPublicLink(ctx, file2.Path, expiry, false)
require.NoError(t, err)
require.NotEqual(t, "", link2, "Link should not be empty")
require.NotEqual(t, link1, link2, "Links to different files should differ")
// sharing file for the 2nd time
link1, err = doPublicLink(ctx, file1.Path, expiry, false)
require.NoError(t, err)
require.NotEqual(t, "", link1, "Link should not be empty")
// sharing directory for the first time
path := path.Dir(file2.Path)
link3, err := doPublicLink(ctx, path, expiry, false)
if err != nil && (errors.Is(err, fs.ErrorCantShareDirectories) || errors.Is(err, fs.ErrorObjectNotFound)) {
t.Log("skipping directory tests as not supported on this backend")
} else {
require.NoError(t, err)
require.NotEqual(t, "", link3, "Link should not be empty")
// sharing directory for the second time
link3, err = doPublicLink(ctx, path, expiry, false)
require.NoError(t, err)
require.NotEqual(t, "", link3, "Link should not be empty")
// sharing the "root" directory in a subremote
subRemote, _, removeSubRemote, err := fstest.RandomRemote()
require.NoError(t, err)
defer removeSubRemote()
// ensure sub remote isn't empty
buf := bytes.NewBufferString("somecontent")
obji := object.NewStaticObjectInfo("somefile", time.Now(), int64(buf.Len()), true, nil, nil)
retry(t, "Put", func() error {
_, err := subRemote.Put(ctx, buf, obji)
return err
})
link4, err := wrapPublicLinkFunc(subRemote.Features().PublicLink)(ctx, "", expiry, false)
require.NoError(t, err, "Sharing root in a sub-remote should work")
require.NotEqual(t, "", link4, "Link should not be empty")
}
})
// TestSetTier tests SetTier and GetTier functionality
t.Run("SetTier", func(t *testing.T) {
skipIfNotSetTier(t)
obj := fstest.NewObject(ctx, t, f, file1.Path)
setter, ok := obj.(fs.SetTierer)
assert.NotNil(t, ok)
getter, ok := obj.(fs.GetTierer)
assert.NotNil(t, ok)
// If interfaces are supported TiersToTest should contain
// at least one entry
supportedTiers := opt.TiersToTest
assert.NotEmpty(t, supportedTiers)
// test set tier changes on supported storage classes or tiers
for _, tier := range supportedTiers {
err := setter.SetTier(tier)
assert.Nil(t, err)
got := getter.GetTier()
assert.Equal(t, tier, got)
}
})
// Check to see if Fs that wrap other Objects implement all the optional methods
t.Run("ObjectCheckWrap", func(t *testing.T) {
skipIfNotOk(t)
if opt.SkipObjectCheckWrap {
t.Skip("Skipping FsCheckWrap on this Fs")
}
ft := new(fs.Features).Fill(ctx, f)
if ft.UnWrap == nil {
t.Skip("Not a wrapping Fs")
}
obj := fstest.NewObject(ctx, t, f, file1.Path)
_, unsupported := fs.ObjectOptionalInterfaces(obj)
for _, name := range unsupported {
if !stringsContains(name, opt.UnimplementableObjectMethods) {
t.Errorf("Missing Object wrapper for %s", name)
}
}
})
// State of remote at the moment the internal tests are called
InternalTestFiles = []fstest.Item{file1, file2}
// TestObjectRemove tests Remove
t.Run("ObjectRemove", func(t *testing.T) {
skipIfNotOk(t)
// remove file1
obj := fstest.NewObject(ctx, t, f, file1.Path)
err := obj.Remove(ctx)
require.NoError(t, err)
// check listing without modtime as TestPublicLink may change the modtime
fstest.CheckListingWithPrecision(t, f, []fstest.Item{file2}, nil, fs.ModTimeNotSupported)
// Show the internal tests file2 is gone
InternalTestFiles = []fstest.Item{file2}
})
// TestAbout tests the About optional interface
t.Run("ObjectAbout", func(t *testing.T) {
skipIfNotOk(t)
// Check have About
doAbout := f.Features().About
if doAbout == nil {
t.Skip("FS does not support About")
}
// Can't really check the output much!
usage, err := doAbout(context.Background())
require.NoError(t, err)
require.NotNil(t, usage)
assert.NotEqual(t, int64(0), usage.Total)
})
// Just file2 remains for Purge to clean up
// TestFsPutStream tests uploading files when size isn't known in advance.
// This may trigger large buffer allocation in some backends, keep it
// close to the end of suite. (See fs/operations/xtra_operations_test.go)
t.Run("FsPutStream", func(t *testing.T) {
skipIfNotOk(t)
if f.Features().PutStream == nil {
t.Skip("FS has no PutStream interface")
}
for _, contentSize := range []int{0, 100} {
t.Run(strconv.Itoa(contentSize), func(t *testing.T) {
file := fstest.Item{
ModTime: fstest.Time("2001-02-03T04:05:06.499999999Z"),
Path: "piped data.txt",
Size: -1, // use unknown size during upload
}
var (
err error
obj fs.Object
uploadHash *hash.MultiHasher
)
retry(t, "PutStream", func() error {
contents := random.String(contentSize)
buf := bytes.NewBufferString(contents)
uploadHash = hash.NewMultiHasher()
in := io.TeeReader(buf, uploadHash)
file.Size = -1
obji := object.NewStaticObjectInfo(file.Path, file.ModTime, file.Size, true, nil, nil)
obj, err = f.Features().PutStream(ctx, in, obji)
return err
})
file.Hashes = uploadHash.Sums()
file.Size = int64(contentSize) // use correct size when checking
file.Check(t, obj, f.Precision())
// Re-read the object and check again
obj = fstest.NewObject(ctx, t, f, file.Path)
file.Check(t, obj, f.Precision())
require.NoError(t, obj.Remove(ctx))
})
}
})
// TestInternal calls InternalTest() on the Fs
t.Run("Internal", func(t *testing.T) {
skipIfNotOk(t)
if it, ok := f.(InternalTester); ok {
it.InternalTest(t)
} else {
t.Skipf("%T does not implement InternalTester", f)
}
})
})
// TestFsPutChunked may trigger large buffer allocation with
// some backends (see fs/operations/xtra_operations_test.go),
// keep it closer to the end of suite.
t.Run("FsPutChunked", func(t *testing.T) {
skipIfNotOk(t)
if testing.Short() {
t.Skip("not running with -short")
}
if opt.ChunkedUpload.Skip {
t.Skip("skipping as ChunkedUpload.Skip is set")
}
setUploadChunkSizer, _ := f.(SetUploadChunkSizer)
if setUploadChunkSizer == nil {
t.Skipf("%T does not implement SetUploadChunkSizer", f)
}
setUploadCutoffer, _ := f.(SetUploadCutoffer)
minChunkSize := opt.ChunkedUpload.MinChunkSize
if minChunkSize < 100 {
minChunkSize = 100
}
if opt.ChunkedUpload.CeilChunkSize != nil {
minChunkSize = opt.ChunkedUpload.CeilChunkSize(minChunkSize)
}
maxChunkSize := 2 * fs.Mebi
if maxChunkSize < 2*minChunkSize {
maxChunkSize = 2 * minChunkSize
}
if opt.ChunkedUpload.MaxChunkSize > 0 && maxChunkSize > opt.ChunkedUpload.MaxChunkSize {
maxChunkSize = opt.ChunkedUpload.MaxChunkSize
}
if opt.ChunkedUpload.CeilChunkSize != nil {
maxChunkSize = opt.ChunkedUpload.CeilChunkSize(maxChunkSize)
}
next := func(f func(fs.SizeSuffix) fs.SizeSuffix) fs.SizeSuffix {
s := f(minChunkSize)
if s > maxChunkSize {
s = minChunkSize
}
return s
}
chunkSizes := fs.SizeSuffixList{
minChunkSize,
minChunkSize + (maxChunkSize-minChunkSize)/3,
next(NextPowerOfTwo),
next(NextMultipleOf(100000)),
next(NextMultipleOf(100001)),
maxChunkSize,
}
chunkSizes.Sort()
// Set the minimum chunk size, upload cutoff and reset it at the end
oldChunkSize, err := setUploadChunkSizer.SetUploadChunkSize(minChunkSize)
require.NoError(t, err)
var oldUploadCutoff fs.SizeSuffix
if setUploadCutoffer != nil {
oldUploadCutoff, err = setUploadCutoffer.SetUploadCutoff(minChunkSize)
require.NoError(t, err)
}
defer func() {
_, err := setUploadChunkSizer.SetUploadChunkSize(oldChunkSize)
assert.NoError(t, err)
if setUploadCutoffer != nil {
_, err := setUploadCutoffer.SetUploadCutoff(oldUploadCutoff)
assert.NoError(t, err)
}
}()
var lastCs fs.SizeSuffix
for _, cs := range chunkSizes {
if cs <= lastCs {
continue
}
if opt.ChunkedUpload.CeilChunkSize != nil {
cs = opt.ChunkedUpload.CeilChunkSize(cs)
}
lastCs = cs
t.Run(cs.String(), func(t *testing.T) {
_, err := setUploadChunkSizer.SetUploadChunkSize(cs)
require.NoError(t, err)
if setUploadCutoffer != nil {
_, err = setUploadCutoffer.SetUploadCutoff(cs)
require.NoError(t, err)
}
var testChunks []fs.SizeSuffix
if opt.ChunkedUpload.NeedMultipleChunks {
// If NeedMultipleChunks is set then test with > cs
testChunks = []fs.SizeSuffix{cs + 1, 2 * cs, 2*cs + 1}
} else {
testChunks = []fs.SizeSuffix{cs - 1, cs, 2*cs + 1}
}
for _, fileSize := range testChunks {
t.Run(fmt.Sprintf("%d", fileSize), func(t *testing.T) {
TestPutLarge(ctx, t, f, &fstest.Item{
ModTime: fstest.Time("2001-02-03T04:05:06.499999999Z"),
Path: fmt.Sprintf("chunked-%s-%s.bin", cs.String(), fileSize.String()),
Size: int64(fileSize),
})
t.Run("Streamed", func(t *testing.T) {
if f.Features().PutStream == nil {
t.Skip("FS has no PutStream interface")
}
TestPutLargeStreamed(ctx, t, f, &fstest.Item{
ModTime: fstest.Time("2001-02-03T04:05:06.499999999Z"),
Path: fmt.Sprintf("chunked-%s-%s-streamed.bin", cs.String(), fileSize.String()),
Size: int64(fileSize),
})
})
})
}
})
}
})
// Copy files with chunked copy if available
t.Run("FsCopyChunked", func(t *testing.T) {
skipIfNotOk(t)
if testing.Short() {
t.Skip("not running with -short")
}
// Check have Copy
doCopy := f.Features().Copy
if doCopy == nil {
t.Skip("FS has no Copier interface")
}
if opt.ChunkedUpload.Skip {
t.Skip("skipping as ChunkedUpload.Skip is set")
}
do, _ := f.(SetCopyCutoffer)
if do == nil {
t.Skipf("%T does not implement SetCopyCutoff", f)
}
minChunkSize := opt.ChunkedUpload.MinChunkSize
if minChunkSize < 100 {
minChunkSize = 100
}
if opt.ChunkedUpload.CeilChunkSize != nil {
minChunkSize = opt.ChunkedUpload.CeilChunkSize(minChunkSize)
}
// Test setting the copy cutoff before we get going
_, err := do.SetCopyCutoff(minChunkSize)
if errors.Is(err, fs.ErrorNotImplemented) {
t.Skipf("%T does not support SetCopyCutoff: %v", f, err)
}
require.NoError(t, err)
chunkSizes := fs.SizeSuffixList{
minChunkSize,
minChunkSize + 1,
2*minChunkSize - 1,
2 * minChunkSize,
2*minChunkSize + 1,
}
for _, chunkSize := range chunkSizes {
t.Run(fmt.Sprintf("%d", chunkSize), func(t *testing.T) {
contents := random.String(int(chunkSize))
item := fstest.NewItem("chunked-copy", contents, fstest.Time("2001-05-06T04:05:06.499999999Z"))
src := PutTestContents(ctx, t, f, &item, contents, true)
defer func() {
assert.NoError(t, src.Remove(ctx))
}()
var itemCopy = item
itemCopy.Path += ".copy"
// Set copy cutoff to mininum value so we make chunks
origCutoff, err := do.SetCopyCutoff(minChunkSize)
require.NoError(t, err)
defer func() {
_, err = do.SetCopyCutoff(origCutoff)
require.NoError(t, err)
}()
// Do the copy
dst, err := doCopy(ctx, src, itemCopy.Path)
require.NoError(t, err)
defer func() {
assert.NoError(t, dst.Remove(ctx))
}()
// Check size
assert.Equal(t, src.Size(), dst.Size())
// Check modtime
srcModTime := src.ModTime(ctx)
dstModTime := dst.ModTime(ctx)
assert.True(t, srcModTime.Equal(dstModTime))
// Make sure contents are correct
gotContents := ReadObject(ctx, t, dst, -1)
assert.Equal(t, contents, gotContents)
})
}
})
// TestFsUploadUnknownSize ensures Fs.Put() and Object.Update() don't panic when
// src.Size() == -1
//
// This may trigger large buffer allocation in some backends, keep it
// closer to the suite end. (See fs/operations/xtra_operations_test.go)
t.Run("FsUploadUnknownSize", func(t *testing.T) {
skipIfNotOk(t)
t.Run("FsPutUnknownSize", func(t *testing.T) {
defer func() {
assert.Nil(t, recover(), "Fs.Put() should not panic when src.Size() == -1")
}()
contents := random.String(100)
in := bytes.NewBufferString(contents)
obji := object.NewStaticObjectInfo("unknown-size-put.txt", fstest.Time("2002-02-03T04:05:06.499999999Z"), -1, true, nil, nil)
obj, err := f.Put(ctx, in, obji)
if err == nil {
require.NoError(t, obj.Remove(ctx), "successfully uploaded unknown-sized file but failed to remove")
}
// if err != nil: it's okay as long as no panic
})
t.Run("FsUpdateUnknownSize", func(t *testing.T) {
unknownSizeUpdateFile := fstest.Item{
ModTime: fstest.Time("2002-02-03T04:05:06.499999999Z"),
Path: "unknown-size-update.txt",
}
testPut(ctx, t, f, &unknownSizeUpdateFile)
defer func() {
assert.Nil(t, recover(), "Object.Update() should not panic when src.Size() == -1")
}()
newContents := random.String(200)
in := bytes.NewBufferString(newContents)
obj := fstest.NewObject(ctx, t, f, unknownSizeUpdateFile.Path)
obji := object.NewStaticObjectInfo(unknownSizeUpdateFile.Path, unknownSizeUpdateFile.ModTime, -1, true, nil, obj.Fs())
err := obj.Update(ctx, in, obji)
if err == nil {
require.NoError(t, obj.Remove(ctx), "successfully updated object with unknown-sized source but failed to remove")
}
// if err != nil: it's okay as long as no panic
})
})
// TestFsRootCollapse tests if the root of an fs "collapses" to the
// absolute root. It creates a new fs of the same backend type with its
// root set to a *nonexistent* folder, and attempts to read the info of
// an object in that folder, whose name is taken from a directory that
// exists in the absolute root.
// This test is added after
// https://github.com/rclone/rclone/issues/3164.
t.Run("FsRootCollapse", func(t *testing.T) {
deepRemoteName := subRemoteName + "/deeper/nonexisting/directory"
deepRemote, err := fs.NewFs(context.Background(), deepRemoteName)
require.NoError(t, err)
colonIndex := strings.IndexRune(deepRemoteName, ':')
firstSlashIndex := strings.IndexRune(deepRemoteName, '/')
firstDir := deepRemoteName[colonIndex+1 : firstSlashIndex]
_, err = deepRemote.NewObject(ctx, firstDir)
require.Equal(t, fs.ErrorObjectNotFound, err)
// If err is not fs.ErrorObjectNotFound, it means the backend is
// somehow confused about root and absolute root.
})
// FsDirSetModTime tests setting the mod time on a directory if possible
t.Run("FsDirSetModTime", func(t *testing.T) {
const name = "dir-mod-time"
do := f.Features().DirSetModTime
if do == nil {
t.Skip("FS has no DirSetModTime interface")
}
// Set ModTime on non existing directory should return error
t1 := fstest.Time("2001-02-03T04:05:06.499999999Z")
err := do(ctx, name, t1)
require.Error(t, err)
// Make the directory and try again
err = f.Mkdir(ctx, name)
require.NoError(t, err)
err = do(ctx, name, t1)
require.NoError(t, err)
// Check the modtime got set properly
dir := fstest.NewDirectory(ctx, t, f, name)
fstest.CheckDirModTime(ctx, t, f, dir, t1)
// Tidy up
err = f.Rmdir(ctx, name)
require.NoError(t, err)
})
var testMetadata = fs.Metadata{
// System metadata supported by all backends
"mtime": "2001-02-03T04:05:06.499999999Z",
// User metadata
"potato": "jersey",
}
var testMetadata2 = fs.Metadata{
// System metadata supported by all backends
"mtime": "2002-02-03T04:05:06.499999999Z",
// User metadata
"potato": "king edwards",
}
// FsMkdirMetadata tests creating a directory with metadata if possible
t.Run("FsMkdirMetadata", func(t *testing.T) {
ctx, ci := fs.AddConfig(ctx)
ci.Metadata = true
const name = "dir-metadata"
do := f.Features().MkdirMetadata
if do == nil {
t.Skip("FS has no MkdirMetadata interface")
}
assert.True(t, f.Features().WriteDirMetadata, "Backends must support Directory.SetMetadata and Fs.MkdirMetadata")
// Create the directory from fresh
dir, err := do(ctx, name, testMetadata)
require.NoError(t, err)
require.NotNil(t, dir)
// Check the returned directory and one read from the listing
fstest.CheckEntryMetadata(ctx, t, f, dir, testMetadata)
fstest.CheckEntryMetadata(ctx, t, f, fstest.NewDirectory(ctx, t, f, name), testMetadata)
// Now update the metadata on the existing directory
t.Run("Update", func(t *testing.T) {
dir, err := do(ctx, name, testMetadata2)
require.NoError(t, err)
require.NotNil(t, dir)
// Check the returned directory and one read from the listing
fstest.CheckEntryMetadata(ctx, t, f, dir, testMetadata2)
// The TestUnionPolicy2 has randomness in it so it sets metadata on
// one directory but can read a different one from the listing.
if f.Name() != "TestUnionPolicy2" {
fstest.CheckEntryMetadata(ctx, t, f, fstest.NewDirectory(ctx, t, f, name), testMetadata2)
}
})
// Now test the Directory methods
t.Run("CheckDirectory", func(t *testing.T) {
_, ok := dir.(fs.Object)
assert.False(t, ok, "Directory must not type assert to Object")
_, ok = dir.(fs.ObjectInfo)
assert.False(t, ok, "Directory must not type assert to ObjectInfo")
})
// Tidy up
err = f.Rmdir(ctx, name)
require.NoError(t, err)
})
// FsDirectory checks methods on the directory object
t.Run("FsDirectory", func(t *testing.T) {
ctx, ci := fs.AddConfig(ctx)
ci.Metadata = true
const name = "dir-methods"
features := f.Features()
if !features.CanHaveEmptyDirectories {
t.Skip("Can't test if can't have empty directories")
}
if !features.ReadDirMetadata &&
!features.WriteDirMetadata &&
!features.WriteDirSetModTime &&
!features.UserDirMetadata &&
!features.Overlay &&
features.UnWrap == nil {
t.Skip("FS has no Directory methods and doesn't Wrap")
}
// Create a directory to start with
err := f.Mkdir(ctx, name)
require.NoError(t, err)
// Get the directory object
dir := fstest.NewDirectory(ctx, t, f, name)
_, ok := dir.(fs.Object)
assert.False(t, ok, "Directory must not type assert to Object")
_, ok = dir.(fs.ObjectInfo)
assert.False(t, ok, "Directory must not type assert to ObjectInfo")
// Now test the directory methods
t.Run("ReadDirMetadata", func(t *testing.T) {
if !features.ReadDirMetadata {
t.Skip("Directories don't support ReadDirMetadata")
}
if f.Name() == "TestUnionPolicy3" {
t.Skipf("Test unreliable on %q", f.Name())
}
fstest.CheckEntryMetadata(ctx, t, f, dir, fs.Metadata{
"mtime": dir.ModTime(ctx).Format(time.RFC3339Nano),
})
})
t.Run("WriteDirMetadata", func(t *testing.T) {
if !features.WriteDirMetadata {
t.Skip("Directories don't support WriteDirMetadata")
}
assert.NotNil(t, features.MkdirMetadata, "Backends must support Directory.SetMetadata and Fs.MkdirMetadata")
do, ok := dir.(fs.SetMetadataer)
require.True(t, ok, "Expected to find SetMetadata method on Directory")
err := do.SetMetadata(ctx, testMetadata)
require.NoError(t, err)
fstest.CheckEntryMetadata(ctx, t, f, dir, testMetadata)
fstest.CheckEntryMetadata(ctx, t, f, fstest.NewDirectory(ctx, t, f, name), testMetadata)
})
t.Run("WriteDirSetModTime", func(t *testing.T) {
if !features.WriteDirSetModTime {
t.Skip("Directories don't support WriteDirSetModTime")
}
assert.NotNil(t, features.DirSetModTime, "Backends must support Directory.SetModTime and Fs.DirSetModTime")
t1 := fstest.Time("2001-02-03T04:05:10.123123123Z")
do, ok := dir.(fs.SetModTimer)
require.True(t, ok, "Expected to find SetMetadata method on Directory")
err := do.SetModTime(ctx, t1)
require.NoError(t, err)
fstest.CheckDirModTime(ctx, t, f, dir, t1)
fstest.CheckDirModTime(ctx, t, f, fstest.NewDirectory(ctx, t, f, name), t1)
})
// Check to see if Fs that wrap other Directories implement all the optional methods
t.Run("DirectoryCheckWrap", func(t *testing.T) {
if opt.SkipDirectoryCheckWrap {
t.Skip("Skipping DirectoryCheckWrap on this Fs")
}
if !features.Overlay && features.UnWrap == nil {
t.Skip("Not a wrapping Fs")
}
_, unsupported := fs.DirectoryOptionalInterfaces(dir)
for _, name := range unsupported {
if !stringsContains(name, opt.UnimplementableDirectoryMethods) {
t.Errorf("Missing Directory wrapper for %s", name)
}
}
})
// Tidy up
err = f.Rmdir(ctx, name)
require.NoError(t, err)
})
// Purge the folder
err = operations.Purge(ctx, f, "")
if !errors.Is(err, fs.ErrorDirNotFound) {
require.NoError(t, err)
}
purged = true
fstest.CheckListing(t, f, []fstest.Item{})
// Check purging again if not bucket-based
if !isBucketBasedButNotRoot(f) {
err = operations.Purge(ctx, f, "")
assert.Error(t, err, "Expecting error after on second purge")
if !errors.Is(err, fs.ErrorDirNotFound) {
t.Log("Warning: this should produce fs.ErrorDirNotFound")
}
}
})
// Check directory is purged
if !purged {
_ = operations.Purge(ctx, f, "")
}
t.Run("FsShutdown", func(t *testing.T) {
do := f.Features().Shutdown
if do == nil {
t.Skip("Shutdown method not supported")
}
require.NoError(t, do(ctx))
require.NoError(t, do(ctx), "must be able to call Shutdown twice")
})
// Remove the local directory so we don't clutter up /tmp
if strings.HasPrefix(remoteName, "/") {
t.Log("remoteName", remoteName)
// Remove temp directory
err := os.Remove(remoteName)
require.NoError(t, err)
}
}