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 }