github.com/ncw/rclone@v1.48.1-0.20190724201158-a35aa1360e3e/fstest/fstest.go (about)

     1  // Package fstest provides utilities for testing the Fs
     2  package fstest
     3  
     4  // FIXME put name of test FS in Fs structure
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"flag"
    10  	"fmt"
    11  	"io"
    12  	"io/ioutil"
    13  	"log"
    14  	"math/rand"
    15  	"os"
    16  	"path"
    17  	"path/filepath"
    18  	"regexp"
    19  	"runtime"
    20  	"sort"
    21  	"strings"
    22  	"testing"
    23  	"time"
    24  
    25  	"github.com/ncw/rclone/fs"
    26  	"github.com/ncw/rclone/fs/accounting"
    27  	"github.com/ncw/rclone/fs/config"
    28  	"github.com/ncw/rclone/fs/hash"
    29  	"github.com/ncw/rclone/fs/walk"
    30  	"github.com/stretchr/testify/assert"
    31  	"github.com/stretchr/testify/require"
    32  	"golang.org/x/text/unicode/norm"
    33  )
    34  
    35  // Globals
    36  var (
    37  	RemoteName      = flag.String("remote", "", "Remote to test with, defaults to local filesystem")
    38  	SubDir          = flag.Bool("subdir", false, "Set to test with a sub directory")
    39  	Verbose         = flag.Bool("verbose", false, "Set to enable logging")
    40  	DumpHeaders     = flag.Bool("dump-headers", false, "Set to dump headers (needs -verbose)")
    41  	DumpBodies      = flag.Bool("dump-bodies", false, "Set to dump bodies (needs -verbose)")
    42  	Individual      = flag.Bool("individual", false, "Make individual bucket/container/directory for each test - much slower")
    43  	LowLevelRetries = flag.Int("low-level-retries", 10, "Number of low level retries")
    44  	UseListR        = flag.Bool("fast-list", false, "Use recursive list if available. Uses more memory but fewer transactions.")
    45  	// ListRetries is the number of times to retry a listing to overcome eventual consistency
    46  	ListRetries = flag.Int("list-retries", 6, "Number or times to retry listing")
    47  	// MatchTestRemote matches the remote names used for testing
    48  	MatchTestRemote = regexp.MustCompile(`^rclone-test-[abcdefghijklmnopqrstuvwxyz0123456789]{24}$`)
    49  )
    50  
    51  // Seed the random number generator
    52  func init() {
    53  	rand.Seed(time.Now().UnixNano())
    54  
    55  }
    56  
    57  // Initialise rclone for testing
    58  func Initialise() {
    59  	// Never ask for passwords, fail instead.
    60  	// If your local config is encrypted set environment variable
    61  	// "RCLONE_CONFIG_PASS=hunter2" (or your password)
    62  	fs.Config.AskPassword = false
    63  	// Override the config file from the environment - we don't
    64  	// parse the flags any more so this doesn't happen
    65  	// automatically
    66  	if envConfig := os.Getenv("RCLONE_CONFIG"); envConfig != "" {
    67  		config.ConfigPath = envConfig
    68  	}
    69  	config.LoadConfig()
    70  	if *Verbose {
    71  		fs.Config.LogLevel = fs.LogLevelDebug
    72  	}
    73  	if *DumpHeaders {
    74  		fs.Config.Dump |= fs.DumpHeaders
    75  	}
    76  	if *DumpBodies {
    77  		fs.Config.Dump |= fs.DumpBodies
    78  	}
    79  	fs.Config.LowLevelRetries = *LowLevelRetries
    80  	fs.Config.UseListR = *UseListR
    81  }
    82  
    83  // Item represents an item for checking
    84  type Item struct {
    85  	Path    string
    86  	Hashes  map[hash.Type]string
    87  	ModTime time.Time
    88  	Size    int64
    89  	WinPath string
    90  }
    91  
    92  // NewItem creates an item from a string content
    93  func NewItem(Path, Content string, modTime time.Time) Item {
    94  	i := Item{
    95  		Path:    Path,
    96  		ModTime: modTime,
    97  		Size:    int64(len(Content)),
    98  	}
    99  	hash := hash.NewMultiHasher()
   100  	buf := bytes.NewBufferString(Content)
   101  	_, err := io.Copy(hash, buf)
   102  	if err != nil {
   103  		log.Fatalf("Failed to create item: %v", err)
   104  	}
   105  	i.Hashes = hash.Sums()
   106  	return i
   107  }
   108  
   109  // CheckTimeEqualWithPrecision checks the times are equal within the
   110  // precision, returns the delta and a flag
   111  func CheckTimeEqualWithPrecision(t0, t1 time.Time, precision time.Duration) (time.Duration, bool) {
   112  	dt := t0.Sub(t1)
   113  	if dt >= precision || dt <= -precision {
   114  		return dt, false
   115  	}
   116  	return dt, true
   117  }
   118  
   119  // CheckModTime checks the mod time to the given precision
   120  func (i *Item) CheckModTime(t *testing.T, obj fs.Object, modTime time.Time, precision time.Duration) {
   121  	dt, ok := CheckTimeEqualWithPrecision(modTime, i.ModTime, precision)
   122  	assert.True(t, ok, fmt.Sprintf("%s: Modification time difference too big |%s| > %s (%s vs %s) (precision %s)", obj.Remote(), dt, precision, modTime, i.ModTime, precision))
   123  }
   124  
   125  // CheckHashes checks all the hashes the object supports are correct
   126  func (i *Item) CheckHashes(t *testing.T, obj fs.Object) {
   127  	require.NotNil(t, obj)
   128  	types := obj.Fs().Hashes().Array()
   129  	for _, Hash := range types {
   130  		// Check attributes
   131  		sum, err := obj.Hash(context.Background(), Hash)
   132  		require.NoError(t, err)
   133  		assert.True(t, hash.Equals(i.Hashes[Hash], sum), fmt.Sprintf("%s/%s: %v hash incorrect - expecting %q got %q", obj.Fs().String(), obj.Remote(), Hash, i.Hashes[Hash], sum))
   134  	}
   135  }
   136  
   137  // Check checks all the attributes of the object are correct
   138  func (i *Item) Check(t *testing.T, obj fs.Object, precision time.Duration) {
   139  	i.CheckHashes(t, obj)
   140  	assert.Equal(t, i.Size, obj.Size(), fmt.Sprintf("%s: size incorrect file=%d vs obj=%d", i.Path, i.Size, obj.Size()))
   141  	i.CheckModTime(t, obj, obj.ModTime(context.Background()), precision)
   142  }
   143  
   144  // WinPath converts a path into a windows safe path
   145  func WinPath(s string) string {
   146  	return strings.Map(func(r rune) rune {
   147  		switch r {
   148  		case '<', '>', '"', '|', '?', '*', ':':
   149  			return '_'
   150  		}
   151  		return r
   152  	}, s)
   153  }
   154  
   155  // Normalize runs a utf8 normalization on the string if running on OS
   156  // X.  This is because OS X denormalizes file names it writes to the
   157  // local file system.
   158  func Normalize(name string) string {
   159  	if runtime.GOOS == "darwin" {
   160  		name = norm.NFC.String(name)
   161  	}
   162  	return name
   163  }
   164  
   165  // Items represents all items for checking
   166  type Items struct {
   167  	byName    map[string]*Item
   168  	byNameAlt map[string]*Item
   169  	items     []Item
   170  }
   171  
   172  // NewItems makes an Items
   173  func NewItems(items []Item) *Items {
   174  	is := &Items{
   175  		byName:    make(map[string]*Item),
   176  		byNameAlt: make(map[string]*Item),
   177  		items:     items,
   178  	}
   179  	// Fill up byName
   180  	for i := range items {
   181  		is.byName[Normalize(items[i].Path)] = &items[i]
   182  		is.byNameAlt[Normalize(items[i].WinPath)] = &items[i]
   183  	}
   184  	return is
   185  }
   186  
   187  // Find checks off an item
   188  func (is *Items) Find(t *testing.T, obj fs.Object, precision time.Duration) {
   189  	remote := Normalize(obj.Remote())
   190  	i, ok := is.byName[remote]
   191  	if !ok {
   192  		i, ok = is.byNameAlt[remote]
   193  		assert.True(t, ok, fmt.Sprintf("Unexpected file %q", remote))
   194  	}
   195  	if i != nil {
   196  		delete(is.byName, i.Path)
   197  		delete(is.byName, i.WinPath)
   198  		i.Check(t, obj, precision)
   199  	}
   200  }
   201  
   202  // Done checks all finished
   203  func (is *Items) Done(t *testing.T) {
   204  	if len(is.byName) != 0 {
   205  		for name := range is.byName {
   206  			t.Logf("Not found %q", name)
   207  		}
   208  	}
   209  	assert.Equal(t, 0, len(is.byName), fmt.Sprintf("%d objects not found", len(is.byName)))
   210  }
   211  
   212  // makeListingFromItems returns a string representation of the items
   213  //
   214  // it returns two possible strings, one normal and one for windows
   215  func makeListingFromItems(items []Item) (string, string) {
   216  	nameLengths1 := make([]string, len(items))
   217  	nameLengths2 := make([]string, len(items))
   218  	for i, item := range items {
   219  		remote1 := Normalize(item.Path)
   220  		remote2 := remote1
   221  		if item.WinPath != "" {
   222  			remote2 = item.WinPath
   223  		}
   224  		nameLengths1[i] = fmt.Sprintf("%s (%d)", remote1, item.Size)
   225  		nameLengths2[i] = fmt.Sprintf("%s (%d)", remote2, item.Size)
   226  	}
   227  	sort.Strings(nameLengths1)
   228  	sort.Strings(nameLengths2)
   229  	return strings.Join(nameLengths1, ", "), strings.Join(nameLengths2, ", ")
   230  }
   231  
   232  // makeListingFromObjects returns a string representation of the objects
   233  func makeListingFromObjects(objs []fs.Object) string {
   234  	nameLengths := make([]string, len(objs))
   235  	for i, obj := range objs {
   236  		nameLengths[i] = fmt.Sprintf("%s (%d)", Normalize(obj.Remote()), obj.Size())
   237  	}
   238  	sort.Strings(nameLengths)
   239  	return strings.Join(nameLengths, ", ")
   240  }
   241  
   242  // filterEmptyDirs removes any empty (or containing only directories)
   243  // directories from expectedDirs
   244  func filterEmptyDirs(t *testing.T, items []Item, expectedDirs []string) (newExpectedDirs []string) {
   245  	dirs := map[string]struct{}{"": struct{}{}}
   246  	for _, item := range items {
   247  		base := item.Path
   248  		for {
   249  			base = path.Dir(base)
   250  			if base == "." || base == "/" {
   251  				break
   252  			}
   253  			dirs[base] = struct{}{}
   254  		}
   255  	}
   256  	for _, expectedDir := range expectedDirs {
   257  		if _, found := dirs[expectedDir]; found {
   258  			newExpectedDirs = append(newExpectedDirs, expectedDir)
   259  		} else {
   260  			t.Logf("Filtering empty directory %q", expectedDir)
   261  		}
   262  	}
   263  	return newExpectedDirs
   264  }
   265  
   266  // CheckListingWithPrecision checks the fs to see if it has the
   267  // expected contents with the given precision.
   268  //
   269  // If expectedDirs is non nil then we check those too.  Note that no
   270  // directories returned is also OK as some remotes don't return
   271  // directories.
   272  func CheckListingWithPrecision(t *testing.T, f fs.Fs, items []Item, expectedDirs []string, precision time.Duration) {
   273  	if expectedDirs != nil && !f.Features().CanHaveEmptyDirectories {
   274  		expectedDirs = filterEmptyDirs(t, items, expectedDirs)
   275  	}
   276  	is := NewItems(items)
   277  	oldErrors := accounting.Stats.GetErrors()
   278  	var objs []fs.Object
   279  	var dirs []fs.Directory
   280  	var err error
   281  	var retries = *ListRetries
   282  	sleep := time.Second / 2
   283  	wantListing1, wantListing2 := makeListingFromItems(items)
   284  	gotListing := "<unset>"
   285  	listingOK := false
   286  	ctx := context.Background()
   287  	for i := 1; i <= retries; i++ {
   288  		objs, dirs, err = walk.GetAll(ctx, f, "", true, -1)
   289  		if err != nil && err != fs.ErrorDirNotFound {
   290  			t.Fatalf("Error listing: %v", err)
   291  		}
   292  
   293  		gotListing = makeListingFromObjects(objs)
   294  		listingOK = wantListing1 == gotListing || wantListing2 == gotListing
   295  		if listingOK && (expectedDirs == nil || len(dirs) == len(expectedDirs)) {
   296  			// Put an extra sleep in if we did any retries just to make sure it really
   297  			// is consistent (here is looking at you Amazon Drive!)
   298  			if i != 1 {
   299  				extraSleep := 5*time.Second + sleep
   300  				t.Logf("Sleeping for %v just to make sure", extraSleep)
   301  				time.Sleep(extraSleep)
   302  			}
   303  			break
   304  		}
   305  		sleep *= 2
   306  		t.Logf("Sleeping for %v for list eventual consistency: %d/%d", sleep, i, retries)
   307  		time.Sleep(sleep)
   308  		if doDirCacheFlush := f.Features().DirCacheFlush; doDirCacheFlush != nil {
   309  			t.Logf("Flushing the directory cache")
   310  			doDirCacheFlush()
   311  		}
   312  	}
   313  	assert.True(t, listingOK, fmt.Sprintf("listing wrong, want\n  %s or\n  %s got\n  %s", wantListing1, wantListing2, gotListing))
   314  	for _, obj := range objs {
   315  		require.NotNil(t, obj)
   316  		is.Find(t, obj, precision)
   317  	}
   318  	is.Done(t)
   319  	// Don't notice an error when listing an empty directory
   320  	if len(items) == 0 && oldErrors == 0 && accounting.Stats.GetErrors() == 1 {
   321  		accounting.Stats.ResetErrors()
   322  	}
   323  	// Check the directories
   324  	if expectedDirs != nil {
   325  		expectedDirsCopy := make([]string, len(expectedDirs))
   326  		for i, dir := range expectedDirs {
   327  			expectedDirsCopy[i] = WinPath(Normalize(dir))
   328  		}
   329  		actualDirs := []string{}
   330  		for _, dir := range dirs {
   331  			actualDirs = append(actualDirs, WinPath(Normalize(dir.Remote())))
   332  		}
   333  		sort.Strings(actualDirs)
   334  		sort.Strings(expectedDirsCopy)
   335  		assert.Equal(t, expectedDirsCopy, actualDirs, "directories")
   336  	}
   337  }
   338  
   339  // CheckListing checks the fs to see if it has the expected contents
   340  func CheckListing(t *testing.T, f fs.Fs, items []Item) {
   341  	precision := f.Precision()
   342  	CheckListingWithPrecision(t, f, items, nil, precision)
   343  }
   344  
   345  // CheckItems checks the fs to see if it has only the items passed in
   346  // using a precision of fs.Config.ModifyWindow
   347  func CheckItems(t *testing.T, f fs.Fs, items ...Item) {
   348  	CheckListingWithPrecision(t, f, items, nil, fs.GetModifyWindow(f))
   349  }
   350  
   351  // Time parses a time string or logs a fatal error
   352  func Time(timeString string) time.Time {
   353  	t, err := time.Parse(time.RFC3339Nano, timeString)
   354  	if err != nil {
   355  		log.Fatalf("Failed to parse time %q: %v", timeString, err)
   356  	}
   357  	return t
   358  }
   359  
   360  // RandomString create a random string for test purposes
   361  func RandomString(n int) string {
   362  	const (
   363  		vowel     = "aeiou"
   364  		consonant = "bcdfghjklmnpqrstvwxyz"
   365  		digit     = "0123456789"
   366  	)
   367  	pattern := []string{consonant, vowel, consonant, vowel, consonant, vowel, consonant, digit}
   368  	out := make([]byte, n)
   369  	p := 0
   370  	for i := range out {
   371  		source := pattern[p]
   372  		p = (p + 1) % len(pattern)
   373  		out[i] = source[rand.Intn(len(source))]
   374  	}
   375  	return string(out)
   376  }
   377  
   378  // LocalRemote creates a temporary directory name for local remotes
   379  func LocalRemote() (path string, err error) {
   380  	path, err = ioutil.TempDir("", "rclone")
   381  	if err == nil {
   382  		// Now remove the directory
   383  		err = os.Remove(path)
   384  	}
   385  	path = filepath.ToSlash(path)
   386  	return
   387  }
   388  
   389  // RandomRemoteName makes a random bucket or subdirectory name
   390  //
   391  // Returns a random remote name plus the leaf name
   392  func RandomRemoteName(remoteName string) (string, string, error) {
   393  	var err error
   394  	var leafName string
   395  
   396  	// Make a directory if remote name is null
   397  	if remoteName == "" {
   398  		remoteName, err = LocalRemote()
   399  		if err != nil {
   400  			return "", "", err
   401  		}
   402  	} else {
   403  		if !strings.HasSuffix(remoteName, ":") {
   404  			remoteName += "/"
   405  		}
   406  		leafName = "rclone-test-" + RandomString(24)
   407  		if !MatchTestRemote.MatchString(leafName) {
   408  			log.Fatalf("%q didn't match the test remote name regexp", leafName)
   409  		}
   410  		remoteName += leafName
   411  	}
   412  	return remoteName, leafName, nil
   413  }
   414  
   415  // RandomRemote makes a random bucket or subdirectory on the remote
   416  //
   417  // Call the finalise function returned to Purge the fs at the end (and
   418  // the parent if necessary)
   419  //
   420  // Returns the remote, its url, a finaliser and an error
   421  func RandomRemote(remoteName string, subdir bool) (fs.Fs, string, func(), error) {
   422  	var err error
   423  	var parentRemote fs.Fs
   424  
   425  	remoteName, _, err = RandomRemoteName(remoteName)
   426  	if err != nil {
   427  		return nil, "", nil, err
   428  	}
   429  
   430  	if subdir {
   431  		parentRemote, err = fs.NewFs(remoteName)
   432  		if err != nil {
   433  			return nil, "", nil, err
   434  		}
   435  		remoteName += "/rclone-test-subdir-" + RandomString(8)
   436  	}
   437  
   438  	remote, err := fs.NewFs(remoteName)
   439  	if err != nil {
   440  		return nil, "", nil, err
   441  	}
   442  
   443  	finalise := func() {
   444  		Purge(remote)
   445  		if parentRemote != nil {
   446  			Purge(parentRemote)
   447  			if err != nil {
   448  				log.Printf("Failed to purge %v: %v", parentRemote, err)
   449  			}
   450  		}
   451  	}
   452  
   453  	return remote, remoteName, finalise, nil
   454  }
   455  
   456  // Purge is a simplified re-implementation of operations.Purge for the
   457  // test routine cleanup to avoid circular dependencies.
   458  //
   459  // It logs errors rather than returning them
   460  func Purge(f fs.Fs) {
   461  	ctx := context.Background()
   462  	var err error
   463  	doFallbackPurge := true
   464  	if doPurge := f.Features().Purge; doPurge != nil {
   465  		doFallbackPurge = false
   466  		fs.Debugf(f, "Purge remote")
   467  		err = doPurge(ctx)
   468  		if err == fs.ErrorCantPurge {
   469  			doFallbackPurge = true
   470  		}
   471  	}
   472  	if doFallbackPurge {
   473  		dirs := []string{""}
   474  		err = walk.ListR(ctx, f, "", true, -1, walk.ListAll, func(entries fs.DirEntries) error {
   475  			var err error
   476  			entries.ForObject(func(obj fs.Object) {
   477  				fs.Debugf(f, "Purge object %q", obj.Remote())
   478  				err = obj.Remove(ctx)
   479  				if err != nil {
   480  					log.Printf("purge failed to remove %q: %v", obj.Remote(), err)
   481  				}
   482  			})
   483  			entries.ForDir(func(dir fs.Directory) {
   484  				dirs = append(dirs, dir.Remote())
   485  			})
   486  			return nil
   487  		})
   488  		sort.Strings(dirs)
   489  		for i := len(dirs) - 1; i >= 0; i-- {
   490  			dir := dirs[i]
   491  			fs.Debugf(f, "Purge dir %q", dir)
   492  			err := f.Rmdir(ctx, dir)
   493  			if err != nil {
   494  				log.Printf("purge failed to rmdir %q: %v", dir, err)
   495  			}
   496  		}
   497  	}
   498  	if err != nil {
   499  		log.Printf("purge failed: %v", err)
   500  	}
   501  }