1package blob 2 3import ( 4 "bytes" 5 "errors" 6 "fmt" 7 "io" 8 "os" 9 "path/filepath" 10 "sort" 11 "strings" 12 "testing" 13 14 "github.com/golang/protobuf/proto" 15 "github.com/stretchr/testify/require" 16 "gitlab.com/gitlab-org/gitaly/v14/internal/git" 17 "gitlab.com/gitlab-org/gitaly/v14/internal/git/gittest" 18 "gitlab.com/gitlab-org/gitaly/v14/internal/git/localrepo" 19 "gitlab.com/gitlab-org/gitaly/v14/internal/helper/chunk" 20 "gitlab.com/gitlab-org/gitaly/v14/internal/helper/text" 21 "gitlab.com/gitlab-org/gitaly/v14/internal/testhelper" 22 "gitlab.com/gitlab-org/gitaly/v14/internal/testhelper/testcfg" 23 "gitlab.com/gitlab-org/gitaly/v14/proto/go/gitalypb" 24 "google.golang.org/grpc/codes" 25 "google.golang.org/grpc/status" 26) 27 28const ( 29 lfsPointer1 = "0c304a93cb8430108629bbbcaa27db3343299bc0" 30 lfsPointer2 = "f78df813119a79bfbe0442ab92540a61d3ab7ff3" 31 lfsPointer3 = "bab31d249f78fba464d1b75799aad496cc07fa3b" 32 lfsPointer4 = "125fcc9f6e33175cb278b9b2809154d2535fe19f" 33 lfsPointer5 = "0360724a0d64498331888f1eaef2d24243809230" 34 lfsPointer6 = "ff0ab3afd1616ff78d0331865d922df103b64cf0" 35) 36 37var ( 38 lfsPointers = map[string]*gitalypb.LFSPointer{ 39 lfsPointer1: &gitalypb.LFSPointer{ 40 Size: 133, 41 Data: []byte("version https://git-lfs.github.com/spec/v1\noid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897\nsize 1575078\n\n"), 42 Oid: lfsPointer1, 43 }, 44 lfsPointer2: &gitalypb.LFSPointer{ 45 Size: 127, 46 Data: []byte("version https://git-lfs.github.com/spec/v1\noid sha256:f2b0a1e7550e9b718dafc9b525a04879a766de62e4fbdfc46593d47f7ab74636\nsize 20\n"), 47 Oid: lfsPointer2, 48 }, 49 lfsPointer3: &gitalypb.LFSPointer{ 50 Size: 127, 51 Data: []byte("version https://git-lfs.github.com/spec/v1\noid sha256:bad71f905b60729f502ca339f7c9f001281a3d12c68a5da7f15de8009f4bd63d\nsize 18\n"), 52 Oid: lfsPointer3, 53 }, 54 lfsPointer4: &gitalypb.LFSPointer{ 55 Size: 129, 56 Data: []byte("version https://git-lfs.github.com/spec/v1\noid sha256:47997ea7ecff33be61e3ca1cc287ee72a2125161518f1a169f2893a5a82e9d95\nsize 7501\n"), 57 Oid: lfsPointer4, 58 }, 59 lfsPointer5: &gitalypb.LFSPointer{ 60 Size: 129, 61 Data: []byte("version https://git-lfs.github.com/spec/v1\noid sha256:8c1e8de917525f83104736f6c64d32f0e2a02f5bf2ee57843a54f222cba8c813\nsize 2797\n"), 62 Oid: lfsPointer5, 63 }, 64 lfsPointer6: &gitalypb.LFSPointer{ 65 Size: 132, 66 Data: []byte("version https://git-lfs.github.com/spec/v1\noid sha256:96f74c6fe7a2979eefb9ec74a5dfc6888fb25543cf99b77586b79afea1da6f97\nsize 1219696\n"), 67 Oid: lfsPointer6, 68 }, 69 } 70) 71 72func TestListLFSPointers(t *testing.T) { 73 _, repo, _, client := setup(t) 74 75 ctx, cancel := testhelper.Context() 76 defer cancel() 77 78 for _, tc := range []struct { 79 desc string 80 revs []string 81 limit int32 82 expectedPointers []*gitalypb.LFSPointer 83 expectedErr error 84 }{ 85 { 86 desc: "missing revisions", 87 revs: []string{}, 88 expectedErr: status.Error(codes.InvalidArgument, "missing revisions"), 89 }, 90 { 91 desc: "invalid revision", 92 revs: []string{"-dashed"}, 93 expectedErr: status.Error(codes.InvalidArgument, "invalid revision: \"-dashed\""), 94 }, 95 { 96 desc: "object IDs", 97 revs: []string{ 98 lfsPointer1, 99 lfsPointer2, 100 lfsPointer3, 101 "d5b560e9c17384cf8257347db63167b54e0c97ff", // tree 102 "60ecb67744cb56576c30214ff52294f8ce2def98", // commit 103 }, 104 expectedPointers: []*gitalypb.LFSPointer{ 105 lfsPointers[lfsPointer1], 106 lfsPointers[lfsPointer2], 107 lfsPointers[lfsPointer3], 108 }, 109 }, 110 { 111 desc: "revision", 112 revs: []string{"refs/heads/master"}, 113 expectedPointers: []*gitalypb.LFSPointer{ 114 lfsPointers[lfsPointer1], 115 }, 116 }, 117 { 118 desc: "pseudo-revisions", 119 revs: []string{"refs/heads/master", "--not", "--all"}, 120 }, 121 { 122 desc: "partial graph walk", 123 revs: []string{"--all", "--not", "refs/heads/master"}, 124 expectedPointers: []*gitalypb.LFSPointer{ 125 lfsPointers[lfsPointer2], 126 lfsPointers[lfsPointer3], 127 lfsPointers[lfsPointer4], 128 lfsPointers[lfsPointer5], 129 lfsPointers[lfsPointer6], 130 }, 131 }, 132 { 133 desc: "partial graph walk with matching limit", 134 revs: []string{"--all", "--not", "refs/heads/master"}, 135 limit: 5, 136 expectedPointers: []*gitalypb.LFSPointer{ 137 lfsPointers[lfsPointer2], 138 lfsPointers[lfsPointer3], 139 lfsPointers[lfsPointer4], 140 lfsPointers[lfsPointer5], 141 lfsPointers[lfsPointer6], 142 }, 143 }, 144 { 145 desc: "partial graph walk with limiting limit", 146 revs: []string{"--all", "--not", "refs/heads/master"}, 147 limit: 3, 148 expectedPointers: []*gitalypb.LFSPointer{ 149 lfsPointers[lfsPointer4], 150 lfsPointers[lfsPointer5], 151 lfsPointers[lfsPointer6], 152 }, 153 }, 154 } { 155 t.Run(tc.desc, func(t *testing.T) { 156 stream, err := client.ListLFSPointers(ctx, &gitalypb.ListLFSPointersRequest{ 157 Repository: repo, 158 Revisions: tc.revs, 159 Limit: tc.limit, 160 }) 161 require.NoError(t, err) 162 163 var actualLFSPointers []*gitalypb.LFSPointer 164 for { 165 resp, err := stream.Recv() 166 if err == io.EOF { 167 break 168 } 169 require.Equal(t, err, tc.expectedErr) 170 if err != nil { 171 break 172 } 173 174 actualLFSPointers = append(actualLFSPointers, resp.GetLfsPointers()...) 175 } 176 lfsPointersEqual(t, tc.expectedPointers, actualLFSPointers) 177 }) 178 } 179} 180 181func TestListAllLFSPointers(t *testing.T) { 182 receivePointers := func(t *testing.T, stream gitalypb.BlobService_ListAllLFSPointersClient) []*gitalypb.LFSPointer { 183 t.Helper() 184 185 var pointers []*gitalypb.LFSPointer 186 for { 187 resp, err := stream.Recv() 188 if err == io.EOF { 189 break 190 } 191 require.Nil(t, err) 192 pointers = append(pointers, resp.GetLfsPointers()...) 193 } 194 return pointers 195 } 196 197 ctx, cancel := testhelper.Context() 198 defer cancel() 199 200 lfsPointerContents := `version https://git-lfs.github.com/spec/v1 201oid sha256:1111111111111111111111111111111111111111111111111111111111111111 202size 12345` 203 204 t.Run("normal repository", func(t *testing.T) { 205 _, repo, _, client := setup(t) 206 stream, err := client.ListAllLFSPointers(ctx, &gitalypb.ListAllLFSPointersRequest{ 207 Repository: repo, 208 }) 209 require.NoError(t, err) 210 lfsPointersEqual(t, []*gitalypb.LFSPointer{ 211 lfsPointers[lfsPointer1], 212 lfsPointers[lfsPointer2], 213 lfsPointers[lfsPointer3], 214 lfsPointers[lfsPointer4], 215 lfsPointers[lfsPointer5], 216 lfsPointers[lfsPointer6], 217 }, receivePointers(t, stream)) 218 }) 219 220 t.Run("dangling LFS pointer", func(t *testing.T) { 221 cfg, repo, repoPath, client := setup(t) 222 223 hash := gittest.ExecStream(t, cfg, strings.NewReader(lfsPointerContents), "-C", repoPath, "hash-object", "-w", "--stdin") 224 lfsPointerOID := text.ChompBytes(hash) 225 226 stream, err := client.ListAllLFSPointers(ctx, &gitalypb.ListAllLFSPointersRequest{ 227 Repository: repo, 228 }) 229 require.NoError(t, err) 230 lfsPointersEqual(t, []*gitalypb.LFSPointer{ 231 &gitalypb.LFSPointer{ 232 Oid: lfsPointerOID, 233 Data: []byte(lfsPointerContents), 234 Size: int64(len(lfsPointerContents)), 235 }, 236 lfsPointers[lfsPointer1], 237 lfsPointers[lfsPointer2], 238 lfsPointers[lfsPointer3], 239 lfsPointers[lfsPointer4], 240 lfsPointers[lfsPointer5], 241 lfsPointers[lfsPointer6], 242 }, receivePointers(t, stream)) 243 }) 244 245 t.Run("quarantine", func(t *testing.T) { 246 cfg, repoProto, repoPath, client := setup(t) 247 248 // We're emulating the case where git is receiving data via a push, where objects 249 // are stored in a separate quarantine environment. In this case, LFS pointer checks 250 // may want to inspect all newly pushed objects, denoted by a repository proto 251 // message which only has its object directory set to the quarantine directory. 252 quarantineDir := "objects/incoming-123456" 253 require.NoError(t, os.Mkdir(filepath.Join(repoPath, quarantineDir), 0777)) 254 repoProto.GitObjectDirectory = quarantineDir 255 repoProto.GitAlternateObjectDirectories = nil 256 257 // There are no quarantined objects yet, so none should be returned here. 258 stream, err := client.ListAllLFSPointers(ctx, &gitalypb.ListAllLFSPointersRequest{ 259 Repository: repoProto, 260 }) 261 require.NoError(t, err) 262 require.Empty(t, receivePointers(t, stream)) 263 264 // Write a new object into the repository. Because we set GIT_OBJECT_DIRECTORY to 265 // the quarantine directory, objects will be written in there instead of into the 266 // repository's normal object directory. 267 repo := localrepo.NewTestRepo(t, cfg, repoProto) 268 var buffer, stderr bytes.Buffer 269 err = repo.ExecAndWait(ctx, git.SubCmd{ 270 Name: "hash-object", 271 Flags: []git.Option{ 272 git.Flag{Name: "-w"}, 273 git.Flag{Name: "--stdin"}, 274 }, 275 }, git.WithStdin(strings.NewReader(lfsPointerContents)), git.WithStdout(&buffer), git.WithStderr(&stderr)) 276 require.NoError(t, err) 277 278 stream, err = client.ListAllLFSPointers(ctx, &gitalypb.ListAllLFSPointersRequest{ 279 Repository: repoProto, 280 }) 281 require.NoError(t, err) 282 283 // We only expect to find a single LFS pointer, which is the one we've just written 284 // into the quarantine directory. 285 lfsPointersEqual(t, []*gitalypb.LFSPointer{ 286 &gitalypb.LFSPointer{ 287 Oid: text.ChompBytes(buffer.Bytes()), 288 Data: []byte(lfsPointerContents), 289 Size: int64(len(lfsPointerContents)), 290 }, 291 }, receivePointers(t, stream)) 292 }) 293} 294 295func TestSuccessfulGetLFSPointersRequest(t *testing.T) { 296 _, repo, _, client := setup(t) 297 298 ctx, cancel := testhelper.Context() 299 defer cancel() 300 301 lfsPointerIds := []string{ 302 lfsPointer1, 303 lfsPointer2, 304 lfsPointer3, 305 } 306 otherObjectIds := []string{ 307 "d5b560e9c17384cf8257347db63167b54e0c97ff", // tree 308 "60ecb67744cb56576c30214ff52294f8ce2def98", // commit 309 } 310 311 expectedLFSPointers := []*gitalypb.LFSPointer{ 312 lfsPointers[lfsPointer1], 313 lfsPointers[lfsPointer2], 314 lfsPointers[lfsPointer3], 315 } 316 317 request := &gitalypb.GetLFSPointersRequest{ 318 Repository: repo, 319 BlobIds: append(lfsPointerIds, otherObjectIds...), 320 } 321 322 stream, err := client.GetLFSPointers(ctx, request) 323 require.NoError(t, err) 324 325 var receivedLFSPointers []*gitalypb.LFSPointer 326 for { 327 resp, err := stream.Recv() 328 if err == io.EOF { 329 break 330 } else if err != nil { 331 t.Fatal(err) 332 } 333 334 receivedLFSPointers = append(receivedLFSPointers, resp.GetLfsPointers()...) 335 } 336 337 lfsPointersEqual(t, receivedLFSPointers, expectedLFSPointers) 338} 339 340func TestFailedGetLFSPointersRequestDueToValidations(t *testing.T) { 341 _, repo, _, client := setup(t) 342 343 ctx, cancel := testhelper.Context() 344 defer cancel() 345 346 testCases := []struct { 347 desc string 348 request *gitalypb.GetLFSPointersRequest 349 code codes.Code 350 }{ 351 { 352 desc: "empty Repository", 353 request: &gitalypb.GetLFSPointersRequest{ 354 Repository: nil, 355 BlobIds: []string{"f00"}, 356 }, 357 code: codes.InvalidArgument, 358 }, 359 { 360 desc: "empty BlobIds", 361 request: &gitalypb.GetLFSPointersRequest{ 362 Repository: repo, 363 BlobIds: nil, 364 }, 365 code: codes.InvalidArgument, 366 }, 367 } 368 369 for _, testCase := range testCases { 370 t.Run(testCase.desc, func(t *testing.T) { 371 stream, err := client.GetLFSPointers(ctx, testCase.request) 372 require.NoError(t, err) 373 374 _, err = stream.Recv() 375 require.NotEqual(t, io.EOF, err) 376 testhelper.RequireGrpcError(t, err, testCase.code) 377 }) 378 } 379} 380 381func TestFindLFSPointersByRevisions(t *testing.T) { 382 cfg := testcfg.Build(t) 383 384 gitCmdFactory := git.NewExecCommandFactory(cfg) 385 386 repoProto, _, cleanup := gittest.CloneRepoAtStorage(t, cfg, cfg.Storages[0], t.Name()) 387 t.Cleanup(cleanup) 388 repo := localrepo.NewTestRepo(t, cfg, repoProto) 389 390 ctx, cancel := testhelper.Context() 391 defer cancel() 392 393 for _, tc := range []struct { 394 desc string 395 revs []string 396 limit int 397 expectedErr error 398 expectedLFSPointers []*gitalypb.LFSPointer 399 }{ 400 { 401 desc: "--all", 402 revs: []string{"--all"}, 403 expectedLFSPointers: []*gitalypb.LFSPointer{ 404 lfsPointers[lfsPointer1], 405 lfsPointers[lfsPointer2], 406 lfsPointers[lfsPointer3], 407 lfsPointers[lfsPointer4], 408 lfsPointers[lfsPointer5], 409 lfsPointers[lfsPointer6], 410 }, 411 }, 412 { 413 desc: "--all with high limit", 414 revs: []string{"--all"}, 415 limit: 7, 416 expectedLFSPointers: []*gitalypb.LFSPointer{ 417 lfsPointers[lfsPointer1], 418 lfsPointers[lfsPointer2], 419 lfsPointers[lfsPointer3], 420 lfsPointers[lfsPointer4], 421 lfsPointers[lfsPointer5], 422 lfsPointers[lfsPointer6], 423 }, 424 }, 425 { 426 desc: "--all with truncating limit", 427 revs: []string{"--all"}, 428 limit: 3, 429 expectedLFSPointers: []*gitalypb.LFSPointer{ 430 lfsPointers[lfsPointer1], 431 lfsPointers[lfsPointer5], 432 lfsPointers[lfsPointer6], 433 }, 434 expectedErr: errLimitReached, 435 }, 436 { 437 desc: "--not --all", 438 revs: []string{"--not", "--all"}, 439 }, 440 { 441 desc: "initial commit", 442 revs: []string{"1a0b36b3cdad1d2ee32457c102a8c0b7056fa863"}, 443 }, 444 { 445 desc: "master", 446 revs: []string{"master"}, 447 expectedLFSPointers: []*gitalypb.LFSPointer{ 448 lfsPointers[lfsPointer1], 449 }, 450 }, 451 { 452 desc: "multiple revisions", 453 revs: []string{"master", "moar-lfs-ptrs"}, 454 expectedLFSPointers: []*gitalypb.LFSPointer{ 455 lfsPointers[lfsPointer1], 456 lfsPointers[lfsPointer2], 457 lfsPointers[lfsPointer3], 458 }, 459 }, 460 { 461 desc: "invalid dashed option", 462 revs: []string{"master", "--foobar"}, 463 expectedErr: fmt.Errorf("invalid revision: \"--foobar\""), 464 }, 465 { 466 desc: "invalid revision", 467 revs: []string{"does-not-exist"}, 468 expectedErr: fmt.Errorf("fatal: ambiguous argument 'does-not-exist'"), 469 }, 470 } { 471 t.Run(tc.desc, func(t *testing.T) { 472 var collector lfsPointerCollector 473 474 err := findLFSPointersByRevisions(ctx, repo, gitCmdFactory, 475 collector.chunker(), tc.limit, tc.revs...) 476 if tc.expectedErr == nil { 477 require.NoError(t, err) 478 } else { 479 require.Contains(t, err.Error(), tc.expectedErr.Error()) 480 } 481 lfsPointersEqual(t, tc.expectedLFSPointers, collector.pointers) 482 }) 483 } 484} 485 486func BenchmarkFindLFSPointers(b *testing.B) { 487 cfg := testcfg.Build(b) 488 489 gitCmdFactory := git.NewExecCommandFactory(cfg) 490 491 repoProto, _, cleanup := gittest.CloneBenchRepo(b, cfg) 492 b.Cleanup(cleanup) 493 repo := localrepo.NewTestRepo(b, cfg, repoProto) 494 495 ctx, cancel := testhelper.Context() 496 defer cancel() 497 498 b.Run("limitless", func(b *testing.B) { 499 var collector lfsPointerCollector 500 err := findLFSPointersByRevisions(ctx, repo, gitCmdFactory, collector.chunker(), 0, "--all") 501 require.NoError(b, err) 502 }) 503 504 b.Run("limit", func(b *testing.B) { 505 var collector lfsPointerCollector 506 err := findLFSPointersByRevisions(ctx, repo, gitCmdFactory, collector.chunker(), 1, "--all") 507 require.NoError(b, err) 508 require.Len(b, collector.pointers, 1) 509 }) 510} 511 512func BenchmarkReadLFSPointers(b *testing.B) { 513 cfg := testcfg.Build(b) 514 515 repoProto, path, cleanup := gittest.CloneBenchRepo(b, cfg) 516 b.Cleanup(cleanup) 517 repo := localrepo.NewTestRepo(b, cfg, repoProto) 518 519 ctx, cancel := testhelper.Context() 520 defer cancel() 521 522 candidates := gittest.Exec(b, cfg, "-C", path, "rev-list", "--in-commit-order", "--objects", "--no-object-names", "--filter=blob:limit=200", "--all") 523 524 b.Run("limitless", func(b *testing.B) { 525 var collector lfsPointerCollector 526 err := readLFSPointers(ctx, repo, collector.chunker(), bytes.NewReader(candidates), 0) 527 require.NoError(b, err) 528 }) 529 530 b.Run("limit", func(b *testing.B) { 531 var collector lfsPointerCollector 532 err := readLFSPointers(ctx, repo, collector.chunker(), bytes.NewReader(candidates), 1) 533 require.Equal(b, errLimitReached, err) 534 require.Equal(b, 1, len(collector.pointers)) 535 }) 536} 537 538func TestReadLFSPointers(t *testing.T) { 539 cfg, repo, _, _ := setup(t) 540 541 localRepo := localrepo.NewTestRepo(t, cfg, repo) 542 543 ctx, cancel := testhelper.Context() 544 defer cancel() 545 546 for _, tc := range []struct { 547 desc string 548 input string 549 limit int 550 expectedErr error 551 expectedLFSPointers []*gitalypb.LFSPointer 552 }{ 553 { 554 desc: "single object ID", 555 input: strings.Join([]string{lfsPointer1}, "\n"), 556 expectedLFSPointers: []*gitalypb.LFSPointer{ 557 lfsPointers[lfsPointer1], 558 }, 559 }, 560 { 561 desc: "multiple object IDs", 562 input: strings.Join([]string{ 563 lfsPointer1, 564 lfsPointer2, 565 lfsPointer3, 566 lfsPointer4, 567 lfsPointer5, 568 lfsPointer6, 569 }, "\n"), 570 expectedLFSPointers: []*gitalypb.LFSPointer{ 571 lfsPointers[lfsPointer1], 572 lfsPointers[lfsPointer2], 573 lfsPointers[lfsPointer3], 574 lfsPointers[lfsPointer4], 575 lfsPointers[lfsPointer5], 576 lfsPointers[lfsPointer6], 577 }, 578 }, 579 { 580 desc: "multiple object IDs with high limit", 581 input: strings.Join([]string{ 582 lfsPointer1, 583 lfsPointer2, 584 lfsPointer3, 585 lfsPointer4, 586 lfsPointer5, 587 lfsPointer6, 588 }, "\n"), 589 limit: 7, 590 expectedLFSPointers: []*gitalypb.LFSPointer{ 591 lfsPointers[lfsPointer1], 592 lfsPointers[lfsPointer2], 593 lfsPointers[lfsPointer3], 594 lfsPointers[lfsPointer4], 595 lfsPointers[lfsPointer5], 596 lfsPointers[lfsPointer6], 597 }, 598 }, 599 { 600 desc: "multiple object IDs with truncating limit", 601 input: strings.Join([]string{ 602 lfsPointer1, 603 lfsPointer2, 604 lfsPointer3, 605 lfsPointer4, 606 lfsPointer5, 607 lfsPointer6, 608 }, "\n"), 609 limit: 3, 610 expectedLFSPointers: []*gitalypb.LFSPointer{ 611 lfsPointers[lfsPointer1], 612 lfsPointers[lfsPointer2], 613 lfsPointers[lfsPointer3], 614 }, 615 expectedErr: errLimitReached, 616 }, 617 { 618 desc: "multiple object IDs with name filter", 619 input: strings.Join([]string{ 620 lfsPointer1, 621 lfsPointer2, 622 lfsPointer3 + " x", 623 lfsPointer4, 624 lfsPointer5 + " z", 625 lfsPointer6 + " a", 626 }, "\n"), 627 expectedLFSPointers: []*gitalypb.LFSPointer{ 628 lfsPointers[lfsPointer1], 629 lfsPointers[lfsPointer2], 630 }, 631 expectedErr: errors.New("object not found"), 632 }, 633 { 634 desc: "non-pointer object", 635 input: strings.Join([]string{ 636 "60ecb67744cb56576c30214ff52294f8ce2def98", 637 }, "\n"), 638 }, 639 { 640 desc: "mixed objects", 641 input: strings.Join([]string{ 642 "60ecb67744cb56576c30214ff52294f8ce2def98", 643 lfsPointer2, 644 }, "\n"), 645 expectedLFSPointers: []*gitalypb.LFSPointer{ 646 lfsPointers[lfsPointer2], 647 }, 648 }, 649 { 650 desc: "missing object", 651 input: strings.Join([]string{ 652 "0101010101010101010101010101010101010101", 653 }, "\n"), 654 expectedErr: errors.New("object not found"), 655 }, 656 } { 657 t.Run(tc.desc, func(t *testing.T) { 658 reader := strings.NewReader(tc.input) 659 660 var collector lfsPointerCollector 661 662 err := readLFSPointers(ctx, localRepo, collector.chunker(), reader, tc.limit) 663 if tc.expectedErr == nil { 664 require.NoError(t, err) 665 } else { 666 require.Contains(t, err.Error(), tc.expectedErr.Error()) 667 } 668 669 lfsPointersEqual(t, tc.expectedLFSPointers, collector.pointers) 670 }) 671 } 672} 673 674func lfsPointersEqual(tb testing.TB, expected, actual []*gitalypb.LFSPointer) { 675 tb.Helper() 676 677 for _, slice := range [][]*gitalypb.LFSPointer{expected, actual} { 678 sort.Slice(slice, func(i, j int) bool { 679 return strings.Compare(slice[i].Oid, slice[j].Oid) < 0 680 }) 681 } 682 683 require.Equal(tb, len(expected), len(actual)) 684 for i := range expected { 685 testhelper.ProtoEqual(tb, expected[i], actual[i]) 686 } 687} 688 689type lfsPointerCollector struct { 690 pointers []*gitalypb.LFSPointer 691} 692 693func (c *lfsPointerCollector) Append(m proto.Message) { 694 c.pointers = append(c.pointers, m.(*gitalypb.LFSPointer)) 695} 696 697func (c *lfsPointerCollector) Reset() { 698 // We don'c reset anything given that we want to collect all pointers. 699} 700 701func (c *lfsPointerCollector) Send() error { 702 // And neither do we anything here. 703 return nil 704} 705 706func (c *lfsPointerCollector) chunker() *chunk.Chunker { 707 return chunk.New(c) 708} 709