rclone/fstest/run.go
database64128 a7a8372976
🧪 fstest: fix time tests on Windows and add convenience methods to check local and remote fs with precision
Previously only the fs being checked on gets passed to
GetModifyWindow(). However, in most tests, the test files are
generated in the local fs and transferred to the remote fs. So the
local fs time precision has to be taken into account.

This meant that on Windows the time tests failed because the
local fs has a time precision of 100ns. Checking remote items uploaded
from local fs on Windows also requires a modify window of 100ns.
2021-11-09 11:43:36 +00:00

381 lines
10 KiB
Go

/*
This provides Run for use in creating test suites
To use this declare a TestMain
// TestMain drives the tests
func TestMain(m *testing.M) {
fstest.TestMain(m)
}
And then make and destroy a Run in each test
func TestMkdir(t *testing.T) {
r := fstest.NewRun(t)
defer r.Finalise()
// test stuff
}
This will make r.Fremote and r.Flocal for a remote remote and a local
remote. The remote is determined by the -remote flag passed in.
*/
package fstest
import (
"bytes"
"context"
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"path"
"path/filepath"
"sort"
"testing"
"time"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/cache"
"github.com/rclone/rclone/fs/fserrors"
"github.com/rclone/rclone/fs/hash"
"github.com/rclone/rclone/fs/object"
"github.com/rclone/rclone/fs/walk"
"github.com/rclone/rclone/lib/file"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// Run holds the remotes for a test run
type Run struct {
LocalName string
Flocal fs.Fs
Fremote fs.Fs
FremoteName string
Precision time.Duration
cleanRemote func()
mkdir map[string]bool // whether the remote has been made yet for the fs name
Logf, Fatalf func(text string, args ...interface{})
}
// TestMain drives the tests
func TestMain(m *testing.M) {
flag.Parse()
if !*Individual {
oneRun = newRun()
}
rc := m.Run()
if !*Individual {
oneRun.Finalise()
}
os.Exit(rc)
}
// oneRun holds the master run data if individual is not set
var oneRun *Run
// newRun initialise the remote and local for testing and returns a
// run object.
//
// r.Flocal is an empty local Fs
// r.Fremote is an empty remote Fs
//
// Finalise() will tidy them away when done.
func newRun() *Run {
r := &Run{
Logf: log.Printf,
Fatalf: log.Fatalf,
mkdir: make(map[string]bool),
}
Initialise()
var err error
r.Fremote, r.FremoteName, r.cleanRemote, err = RandomRemote()
if err != nil {
r.Fatalf("Failed to open remote %q: %v", *RemoteName, err)
}
r.LocalName, err = ioutil.TempDir("", "rclone")
if err != nil {
r.Fatalf("Failed to create temp dir: %v", err)
}
r.LocalName = filepath.ToSlash(r.LocalName)
r.Flocal, err = fs.NewFs(context.Background(), r.LocalName)
if err != nil {
r.Fatalf("Failed to make %q: %v", r.LocalName, err)
}
r.Precision = fs.GetModifyWindow(context.Background(), r.Fremote, r.Flocal)
return r
}
// run f(), retrying it until it returns with no error or the limit
// expires and it calls t.Fatalf
func retry(t *testing.T, what string, f func() error) {
var err error
for try := 1; try <= *ListRetries; try++ {
err = f()
if err == nil {
return
}
t.Logf("%s failed - try %d/%d: %v", what, try, *ListRetries, err)
time.Sleep(time.Second)
}
t.Logf("%s failed: %v", what, err)
}
// newRunIndividual initialise the remote and local for testing and
// returns a run object. Pass in true to make individual tests or
// false to use the global one.
//
// r.Flocal is an empty local Fs
// r.Fremote is an empty remote Fs
//
// Finalise() will tidy them away when done.
func newRunIndividual(t *testing.T, individual bool) *Run {
ctx := context.Background()
var r *Run
if individual {
r = newRun()
} else {
// If not individual, use the global one with the clean method overridden
r = new(Run)
*r = *oneRun
r.cleanRemote = func() {
var toDelete []string
err := walk.ListR(ctx, r.Fremote, "", true, -1, walk.ListAll, func(entries fs.DirEntries) error {
for _, entry := range entries {
switch x := entry.(type) {
case fs.Object:
retry(t, fmt.Sprintf("removing file %q", x.Remote()), func() error { return x.Remove(ctx) })
case fs.Directory:
toDelete = append(toDelete, x.Remote())
}
}
return nil
})
if err == fs.ErrorDirNotFound {
return
}
require.NoError(t, err)
sort.Strings(toDelete)
for i := len(toDelete) - 1; i >= 0; i-- {
dir := toDelete[i]
retry(t, fmt.Sprintf("removing dir %q", dir), func() error {
return r.Fremote.Rmdir(ctx, dir)
})
}
// Check remote is empty
CheckListingWithPrecision(t, r.Fremote, []Item{}, []string{}, r.Fremote.Precision())
// Clear the remote cache
cache.Clear()
}
}
r.Logf = t.Logf
r.Fatalf = t.Fatalf
r.Logf("Remote %q, Local %q, Modify Window %q", r.Fremote, r.Flocal, fs.GetModifyWindow(ctx, r.Fremote))
return r
}
// NewRun initialise the remote and local for testing and returns a
// run object. Call this from the tests.
//
// r.Flocal is an empty local Fs
// r.Fremote is an empty remote Fs
//
// Finalise() will tidy them away when done.
func NewRun(t *testing.T) *Run {
return newRunIndividual(t, *Individual)
}
// NewRunIndividual as per NewRun but makes an individual remote for this test
func NewRunIndividual(t *testing.T) *Run {
return newRunIndividual(t, true)
}
// RenameFile renames a file in local
func (r *Run) RenameFile(item Item, newpath string) Item {
oldFilepath := path.Join(r.LocalName, item.Path)
newFilepath := path.Join(r.LocalName, newpath)
if err := os.Rename(oldFilepath, newFilepath); err != nil {
r.Fatalf("Failed to rename file from %q to %q: %v", item.Path, newpath, err)
}
item.Path = newpath
return item
}
// WriteFile writes a file to local
func (r *Run) WriteFile(filePath, content string, t time.Time) Item {
item := NewItem(filePath, content, t)
// FIXME make directories?
filePath = path.Join(r.LocalName, filePath)
dirPath := path.Dir(filePath)
err := file.MkdirAll(dirPath, 0770)
if err != nil {
r.Fatalf("Failed to make directories %q: %v", dirPath, err)
}
err = ioutil.WriteFile(filePath, []byte(content), 0600)
if err != nil {
r.Fatalf("Failed to write file %q: %v", filePath, err)
}
err = os.Chtimes(filePath, t, t)
if err != nil {
r.Fatalf("Failed to chtimes file %q: %v", filePath, err)
}
return item
}
// ForceMkdir creates the remote
func (r *Run) ForceMkdir(ctx context.Context, f fs.Fs) {
err := f.Mkdir(ctx, "")
if err != nil {
r.Fatalf("Failed to mkdir %q: %v", f, err)
}
r.mkdir[f.String()] = true
}
// Mkdir creates the remote if it hasn't been created already
func (r *Run) Mkdir(ctx context.Context, f fs.Fs) {
if !r.mkdir[f.String()] {
r.ForceMkdir(ctx, f)
}
}
// WriteObjectTo writes an object to the fs, remote passed in
func (r *Run) WriteObjectTo(ctx context.Context, f fs.Fs, remote, content string, modTime time.Time, useUnchecked bool) Item {
put := f.Put
if useUnchecked {
put = f.Features().PutUnchecked
if put == nil {
r.Fatalf("Fs doesn't support PutUnchecked")
}
}
r.Mkdir(ctx, f)
// calculate all hashes f supports for content
hash, err := hash.NewMultiHasherTypes(f.Hashes())
if err != nil {
r.Fatalf("Failed to make new multi hasher: %v", err)
}
_, err = hash.Write([]byte(content))
if err != nil {
r.Fatalf("Failed to make write to hash: %v", err)
}
hashSums := hash.Sums()
const maxTries = 10
for tries := 1; ; tries++ {
in := bytes.NewBufferString(content)
objinfo := object.NewStaticObjectInfo(remote, modTime, int64(len(content)), true, hashSums, nil)
_, err := put(ctx, in, objinfo)
if err == nil {
break
}
// Retry if err returned a retry error
if fserrors.IsRetryError(err) && tries < maxTries {
r.Logf("Retry Put of %q to %v: %d/%d (%v)", remote, f, tries, maxTries, err)
time.Sleep(2 * time.Second)
continue
}
r.Fatalf("Failed to put %q to %q: %v", remote, f, err)
}
return NewItem(remote, content, modTime)
}
// WriteObject writes an object to the remote
func (r *Run) WriteObject(ctx context.Context, remote, content string, modTime time.Time) Item {
return r.WriteObjectTo(ctx, r.Fremote, remote, content, modTime, false)
}
// WriteUncheckedObject writes an object to the remote not checking for duplicates
func (r *Run) WriteUncheckedObject(ctx context.Context, remote, content string, modTime time.Time) Item {
return r.WriteObjectTo(ctx, r.Fremote, remote, content, modTime, true)
}
// WriteBoth calls WriteObject and WriteFile with the same arguments
func (r *Run) WriteBoth(ctx context.Context, remote, content string, modTime time.Time) Item {
r.WriteFile(remote, content, modTime)
return r.WriteObject(ctx, remote, content, modTime)
}
// CheckWithDuplicates does a test but allows duplicates
func (r *Run) CheckWithDuplicates(t *testing.T, items ...Item) {
var want, got []string
// construct a []string of desired items
for _, item := range items {
want = append(want, fmt.Sprintf("%q %d", item.Path, item.Size))
}
sort.Strings(want)
// do the listing
objs, _, err := walk.GetAll(context.Background(), r.Fremote, "", true, -1)
if err != nil && err != fs.ErrorDirNotFound {
t.Fatalf("Error listing: %v", err)
}
// construct a []string of actual items
for _, o := range objs {
got = append(got, fmt.Sprintf("%q %d", o.Remote(), o.Size()))
}
sort.Strings(got)
assert.Equal(t, want, got)
}
// CheckLocalItems checks the local fs with proper precision
// to see if it has the expected items.
func (r *Run) CheckLocalItems(t *testing.T, items ...Item) {
CheckItemsWithPrecision(t, r.Flocal, r.Precision, items...)
}
// CheckRemoteItems checks the remote fs with proper precision
// to see if it has the expected items.
func (r *Run) CheckRemoteItems(t *testing.T, items ...Item) {
CheckItemsWithPrecision(t, r.Fremote, r.Precision, items...)
}
// CheckLocalListing checks the local fs with proper precision
// to see if it has the expected contents.
//
// If expectedDirs is non nil then we check those too. Note that no
// directories returned is also OK as some remotes don't return
// directories.
func (r *Run) CheckLocalListing(t *testing.T, items []Item, expectedDirs []string) {
CheckListingWithPrecision(t, r.Flocal, items, expectedDirs, r.Precision)
}
// CheckRemoteListing checks the remote fs with proper precision
// to see if it has the expected contents.
//
// If expectedDirs is non nil then we check those too. Note that no
// directories returned is also OK as some remotes don't return
// directories.
func (r *Run) CheckRemoteListing(t *testing.T, items []Item, expectedDirs []string) {
CheckListingWithPrecision(t, r.Fremote, items, expectedDirs, r.Precision)
}
// Clean the temporary directory
func (r *Run) cleanTempDir() {
err := os.RemoveAll(r.LocalName)
if err != nil {
r.Logf("Failed to clean temporary directory %q: %v", r.LocalName, err)
}
}
// Finalise cleans the remote and local
func (r *Run) Finalise() {
// r.Logf("Cleaning remote %q", r.Fremote)
r.cleanRemote()
// r.Logf("Cleaning local %q", r.LocalName)
r.cleanTempDir()
// Clear the remote cache
cache.Clear()
}