github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/fs/operations/check_test.go (about) 1 package operations_test 2 3 import ( 4 "bytes" 5 "context" 6 "errors" 7 "fmt" 8 "io" 9 "log" 10 "os" 11 "sort" 12 "strings" 13 "testing" 14 15 "github.com/rclone/rclone/fs" 16 "github.com/rclone/rclone/fs/accounting" 17 "github.com/rclone/rclone/fs/hash" 18 "github.com/rclone/rclone/fs/operations" 19 "github.com/rclone/rclone/fstest" 20 "github.com/rclone/rclone/lib/readers" 21 "github.com/stretchr/testify/assert" 22 "github.com/stretchr/testify/require" 23 "golang.org/x/text/unicode/norm" 24 ) 25 26 func testCheck(t *testing.T, checkFunction func(ctx context.Context, opt *operations.CheckOpt) error) { 27 r := fstest.NewRun(t) 28 ctx := context.Background() 29 ci := fs.GetConfig(ctx) 30 31 addBuffers := func(opt *operations.CheckOpt) { 32 opt.Combined = new(bytes.Buffer) 33 opt.MissingOnSrc = new(bytes.Buffer) 34 opt.MissingOnDst = new(bytes.Buffer) 35 opt.Match = new(bytes.Buffer) 36 opt.Differ = new(bytes.Buffer) 37 opt.Error = new(bytes.Buffer) 38 } 39 40 sortLines := func(in string) []string { 41 if in == "" { 42 return []string{} 43 } 44 lines := strings.Split(in, "\n") 45 sort.Strings(lines) 46 return lines 47 } 48 49 checkBuffer := func(name string, want map[string]string, out io.Writer) { 50 expected := want[name] 51 buf, ok := out.(*bytes.Buffer) 52 require.True(t, ok) 53 assert.Equal(t, sortLines(expected), sortLines(buf.String()), name) 54 } 55 56 checkBuffers := func(opt *operations.CheckOpt, want map[string]string) { 57 checkBuffer("combined", want, opt.Combined) 58 checkBuffer("missingonsrc", want, opt.MissingOnSrc) 59 checkBuffer("missingondst", want, opt.MissingOnDst) 60 checkBuffer("match", want, opt.Match) 61 checkBuffer("differ", want, opt.Differ) 62 checkBuffer("error", want, opt.Error) 63 } 64 65 check := func(i int, wantErrors int64, wantChecks int64, oneway bool, wantOutput map[string]string) { 66 t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { 67 accounting.GlobalStats().ResetCounters() 68 var buf bytes.Buffer 69 log.SetOutput(&buf) 70 defer func() { 71 log.SetOutput(os.Stderr) 72 }() 73 opt := operations.CheckOpt{ 74 Fdst: r.Fremote, 75 Fsrc: r.Flocal, 76 OneWay: oneway, 77 } 78 addBuffers(&opt) 79 err := checkFunction(ctx, &opt) 80 gotErrors := accounting.GlobalStats().GetErrors() 81 gotChecks := accounting.GlobalStats().GetChecks() 82 if wantErrors == 0 && err != nil { 83 t.Errorf("%d: Got error when not expecting one: %v", i, err) 84 } 85 if wantErrors != 0 && err == nil { 86 t.Errorf("%d: No error when expecting one", i) 87 } 88 if wantErrors != gotErrors { 89 t.Errorf("%d: Expecting %d errors but got %d", i, wantErrors, gotErrors) 90 } 91 if gotChecks > 0 && !strings.Contains(buf.String(), "matching files") { 92 t.Errorf("%d: Total files matching line missing", i) 93 } 94 if wantChecks != gotChecks { 95 t.Errorf("%d: Expecting %d total matching files but got %d", i, wantChecks, gotChecks) 96 } 97 checkBuffers(&opt, wantOutput) 98 }) 99 } 100 101 file1 := r.WriteBoth(ctx, "rutabaga", "is tasty", t3) 102 r.CheckRemoteItems(t, file1) 103 r.CheckLocalItems(t, file1) 104 check(1, 0, 1, false, map[string]string{ 105 "combined": "= rutabaga\n", 106 "missingonsrc": "", 107 "missingondst": "", 108 "match": "rutabaga\n", 109 "differ": "", 110 "error": "", 111 }) 112 113 file2 := r.WriteFile("potato2", "------------------------------------------------------------", t1) 114 r.CheckLocalItems(t, file1, file2) 115 check(2, 1, 1, false, map[string]string{ 116 "combined": "+ potato2\n= rutabaga\n", 117 "missingonsrc": "", 118 "missingondst": "potato2\n", 119 "match": "rutabaga\n", 120 "differ": "", 121 "error": "", 122 }) 123 124 file3 := r.WriteObject(ctx, "empty space", "-", t2) 125 r.CheckRemoteItems(t, file1, file3) 126 check(3, 2, 1, false, map[string]string{ 127 "combined": "- empty space\n+ potato2\n= rutabaga\n", 128 "missingonsrc": "empty space\n", 129 "missingondst": "potato2\n", 130 "match": "rutabaga\n", 131 "differ": "", 132 "error": "", 133 }) 134 135 file2r := file2 136 if ci.SizeOnly { 137 file2r = r.WriteObject(ctx, "potato2", "--Some-Differences-But-Size-Only-Is-Enabled-----------------", t1) 138 } else { 139 r.WriteObject(ctx, "potato2", "------------------------------------------------------------", t1) 140 } 141 r.CheckRemoteItems(t, file1, file2r, file3) 142 check(4, 1, 2, false, map[string]string{ 143 "combined": "- empty space\n= potato2\n= rutabaga\n", 144 "missingonsrc": "empty space\n", 145 "missingondst": "", 146 "match": "rutabaga\npotato2\n", 147 "differ": "", 148 "error": "", 149 }) 150 151 file3r := file3 152 file3l := r.WriteFile("empty space", "DIFFER", t2) 153 r.CheckLocalItems(t, file1, file2, file3l) 154 check(5, 1, 3, false, map[string]string{ 155 "combined": "* empty space\n= potato2\n= rutabaga\n", 156 "missingonsrc": "", 157 "missingondst": "", 158 "match": "potato2\nrutabaga\n", 159 "differ": "empty space\n", 160 "error": "", 161 }) 162 163 file4 := r.WriteObject(ctx, "remotepotato", "------------------------------------------------------------", t1) 164 r.CheckRemoteItems(t, file1, file2r, file3r, file4) 165 check(6, 2, 3, false, map[string]string{ 166 "combined": "* empty space\n= potato2\n= rutabaga\n- remotepotato\n", 167 "missingonsrc": "remotepotato\n", 168 "missingondst": "", 169 "match": "potato2\nrutabaga\n", 170 "differ": "empty space\n", 171 "error": "", 172 }) 173 check(7, 1, 3, true, map[string]string{ 174 "combined": "* empty space\n= potato2\n= rutabaga\n", 175 "missingonsrc": "", 176 "missingondst": "", 177 "match": "potato2\nrutabaga\n", 178 "differ": "empty space\n", 179 "error": "", 180 }) 181 } 182 183 func TestCheck(t *testing.T) { 184 testCheck(t, operations.Check) 185 } 186 187 func TestCheckFsError(t *testing.T) { 188 ctx := context.Background() 189 dstFs, err := fs.NewFs(ctx, "nonexistent") 190 if err != nil { 191 t.Fatal(err) 192 } 193 srcFs, err := fs.NewFs(ctx, "nonexistent") 194 if err != nil { 195 t.Fatal(err) 196 } 197 opt := operations.CheckOpt{ 198 Fdst: dstFs, 199 Fsrc: srcFs, 200 OneWay: false, 201 } 202 err = operations.Check(ctx, &opt) 203 require.Error(t, err) 204 } 205 206 func TestCheckDownload(t *testing.T) { 207 testCheck(t, operations.CheckDownload) 208 } 209 210 func TestCheckSizeOnly(t *testing.T) { 211 ctx := context.Background() 212 ci := fs.GetConfig(ctx) 213 ci.SizeOnly = true 214 defer func() { ci.SizeOnly = false }() 215 TestCheck(t) 216 } 217 218 func TestCheckEqualReaders(t *testing.T) { 219 b65a := make([]byte, 65*1024) 220 b65b := make([]byte, 65*1024) 221 b65b[len(b65b)-1] = 1 222 b66 := make([]byte, 66*1024) 223 224 differ, err := operations.CheckEqualReaders(bytes.NewBuffer(b65a), bytes.NewBuffer(b65a)) 225 assert.NoError(t, err) 226 assert.Equal(t, differ, false) 227 228 differ, err = operations.CheckEqualReaders(bytes.NewBuffer(b65a), bytes.NewBuffer(b65b)) 229 assert.NoError(t, err) 230 assert.Equal(t, differ, true) 231 232 differ, err = operations.CheckEqualReaders(bytes.NewBuffer(b65a), bytes.NewBuffer(b66)) 233 assert.NoError(t, err) 234 assert.Equal(t, differ, true) 235 236 differ, err = operations.CheckEqualReaders(bytes.NewBuffer(b66), bytes.NewBuffer(b65a)) 237 assert.NoError(t, err) 238 assert.Equal(t, differ, true) 239 240 myErr := errors.New("sentinel") 241 wrap := func(b []byte) io.Reader { 242 r := bytes.NewBuffer(b) 243 e := readers.ErrorReader{Err: myErr} 244 return io.MultiReader(r, e) 245 } 246 247 differ, err = operations.CheckEqualReaders(wrap(b65a), bytes.NewBuffer(b65a)) 248 assert.Equal(t, myErr, err) 249 assert.Equal(t, differ, true) 250 251 differ, err = operations.CheckEqualReaders(wrap(b65a), bytes.NewBuffer(b65b)) 252 assert.Equal(t, myErr, err) 253 assert.Equal(t, differ, true) 254 255 differ, err = operations.CheckEqualReaders(wrap(b65a), bytes.NewBuffer(b66)) 256 assert.Equal(t, myErr, err) 257 assert.Equal(t, differ, true) 258 259 differ, err = operations.CheckEqualReaders(wrap(b66), bytes.NewBuffer(b65a)) 260 assert.Equal(t, myErr, err) 261 assert.Equal(t, differ, true) 262 263 differ, err = operations.CheckEqualReaders(bytes.NewBuffer(b65a), wrap(b65a)) 264 assert.Equal(t, myErr, err) 265 assert.Equal(t, differ, true) 266 267 differ, err = operations.CheckEqualReaders(bytes.NewBuffer(b65a), wrap(b65b)) 268 assert.Equal(t, myErr, err) 269 assert.Equal(t, differ, true) 270 271 differ, err = operations.CheckEqualReaders(bytes.NewBuffer(b65a), wrap(b66)) 272 assert.Equal(t, myErr, err) 273 assert.Equal(t, differ, true) 274 275 differ, err = operations.CheckEqualReaders(bytes.NewBuffer(b66), wrap(b65a)) 276 assert.Equal(t, myErr, err) 277 assert.Equal(t, differ, true) 278 } 279 280 func TestParseSumFile(t *testing.T) { 281 r := fstest.NewRun(t) 282 ctx := context.Background() 283 284 const sumFile = "test.sum" 285 286 samples := []struct { 287 hash, sep, name string 288 ok bool 289 }{ 290 {"1", " ", "file1", true}, 291 {"2", " *", "file2", true}, 292 {"3", " ", " file3 ", true}, 293 {"4", " ", "\tfile3\t", true}, 294 {"5", " ", "file5", false}, 295 {"6", "\t", "file6", false}, 296 {"7", " \t", " file7 ", false}, 297 {"", " ", "file8", false}, 298 {"", "", "file9", false}, 299 } 300 301 for _, eol := range []string{"\n", "\r\n"} { 302 data := &bytes.Buffer{} 303 wantNum := 0 304 for _, s := range samples { 305 _, _ = data.WriteString(s.hash + s.sep + s.name + eol) 306 if s.ok { 307 wantNum++ 308 } 309 } 310 311 _ = r.WriteObject(ctx, sumFile, data.String(), t1) 312 file, err := r.Fremote.NewObject(ctx, sumFile) 313 assert.NoError(t, err) 314 sums, err := operations.ParseSumFile(ctx, file) 315 assert.NoError(t, err) 316 317 assert.Equal(t, wantNum, len(sums)) 318 for _, s := range samples { 319 if s.ok { 320 assert.Equal(t, s.hash, sums[s.name]) 321 } 322 } 323 } 324 } 325 326 func testCheckSum(t *testing.T, download bool) { 327 const dataDir = "data" 328 const sumFile = "test.sum" 329 330 hashType := hash.MD5 331 const ( 332 testString1 = "Hello, World!" 333 testDigest1 = "65a8e27d8879283831b664bd8b7f0ad4" 334 testDigest1Upper = "65A8E27D8879283831B664BD8B7F0AD4" 335 testString2 = "I am the walrus" 336 testDigest2 = "87396e030ef3f5b35bbf85c0a09a4fb3" 337 testDigest2Mixed = "87396e030EF3f5b35BBf85c0a09a4FB3" 338 ) 339 340 type wantType map[string]string 341 342 ctx := context.Background() 343 r := fstest.NewRun(t) 344 345 subRemote := r.FremoteName 346 if !strings.HasSuffix(subRemote, ":") { 347 subRemote += "/" 348 } 349 subRemote += dataDir 350 dataFs, err := fs.NewFs(ctx, subRemote) 351 require.NoError(t, err) 352 353 if !download && !dataFs.Hashes().Contains(hashType) { 354 t.Skipf("%s lacks %s, skipping", dataFs, hashType) 355 } 356 357 makeFile := func(name, content string) fstest.Item { 358 remote := dataDir + "/" + name 359 return r.WriteObject(ctx, remote, content, t1) 360 } 361 362 makeSums := func(sums operations.HashSums) fstest.Item { 363 files := make([]string, 0, len(sums)) 364 for name := range sums { 365 files = append(files, name) 366 } 367 sort.Strings(files) 368 buf := &bytes.Buffer{} 369 for _, name := range files { 370 _, _ = fmt.Fprintf(buf, "%s %s\n", sums[name], name) 371 } 372 return r.WriteObject(ctx, sumFile, buf.String(), t1) 373 } 374 375 sortLines := func(in string) []string { 376 if in == "" { 377 return []string{} 378 } 379 lines := strings.Split(in, "\n") 380 sort.Strings(lines) 381 return lines 382 } 383 384 checkResult := func(runNo int, want wantType, name string, out io.Writer) { 385 expected := want[name] 386 buf, ok := out.(*bytes.Buffer) 387 require.True(t, ok) 388 assert.Equal(t, sortLines(expected), sortLines(buf.String()), "wrong %s result in run %d", name, runNo) 389 } 390 391 checkRun := func(runNo, wantChecks, wantErrors int, want wantType) { 392 accounting.GlobalStats().ResetCounters() 393 buf := new(bytes.Buffer) 394 log.SetOutput(buf) 395 defer log.SetOutput(os.Stderr) 396 397 opt := operations.CheckOpt{ 398 Combined: new(bytes.Buffer), 399 Match: new(bytes.Buffer), 400 Differ: new(bytes.Buffer), 401 Error: new(bytes.Buffer), 402 MissingOnSrc: new(bytes.Buffer), 403 MissingOnDst: new(bytes.Buffer), 404 } 405 err := operations.CheckSum(ctx, dataFs, r.Fremote, sumFile, hashType, &opt, download) 406 407 gotErrors := int(accounting.GlobalStats().GetErrors()) 408 if wantErrors == 0 { 409 assert.NoError(t, err, "unexpected error in run %d", runNo) 410 } 411 if wantErrors > 0 { 412 assert.Error(t, err, "no expected error in run %d", runNo) 413 } 414 assert.Equal(t, wantErrors, gotErrors, "wrong error count in run %d", runNo) 415 416 gotChecks := int(accounting.GlobalStats().GetChecks()) 417 if wantChecks > 0 || gotChecks > 0 { 418 assert.Contains(t, buf.String(), "matching files", "missing matching files in run %d", runNo) 419 } 420 assert.Equal(t, wantChecks, gotChecks, "wrong number of checks in run %d", runNo) 421 422 checkResult(runNo, want, "combined", opt.Combined) 423 checkResult(runNo, want, "missingonsrc", opt.MissingOnSrc) 424 checkResult(runNo, want, "missingondst", opt.MissingOnDst) 425 checkResult(runNo, want, "match", opt.Match) 426 checkResult(runNo, want, "differ", opt.Differ) 427 checkResult(runNo, want, "error", opt.Error) 428 } 429 430 check := func(runNo, wantChecks, wantErrors int, wantResults wantType) { 431 runName := fmt.Sprintf("subtest%d", runNo) 432 t.Run(runName, func(t *testing.T) { 433 checkRun(runNo, wantChecks, wantErrors, wantResults) 434 }) 435 } 436 437 file1 := makeFile("banana", testString1) 438 fcsums := makeSums(operations.HashSums{ 439 "banana": testDigest1, 440 }) 441 r.CheckRemoteItems(t, fcsums, file1) 442 check(1, 1, 0, wantType{ 443 "combined": "= banana\n", 444 "missingonsrc": "", 445 "missingondst": "", 446 "match": "banana\n", 447 "differ": "", 448 "error": "", 449 }) 450 451 file2 := makeFile("potato", testString2) 452 fcsums = makeSums(operations.HashSums{ 453 "banana": testDigest1, 454 }) 455 r.CheckRemoteItems(t, fcsums, file1, file2) 456 check(2, 2, 1, wantType{ 457 "combined": "- potato\n= banana\n", 458 "missingonsrc": "potato\n", 459 "missingondst": "", 460 "match": "banana\n", 461 "differ": "", 462 "error": "", 463 }) 464 465 fcsums = makeSums(operations.HashSums{ 466 "banana": testDigest1, 467 "potato": testDigest2, 468 }) 469 r.CheckRemoteItems(t, fcsums, file1, file2) 470 check(3, 2, 0, wantType{ 471 "combined": "= potato\n= banana\n", 472 "missingonsrc": "", 473 "missingondst": "", 474 "match": "banana\npotato\n", 475 "differ": "", 476 "error": "", 477 }) 478 479 fcsums = makeSums(operations.HashSums{ 480 "banana": testDigest2, 481 "potato": testDigest2, 482 }) 483 r.CheckRemoteItems(t, fcsums, file1, file2) 484 check(4, 2, 1, wantType{ 485 "combined": "* banana\n= potato\n", 486 "missingonsrc": "", 487 "missingondst": "", 488 "match": "potato\n", 489 "differ": "banana\n", 490 "error": "", 491 }) 492 493 fcsums = makeSums(operations.HashSums{ 494 "banana": testDigest1, 495 "potato": testDigest2, 496 "orange": testDigest2, 497 }) 498 r.CheckRemoteItems(t, fcsums, file1, file2) 499 check(5, 2, 1, wantType{ 500 "combined": "+ orange\n= potato\n= banana\n", 501 "missingonsrc": "", 502 "missingondst": "orange\n", 503 "match": "banana\npotato\n", 504 "differ": "", 505 "error": "", 506 }) 507 508 fcsums = makeSums(operations.HashSums{ 509 "banana": testDigest1, 510 "potato": testDigest1, 511 "orange": testDigest2, 512 }) 513 r.CheckRemoteItems(t, fcsums, file1, file2) 514 check(6, 2, 2, wantType{ 515 "combined": "+ orange\n* potato\n= banana\n", 516 "missingonsrc": "", 517 "missingondst": "orange\n", 518 "match": "banana\n", 519 "differ": "potato\n", 520 "error": "", 521 }) 522 523 // test mixed-case checksums 524 file1 = makeFile("banana", testString1) 525 file2 = makeFile("potato", testString2) 526 fcsums = makeSums(operations.HashSums{ 527 "banana": testDigest1Upper, 528 "potato": testDigest2Mixed, 529 }) 530 r.CheckRemoteItems(t, fcsums, file1, file2) 531 check(7, 2, 0, wantType{ 532 "combined": "= banana\n= potato\n", 533 "missingonsrc": "", 534 "missingondst": "", 535 "match": "banana\npotato\n", 536 "differ": "", 537 "error": "", 538 }) 539 } 540 541 func TestCheckSum(t *testing.T) { 542 testCheckSum(t, false) 543 } 544 545 func TestCheckSumDownload(t *testing.T) { 546 testCheckSum(t, true) 547 } 548 549 func TestApplyTransforms(t *testing.T) { 550 var ( 551 hashType = hash.MD5 552 content = "Hello, World!" 553 hash = "65a8e27d8879283831b664bd8b7f0ad4" 554 nfc = norm.NFC.String(norm.NFD.String("測試_Русский___ě_áñ")) 555 nfd = norm.NFD.String(nfc) 556 nfcx2 = nfc + nfc 557 nfdx2 = nfd + nfd 558 both = nfc + nfd 559 upper = "HELLO, WORLD!" 560 lower = "hello, world!" 561 upperlowermixed = "HeLlO, wOrLd!" 562 ) 563 564 testScenario := func(checkfileName, remotefileName, scenario string) { 565 r := fstest.NewRunIndividual(t) 566 ctx := context.Background() 567 ci := fs.GetConfig(ctx) 568 opt := operations.CheckOpt{} 569 570 remotefile := r.WriteObject(ctx, remotefileName, content, t2) 571 // test whether remote is capable of running test 572 entries, err := r.Fremote.List(ctx, "") 573 assert.NoError(t, err) 574 if entries.Len() == 1 && entries[0].Remote() != remotefileName { 575 t.Skipf("Fs is incapable of running test, skipping: %s (expected: %s (%s) actual: %s (%s))", scenario, remotefileName, detectEncoding(remotefileName), entries[0].Remote(), detectEncoding(entries[0].Remote())) 576 } 577 578 checkfile := r.WriteFile("test.sum", hash+" "+checkfileName, t2) 579 r.CheckLocalItems(t, checkfile) 580 assert.False(t, checkfileName == remotefile.Path, "Values match but should not: %s %s", checkfileName, remotefile.Path) 581 582 testname := scenario + " (without normalization)" 583 println(testname) 584 ci.NoUnicodeNormalization = true 585 ci.IgnoreCaseSync = false 586 accounting.GlobalStats().ResetCounters() 587 err = operations.CheckSum(ctx, r.Fremote, r.Flocal, "test.sum", hashType, &opt, true) 588 assert.Error(t, err, "no expected error for %s %v %v", testname, checkfileName, remotefileName) 589 590 testname = scenario + " (with normalization)" 591 println(testname) 592 ci.NoUnicodeNormalization = false 593 ci.IgnoreCaseSync = true 594 accounting.GlobalStats().ResetCounters() 595 err = operations.CheckSum(ctx, r.Fremote, r.Flocal, "test.sum", hashType, &opt, true) 596 assert.NoError(t, err, "unexpected error for %s %v %v", testname, checkfileName, remotefileName) 597 } 598 599 testScenario(upper, lower, "upper checkfile vs. lower remote") 600 testScenario(lower, upper, "lower checkfile vs. upper remote") 601 testScenario(lower, upperlowermixed, "lower checkfile vs. upperlowermixed remote") 602 testScenario(upperlowermixed, upper, "upperlowermixed checkfile vs. upper remote") 603 testScenario(nfd, nfc, "NFD checkfile vs. NFC remote") 604 testScenario(nfc, nfd, "NFC checkfile vs. NFD remote") 605 testScenario(nfdx2, both, "NFDx2 checkfile vs. both remote") 606 testScenario(nfcx2, both, "NFCx2 checkfile vs. both remote") 607 testScenario(both, nfdx2, "both checkfile vs. NFDx2 remote") 608 testScenario(both, nfcx2, "both checkfile vs. NFCx2 remote") 609 } 610 611 func detectEncoding(s string) string { 612 if norm.NFC.IsNormalString(s) && norm.NFD.IsNormalString(s) { 613 return "BOTH" 614 } 615 if !norm.NFC.IsNormalString(s) && norm.NFD.IsNormalString(s) { 616 return "NFD" 617 } 618 if norm.NFC.IsNormalString(s) && !norm.NFD.IsNormalString(s) { 619 return "NFC" 620 } 621 return "OTHER" 622 }