github.com/bazelbuild/remote-apis-sdks@v0.0.0-20240425170053-8a36686a6350/go/pkg/cas/upload_test.go (about) 1 package cas 2 3 import ( 4 "context" 5 "os" 6 "path/filepath" 7 "regexp" 8 "sort" 9 "strings" 10 "sync" 11 "testing" 12 "time" 13 14 "github.com/google/go-cmp/cmp" 15 "github.com/pkg/errors" 16 "google.golang.org/grpc" 17 "google.golang.org/grpc/codes" 18 "google.golang.org/grpc/status" 19 "google.golang.org/protobuf/proto" 20 21 "github.com/bazelbuild/remote-apis-sdks/go/pkg/digest" 22 "github.com/bazelbuild/remote-apis-sdks/go/pkg/fakes" 23 // Redundant imports are required for the google3 mirror. Aliases should not be changed. 24 regrpc "github.com/bazelbuild/remote-apis/build/bazel/remote/execution/v2" 25 repb "github.com/bazelbuild/remote-apis/build/bazel/remote/execution/v2" 26 ) 27 28 func TestFS(t *testing.T) { 29 t.Parallel() 30 ctx := context.Background() 31 32 tmpDir := t.TempDir() 33 putFile(t, filepath.Join(tmpDir, "root", "a"), "a") 34 aItem := uploadItemFromBlob(filepath.Join(tmpDir, "root", "a"), []byte("a")) 35 36 putFile(t, filepath.Join(tmpDir, "root", "b"), "b") 37 bItem := uploadItemFromBlob(filepath.Join(tmpDir, "root", "b"), []byte("b")) 38 39 putFile(t, filepath.Join(tmpDir, "root", "subdir", "c"), "c") 40 cItem := uploadItemFromBlob(filepath.Join(tmpDir, "root", "subdir", "c"), []byte("c")) 41 42 putFile(t, filepath.Join(tmpDir, "root", "subdir", "d"), "d") 43 dItem := uploadItemFromBlob(filepath.Join(tmpDir, "root", "subdir", "d"), []byte("d")) 44 45 subdirItem := uploadItemFromDirMsg(filepath.Join(tmpDir, "root", "subdir"), &repb.Directory{ 46 Files: []*repb.FileNode{ 47 { 48 Name: "c", 49 Digest: cItem.Digest, 50 }, 51 { 52 Name: "d", 53 Digest: dItem.Digest, 54 }, 55 }, 56 }) 57 subdirWithoutDItem := uploadItemFromDirMsg(filepath.Join(tmpDir, "root", "subdir"), &repb.Directory{ 58 Files: []*repb.FileNode{ 59 { 60 Name: "c", 61 Digest: cItem.Digest, 62 }, 63 }, 64 }) 65 66 rootItem := uploadItemFromDirMsg(filepath.Join(tmpDir, "root"), &repb.Directory{ 67 Files: []*repb.FileNode{ 68 {Name: "a", Digest: aItem.Digest}, 69 {Name: "b", Digest: bItem.Digest}, 70 }, 71 Directories: []*repb.DirectoryNode{ 72 {Name: "subdir", Digest: subdirItem.Digest}, 73 }, 74 }) 75 rootWithoutAItem := uploadItemFromDirMsg(filepath.Join(tmpDir, "root"), &repb.Directory{ 76 Files: []*repb.FileNode{ 77 {Name: "b", Digest: bItem.Digest}, 78 }, 79 Directories: []*repb.DirectoryNode{ 80 {Name: "subdir", Digest: subdirItem.Digest}, 81 }, 82 }) 83 rootWithoutSubdirItem := uploadItemFromDirMsg(filepath.Join(tmpDir, "root"), &repb.Directory{ 84 Files: []*repb.FileNode{ 85 {Name: "a", Digest: aItem.Digest}, 86 {Name: "b", Digest: bItem.Digest}, 87 }, 88 }) 89 rootWithoutDItem := uploadItemFromDirMsg(filepath.Join(tmpDir, "root"), &repb.Directory{ 90 Files: []*repb.FileNode{ 91 {Name: "a", Digest: aItem.Digest}, 92 {Name: "b", Digest: bItem.Digest}, 93 }, 94 Directories: []*repb.DirectoryNode{ 95 {Name: "subdir", Digest: subdirWithoutDItem.Digest}, 96 }, 97 }) 98 99 putFile(t, filepath.Join(tmpDir, "medium-dir", "medium"), "medium") 100 mediumItem := uploadItemFromBlob(filepath.Join(tmpDir, "medium-dir", "medium"), []byte("medium")) 101 mediumDirItem := uploadItemFromDirMsg(filepath.Join(tmpDir, "medium-dir"), &repb.Directory{ 102 Files: []*repb.FileNode{{ 103 Name: "medium", 104 Digest: mediumItem.Digest, 105 }}, 106 }) 107 108 putSymlink(t, filepath.Join(tmpDir, "with-symlinks", "file"), filepath.Join("..", "root", "a")) 109 putSymlink(t, filepath.Join(tmpDir, "with-symlinks", "dir"), filepath.Join("..", "root", "subdir")) 110 withSymlinksItemPreserved := uploadItemFromDirMsg(filepath.Join(tmpDir, "with-symlinks"), &repb.Directory{ 111 Symlinks: []*repb.SymlinkNode{ 112 { 113 Name: "file", 114 Target: "../root/a", 115 }, 116 { 117 Name: "dir", 118 Target: "../root/subdir", 119 }, 120 }, 121 }) 122 123 withSymlinksItemNotPreserved := uploadItemFromDirMsg(filepath.Join(tmpDir, "with-symlinks"), &repb.Directory{ 124 Files: []*repb.FileNode{ 125 {Name: "a", Digest: aItem.Digest}, 126 }, 127 Directories: []*repb.DirectoryNode{ 128 {Name: "subdir", Digest: subdirItem.Digest}, 129 }, 130 }) 131 132 putSymlink(t, filepath.Join(tmpDir, "with-dangling-symlink", "dangling"), "non-existent") 133 withDanglingSymlinksItem := uploadItemFromDirMsg(filepath.Join(tmpDir, "with-dangling-symlink"), &repb.Directory{ 134 Symlinks: []*repb.SymlinkNode{ 135 {Name: "dangling", Target: "non-existent"}, 136 }, 137 }) 138 139 digSlice := func(items ...*uploadItem) []digest.Digest { 140 ret := make([]digest.Digest, len(items)) 141 for i, item := range items { 142 ret[i] = digest.NewFromProtoUnvalidated(item.Digest) 143 } 144 return ret 145 } 146 147 tests := []struct { 148 desc string 149 inputs []*UploadInput 150 wantDigests []digest.Digest 151 wantScheduledChecks []*uploadItem 152 wantErr error 153 opt UploadOptions 154 }{ 155 { 156 desc: "root", 157 inputs: []*UploadInput{{Path: filepath.Join(tmpDir, "root")}}, 158 wantDigests: digSlice(rootItem), 159 wantScheduledChecks: []*uploadItem{rootItem, aItem, bItem, subdirItem, cItem, dItem}, 160 }, 161 { 162 desc: "root-without-a-using-callback", 163 inputs: []*UploadInput{{Path: filepath.Join(tmpDir, "root")}}, 164 wantDigests: digSlice(rootWithoutAItem), 165 opt: UploadOptions{ 166 Prelude: func(absPath string, mode os.FileMode) error { 167 if filepath.Base(absPath) == "a" { 168 return ErrSkip 169 } 170 return nil 171 }, 172 }, 173 wantScheduledChecks: []*uploadItem{rootWithoutAItem, bItem, subdirItem, cItem, dItem}, 174 }, 175 { 176 desc: "root-without-a-using-allowlist", 177 inputs: []*UploadInput{{Path: filepath.Join(tmpDir, "root"), Allowlist: []string{"b", "subdir"}}}, 178 wantDigests: digSlice(rootWithoutAItem), 179 wantScheduledChecks: []*uploadItem{rootWithoutAItem, bItem, subdirItem, cItem, dItem}, 180 }, 181 { 182 desc: "root-without-subdir-using-allowlist", 183 inputs: []*UploadInput{{Path: filepath.Join(tmpDir, "root"), Allowlist: []string{"a", "b"}}}, 184 wantDigests: digSlice(rootWithoutSubdirItem), 185 wantScheduledChecks: []*uploadItem{rootWithoutSubdirItem, aItem, bItem}, 186 }, 187 { 188 desc: "root-without-d-using-allowlist", 189 inputs: []*UploadInput{{Path: filepath.Join(tmpDir, "root"), Allowlist: []string{"a", "b", filepath.Join("subdir", "c")}}}, 190 wantDigests: digSlice(rootWithoutDItem), 191 wantScheduledChecks: []*uploadItem{rootWithoutDItem, aItem, bItem, subdirWithoutDItem, cItem}, 192 }, 193 { 194 desc: "root-without-b-using-exclude", 195 inputs: []*UploadInput{{ 196 Path: filepath.Join(tmpDir, "root"), 197 Exclude: regexp.MustCompile(`[/\\]a$`), 198 }}, 199 wantDigests: digSlice(rootWithoutAItem), 200 wantScheduledChecks: []*uploadItem{rootWithoutAItem, bItem, subdirItem, cItem, dItem}, 201 }, 202 { 203 desc: "same-regular-file-is-read-only-once", 204 // The two regexps below do not exclude anything. 205 // This test ensures that same files aren't checked twice. 206 inputs: []*UploadInput{ 207 { 208 Path: filepath.Join(tmpDir, "root"), 209 Exclude: regexp.MustCompile(`1$`), 210 }, 211 { 212 Path: filepath.Join(tmpDir, "root"), 213 Exclude: regexp.MustCompile(`2$`), 214 }, 215 }, 216 // OnDigest is called for each UploadItem separately. 217 wantDigests: digSlice(rootItem, rootItem), 218 // Directories are checked twice, but files are checked only once. 219 wantScheduledChecks: []*uploadItem{rootItem, rootItem, aItem, bItem, subdirItem, subdirItem, cItem, dItem}, 220 }, 221 { 222 desc: "root-without-subdir", 223 inputs: []*UploadInput{{Path: filepath.Join(tmpDir, "root")}}, 224 opt: UploadOptions{ 225 Prelude: func(absPath string, mode os.FileMode) error { 226 if strings.Contains(absPath, "subdir") { 227 return ErrSkip 228 } 229 return nil 230 }, 231 }, 232 wantDigests: digSlice(rootWithoutSubdirItem), 233 wantScheduledChecks: []*uploadItem{rootWithoutSubdirItem, aItem, bItem}, 234 }, 235 { 236 desc: "medium", 237 inputs: []*UploadInput{{Path: filepath.Join(tmpDir, "medium-dir")}}, 238 wantDigests: digSlice(mediumDirItem), 239 wantScheduledChecks: []*uploadItem{mediumDirItem, mediumItem}, 240 }, 241 { 242 desc: "symlinks-preserved", 243 opt: UploadOptions{PreserveSymlinks: true}, 244 inputs: []*UploadInput{{Path: filepath.Join(tmpDir, "with-symlinks")}}, 245 wantDigests: digSlice(withSymlinksItemPreserved), 246 wantScheduledChecks: []*uploadItem{withSymlinksItemPreserved}, 247 }, 248 { 249 desc: "symlinks-not-preserved", 250 inputs: []*UploadInput{{Path: filepath.Join(tmpDir, "with-symlinks")}}, 251 wantDigests: digSlice(withSymlinksItemNotPreserved), 252 wantScheduledChecks: []*uploadItem{aItem, subdirItem, cItem, dItem, withSymlinksItemNotPreserved}, 253 }, 254 { 255 desc: "dangling-symlinks-disallow", 256 inputs: []*UploadInput{{Path: filepath.Join(tmpDir, "with-dangling-symlinks")}}, 257 wantErr: os.ErrNotExist, 258 }, 259 { 260 desc: "dangling-symlinks-allow", 261 opt: UploadOptions{PreserveSymlinks: true, AllowDanglingSymlinks: true}, 262 inputs: []*UploadInput{{Path: filepath.Join(tmpDir, "with-dangling-symlink")}}, 263 wantDigests: digSlice(withDanglingSymlinksItem), 264 wantScheduledChecks: []*uploadItem{withDanglingSymlinksItem}, 265 }, 266 { 267 desc: "dangling-symlink-via-filtering", 268 opt: UploadOptions{PreserveSymlinks: true}, 269 inputs: []*UploadInput{{ 270 Path: filepath.Join(tmpDir, "with-symlinks"), 271 Exclude: regexp.MustCompile("root"), 272 }}, 273 wantDigests: digSlice(withSymlinksItemPreserved), 274 wantScheduledChecks: []*uploadItem{withSymlinksItemPreserved}, 275 }, 276 { 277 desc: "dangling-symlink-via-filtering-allow", 278 opt: UploadOptions{PreserveSymlinks: true, AllowDanglingSymlinks: true}, 279 inputs: []*UploadInput{{ 280 Path: filepath.Join(tmpDir, "with-symlinks"), 281 Exclude: regexp.MustCompile("root"), 282 }}, 283 wantDigests: digSlice(withSymlinksItemPreserved), 284 wantScheduledChecks: []*uploadItem{withSymlinksItemPreserved}, 285 }, 286 } 287 288 for _, tc := range tests { 289 t.Run(tc.desc, func(t *testing.T) { 290 var mu sync.Mutex 291 var gotScheduledChecks []*uploadItem 292 293 client := &Client{ 294 Config: DefaultClientConfig(), 295 testScheduleCheck: func(ctx context.Context, item *uploadItem) error { 296 mu.Lock() 297 defer mu.Unlock() 298 gotScheduledChecks = append(gotScheduledChecks, item) 299 return nil 300 }, 301 } 302 client.Config.SmallFileThreshold = 5 303 client.Config.LargeFileThreshold = 10 304 client.init() 305 306 _, err := client.Upload(ctx, tc.opt, uploadInputChanFrom(tc.inputs...)) 307 if tc.wantErr != nil { 308 if !errors.Is(err, tc.wantErr) { 309 t.Fatalf("error mismatch: want %q, got %q", tc.wantErr, err) 310 } 311 return 312 } 313 if err != nil { 314 t.Fatal(err) 315 } 316 317 sort.Slice(gotScheduledChecks, func(i, j int) bool { 318 return gotScheduledChecks[i].Title < gotScheduledChecks[j].Title 319 }) 320 if diff := cmp.Diff(tc.wantScheduledChecks, gotScheduledChecks, cmp.Comparer(compareUploadItems)); diff != "" { 321 t.Errorf("unexpected scheduled checks (-want +got):\n%s", diff) 322 } 323 324 gotDigests := make([]digest.Digest, 0, len(tc.inputs)) 325 for _, in := range tc.inputs { 326 dig, err := in.Digest(".") 327 if err != nil { 328 t.Errorf("UploadResult.Digest(%#v) failed: %s", in.Path, err) 329 } else { 330 gotDigests = append(gotDigests, dig) 331 } 332 } 333 if diff := cmp.Diff(tc.wantDigests, gotDigests); diff != "" { 334 t.Errorf("unexpected digests (-want +got):\n%s", diff) 335 } 336 }) 337 } 338 } 339 340 func TestDigest(t *testing.T) { 341 t.Parallel() 342 ctx := context.Background() 343 344 tmpDir := t.TempDir() 345 putFile(t, filepath.Join(tmpDir, "root", "a"), "a") 346 putFile(t, filepath.Join(tmpDir, "root", "b"), "b") 347 putFile(t, filepath.Join(tmpDir, "root", "subdir", "c"), "c") 348 putFile(t, filepath.Join(tmpDir, "root", "subdir", "d"), "d") 349 350 e, cleanup := fakes.NewTestEnv(t) 351 defer cleanup() 352 conn, err := e.Server.NewClientConn(ctx) 353 if err != nil { 354 t.Fatal(err) 355 } 356 357 client, err := NewClientWithConfig(ctx, conn, "instance", DefaultClientConfig()) 358 if err != nil { 359 t.Fatal(err) 360 } 361 362 inputs := []struct { 363 input *UploadInput 364 wantDigests map[string]digest.Digest 365 }{ 366 { 367 input: &UploadInput{ 368 Path: filepath.Join(tmpDir, "root"), 369 Allowlist: []string{"a", "b", filepath.Join("subdir", "c")}, 370 }, 371 wantDigests: map[string]digest.Digest{ 372 ".": {Hash: "9a0af914385de712675cd780ae2dcb5e17b8943dc62cf9fc6fbf8ccd6f8c940d", Size: 230}, 373 "a": {Hash: "ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb", Size: 1}, 374 "subdir": {Hash: "2d5c8ba78600fcadae65bab790bdf1f6f88278ec4abe1dc3aa7c26e60137dfc8", Size: 75}, 375 }, 376 }, 377 { 378 input: &UploadInput{ 379 Path: filepath.Join(tmpDir, "root"), 380 Allowlist: []string{"a", "b", filepath.Join("subdir", "d")}, 381 }, 382 wantDigests: map[string]digest.Digest{ 383 ".": {Hash: "2ab9cc3c9d504c883a66da62b57eb44fc9ca57abe05e75633b435e017920d8df", Size: 230}, 384 "a": {Hash: "ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb", Size: 1}, 385 "subdir": {Hash: "ce33c7475f9ff2f2ee501eafcb2f21825b24a63de6fbabf7fbb886d606a448b9", Size: 75}, 386 }, 387 }, 388 } 389 390 uploadInputs := make([]*UploadInput, len(inputs)) 391 for i, in := range inputs { 392 uploadInputs[i] = in.input 393 if in.input.DigestsComputed() == nil { 394 t.Fatalf("DigestCopmuted() returned nil") 395 } 396 } 397 398 if _, err := client.Upload(ctx, UploadOptions{}, uploadInputChanFrom(uploadInputs...)); err != nil { 399 t.Fatal(err) 400 } 401 402 for i, in := range inputs { 403 t.Logf("input %d", i) 404 select { 405 case <-in.input.DigestsComputed(): 406 // Good 407 case <-time.After(time.Second): 408 t.Errorf("Upload succeeded, but DigestsComputed() is not closed") 409 } 410 411 for relPath, wantDig := range in.wantDigests { 412 gotDig, err := in.input.Digest(relPath) 413 if err != nil { 414 t.Error(err) 415 continue 416 } 417 if diff := cmp.Diff(gotDig, wantDig); diff != "" { 418 t.Errorf("unexpected digest for %s (-want +got):\n%s", relPath, diff) 419 } 420 } 421 } 422 } 423 func TestSmallFiles(t *testing.T) { 424 t.Parallel() 425 ctx := context.Background() 426 427 var mu sync.Mutex 428 var gotDigestChecks []*repb.Digest 429 var gotDigestCheckRequestSizes []int 430 var gotUploadBlobReqs []*repb.BatchUpdateBlobsRequest_Request 431 var missing []*repb.Digest 432 failFirstMissing := true 433 cas := &fakeCAS{ 434 findMissingBlobs: func(ctx context.Context, in *repb.FindMissingBlobsRequest, opts ...grpc.CallOption) (*repb.FindMissingBlobsResponse, error) { 435 mu.Lock() 436 defer mu.Unlock() 437 gotDigestChecks = append(gotDigestChecks, in.BlobDigests...) 438 gotDigestCheckRequestSizes = append(gotDigestCheckRequestSizes, len(in.BlobDigests)) 439 missing = append(missing, in.BlobDigests[0]) 440 return &repb.FindMissingBlobsResponse{MissingBlobDigests: in.BlobDigests[:1]}, nil 441 }, 442 batchUpdateBlobs: func(ctx context.Context, in *repb.BatchUpdateBlobsRequest, opts ...grpc.CallOption) (*repb.BatchUpdateBlobsResponse, error) { 443 mu.Lock() 444 defer mu.Unlock() 445 446 gotUploadBlobReqs = append(gotUploadBlobReqs, in.Requests...) 447 448 res := &repb.BatchUpdateBlobsResponse{ 449 Responses: make([]*repb.BatchUpdateBlobsResponse_Response, len(in.Requests)), 450 } 451 for i, r := range in.Requests { 452 res.Responses[i] = &repb.BatchUpdateBlobsResponse_Response{Digest: r.Digest} 453 if proto.Equal(r.Digest, missing[0]) && failFirstMissing { 454 res.Responses[i].Status = status.New(codes.Internal, "internal retrible error").Proto() 455 failFirstMissing = false 456 } 457 } 458 return res, nil 459 }, 460 } 461 client := &Client{ 462 InstanceName: "projects/p/instances/i", 463 Config: DefaultClientConfig(), 464 cas: cas, 465 } 466 client.Config.FindMissingBlobs.MaxItems = 2 467 client.init() 468 469 tmpDir := t.TempDir() 470 putFile(t, filepath.Join(tmpDir, "a"), "a") 471 putFile(t, filepath.Join(tmpDir, "b"), "b") 472 putFile(t, filepath.Join(tmpDir, "c"), "c") 473 putFile(t, filepath.Join(tmpDir, "d"), "d") 474 inputC := uploadInputChanFrom( 475 &UploadInput{Path: filepath.Join(tmpDir, "a")}, 476 &UploadInput{Path: filepath.Join(tmpDir, "b")}, 477 &UploadInput{Path: filepath.Join(tmpDir, "c")}, 478 &UploadInput{Path: filepath.Join(tmpDir, "d")}, 479 ) 480 if _, err := client.Upload(ctx, UploadOptions{}, inputC); err != nil { 481 t.Fatalf("failed to upload: %s", err) 482 } 483 484 wantDigestChecks := []*repb.Digest{ 485 {Hash: "18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4", SizeBytes: 1}, 486 {Hash: "2e7d2c03a9507ae265ecf5b5356885a53393a2029d241394997265a1a25aefc6", SizeBytes: 1}, 487 {Hash: "3e23e8160039594a33894f6564e1b1348bbd7a0088d42c4acb73eeaed59c009d", SizeBytes: 1}, 488 {Hash: "ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb", SizeBytes: 1}, 489 } 490 sort.Slice(gotDigestChecks, func(i, j int) bool { 491 return gotDigestChecks[i].Hash < gotDigestChecks[j].Hash 492 }) 493 if diff := cmp.Diff(wantDigestChecks, gotDigestChecks, cmp.Comparer(proto.Equal)); diff != "" { 494 t.Error(diff) 495 } 496 if diff := cmp.Diff([]int{2, 2}, gotDigestCheckRequestSizes); diff != "" { 497 t.Error(diff) 498 } 499 500 if len(missing) != 2 { 501 t.Fatalf("want 2 missing, got %d", len(missing)) 502 } 503 var wantUploadBlobsReqs []*repb.BatchUpdateBlobsRequest_Request 504 for _, blob := range []string{"a", "b", "c", "d"} { 505 blobBytes := []byte(blob) 506 req := &repb.BatchUpdateBlobsRequest_Request{Data: blobBytes, Digest: digest.NewFromBlob(blobBytes).ToProto()} 507 switch { 508 case proto.Equal(req.Digest, missing[0]): 509 wantUploadBlobsReqs = append(wantUploadBlobsReqs, req, req) 510 case proto.Equal(req.Digest, missing[1]): 511 wantUploadBlobsReqs = append(wantUploadBlobsReqs, req) 512 } 513 514 } 515 sort.Slice(wantUploadBlobsReqs, func(i, j int) bool { 516 return wantUploadBlobsReqs[i].Digest.Hash < wantUploadBlobsReqs[j].Digest.Hash 517 }) 518 sort.Slice(gotUploadBlobReqs, func(i, j int) bool { 519 return gotUploadBlobReqs[i].Digest.Hash < gotUploadBlobReqs[j].Digest.Hash 520 }) 521 if diff := cmp.Diff(wantUploadBlobsReqs, gotUploadBlobReqs, cmp.Comparer(proto.Equal)); diff != "" { 522 t.Error(diff) 523 } 524 } 525 526 func TestStreaming(t *testing.T) { 527 t.Parallel() 528 ctx := context.Background() 529 530 // TODO(nodir): add tests for retries. 531 532 e, cleanup := fakes.NewTestEnv(t) 533 defer cleanup() 534 conn, err := e.Server.NewClientConn(ctx) 535 if err != nil { 536 t.Fatal(err) 537 } 538 539 cfg := DefaultClientConfig() 540 cfg.BatchUpdateBlobs.MaxSizeBytes = 1 541 cfg.ByteStreamWrite.MaxSizeBytes = 2 // force multiple requests in a stream 542 cfg.SmallFileThreshold = 2 543 cfg.LargeFileThreshold = 3 544 cfg.CompressedBytestreamThreshold = 7 // between medium and large 545 client, err := NewClientWithConfig(ctx, conn, "instance", cfg) 546 if err != nil { 547 t.Fatal(err) 548 } 549 550 tmpDir := t.TempDir() 551 largeFilePath := filepath.Join(tmpDir, "testdata", "large") 552 putFile(t, largeFilePath, "laaaaaaaaaaarge") 553 554 res, err := client.Upload(ctx, UploadOptions{}, uploadInputChanFrom( 555 &UploadInput{Path: largeFilePath}, // large file 556 )) 557 if err != nil { 558 t.Fatalf("failed to upload: %s", err) 559 } 560 561 cas := e.Server.CAS 562 if cas.WriteReqs() != 1 { 563 t.Errorf("want 1 write requests, got %d", cas.WriteReqs()) 564 } 565 566 fileDigest := digest.Digest{Hash: "71944dd83e7e86354c3a9284e299e0d76c0b1108be62c8e7cefa72adf22128bf", Size: 15} 567 if got := cas.BlobWrites(fileDigest); got != 1 { 568 t.Errorf("want 1 write of %s, got %d", fileDigest, got) 569 } 570 571 wantStats := &TransferStats{ 572 CacheMisses: DigestStat{Digests: 1, Bytes: 15}, 573 Streamed: DigestStat{Digests: 1, Bytes: 15}, 574 } 575 if diff := cmp.Diff(wantStats, &res.Stats); diff != "" { 576 t.Errorf("unexpected stats (-want +got):\n%s", diff) 577 } 578 579 // Upload the large file again. 580 if _, err := client.Upload(ctx, UploadOptions{}, uploadInputChanFrom(&UploadInput{Path: largeFilePath})); err != nil { 581 t.Fatalf("failed to upload: %s", err) 582 } 583 } 584 585 func TestPartialMerkleTree(t *testing.T) { 586 t.Parallel() 587 588 mustDigest := func(m proto.Message) *repb.Digest { 589 d, err := digest.NewFromMessage(m) 590 if err != nil { 591 t.Fatal(err) 592 } 593 return d.ToProto() 594 } 595 596 type testCase struct { 597 tree map[string]*digested 598 wantItems []*uploadItem 599 } 600 601 test := func(t *testing.T, tc testCase) { 602 in := &UploadInput{ 603 tree: tc.tree, 604 cleanPath: "/", 605 } 606 gotItems := in.partialMerkleTree() 607 sort.Slice(gotItems, func(i, j int) bool { 608 return gotItems[i].Title < gotItems[j].Title 609 }) 610 611 if diff := cmp.Diff(tc.wantItems, gotItems, cmp.Comparer(compareUploadItems)); diff != "" { 612 t.Errorf("unexpected digests (-want +got):\n%s", diff) 613 } 614 } 615 616 t.Run("works", func(t *testing.T) { 617 barDigest := digest.NewFromBlob([]byte("bar")).ToProto() 618 bazDigest := mustDigest(&repb.Directory{}) 619 620 foo := &repb.Directory{ 621 Files: []*repb.FileNode{{ 622 Name: "bar", 623 Digest: barDigest, 624 }}, 625 Directories: []*repb.DirectoryNode{{ 626 Name: "baz", 627 Digest: bazDigest, 628 }}, 629 } 630 631 root := &repb.Directory{ 632 Directories: []*repb.DirectoryNode{{ 633 Name: "foo", 634 Digest: mustDigest(foo), 635 }}, 636 } 637 638 test(t, testCase{ 639 tree: map[string]*digested{ 640 "foo/bar": { 641 dirEntry: &repb.FileNode{ 642 Name: "bar", 643 Digest: barDigest, 644 }, 645 digest: barDigest, 646 }, 647 "foo/baz": { 648 dirEntry: &repb.DirectoryNode{ 649 Name: "baz", 650 Digest: bazDigest, 651 }, 652 digest: bazDigest, 653 }, 654 }, 655 wantItems: []*uploadItem{ 656 uploadItemFromDirMsg("/", root), 657 uploadItemFromDirMsg("/foo", foo), 658 }, 659 }) 660 }) 661 662 t.Run("redundant info in the tree", func(t *testing.T) { 663 barDigest := mustDigest(&repb.Directory{}) 664 barNode := &repb.DirectoryNode{ 665 Name: "bar", 666 Digest: barDigest, 667 } 668 foo := &repb.Directory{ 669 Directories: []*repb.DirectoryNode{barNode}, 670 } 671 root := &repb.Directory{ 672 Directories: []*repb.DirectoryNode{{ 673 Name: "foo", 674 Digest: mustDigest(foo), 675 }}, 676 } 677 678 test(t, testCase{ 679 tree: map[string]*digested{ 680 "foo/bar": {dirEntry: barNode, digest: barDigest}, 681 // Redundant 682 "foo/bar/baz": {}, // content doesn't matter 683 }, 684 wantItems: []*uploadItem{ 685 uploadItemFromDirMsg("/", root), 686 uploadItemFromDirMsg("/foo", foo), 687 }, 688 }) 689 }) 690 691 t.Run("nodes at different levels", func(t *testing.T) { 692 barDigest := digest.NewFromBlob([]byte("bar")).ToProto() 693 barNode := &repb.FileNode{ 694 Name: "bar", 695 Digest: barDigest, 696 } 697 698 bazDigest := digest.NewFromBlob([]byte("bar")).ToProto() 699 bazNode := &repb.FileNode{ 700 Name: "baz", 701 Digest: bazDigest, 702 } 703 704 foo := &repb.Directory{ 705 Files: []*repb.FileNode{barNode}, 706 } 707 root := &repb.Directory{ 708 Directories: []*repb.DirectoryNode{{ 709 Name: "foo", 710 Digest: mustDigest(foo), 711 }}, 712 Files: []*repb.FileNode{bazNode}, 713 } 714 715 test(t, testCase{ 716 tree: map[string]*digested{ 717 "foo/bar": {dirEntry: barNode, digest: barDigest}, 718 "baz": {dirEntry: bazNode, digest: bazDigest}, // content doesn't matter 719 }, 720 wantItems: []*uploadItem{ 721 uploadItemFromDirMsg("/", root), 722 uploadItemFromDirMsg("/foo", foo), 723 }, 724 }) 725 }) 726 } 727 728 func TestUploadInputInit(t *testing.T) { 729 t.Parallel() 730 absPath := filepath.Join(t.TempDir(), "foo") 731 testCases := []struct { 732 desc string 733 in *UploadInput 734 dir bool 735 wantCleanAllowlist []string 736 wantErrContain string 737 }{ 738 { 739 desc: "valid", 740 in: &UploadInput{Path: absPath}, 741 }, 742 { 743 desc: "relative path", 744 in: &UploadInput{Path: "foo"}, 745 wantErrContain: "not absolute", 746 }, 747 { 748 desc: "relative path", 749 in: &UploadInput{Path: "foo"}, 750 wantErrContain: "not absolute", 751 }, 752 { 753 desc: "regular file with allowlist", 754 in: &UploadInput{Path: absPath, Allowlist: []string{"x"}}, 755 wantErrContain: "the Allowlist is not supported for regular files", 756 }, 757 { 758 desc: "not clean allowlisted path", 759 in: &UploadInput{Path: absPath, Allowlist: []string{"bar/"}}, 760 dir: true, 761 wantCleanAllowlist: []string{"bar"}, 762 }, 763 { 764 desc: "absolute allowlisted path", 765 in: &UploadInput{Path: absPath, Allowlist: []string{"/bar"}}, 766 dir: true, 767 wantErrContain: "not relative", 768 }, 769 { 770 desc: "parent dir in allowlisted path", 771 in: &UploadInput{Path: absPath, Allowlist: []string{"bar/../.."}}, 772 dir: true, 773 wantErrContain: "..", 774 }, 775 { 776 desc: "no allowlist", 777 in: &UploadInput{Path: absPath}, 778 dir: true, 779 wantCleanAllowlist: []string{"."}, 780 }, 781 } 782 783 for _, tc := range testCases { 784 tc := tc 785 t.Run(tc.desc, func(t *testing.T) { 786 tmpFilePath := absPath 787 if tc.dir { 788 tmpFilePath = filepath.Join(absPath, "bar") 789 } 790 putFile(t, tmpFilePath, "") 791 defer os.RemoveAll(absPath) 792 793 err := tc.in.init(&uploader{}) 794 if tc.wantErrContain == "" { 795 if err != nil { 796 t.Error(err) 797 } 798 } else { 799 if err == nil || !strings.Contains(err.Error(), tc.wantErrContain) { 800 t.Errorf("expected err to contain %q; got %v", tc.wantErrContain, err) 801 } 802 } 803 804 if len(tc.wantCleanAllowlist) != 0 { 805 if diff := cmp.Diff(tc.wantCleanAllowlist, tc.in.cleanAllowlist); diff != "" { 806 t.Errorf("unexpected cleanAllowlist (-want +got):\n%s", diff) 807 } 808 } 809 }) 810 } 811 } 812 813 func compareUploadItems(x, y *uploadItem) bool { 814 return x.Title == y.Title && 815 proto.Equal(x.Digest, y.Digest) && 816 ((x.Open == nil && y.Open == nil) || cmp.Equal(mustReadAll(x), mustReadAll(y))) 817 } 818 819 func mustReadAll(item *uploadItem) []byte { 820 data, err := item.ReadAll() 821 if err != nil { 822 panic(err) 823 } 824 return data 825 } 826 827 func uploadInputChanFrom(inputs ...*UploadInput) chan *UploadInput { 828 ch := make(chan *UploadInput, len(inputs)) 829 for _, in := range inputs { 830 ch <- in 831 } 832 close(ch) 833 return ch 834 } 835 836 type fakeCAS struct { 837 regrpc.ContentAddressableStorageClient 838 findMissingBlobs func(ctx context.Context, in *repb.FindMissingBlobsRequest, opts ...grpc.CallOption) (*repb.FindMissingBlobsResponse, error) 839 batchUpdateBlobs func(ctx context.Context, in *repb.BatchUpdateBlobsRequest, opts ...grpc.CallOption) (*repb.BatchUpdateBlobsResponse, error) 840 } 841 842 func (c *fakeCAS) FindMissingBlobs(ctx context.Context, in *repb.FindMissingBlobsRequest, opts ...grpc.CallOption) (*repb.FindMissingBlobsResponse, error) { 843 return c.findMissingBlobs(ctx, in, opts...) 844 } 845 846 func (c *fakeCAS) BatchUpdateBlobs(ctx context.Context, in *repb.BatchUpdateBlobsRequest, opts ...grpc.CallOption) (*repb.BatchUpdateBlobsResponse, error) { 847 return c.batchUpdateBlobs(ctx, in, opts...) 848 } 849 850 func putFile(t *testing.T, path, contents string) { 851 if err := os.MkdirAll(filepath.Dir(path), 0777); err != nil { 852 t.Fatal(err) 853 } 854 if err := os.WriteFile(path, []byte(contents), 0600); err != nil { 855 t.Fatal(err) 856 } 857 } 858 859 func putSymlink(t *testing.T, path, target string) { 860 if err := os.MkdirAll(filepath.Dir(path), 0777); err != nil { 861 t.Fatal(err) 862 } 863 if err := os.Symlink(target, path); err != nil { 864 t.Fatal(err) 865 } 866 }